Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
- Read README and the relevant development/deployment guide before changes. Use English for source and repository documentation.
- Collect the relevant issues, decide a bounded plan, implement and verify. Use a separate worktree; leave adjacent repositories and primary checkouts unchanged.
- Preserve AGPL-3.0-only and third-party notices. Consume exact published Contracts packages; no sibling source imports or submodules.
- Use the pinned Node/npm toolchain, strict TypeScript, one root workspace lockfile and the documented commands. Business APIs, sessions, billing and AI execution are outside this static Web bootstrap.
- Use the pinned Node/npm toolchain, strict TypeScript, one root workspace lockfile and the documented commands. The server connection page only prepares the published Hello packaging example; business APIs, sessions, billing and AI execution remain outside this Web bootstrap.
- Keep PR checks credential-free. Never print or commit tokens or put private values in browser bundles. A Cloudflare API token belongs in the GitHub environment secret, not a client variable.
- Build and verify once, then deploy those same bytes. Distinguish mocked contract tests, local production-browser tests, real Cloudflare delivery and future C# product integration.
- Follow the user's authorization for remote settings/deployment. Do not merge a PR without authorization.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

React and TypeScript Web foundation for the ArcForges family. This first increment contains a public Hello World site, an interactive local greeting, shared UI, published Contracts consumption and an automated Cloudflare Workers Static Assets delivery pipeline.

It does not implement the planned Account/Chat application, authentication, payments or a C# backend. The greeting runs locally and sends no name to a server. Business authority remains in ArcForges Cloud.
It does not implement the planned Account/Chat application, authentication, payments or a C# backend. The `/hello/` greeting runs locally and sends no name to a server. A separate `/cloud-hello/` page prepares the published gRPC-Web Hello call for the future Cloud container; it currently reports unavailable until that service is deployed. Business authority remains in ArcForges Cloud.

## Start locally

