mono updates

This commit is contained in:
Devin Zuczek
2026-08-15 16:51:50 -04:00
parent 66c09806f9
commit 8e0f090d18
15 changed files with 185 additions and 28 deletions
+1
View File
@@ -9,6 +9,7 @@
"check:lint": "run-oxlint",
"check:types": "run-tsc",
"check:workers-types": "run-wrangler-types --check",
"deploy:mono": "run-wrangler-deploy",
"dev": "run-wrangler-dev",
"fix:workers-types": "run-wrangler-types",
"test": "run-vitest"
+13
View File
@@ -15,6 +15,19 @@ import type { NotificationsHub } from '../../notify/src/notifications-hub'
* each app's narrower `Env`, so the sub-apps type-check unchanged.
*/
export type Env = SharedHonoEnv & {
/**
* Base domain this worker answers on, e.g. `rec.example.com` — injected from
* `RECFLARE_DOMAIN` by both `run-wrangler-dev` and `run-wrangler-deploy`, with a
* placeholder default in `wrangler.jsonc` for tests and an unconfigured checkout.
*
* Read by the mounted `ns` app to build the service-discovery document — the thing a
* client is pointed at — so it has to name the host that actually reaches this worker:
* the tunnel/LAN hostname when running it locally, and the apex of the domain when
* deployed (`RECFLARE_SUBDOMAINS='{"mono":"@"}'`, `just deploy-mono`). Every service
* mounted here is served from a PATH on that one host, so the document says
* `https://<domain>/rooms` and nothing else would answer there.
*/
DOMAIN: string
// HS256 JWT signing key (shared Secrets Store). Tokens signed by `auth` verify everywhere.
JWT_SECRET: SecretsStoreSecret
// Meta (Oculus) app secret, from the same store. Read only by `auth`, to validate a
+39 -13
View File
@@ -3,22 +3,31 @@
*
* Mounts each RecFlare worker inside a single deployable Worker WITHOUT modifying the
* originals: every app is imported by relative path and bundled by esbuild at build
* time. Production routing mirrors the split deployment — requests are dispatched on
* the request's subdomain (`accounts.<domain>` -> the `accounts` app), so the sub-app
* paths (and therefore the client contract) are untouched.
* time. A request selects its service two ways, and the sub-app paths (and therefore the
* client contract) are untouched either way.
*
* Local dev has no subdomain, so the first path segment selects the service and is
* stripped before the request is forwarded, e.g.
* http://localhost:8787/accounts/ -> accounts app sees /
* http://localhost:8787/match/player/login -> match app sees /player/login
* http://localhost:8787/api/api/config/v2 -> api app sees /api/config/v2
* By PATH — how this worker is meant to be deployed, at the apex of `DOMAIN`, and the
* only way that works in local dev, which has no subdomain. The first path segment names
* the service and is stripped before the request is forwarded, e.g.
* https://<domain>/accounts/ -> accounts app sees /
* https://<domain>/match/player/login -> match app sees /player/login
* https://<domain>/api/api/config/v2 -> api app sees /api/config/v2
*
* By SUBDOMAIN — `accounts.<domain>` -> the `accounts` app, with the path forwarded
* unchanged. That mirrors the split deployment, so a client (or a stray DNS record) still
* pointed at the per-service hosts keeps working if they're routed here.
*
* A request with no path (just `/`) that selects no service serves the `ns` discovery
* document, so a bare hit to the facade root returns the service map to bootstrap from.
* The document is built in the PATH style (`https://<domain>/rooms`, every service on
* this one host) — see ENDPOINT_STYLE below — so deploy this worker at the apex of
* `DOMAIN` and point the client at nothing else.
*
* NOT mounted here: `www`, `img`, `econ`. Each binds a static `assets` directory and
* Cloudflare allows only one static-assets binding per Worker. Resolve that (serve
* their static trees from R2, or keep those three as their own Workers) before adding.
* The discovery document still puts them on this host, since a single-service run is the
* whole point of this worker — so until they're mounted, their paths 404 here.
*/
import accounts from '../../accounts/src/accounts.app'
import api from '../../api/src/api.app'
@@ -67,16 +76,24 @@ const services = {
type ServiceName = keyof typeof services
/**
* This worker is one host, so its discovery document has to name one host: every service
* is advertised as `https://<domain>/<name>`, never `https://<name>.<domain>`. Handed to
* the mounted `ns` app, which defaults to the per-host document the split deployment wants.
*/
const ENDPOINT_STYLE = 'path'
function resolve(request: Request): { name: ServiceName; request: Request } | undefined {
const url = new URL(request.url)
// Production: dispatch on the leftmost DNS label — accounts.<domain> -> accounts.
// The path is forwarded unchanged so the client contract is identical.
// Dispatch on the leftmost DNS label — accounts.<domain> -> accounts. The path is
// forwarded unchanged so the client contract is identical to the split deployment.
const sub = url.hostname.split('.')[0]
if (sub in services) return { name: sub as ServiceName, request }
// Local dev (no service subdomain): the first path segment selects the service and
// is stripped before forwarding — /match/player/login -> match app sees /player/login.
// Apex (and local dev): the first path segment selects the service and is stripped
// before forwarding — /match/player/login -> match app sees /player/login. This is
// what the discovery document advertises; see ENDPOINT_STYLE.
const [, first, ...rest] = url.pathname.split('/')
if (first !== undefined && first in services) {
url.pathname = `/${rest.join('/')}`
@@ -103,11 +120,20 @@ export default {
{ status: 404 }
)
}
// `ns` is the one mounted app whose answer depends on this worker's own shape: the
// addresses it hands out have to be paths on this host. Passed as a var — the same
// way a deploy would — so the app itself stays free of any knowledge of mono.
if (resolved.name === 'ns') return ns.fetch(resolved.request, { ...env, ENDPOINT_STYLE }, ctx)
return services[resolved.name].fetch(resolved.request, env, ctx)
},
// Only `match` runs a cron in the split deployment; this worker owns its presence sweep.
scheduled(controller: ScheduledController, env: Env, ctx: ExecutionContext): Promise<void> | void {
scheduled(
controller: ScheduledController,
env: Env,
ctx: ExecutionContext
): Promise<void> | void {
return matchScheduled(controller, env, ctx)
},
} satisfies ExportedHandler<Env>
+19 -2
View File
@@ -9,6 +9,9 @@ declare module 'cloudflare:test' {
const ORIGIN = 'https://example.com'
// Must match the DOMAIN var default in apps/mono/wrangler.jsonc.
const TEST_DOMAIN = 'rec.example.com'
// The facade's job is routing, not business logic, so one request that reaches a
// mounted app through the path prefix is enough to prove the wiring. `api` serves a
// static game-config with no auth/DB, so it's a clean target. The api worker namespaces
@@ -24,8 +27,22 @@ describe('mono routing', () => {
test('root path (no service, no prefix) serves the ns discovery document', async () => {
const res = await exports.default.fetch(`${ORIGIN}/`)
expect(res.status).toBe(200)
// The ns worker serves the service-discovery document.
expect(await res.json()).toHaveProperty('Auth')
// The ns worker serves the service-discovery document. This worker is one host, so
// every service in it is a path on the base domain (the DOMAIN var default in
// wrangler.jsonc) — no per-service subdomains anywhere in the document.
const doc = (await res.json()) as Record<string, string>
expect(doc).toMatchObject({
Auth: `https://${TEST_DOMAIN}/auth`,
Rooms: `https://${TEST_DOMAIN}/rooms`,
Matchmaking: `https://${TEST_DOMAIN}/match`,
})
expect(Object.values(doc).every((url) => url.startsWith(`https://${TEST_DOMAIN}/`))).toBe(true)
})
test('the ns service prefix serves that same document', async () => {
const res = await exports.default.fetch(`${ORIGIN}/ns/`)
expect(res.status).toBe(200)
expect(await res.json()).toMatchObject({ Rooms: `https://${TEST_DOMAIN}/rooms` })
})
test('unknown service prefix returns the facade 404', async () => {
+6 -1
View File
@@ -76,6 +76,11 @@
"vars": {
"NAME": "mono", // logging tag; split workers derive this per-app
"ENVIRONMENT": "development", // overridden during deployment
"SENTRY_RELEASE": "unknown" // overridden during deployment
"SENTRY_RELEASE": "unknown", // overridden during deployment
// Base domain the discovery document is built from; replaced with RECFLARE_DOMAIN by
// both `just dev` and `just deploy-mono`. It must name the host that actually reaches
// this worker, which serves every service it mounts from a path on that ONE host — so
// deployed, it belongs on the APEX of that domain (see src/context.ts).
"DOMAIN": "rec.example.com"
}
}