Expand Down Expand Up @@ -30,7 +30,7 @@ The preview serves the actual candidate through local Wrangler at `http://127.0.

| Path | Purpose |
| ----------------------------------- | --------------------------------------------------------------------- |
| `apps/site` | Prerendered home and `/hello` pages |
| `apps/site` | Prerendered home, local greeting and server connection pages |
| `apps/app` | Documented boundary for the future Account/Chat profiles |
| `packages/ui` | Shared components and Tailwind/CSS styles |
| `tooling` | TypeScript build, provenance, policy and Cloudflare delivery commands |
Expand All @@ -46,7 +46,7 @@ PRs run source checks on Linux/Windows, dependency auditing/review, secret scann

The main-only GitHub `cloudflare` environment contains the account variable and deployment secret. The custom-domain binding is managed in Cloudflare; CI verifies that it belongs to this Worker before deploying. PR checks remain credential-free. See [deployment setup and recovery](docs/deploying.md) and [evidence](docs/validation.md).

Workers Static Assets supports this static React build directly. Frameworks that need request-time server code require a Workers-compatible adapter/runtime. This setup does not host C# or provide an API proxy. See the [official React guide](https://developers.cloudflare.com/workers/framework-guides/web-apps/react/) and [static assets guide](https://developers.cloudflare.com/workers/static-assets/get-started/).
Workers Static Assets supports this static React build directly. Frameworks that need request-time server code require a Workers-compatible adapter/runtime. This setup does not host C# or provide an API proxy. The future Cloud Worker will own the same-origin `/api/*` route and forward to its Native AOT container; see the [Hello integration boundary and remaining Cloud setup](docs/cloud-hello.md). See also the [official React guide](https://developers.cloudflare.com/workers/framework-guides/web-apps/react/) and [static assets guide](https://developers.cloudflare.com/workers/static-assets/get-started/).

## Contribute

Expand Down
20 changes: 20 additions & 0 deletions apps/site/app/cloud-hello.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: AGPL-3.0-only
import { createHelloClient } from "@arcforges/api-client";

export const helloApiPath = "/api/arcforges.hello.v1.HelloService/SayHello";

export async function checkServerConnection(
origin: string,
signal: AbortSignal,
fetcher: typeof fetch = fetch,
) {
const client = createHelloClient({
baseUrl: new URL("/api", origin).href,
useBinaryFormat: true,
defaultTimeoutMs: 10000,
fetch: (input, init) => fetcher(input, { ...init, credentials: "omit", redirect: "error" }),
});
const response = await client.sayHello({ name: "ArcForges" }, { signal });
if (response.message !== "Hello, ArcForges!") throw new Error("Unexpected Hello response");
return response.message;
}
6 changes: 5 additions & 1 deletion apps/site/app/routes.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
import { index, route, type RouteConfig } from "@react-router/dev/routes";
export default [index("routes/home.tsx"), route("hello", "routes/hello.tsx")] satisfies RouteConfig;
export default [
index("routes/home.tsx"),
route("hello", "routes/hello.tsx"),
route("cloud-hello", "routes/cloud-hello.tsx"),
] satisfies RouteConfig;
75 changes: 75 additions & 0 deletions apps/site/app/routes/cloud-hello.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// SPDX-License-Identifier: AGPL-3.0-only
import { Button } from "@arcforges/web-ui";
import { useEffect, useRef, useState } from "react";
import { checkServerConnection } from "../cloud-hello";

export function meta() {
return [{ title: "Server connection — ArcForges" }];
}

export default function CloudHello() {
const [ready, setReady] = useState(false);
const [state, setState] = useState<"idle" | "pending" | "success" | "error">("idle");
const [reply, setReply] = useState("");
const active = useRef<AbortController | null>(null);
useEffect(() => {
setReady(true);
return () => active.current?.abort();
}, []);

async function connect() {
if (active.current) return;
const controller = new AbortController();
active.current = controller;
setState("pending");
try {
const message = await checkServerConnection(window.location.origin, controller.signal);
if (!controller.signal.aborted) {
setReply(message);
setState("success");
}
} catch {
if (!controller.signal.aborted) setState("error");
} finally {
if (active.current === controller) active.current = null;
}
}

const message = {
idle: "No request sent yet.",
pending: "Contacting the server…",
success: reply,
error: "The server is unavailable or returned an unexpected response. Try again later.",
}[state];

return (
<section className="example">
<a className="back-link" href="/hello/">
← Back to your hello
</a>
<p className="eyebrow">Connection example</p>
<h1>
A hello, <em>from the server.</em>
</h1>
<p className="hero-copy">
Send a fixed “ArcForges” greeting to check the server connection. Your name from the local
example stays in your browser. Nothing is sent until you choose to connect.
</p>
<Button type="button" disabled={!ready || state === "pending"} onClick={connect}>
{state === "pending" ? "Connecting…" : "Check connection"}
</Button>
<noscript>
<p className="no-script">Enable JavaScript to check the server connection.</p>
</noscript>
<div
className="greeting"
role={state === "error" ? "alert" : "status"}
aria-live="polite"
aria-atomic="true"
>
<span className="greeting-label">Server response</span>
<p>{message}</p>
</div>
</section>
);
}
3 changes: 3 additions & 0 deletions apps/site/app/routes/hello.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ export default function Hello() {
<span className="greeting-label">Your greeting</span>
<p>{message}</p>
</div>
<p className="field-hint">
<a href="/cloud-hello/">Check the server connection →</a>
</p>
</section>
);
}
2 changes: 1 addition & 1 deletion apps/site/react-router.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-License-Identifier: AGPL-3.0-only
import type { Config } from "@react-router/dev/config";

export default { ssr: false, prerender: ["/", "/hello"] } satisfies Config;
export default { ssr: false, prerender: ["/", "/hello", "/cloud-hello"] } satisfies Config;
25 changes: 25 additions & 0 deletions docs/cloud-hello-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Cloud Hello preparation

## Boundary and findings

The owner selected a future C# Native AOT service in Cloudflare Containers. The custom-domain site is already deployed and its real three-browser suite passes. The local ArcForges directory has no Cloud checkout and no Cloud service is available for integration yet.

Web currently uses published Contracts `1.0.0-ci.25.1`, whose `HelloService.SayHello` is an explicitly non-product packaging example. The existing greeting is entirely local. This increment prepares a separate server connection example without claiming to implement the Cloud service, authentication, billing or AI.

The existing live delivery gate also expects Web's HTML 404 at `/api/*`. That assertion would conflict with the future Cloud route. Keep API fallback checks in local candidate tests; live Web verification must check only Web-owned paths. Cloud owns the public API's response assertions.

## Decisions and implementation order

1. Keep static Web delivery on `arcforges.com`. Add `/cloud-hello/` as a prerendered connection page. It sends a fixed `ArcForges` greeting only after an explicit click; the name entered in the existing local example is never sent.
2. Use the existing published `@arcforges/api-client` in binary gRPC-Web mode, with same-origin base `/api`. The exact request path is `/api/arcforges.hello.v1.HelloService/SayHello`. No REST replacement, new schema, registry package or browser secret is needed.
3. Use a ten-second deadline, cancel on navigation, suppress concurrent clicks and perform no automatic retries. Check for the contract's exact expected reply. Show unavailable/invalid responses as failure, never a locally generated successful server response.
4. The future Cloud repository will own a Worker route `arcforges.com/api/*`, forwarding requests to its C# container after removing `/api`. This route can precede the existing Web Custom Domain. Web needs neither an API proxy Worker nor a deployment-time binding to a service that does not exist yet.
5. Add browser tests with explicitly labelled gRPC-Web fixtures for request framing, success, unavailable service and recovery. Keep the local API 404 check alongside those local-only tests. Live Web tests check the idle page and Web-owned static paths without asserting Cloud's responses or simulating a backend. Record the separate gate that the future Cloud deployment must satisfy.

## Closure and evidence

Run source checks, candidate build/verification and the three-browser candidate suite; inspect the new page. PR CI must pass without credentials or access to a live C# service. Real Web delivery is distinct from the fixture tests. The actual Native AOT container, public API route and C#/protobuf transport require Cloud's later real integration test; they are not accepted by this Web PR.

Post-implementation review corrected the live API-path ownership assertion and documented the static Worker's actual GET 404 / POST 405 behavior. Local source, candidate and all eighteen browser checks now pass; the live suite excludes the fixture cases. See [validation evidence](validation.md). No Cloud infrastructure or formal Design document was changed.

Reference: [Cloudflare Custom Domains and routes](https://developers.cloudflare.com/workers/configuration/routing/custom-domains/#interaction-with-routes).
40 changes: 40 additions & 0 deletions docs/cloud-hello.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Connecting the future Cloud service

## Current Web behavior

`/hello/` remains local. `/cloud-hello/` uses the published Contracts client to send one fixed diagnostic greeting after the user clicks **Check connection**. It sends no user-entered name, account cookie, authorization header or Cloudflare management token. Requests have a ten-second deadline, are cancelled on navigation, and are never automatically retried. Only the expected server response is displayed as success.

No Cloud service is currently deployed by this repository. Until its API route exists, the static Worker rejects the Hello POST with 405 Method Not Allowed; a GET at that missing path returns 404. The page shows an unavailable response. No mock is deployed and no successful response is generated locally.

## Fixed integration boundary

| Item | Value |
| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Browser SDK | `@arcforges/api-client` and `@arcforges/proto`, both `1.0.0-ci.25.1` |
| Browser base URL | Same origin, `/api` |
| Public method | `POST https://arcforges.com/api/arcforges.hello.v1.HelloService/SayHello` |
| Container method after prefix removal | `/arcforges.hello.v1.HelloService/SayHello` |
| Transport | Unary binary gRPC-Web, `application/grpc-web+proto` |
| Request | Published `SayHelloRequest`, `name = "ArcForges"` |
| Expected reply | Published `SayHelloResponse`, `message = "Hello, ArcForges!"` |
| Authority | [Contracts Hello schema](https://github.com/ArcForges/Contracts/blob/main/public/proto/arcforges/hello/v1/hello.proto), a packaging example, not a product API |

The Web origin stays `https://arcforges.com`; CSP keeps `connect-src 'self'` and no cross-origin exception is needed. A later account/authenticated API must define its own session behavior; this anonymous diagnostic must not silently start forwarding credentials.

## Work owned by the future Cloud repository

1. Build and test the actual C# Native AOT Linux container implementing this published wire contract. Cloudflare currently requires a `linux/amd64` image. Validate its AOT build, startup, gRPC-Web response/trailer framing, and failure statuses; the existing Contracts HelloHost example alone is not AOT evidence.
2. Deploy its Cloud Worker and Container binding in the same Cloudflare account. Forward to the container through that binding, removing only the leading `/api` from the public path. Preserve request/response protobuf bytes and gRPC-Web content type, statuses and framed trailers. Do not convert the payload to ad-hoc JSON or forward back to the public API URL.
3. Attach **Worker route** `arcforges.com/api/*` to the Cloud Worker. Keep the apex **Custom Domain** attached to `arcforges-web`. A route runs ahead of that custom-domain origin. The API route needs no second DNS hostname, browser token, Web service binding or Web rebuild. Unknown API methods must return an API error/404 rather than Web HTML.
4. Keep the example explicitly bounded: only the Hello diagnostic, with input/resource limits and no model call, paid user operation or database mutation. Authentication, quotas and commercial APIs remain a separate product increment. Worker/Container account permissions and plan availability are configured when Cloud is implemented.
5. Cloud's deployment gate must invoke the **public same-origin method using the published client**, verify the expected protobuf reply, and verify both success and failure behavior. Then check the button in the deployed Web page. A container health endpoint, mocked fixture or Web deployment alone does not establish this chain.

This repository does not provision the missing Cloud Worker, container image, API route, billing plan or credentials.

## Evidence and recovery

`cloud-hello-fixture.spec.ts` intercepts the browser API request with explicitly labelled protobuf wire fixtures. It verifies the actual published client's request, unavailable response and recovery. It is excluded from live Web verification so mocked success cannot be reported as a real C# integration. Live Web tests check that the new page loads and sends nothing automatically.

Web delivery still verifies its assets and Web-owned public 404 behavior. Candidate-only tests verify that the static Worker cannot fake a successful API response; the live Web gate does not require Web HTML at `/api/*`, because Cloud will own those paths. Cloud owns the later API/container deployment and its independent real integration gate. Removing Cloud's API route restores the static Worker's rejection of that API request; the connection page reports failure instead of silently falling back to a local greeting.

References: [routes before a Custom Domain](https://developers.cloudflare.com/workers/configuration/routing/custom-domains/#interaction-with-routes), [Cloudflare Containers setup](https://developers.cloudflare.com/containers/get-started/).
2 changes: 1 addition & 1 deletion docs/deploying.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ All content is static and public. HTML revalidates, hashed `/assets/*` files are

The source link identifies the candidate's source revision. Public content contains no secret; open-source code does not grant deployment authority. Adding a backend/AI proxy in future requires its own authentication, authorization, quota and abuse controls. This static preview has no paid model invocation path.

Use `https://arcforges.com`. The former Workers subdomain was another public entry to the same deployment, not a staging environment. Do not re-enable it merely to run CI. Disabling it in the dashboard alone is insufficient if a later Wrangler configuration enables it; the source configuration is authoritative. CORS for C# Cloud, auth cookies, API origins and production profile separation remain future integration work.
Use `https://arcforges.com`. The former Workers subdomain was another public entry to the same deployment, not a staging environment. Do not re-enable it merely to run CI. Disabling it in the dashboard alone is insufficient if a later Wrangler configuration enables it; the source configuration is authoritative. The [Cloud Hello boundary](cloud-hello.md) prepares an anonymous same-origin API call. Authenticated sessions and production profile separation remain future integration work. Live Web delivery checks only Web-owned paths; it must not require its HTML 404 at `/api/*` once Cloud owns that route.

## Failure and recovery

Expand Down
Loading