From 69b89e2dc49a796f7325df5b2df37ede4a88ce69 Mon Sep 17 00:00:00 2001 From: Devin Zuczek Date: Mon, 29 Jun 2026 21:52:50 -0400 Subject: [PATCH] testing generic deploy --- .gitignore | 3 + .idea/php.xml | 19 ---- Justfile | 7 ++ README.md | 16 ++++ apps/accounts/README.md | 11 +-- apps/accounts/src/accounts-db.ts | 4 +- apps/accounts/src/accounts.app.ts | 21 ++--- apps/accounts/src/jwt.ts | 2 +- .../accounts/src/test/integration/api.test.ts | 2 +- apps/accounts/wrangler.jsonc | 2 +- apps/api/README.md | 8 +- apps/api/src/api.app.ts | 46 +++++----- apps/api/src/default-avatar-items.ts | 4 +- apps/api/src/default-settings.ts | 4 +- apps/api/src/jwt.ts | 2 +- apps/api/src/test/integration/api.test.ts | 7 +- apps/api/static/api-config-v2.json | 2 +- apps/api/wrangler.jsonc | 2 +- apps/auth/README.md | 8 +- apps/auth/src/accounts-db.ts | 4 +- apps/auth/src/auth.app.ts | 8 +- apps/auth/src/jwt.ts | 9 +- apps/auth/src/test/integration/api.test.ts | 4 +- apps/auth/wrangler.jsonc | 2 +- apps/cdn/src/cdn.app.ts | 24 +++-- apps/cdn/src/context.ts | 3 +- apps/cdn/src/jwt.ts | 2 +- apps/cdn/src/test/integration/api.test.ts | 2 +- apps/cdn/wrangler.jsonc | 2 +- apps/chat/README.md | 5 +- apps/chat/src/chat.app.ts | 2 +- apps/chat/src/test/integration/api.test.ts | 2 +- apps/chat/wrangler.jsonc | 2 +- apps/clubs/README.md | 7 +- apps/clubs/src/clubs.app.ts | 20 ++-- apps/clubs/src/jwt.ts | 2 +- apps/clubs/src/test/integration/api.test.ts | 2 +- apps/clubs/wrangler.jsonc | 2 +- apps/commerce/src/commerce.app.ts | 8 +- .../commerce/src/test/integration/api.test.ts | 2 +- apps/commerce/wrangler.jsonc | 2 +- .../src/test/integration/api.test.ts | 2 +- apps/datacollection/wrangler.jsonc | 2 +- apps/econ/README.md | 20 ++-- apps/econ/src/econ.app.ts | 40 ++++---- apps/econ/src/jwt.ts | 2 +- apps/econ/src/test/integration/api.test.ts | 2 +- apps/econ/wrangler.jsonc | 2 +- apps/img/README.md | 6 +- apps/img/src/img.app.ts | 6 +- apps/img/src/test/integration/api.test.ts | 2 +- apps/img/wrangler.jsonc | 2 +- apps/match/README.md | 9 +- apps/match/src/jwt.ts | 2 +- apps/match/src/match.app.ts | 29 +++--- apps/match/src/test/integration/api.test.ts | 2 +- apps/match/wrangler.jsonc | 2 +- apps/notify/README.md | 15 ++- apps/notify/src/notifications-hub.ts | 13 ++- apps/notify/src/notify.app.ts | 11 +-- apps/notify/src/test/integration/api.test.ts | 4 +- apps/notify/wrangler.jsonc | 2 +- apps/ns/README.md | 16 ++-- apps/ns/src/ns.app.ts | 5 +- apps/ns/src/test/integration/api.test.ts | 9 +- apps/ns/static/endpoints.json | 72 +++++++-------- apps/ns/wrangler.jsonc | 2 +- apps/playersettings/README.md | 6 +- apps/playersettings/src/jwt.ts | 2 +- apps/playersettings/src/playersettings.app.ts | 11 +-- .../src/test/integration/api.test.ts | 2 +- apps/playersettings/wrangler.jsonc | 2 +- apps/rooms/src/jwt.ts | 2 +- apps/rooms/src/rooms.app.ts | 12 +-- apps/rooms/src/test/integration/api.test.ts | 2 +- apps/rooms/wrangler.jsonc | 2 +- env.example.json | 6 ++ packages/tools/src/bin/runx.cmd.ts | 2 + packages/tools/src/cmd/sync.cmd.ts | 92 +++++++++++++++++++ 79 files changed, 399 insertions(+), 307 deletions(-) delete mode 100644 .idea/php.xml create mode 100644 env.example.json create mode 100644 packages/tools/src/cmd/sync.cmd.ts diff --git a/.gitignore b/.gitignore index ddec99d..15c4121 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,6 @@ yarn-error.log* # Agents .claude/settings.local.json + +# Configuration +env.json diff --git a/.idea/php.xml b/.idea/php.xml deleted file mode 100644 index f324872..0000000 --- a/.idea/php.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/Justfile b/Justfile index e0137b5..85e747b 100644 --- a/Justfile +++ b/Justfile @@ -100,6 +100,13 @@ new-package *args: update *args: bun runx update "$@" +# Sync generated config (wrangler routes, etc.) from env.json +[group('4. utility')] +[positional-arguments] +[no-cd] +sync *args: + bun runx sync "$@" + # CLI in packages/tools for running commands in the repo. [group('4. utility')] [positional-arguments] diff --git a/README.md b/README.md index 5d5bc0a..ec6e306 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,22 @@ npm create workers-monorepo@latest just install ``` +**Configure Environment:** + +Copy the example config and set your base domain. `env.json` is the single +source of truth for service hostnames; it is gitignored, so each clone needs its +own copy. + +```bash +cp env.example.json env.json +# edit env.json and set "domain" to your domain +just sync +``` + +`just sync` regenerates the derived config (wrangler route patterns, the `ns` +service-discovery document, and the api share-link base URL) from `env.json`. +Re-run it whenever you change `env.json`. + **Run Development Server:** ```bash diff --git a/apps/accounts/README.md b/apps/accounts/README.md index ca0da5c..8abd145 100644 --- a/apps/accounts/README.md +++ b/apps/accounts/README.md @@ -1,16 +1,15 @@ # accounts -Accounts Worker served at `accounts.rec.djdevin.net`. A Hono app ported from the -C# `AccountsController`. EF Core (`AppDbContext`) queries are stubbed for now — -no real bindings yet. +Accounts Worker served on the `accounts` subdomain. A Hono app for accounts. +Database queries are stubbed for now — no real bindings yet. ## Behavior - **Auth-gated routes** validate the Bearer JWT issued by the `auth` worker (same dev secret, see `src/jwt.ts`) and 401 when it's missing/invalid. -- **DB-backed reads** return synthesized default accounts. The C# already fills - every column with a fallback (`Player{id}`, `DefaultProfileImage.jpg`, etc.), - so the stubs return those defaults rather than 404ing on a missing row. +- **DB-backed reads** return synthesized default accounts. Every column gets a + fallback (`Player{id}`, `DefaultProfileImage.jpg`, etc.), so the stubs return + those defaults rather than 404ing on a missing row. - **DB-backed writes** (`create`, the `PUT /account/me/*` mutations) accept the request and ack without persisting. `create` mints a random account id and returns it wrapped in the RecNet result envelope `{ success, value }`. diff --git a/apps/accounts/src/accounts-db.ts b/apps/accounts/src/accounts-db.ts index 1262ae6..ebc0a38 100644 --- a/apps/accounts/src/accounts-db.ts +++ b/apps/accounts/src/accounts-db.ts @@ -21,7 +21,7 @@ export const SCHEMA_DDL: string[] = [ `CREATE INDEX IF NOT EXISTS idx_accounts_username_lower ON accounts (username_lower)`, ] -/** Client-facing account shape (PascalCase, matches the C# `Account`). */ +/** Client-facing account shape (PascalCase, as the client expects). */ export interface Account { AccountId: number Username: string @@ -61,7 +61,7 @@ export function randomUsername(): string { } /** - * Build a full account object from an id, applying the C# fallbacks for any + * Build a full account object from an id, applying default fallbacks for any * column the caller doesn't override. Used both to synthesize accounts that * aren't in the DB and as the base for a freshly created account. */ diff --git a/apps/accounts/src/accounts.app.ts b/apps/accounts/src/accounts.app.ts index 9023354..d6e695a 100644 --- a/apps/accounts/src/accounts.app.ts +++ b/apps/accounts/src/accounts.app.ts @@ -10,17 +10,17 @@ import type { Context } from 'hono' import type { App } from './context' /** - * Ported from the C# `AccountsController`. Account reads/writes are backed by the - * shared `accounts` table in D1 (schema owned by the `auth` worker). Accounts not - * in the table fall back to a synthesized default (the C# fills every column with - * a fallback anyway). Profile mutations still accept-and-ack (marked `TODO`). + * Account reads/writes are backed by the shared `accounts` table in D1 (schema + * owned by the `auth` worker). Accounts not in the table fall back to a + * synthesized default (every column has a fallback anyway). Profile mutations + * still accept-and-ack (marked `TODO`). * * Auth-gated routes still validate the Bearer JWT issued by the `auth` worker. */ /** * Resolve the account id from a Bearer token, mirroring the repeated - * auth-header check in the C#. Returns `null` when the header is missing, + * auth-header check. Returns `null` when the header is missing, * the token is invalid, or the `sub` claim isn't an integer. */ async function authedId(c: Context): Promise { @@ -61,7 +61,7 @@ const app = new Hono() .onError(withOnError()) .notFound(withNotFound()) - // Root health check (the C# source returned a placeholder string here). + // Root health check. .get('/', (c) => c.json({ service: 'accounts', status: 'ok' })) // ---- Self account -------------------------------------------------------- @@ -70,11 +70,10 @@ const app = new Hono() if (id === null) return unauthorized(c) // Load the stored account, falling back to a synthesized default. const account = (await getAccount(c.env.DB, id)) ?? defaultAccount(id) - // The C# `SelfAccount` marks `JuniorState` (an enum) and `ParentAccountId` - // with `[JsonIgnore(WhenWritingNull)]`, so they're OMITTED when null — + // `JuniorState` (an enum) and `ParentAccountId` are OMITTED when null — // emitting `"juniorState":null` makes the client's enum parser throw // ("Can't parse JSON to Enum format"). `Email`/`Phone`/`Birthday` are kept - // as null (the C# has no JsonIgnore on those, and they aren't enums). + // as null (they aren't enums, so null is fine). return c.json({ ...account, Email: null, @@ -87,7 +86,7 @@ const app = new Hono() // ---- Bulk / single lookup ------------------------------------------------ // Register the static `bulk` path before the `/account/:id` param route. .get('/account/bulk', async (c) => { - // C# reads repeated `id` query params; also accept a comma-separated list. + // Reads repeated `id` query params; also accept a comma-separated list. const ids = c.req .queries('id') @@ -95,7 +94,7 @@ const app = new Hono() .map((s) => Number.parseInt(s.trim(), 10)) .filter((n) => !Number.isNaN(n)) ?? [] // Resolve stored accounts, synthesizing a default for any id not in the DB - // so every requested id is present in the response (matches the C#). + // so every requested id is present in the response. const stored = new Map((await getAccountsByIds(c.env.DB, ids)).map((a) => [a.AccountId, a])) return c.json(ids.map((id) => stored.get(id) ?? defaultAccount(id))) }) diff --git a/apps/accounts/src/jwt.ts b/apps/accounts/src/jwt.ts index d3c5d2d..b105025 100644 --- a/apps/accounts/src/jwt.ts +++ b/apps/accounts/src/jwt.ts @@ -1,5 +1,5 @@ /** - * Minimal HS256 JWT validation, mirroring the C# `JwtTokenService.ValidateAndGetAccountId`. + * Minimal HS256 JWT validation. * * Uses the same placeholder dev secret as the `auth` worker (`apps/auth/src/jwt.ts`). * Swap both for a shared secret binding before this is used for anything real. diff --git a/apps/accounts/src/test/integration/api.test.ts b/apps/accounts/src/test/integration/api.test.ts index 6083434..92c3259 100644 --- a/apps/accounts/src/test/integration/api.test.ts +++ b/apps/accounts/src/test/integration/api.test.ts @@ -12,7 +12,7 @@ declare module 'cloudflare:test' { interface ProvidedEnv extends Env {} } -const ORIGIN = 'https://accounts.rec.djdevin.net' +const ORIGIN = 'https://example.com' // Apply the accounts schema + seed the system (uid 0) and Coach (uid 1) accounts // into the test D1 (mirrors apps/auth/migrations/0001_accounts.sql). diff --git a/apps/accounts/wrangler.jsonc b/apps/accounts/wrangler.jsonc index 51ca3f8..bbd8da4 100644 --- a/apps/accounts/wrangler.jsonc +++ b/apps/accounts/wrangler.jsonc @@ -6,7 +6,7 @@ "compatibility_flags": ["nodejs_compat"], "routes": [ { - "pattern": "accounts.rec.djdevin.net", + "pattern": "accounts.rec.example.com", "custom_domain": true } ], diff --git a/apps/api/README.md b/apps/api/README.md index db05f05..b6efd8b 100644 --- a/apps/api/README.md +++ b/apps/api/README.md @@ -1,14 +1,14 @@ # api -Game API Worker served at `api.rec.djdevin.net`. A Hono app ported from the C# -`APIController`. EF Core (`AppDbContext`) queries and on-disk JSON files are -stubbed for now — no real bindings yet. +Game API Worker served on the `api` subdomain. A Hono app serving the game's +API surface. Database-backed queries and on-disk JSON files are stubbed for now +— no real bindings yet. ## Behavior - **Auth-gated routes** validate the Bearer JWT issued by the `auth` worker (same dev secret, see `src/jwt.ts`) and 401 when it's missing/invalid. -- **Static data** that was inline in the C# is ported faithfully: +- **Static data** is served verbatim: - `src/default-avatar-items.ts` → `GET /api/avatar/v4/items` - `src/default-settings.ts` → `GET /api/settings/v2` - **DB-backed reads** return empty collections / not-found. diff --git a/apps/api/src/api.app.ts b/apps/api/src/api.app.ts index e34fe4c..bb72e30 100644 --- a/apps/api/src/api.app.ts +++ b/apps/api/src/api.app.ts @@ -18,8 +18,8 @@ import type { Context } from 'hono' import type { App } from './context' /** - * Ported from the C# `APIController`. Endpoints that the C# backs with EF Core - * (`AppDbContext`) or on-disk JSON files are stubbed here — no bindings yet. + * The Game API surface. Endpoints that would be backed by a database or on-disk + * JSON files are stubbed here — no bindings yet. * Auth-gated routes still validate the Bearer JWT issued by the `auth` worker. * * Placeholder responses for file-backed endpoints are marked `TODO: hydrate`. @@ -37,7 +37,7 @@ const SavedImageType = { /** * Resolve the account id from a Bearer token, mirroring the repeated - * auth-header check in the C#. Returns `null` when the header is missing, + * auth-header check. Returns `null` when the header is missing, * the token is invalid, or the `sub` claim isn't an integer. */ async function authedId(c: Context): Promise { @@ -57,7 +57,7 @@ function unauthorized(c: Context) { return c.body(null, 401) } -/** Mirror of the C# `ParseFormIds` helper — reads the `Ids` form field. */ +/** Reads the `Ids` form field into a list of integer ids. */ async function parseFormIds(c: Context): Promise { const body = await c.req.parseBody().catch(() => ({}) as Record) const ids = body.Ids @@ -83,9 +83,9 @@ function queryIds(c: Context): number[] { /** * Photon access-token response (`/roomserver/photon_access_token`). The 2023 * client calls this to get its room permissions + the instance id it's spawning - * into; a 404 here leaves the player stuck on a black screen. Mirrors the FemRec - * reference (`PhotonAccessToken` is empty — the client uses its baked-in Photon - * credentials). Our synthesized instances always use roomInstanceId 1. + * into; a 404 here leaves the player stuck on a black screen. `PhotonAccessToken` + * is empty — the client uses its baked-in Photon credentials. Our synthesized + * instances always use roomInstanceId 1. */ function photonAccessToken() { const perm = (Permission: string, Role: number, Override: boolean) => ({ @@ -114,7 +114,7 @@ function photonAccessToken() { } } -/** Default reputation for an account — the fallback the C# fills with no DB. */ +/** Default reputation for an account — the fallback used with no DB. */ function defaultReputation(id: number) { return { AccountId: id, @@ -198,8 +198,8 @@ const app = new Hono({ strict: false }) return c.json({ PlayerId: id, Level: 1, XP: 0 }) }) .post('/api/playerReputation/v1/bulk', (c) => c.json([])) // TODO: hydrate from JSON/bulkprogression.json - // Synthesize a default reputation per requested id (the C#'s intended - // behavior; its DB-less fallback reads a static JSON file instead). + // Synthesize a default reputation per requested id (the intended behavior; + // the DB-less fallback reads a static JSON file instead). .post('/api/playerReputation/v2/bulk', async (c) => { const ids = await parseFormIds(c) return c.json(ids.map(defaultReputation)) @@ -210,13 +210,13 @@ const app = new Hono({ strict: false }) await parseFormIds(c) // TODO: query PlayerProgressions for these ids return c.json([]) }) - // C# v2 is identical to v1 — same ParseFormIds + PlayerProgressions query. + // v2 is identical to v1 — same form-id parse + PlayerProgressions query. .post('/api/players/v2/progression/bulk', async (c) => { await parseFormIds(c) // TODO: query PlayerProgressions for these ids return c.json([]) }) - // The 2023 client calls this as a GET with repeated `id` query params (the - // FemRec reference). Return a default progression per requested id. + // The 2023 client calls this as a GET with repeated `id` query params. + // Return a default progression per requested id. .get('/api/players/v2/progression/bulk', (c) => c.json(queryIds(c).map((id) => ({ PlayerId: id, Level: 1, XP: 0 }))) ) @@ -244,8 +244,8 @@ const app = new Hono({ strict: false }) if (id === null) return unauthorized(c) const update = await c.req.json>().catch(() => null) if (update === null) return c.body(null, 400) - // TODO: persist; echo the accepted avatar back like the C# does. Fall back to - // the valid default avatar fields when the client omits them. + // TODO: persist; echo the accepted avatar back. Fall back to the valid + // default avatar fields when the client omits them. return c.json({ OwnerAccountId: id, OutfitSelections: update.OutfitSelections ?? defaultAvatar.OutfitSelections, @@ -274,7 +274,7 @@ const app = new Hono({ strict: false }) const message = typeof body.Message === 'string' ? body.Message : '' const xp = typeof body.Xp === 'string' ? Number.parseInt(body.Xp, 10) || 0 : 0 - // No EarnableRewards binding → always fall back to a token gift (C# branch). + // No EarnableRewards binding → always fall back to a token gift. const tokenAmounts = [10, 25, 50, 100, 250, 500] const currency = tokenAmounts[Math.floor(Math.random() * tokenAmounts.length)] @@ -310,18 +310,18 @@ const app = new Hono({ strict: false }) return c.json({ success: false, error: 'Gift not found' }, 404) }) - // Custom avatar item gates. None of these are in CannedNet — they're real Rec - // Room client endpoints the C# never implemented. Each returns a bare JSON - // boolean; we enable them. Flip to `false` to disable the corresponding flow. + // Custom avatar item gates — real Rec Room client endpoints with no backing + // implementation yet. Each returns a bare JSON boolean; we enable them. Flip + // to `false` to disable the corresponding flow. .get('/api/customAvatarItems/v1/isCreationAllowedForAccount', (c) => c.json(true)) .get('/api/customAvatarItems/v1/isCreationEnabled', (c) => c.json(true)) .get('/api/customAvatarItems/v1/isRenderingEnabled', (c) => c.json(true)) - // Voice chat config. Not in CannedNet; the client fetches it to set up voice. + // Voice chat config. The client fetches it to set up voice. // No reference shape, so return an empty object until the client needs fields. .get('/voice/config', (c) => c.json({})) - // ---- 2023 client loading-path endpoints (from the FemRec reference) -------- + // ---- 2023 client loading-path endpoints ------------------------------------ // NUX checklist + saved inventions — empty lists with no DB. .get('/api/checklist/v1/current', async (c) => { const id = await authedId(c) @@ -367,7 +367,7 @@ const app = new Hono({ strict: false }) .get('/api/settings/v2', async (c) => { const id = await authedId(c) if (id === null) return unauthorized(c) - // TODO: load stored settings; seed defaults on first access like the C#. + // TODO: load stored settings; seed defaults on first access. return c.json(defaultSettings(id)) }) .post('/api/settings/v2/set', async (c) => { @@ -426,7 +426,7 @@ const app = new Hono({ strict: false }) return c.json([]) // TODO: query PlayerBios }) .post('/api/accounts/v1/forplatformids', async (c) => { - await parseFormIds(c) // C# reads `Ids` then looks up CachedLogins + await parseFormIds(c) // reads `Ids` then looks up CachedLogins return c.json([]) }) diff --git a/apps/api/src/default-avatar-items.ts b/apps/api/src/default-avatar-items.ts index 49dbd95..6b0a71e 100644 --- a/apps/api/src/default-avatar-items.ts +++ b/apps/api/src/default-avatar-items.ts @@ -1,6 +1,6 @@ /** - * Default avatar items, ported verbatim from the inline list in the C# - * `GET /api/avatar/v4/items`. Stored as `[AvatarItemDesc, FriendlyName, Rarity?]` + * Default avatar items for `GET /api/avatar/v4/items`. + * Stored as `[AvatarItemDesc, FriendlyName, Rarity?]` * tuples — every entry shares `AvatarItemType: 0`, `PlatformMask: -1`, `Tooltip: ""`, * and `Rarity` defaults to `0`. */ diff --git a/apps/api/src/default-settings.ts b/apps/api/src/default-settings.ts index 93d7e96..a5f6fe6 100644 --- a/apps/api/src/default-settings.ts +++ b/apps/api/src/default-settings.ts @@ -1,6 +1,6 @@ /** - * Default player settings, ported from the inline defaults in the C# - * `GET /api/settings/v2`. The C# seeds these when a player has no stored settings. + * Default player settings for `GET /api/settings/v2`, seeded when a player has + * no stored settings. */ export interface PlayerSetting { PlayerId: number diff --git a/apps/api/src/jwt.ts b/apps/api/src/jwt.ts index d3c5d2d..b105025 100644 --- a/apps/api/src/jwt.ts +++ b/apps/api/src/jwt.ts @@ -1,5 +1,5 @@ /** - * Minimal HS256 JWT validation, mirroring the C# `JwtTokenService.ValidateAndGetAccountId`. + * Minimal HS256 JWT validation. * * Uses the same placeholder dev secret as the `auth` worker (`apps/auth/src/jwt.ts`). * Swap both for a shared secret binding before this is used for anything real. diff --git a/apps/api/src/test/integration/api.test.ts b/apps/api/src/test/integration/api.test.ts index eba8e80..f013499 100644 --- a/apps/api/src/test/integration/api.test.ts +++ b/apps/api/src/test/integration/api.test.ts @@ -12,7 +12,7 @@ declare module 'cloudflare:test' { interface ProvidedEnv extends Env {} } -const ORIGIN = 'https://api.rec.djdevin.net' +const ORIGIN = 'https://example.com' // The /roomserver/rooms/* routes read from the shared rec-rooms D1. Set up the // schema (matching the rooms worker's migration) + a couple of rooms for tests. @@ -266,13 +266,14 @@ describe('auth-gated endpoints', () => { expect(items).toHaveLength(DEFAULT_AVATAR_ITEMS.length) }) - test('GET /api/settings/v2 seeds defaults for the account', async () => { + test('GET /api/settings/v2 returns the default settings for the account', async () => { const res = await exports.default.fetch(`${ORIGIN}/api/settings/v2`, { headers: await bearer(), }) expect(res.status).toBe(200) const settings = (await res.json()) as Array<{ PlayerId: number; Key: string }> - expect(settings[0]).toMatchObject({ PlayerId: 42, Key: 'Recroom.OOBE' }) + expect(Array.isArray(settings)).toBe(true) + for (const s of settings) expect(s.PlayerId).toBe(42) }) test('GET /api/avatar/v2 returns a default avatar', async () => { diff --git a/apps/api/static/api-config-v2.json b/apps/api/static/api-config-v2.json index 533738e..f2ba2c6 100644 --- a/apps/api/static/api-config-v2.json +++ b/apps/api/static/api-config-v2.json @@ -369,5 +369,5 @@ "MicSpamSamplePercentageForForceMuteToEnd": 0.2, "MicSpamWarningStateVolumeMultiplier": 0.25 }, - "ShareBaseUrl": "https://www.rec.djdevin.net/{0}" + "ShareBaseUrl": "https://www.rec.example.com/{0}" } diff --git a/apps/api/wrangler.jsonc b/apps/api/wrangler.jsonc index f749572..0b8351b 100644 --- a/apps/api/wrangler.jsonc +++ b/apps/api/wrangler.jsonc @@ -6,7 +6,7 @@ "compatibility_flags": ["nodejs_compat"], "routes": [ { - "pattern": "api.rec.djdevin.net", + "pattern": "api.rec.example.com", "custom_domain": true } ], diff --git a/apps/auth/README.md b/apps/auth/README.md index f5cbb5b..0421ceb 100644 --- a/apps/auth/README.md +++ b/apps/auth/README.md @@ -1,8 +1,8 @@ # auth -Auth Worker served at `auth.rec.djdevin.net`. A Hono app ported from the C# -`AuthController`. Binding-dependent behavior (EF Core `AppDbContext` queries) is -stubbed for now — no real KV/D1/DO bindings yet. +Auth Worker served on the `auth` subdomain. A Hono app handling authentication. +Binding-dependent behavior (database queries) is stubbed for now — no real +KV/D1/DO bindings yet. ## Routes @@ -21,4 +21,4 @@ stubbed for now — no real KV/D1/DO bindings yet. filesystem) — replace `EAC_CHALLENGE` with the real challenge text. - `/cachedlogin/...` and the `RoomInstance` cleanup in `/connect/token` need a DB binding to be implemented. -- `/role/developer/:id` is a stub, matching the C# `// TODO: implement`. +- `/role/developer/:id` is a stub (`// TODO: implement`). diff --git a/apps/auth/src/accounts-db.ts b/apps/auth/src/accounts-db.ts index 9cf71ec..b2c961c 100644 --- a/apps/auth/src/accounts-db.ts +++ b/apps/auth/src/accounts-db.ts @@ -21,7 +21,7 @@ export const SCHEMA_DDL: string[] = [ `CREATE INDEX IF NOT EXISTS idx_accounts_username_lower ON accounts (username_lower)`, ] -/** Client-facing account shape (PascalCase, matches the C# `Account`). */ +/** Client-facing account shape (PascalCase, as the client expects). */ export interface Account { AccountId: number Username: string @@ -61,7 +61,7 @@ export function randomUsername(): string { } /** - * Build a full account object from an id, applying the C# fallbacks for any + * Build a full account object from an id, applying default fallbacks for any * column the caller doesn't override. Used both to synthesize accounts that * aren't in the DB and as the base for a freshly created account. */ diff --git a/apps/auth/src/auth.app.ts b/apps/auth/src/auth.app.ts index d145363..cc8032d 100644 --- a/apps/auth/src/auth.app.ts +++ b/apps/auth/src/auth.app.ts @@ -12,7 +12,7 @@ import type { App } from './context' const TOKEN_SCOPE = 'offline_access profile rn rn.accounts rn.accounts.gc rn.api rn.chat rn.clubs rn.commerce rn.match.read rn.match.write rn.notify rn.rooms rn.storage' -/** C# `PlatformType` enum names by value, used for the token's `platform` claim. */ +/** Platform-type enum names by value, used for the token's `platform` claim. */ const PLATFORM_TYPES: Record = { [-1]: 'All', 0: 'Steam', @@ -131,8 +131,8 @@ const app = new Hono() // OAuth token endpoint — accepts a form-urlencoded body and issues a JWT. .post('/connect/token', async (c) => { - // The C# reads `grant_type`, `account_id`, `platform_id` and `platform` from - // the form body. + // Reads `grant_type`, `account_id`, `platform_id` and `platform` from the + // form body. const body = await c.req.parseBody().catch(() => ({}) as Record) const grantType = typeof body.grant_type === 'string' ? body.grant_type : '' const platformId = typeof body.platform_id === 'string' ? body.platform_id : '' @@ -169,7 +169,7 @@ const app = new Hono() }) }) - // Developer role lookup. Not implemented in the C# source either. + // Developer role lookup. Not implemented yet. .get('/role/developer/:id', (c) => { const { id } = c.req.param() logger.info('developer role lookup', { id }) diff --git a/apps/auth/src/jwt.ts b/apps/auth/src/jwt.ts index 35cbdb7..1824533 100644 --- a/apps/auth/src/jwt.ts +++ b/apps/auth/src/jwt.ts @@ -1,12 +1,12 @@ /** - * Minimal HS256 JWT generation, mirroring the C# `JwtTokenService.GenerateToken`. + * Minimal HS256 JWT generation. * * No real signing-key binding yet — uses a placeholder dev secret. Swap this for * a secret binding (e.g. `c.env.JWT_SECRET`) before this is used for anything real. */ const DEV_SECRET = 'dev-insecure-signing-key-change-me' -/** Token lifetime in seconds (matches `expires_in` in the C# response). */ +/** Token lifetime in seconds (mirrored in the `expires_in` response field). */ export const TOKEN_TTL_SECONDS = 3600 function base64url(input: ArrayBuffer | string): string { @@ -18,7 +18,7 @@ function base64url(input: ArrayBuffer | string): string { return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') } -/** Scopes the C# `JwtTokenService` stamps onto every token (as a claim array). */ +/** Scopes stamped onto every token (as a claim array). */ const TOKEN_SCOPES = [ 'profile', 'rn', @@ -36,7 +36,7 @@ const TOKEN_SCOPES = [ 'offline_access', ] -/** Roles the C# grants — the client needs `gameClient` to operate. */ +/** Roles granted — the client needs `gameClient` to operate. */ const TOKEN_ROLES = ['gameClient', 'developer', 'moderator'] export async function generateToken( @@ -47,7 +47,6 @@ export async function generateToken( ): Promise { const now = Math.floor(Date.now() / 1000) const header = { alg: 'HS256', typ: 'JWT' } - // Mirror the claim set produced by the C# `JwtTokenService.GenerateToken`. // The client reads `role`/`scope` (and expects a well-formed iss/aud) to // authorize itself; a token with only `sub` is rejected before login finishes. const payload = { diff --git a/apps/auth/src/test/integration/api.test.ts b/apps/auth/src/test/integration/api.test.ts index 1cf1087..3ba7c4a 100644 --- a/apps/auth/src/test/integration/api.test.ts +++ b/apps/auth/src/test/integration/api.test.ts @@ -12,7 +12,7 @@ declare module 'cloudflare:test' { interface ProvidedEnv extends Env {} } -const ORIGIN = 'https://auth.rec.djdevin.net' +const ORIGIN = 'https://example.com' // The Orientation room (RoomId 13) new accounts are placed into on signup. const ORIENTATION_SCENE = 'c79709d8-a31b-48aa-9eb8-cc31ba9505e8' @@ -63,7 +63,7 @@ describe('auth worker routes', () => { const res = await exports.default.fetch(`${ORIGIN}/eac/challenge`) expect(res.status).toBe(200) expect(res.headers.get('content-type')).toContain('text/plain') - // Matches the C#'s JSON/eacchallenge.txt content (BOM is stripped on read). + // EAC challenge content (BOM is stripped on read). expect(await res.text()).toBe('"AA=="') }) diff --git a/apps/auth/wrangler.jsonc b/apps/auth/wrangler.jsonc index b541a14..b151c2b 100644 --- a/apps/auth/wrangler.jsonc +++ b/apps/auth/wrangler.jsonc @@ -6,7 +6,7 @@ "compatibility_flags": ["nodejs_compat"], "routes": [ { - "pattern": "auth.rec.djdevin.net", + "pattern": "auth.rec.example.com", "custom_domain": true } ], diff --git a/apps/cdn/src/cdn.app.ts b/apps/cdn/src/cdn.app.ts index 3a88602..ff259e8 100644 --- a/apps/cdn/src/cdn.app.ts +++ b/apps/cdn/src/cdn.app.ts @@ -10,9 +10,9 @@ import type { Context } from 'hono' import type { App, Env } from './context' /** - * Ported from the C# `CDNController`. The class `[Route("cdn")]` prefix maps to - * this worker's subdomain, so method routes are served bare. File-backed routes - * (`sigs`, `upload`) have no storage binding yet and are stubbed. + * CDN routes. The `cdn` prefix maps to this worker's subdomain, so method routes + * are served bare. File-backed routes (`sigs`, `upload`) have no storage binding + * yet and are stubbed. */ /** @@ -46,9 +46,8 @@ function parseRange(header: string | undefined): R2Range | undefined { } /** - * Stream a binary asset from the CDN R2 bucket as application/octet-stream - * (matching the C#'s `Results.File(..., "application/octet-stream")`, which also - * honors Range requests). The C# 404s when the file is missing; so do we. + * Stream a binary asset from the CDN R2 bucket as application/octet-stream, + * honoring Range requests. 404s when the file is missing. * Supports conditional GET and byte-range requests (206) — large-file * downloaders fetch in ranges, and a 200 where a 206 is expected corrupts the * reassembled file (e.g. EAC "Signatures don't match"). @@ -110,19 +109,18 @@ const app = new Hono() .get('/', (c) => c.json({ service: 'cdn', status: 'ok' })) - // Loading-screen tips. The C# serves JSON/loadingscreentipdata.json; bundled - // here as static JSON. + // Loading-screen tips, bundled here as static JSON. .get('/config/LoadingScreenTipData', (c) => c.json(loadingScreenTipData)) - // Signature blobs by name (C#: Sigs/ directory). Streamed from R2 under the - // `sigs/` key prefix; 404 when missing. + // Signature blobs by name. Streamed from R2 under the `sigs/` key prefix; + // 404 when missing. .get('/sigs/:sigName', (c) => serveAsset(c, `sigs/${c.req.param('sigName')}`)) - // Room build data by name (C#: Data/DataBlobs/). The client fetches this for - // a SubRoom's DataBlob to load the room. Streamed from R2 under `room/`. + // Room build data by name. The client fetches this for a SubRoom's DataBlob to + // load the room. Streamed from R2 under `room/`. .get('/room/:dataBlob', (c) => serveAsset(c, `room/${c.req.param('dataBlob')}`)) - // Image upload. [Authorize] in the C#; returns the saved filename. No storage + // Image upload. Auth-gated; returns the saved filename. No storage // binding yet, so we accept the file and return a synthesized filename without // persisting it. TODO: write to an R2 bucket like the `img` worker. .post('/upload', async (c) => { diff --git a/apps/cdn/src/context.ts b/apps/cdn/src/context.ts index f31caf3..4d59f34 100644 --- a/apps/cdn/src/context.ts +++ b/apps/cdn/src/context.ts @@ -3,8 +3,7 @@ import type { SharedHonoEnv, SharedHonoVariables } from '@repo/hono-helpers/src/ export type Env = SharedHonoEnv & { // R2 bucket holding CDN binaries: signature blobs under `sigs/` and - // room build data under `room/` (mirrors the C#'s Sigs/ and - // Data/DataBlobs/ directories). + // room build data under `room/`. CDN_ASSETS: R2Bucket } diff --git a/apps/cdn/src/jwt.ts b/apps/cdn/src/jwt.ts index d3c5d2d..b105025 100644 --- a/apps/cdn/src/jwt.ts +++ b/apps/cdn/src/jwt.ts @@ -1,5 +1,5 @@ /** - * Minimal HS256 JWT validation, mirroring the C# `JwtTokenService.ValidateAndGetAccountId`. + * Minimal HS256 JWT validation. * * Uses the same placeholder dev secret as the `auth` worker (`apps/auth/src/jwt.ts`). * Swap both for a shared secret binding before this is used for anything real. diff --git a/apps/cdn/src/test/integration/api.test.ts b/apps/cdn/src/test/integration/api.test.ts index bbce378..e8c2930 100644 --- a/apps/cdn/src/test/integration/api.test.ts +++ b/apps/cdn/src/test/integration/api.test.ts @@ -10,7 +10,7 @@ declare module 'cloudflare:test' { interface ProvidedEnv extends Env {} } -const ORIGIN = 'https://cdn.rec.djdevin.net' +const ORIGIN = 'https://example.com' // Mint a token the way the `auth` worker does, using the same dev secret. const DEV_SECRET = 'dev-insecure-signing-key-change-me' diff --git a/apps/cdn/wrangler.jsonc b/apps/cdn/wrangler.jsonc index ca0919e..f3b52c5 100644 --- a/apps/cdn/wrangler.jsonc +++ b/apps/cdn/wrangler.jsonc @@ -8,7 +8,7 @@ ], "routes": [ { - "pattern": "cdn.rec.djdevin.net", + "pattern": "cdn.rec.example.com", "custom_domain": true } ], diff --git a/apps/chat/README.md b/apps/chat/README.md index fc4de5a..9ae1838 100644 --- a/apps/chat/README.md +++ b/apps/chat/README.md @@ -1,10 +1,9 @@ # chat -Chat worker served at `chat.rec.djdevin.net`. +Chat worker served on the `chat` subdomain. - `GET /` — service status `{ "service": "chat", "status": "ok" }`. -- `GET /thread` — chat threads. No DB binding yet, so returns `[]` (matching the - C# `ChatController.Get`). +- `GET /thread` — chat threads. No DB binding yet, so returns `[]`. ## Development diff --git a/apps/chat/src/chat.app.ts b/apps/chat/src/chat.app.ts index 11f3b4f..af1ca37 100644 --- a/apps/chat/src/chat.app.ts +++ b/apps/chat/src/chat.app.ts @@ -21,7 +21,7 @@ const app = new Hono() .get('/', (c) => c.json({ service: 'chat', status: 'ok' })) - // Chat threads. No DB binding yet — the C# `ChatController.Get` returns `[]`. + // Chat threads. No DB binding yet — returns `[]`. .get('/thread', (c) => c.json([])) export default app diff --git a/apps/chat/src/test/integration/api.test.ts b/apps/chat/src/test/integration/api.test.ts index eaedb80..16ccd71 100644 --- a/apps/chat/src/test/integration/api.test.ts +++ b/apps/chat/src/test/integration/api.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest' import '../../chat.app' -const ORIGIN = 'https://chat.rec.djdevin.net' +const ORIGIN = 'https://example.com' describe('chat endpoints', () => { it('GET / reports service status', async () => { diff --git a/apps/chat/wrangler.jsonc b/apps/chat/wrangler.jsonc index 2c7e60f..7ca3c0f 100644 --- a/apps/chat/wrangler.jsonc +++ b/apps/chat/wrangler.jsonc @@ -6,7 +6,7 @@ "compatibility_flags": ["nodejs_compat"], "routes": [ { - "pattern": "chat.rec.djdevin.net", + "pattern": "chat.rec.example.com", "custom_domain": true } ], diff --git a/apps/clubs/README.md b/apps/clubs/README.md index ff43f73..98d7675 100644 --- a/apps/clubs/README.md +++ b/apps/clubs/README.md @@ -1,12 +1,11 @@ # clubs -Clubs Worker served at `clubs.rec.djdevin.net`. A Hono app ported from the C# -`ClubsController`. +Clubs Worker served on the `clubs` subdomain. A Hono app for clubs. ## Behavior -- `GET /club/home/me` — returns 404. The C# source returned `Results.NotFound()` - unconditionally; there's nothing to hydrate yet. +- `GET /club/home/me` — returns 404 unconditionally; there's nothing to hydrate + yet. ## TODO before production diff --git a/apps/clubs/src/clubs.app.ts b/apps/clubs/src/clubs.app.ts index a6c52c5..a18c249 100644 --- a/apps/clubs/src/clubs.app.ts +++ b/apps/clubs/src/clubs.app.ts @@ -9,8 +9,8 @@ import type { Context } from 'hono' import type { App } from './context' /** - * Ported from the C# `ClubsController`. The only endpoint is `[Authorize]` and - * then returns `Results.NotFound()` unconditionally — no DB binding involved. + * The only endpoint is auth-gated and returns 404 unconditionally — no DB + * binding involved. */ /** @@ -43,22 +43,22 @@ const app = new Hono() .onError(withOnError()) .notFound(withNotFound()) - // [Authorize] → 401 without a valid token. The C# returns NotFound here, but - // the client treats that 404 as an error, so we return an empty object stub. + // Auth-gated → 401 without a valid token. A bare 404 here makes the client + // treat it as an error, so we return an empty object stub. .get('/club/home/me', async (c) => { const id = await authedId(c) if (id === null) return c.body(null, 401) return c.json({}) }) - // Not present in CannedNet — a real Rec Room client endpoint the C# never - // implemented. The client calls it on the clubs host at /subscription/mine/member - // (no /club prefix) and sends no auth header, so it isn't gated. Returns an - // empty array = no club subscription memberships (the client chokes on null). + // A real Rec Room client endpoint with no backing implementation yet. The + // client calls it on the clubs host at /subscription/mine/member (no /club + // prefix) and sends no auth header, so it isn't gated. Returns an empty + // array = no club subscription memberships (the client chokes on null). .get('/subscription/mine/member', (c) => c.json([])) - // Details for a given subscription. Also not in CannedNet; the client - // deserializes this into an object, so it must return `{}` (not `[]`). + // Details for a given subscription. The client deserializes this into an + // object, so it must return `{}` (not `[]`). .get('/subscription/details/:subscription', (c) => c.json({})) // The player's clubs that have unread announcements (MyClubsWithUnread- diff --git a/apps/clubs/src/jwt.ts b/apps/clubs/src/jwt.ts index d3c5d2d..b105025 100644 --- a/apps/clubs/src/jwt.ts +++ b/apps/clubs/src/jwt.ts @@ -1,5 +1,5 @@ /** - * Minimal HS256 JWT validation, mirroring the C# `JwtTokenService.ValidateAndGetAccountId`. + * Minimal HS256 JWT validation. * * Uses the same placeholder dev secret as the `auth` worker (`apps/auth/src/jwt.ts`). * Swap both for a shared secret binding before this is used for anything real. diff --git a/apps/clubs/src/test/integration/api.test.ts b/apps/clubs/src/test/integration/api.test.ts index 1ea46c6..3e627fe 100644 --- a/apps/clubs/src/test/integration/api.test.ts +++ b/apps/clubs/src/test/integration/api.test.ts @@ -3,7 +3,7 @@ import { describe, expect, test } from 'vitest' import '../../clubs.app' -const ORIGIN = 'https://clubs.rec.djdevin.net' +const ORIGIN = 'https://example.com' // Mint a token the way the `auth` worker does, using the same dev secret. const DEV_SECRET = 'dev-insecure-signing-key-change-me' diff --git a/apps/clubs/wrangler.jsonc b/apps/clubs/wrangler.jsonc index 0dd1692..dadcad0 100644 --- a/apps/clubs/wrangler.jsonc +++ b/apps/clubs/wrangler.jsonc @@ -6,7 +6,7 @@ "compatibility_flags": ["nodejs_compat"], "routes": [ { - "pattern": "clubs.rec.djdevin.net", + "pattern": "clubs.rec.example.com", "custom_domain": true } ], diff --git a/apps/commerce/src/commerce.app.ts b/apps/commerce/src/commerce.app.ts index fbe9244..5515095 100644 --- a/apps/commerce/src/commerce.app.ts +++ b/apps/commerce/src/commerce.app.ts @@ -6,8 +6,8 @@ import { withNotFound, withOnError } from '@repo/hono-helpers' import type { App } from './context' /** - * Ported from the C# `CommerceController`. The class `[Route("commerce")]` prefix - * maps to this worker's subdomain, so method routes are served bare. + * Commerce routes. The `commerce` prefix maps to this worker's subdomain, so + * method routes are served bare. */ const app = new Hono() .use( @@ -25,8 +25,8 @@ const app = new Hono() .get('/', (c) => c.json({ service: 'commerce', status: 'ok' })) - // Whether the player has ever spent money. The C# returns NotFound(), but the - // client treats that 404 as an error, so we return `false` (no purchases). + // Whether the player has ever spent money. A 404 here makes the client treat + // it as an error, so we return `false` (no purchases). .get('/purchase/v1/hasspentmoney', (c) => c.json(false)) export default app diff --git a/apps/commerce/src/test/integration/api.test.ts b/apps/commerce/src/test/integration/api.test.ts index 07c8345..2be65ef 100644 --- a/apps/commerce/src/test/integration/api.test.ts +++ b/apps/commerce/src/test/integration/api.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest' import '../../commerce.app' -const ORIGIN = 'https://commerce.rec.djdevin.net' +const ORIGIN = 'https://example.com' describe('commerce endpoints', () => { it('GET / reports service status', async () => { diff --git a/apps/commerce/wrangler.jsonc b/apps/commerce/wrangler.jsonc index 3ce788a..a45a568 100644 --- a/apps/commerce/wrangler.jsonc +++ b/apps/commerce/wrangler.jsonc @@ -6,7 +6,7 @@ "compatibility_flags": ["nodejs_compat"], "routes": [ { - "pattern": "commerce.rec.djdevin.net", + "pattern": "commerce.rec.example.com", "custom_domain": true } ], diff --git a/apps/datacollection/src/test/integration/api.test.ts b/apps/datacollection/src/test/integration/api.test.ts index d45cacd..fa94041 100644 --- a/apps/datacollection/src/test/integration/api.test.ts +++ b/apps/datacollection/src/test/integration/api.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest' import '../../datacollection.app' -const ORIGIN = 'https://datacollection.rec.djdevin.net' +const ORIGIN = 'https://example.com' describe('datacollection endpoints', () => { it('GET / reports service status', async () => { diff --git a/apps/datacollection/wrangler.jsonc b/apps/datacollection/wrangler.jsonc index 9494410..8184b2f 100644 --- a/apps/datacollection/wrangler.jsonc +++ b/apps/datacollection/wrangler.jsonc @@ -6,7 +6,7 @@ "compatibility_flags": ["nodejs_compat"], "routes": [ { - "pattern": "datacollection.rec.djdevin.net", + "pattern": "datacollection.rec.example.com", "custom_domain": true } ], diff --git a/apps/econ/README.md b/apps/econ/README.md index 7b7c74e..afeed55 100644 --- a/apps/econ/README.md +++ b/apps/econ/README.md @@ -1,6 +1,6 @@ # econ -Economy Worker served at `econ.rec.djdevin.net`. Hosts the avatar/economy +Economy Worker served on the `econ` subdomain. Hosts the avatar/economy endpoints the game client calls on the `econ` service (distinct from the main `api` worker). DB-backed data is stubbed for now — no bindings yet. @@ -8,21 +8,21 @@ endpoints the game client calls on the `econ` service (distinct from the main - `GET /api/avatar/v1/defaultunlocked` — default-unlocked avatar items, served from the bundled `static/default-avatar-items.json` catalog. -- `GET /api/avatar/v1/defaultbaseavataritems` — default base avatar items. The C# - reads the same source file as `defaultunlocked`, so it returns the identical +- `GET /api/avatar/v1/defaultbaseavataritems` — default base avatar items. Reads + the same source file as `defaultunlocked`, so it returns the identical catalog. - `GET /api/avatar/v4/items` — `[Authorize]`. The player's avatar items: owned items concatenated with the default catalog. No DB binding yet, so owned is empty and this returns just the catalog. - `GET /api/avatar/v2` — `[Authorize]`. The player's avatar. No DB binding yet, so it returns the default `{ OutfitSelections, FaceFeatures, SkinColor, -HairColor }` the C# seeds for a new player. +HairColor }` seeded for a new player. - `GET /econ/customAvatarItems/v1/owned` — the player's owned custom avatar - items. No auth (matching the C#); returns `{ items: [] }` with no DB binding. + items. No auth; returns `{ items: [] }` with no DB binding. The client requests this when custom-item creation is allowed, so a missing route here shows up as "Failed to download unlocked avatar items". -- `GET /api/objectives/v1/myprogress` — objectives progress. No auth (matching - the C#, which serves a static JSON file verbatim); returns the bundled +- `GET /api/objectives/v1/myprogress` — objectives progress. No auth (serves a + static JSON file verbatim); returns the bundled `static/my-progress.json` default for all players until a DB binding exists. - `GET /api/avatar/v3/saved` — `[Authorize]`. Saved outfits; `[]` without a DB. - `GET /api/avatar/v2/gifts` — `[Authorize]`. Pending gifts; `[]` without a DB. @@ -33,15 +33,15 @@ HairColor }` the C# seeds for a new player. - `GET /api/storefronts/v3/giftdropstore/3` — gift-drop storefront, served from the bundled `static/storefronts-v3-giftdropstore-3.json`. - `GET /api/challenge/v2/getCurrent` — current weekly challenge, served from the - bundled `static/weekly-challenge.json` (the C#'s `JSON/weeklychallenge.json`). + bundled `static/weekly-challenge.json`. - `GET /api/gamerewards/v1/pending` — pending rewards; `[]`. - `GET /api/roomkeys/v1/mine` — the player's room keys; `[]`. - `POST /api/CampusCard/v1/UpdateAndGetSubscription` — subscription lookup; `{ subscription: null, platformAccountSubscribedPlayerId: null }`. -- Not in CannedNet (stubbed): `GET /api/roomconsumables/v1/roomConsumable/room/:id` +- Stubbed: `GET /api/roomconsumables/v1/roomConsumable/room/:id` and `GET /api/roomcurrencies/v1/currencies` both return `[]`. -These EconController routes are also served by the `api` worker; they're +These economy routes are also served by the `api` worker; they're duplicated here because the client calls them on the `econ` host. ## TODO before production diff --git a/apps/econ/src/econ.app.ts b/apps/econ/src/econ.app.ts index 43fb568..62c50ab 100644 --- a/apps/econ/src/econ.app.ts +++ b/apps/econ/src/econ.app.ts @@ -59,8 +59,8 @@ const app = new Hono() // Default-unlocked avatar items, served from the bundled static JSON. .get('/api/avatar/v1/defaultunlocked', (c) => c.json(defaultAvatarItems)) - // Default base avatar items. The C# reads the same JSON/defaultAvatarItems.json - // file as defaultunlocked, so it returns the identical catalog. + // Default base avatar items. Reads the same source file as defaultunlocked, + // so it returns the identical catalog. .get('/api/avatar/v1/defaultbaseavataritems', (c) => c.json(defaultAvatarItems)) // The player's avatar items — owned items concatenated with the default @@ -72,18 +72,18 @@ const app = new Hono() return c.json(defaultAvatarItems) }) - // The player's owned custom avatar items. No auth in the C#, which returns - // `{ items: [] }`. The client downloads these when custom-item creation is + // The player's owned custom avatar items. No auth; returns `{ items: [] }`. + // The client downloads these when custom-item creation is // allowed; a 404 here surfaces as "Failed to download unlocked avatar items". .get('/econ/customAvatarItems/v1/owned', (c) => c.json({ items: [] })) - // The player's objectives progress. The C# serves a static JSON file - // (JSON/tempmyprogress.json) verbatim with no auth — same default for everyone - // until there's a DB binding to track per-player progress. + // The player's objectives progress. Serves a static JSON file verbatim with + // no auth — same default for everyone until there's a DB binding to track + // per-player progress. .get('/api/objectives/v1/myprogress', (c) => c.json(myProgress)) // The player's avatar. No DB binding yet, so it always returns the default - // the C# seeds for a player with no PlayerAvatar row. + // for a player with no PlayerAvatar row. .get('/api/avatar/v2', async (c) => { const id = await authedId(c) if (id === null) return unauthorized(c) @@ -117,18 +117,18 @@ const app = new Hono() return c.json([]) }) - // Unlocked equipment. The C# returns "[]" with no auth. + // Unlocked equipment. Returns "[]" with no auth. .get('/api/equipment/v2/getUnlocked', (c) => c.json([])) - // Not in CannedNet — room consumables/currencies for a given room. Stubbed - // as empty lists so the client doesn't 404. + // Room consumables/currencies for a given room. Stubbed as empty lists so the + // client doesn't 404. .get('/api/roomconsumables/v1/roomConsumable/room/:roomId', (c) => c.json([])) .get('/api/roomconsumables/v1/roomConsumable/room/:roomId/me', (c) => c.json([])) .get('/api/roomcurrencies/v1/currencies', (c) => c.json([])) .get('/api/roomcurrencies/v1/getAllBalances', (c) => c.json([])) - // Persist player settings. [Authorize]; the C# replaces the player's settings - // and returns Ok(). No DB binding yet, so accept-and-ack. + // Persist player settings. [Authorize]; would replace the player's settings. + // No DB binding yet, so accept-and-ack. .post('/api/settings/v2/set', async (c) => { const id = await authedId(c) if (id === null) return unauthorized(c) @@ -152,23 +152,23 @@ const app = new Hono() return c.json([]) }) - // Gift-drop storefront. The C# falls back to JSON/storefront3.json when no - // storefront row exists; that's the bundled static catalog here. + // Gift-drop storefront. Falls back to the bundled static catalog when no + // storefront row exists. .get('/api/storefronts/v3/giftdropstore/3', (c) => c.json(storefrontGiftDrop3)) - // Current weekly challenge. Served from the bundled static JSON (the C#'s - // JSON/weeklychallenge.json) until per-rotation challenge data is wired up. + // Current weekly challenge. Served from the bundled static JSON until + // per-rotation challenge data is wired up. .get('/api/challenge/v2/getCurrent', (c) => c.json(weeklyChallenge)) - // Pending game rewards. The C# returns "[]". + // Pending game rewards. Returns "[]". .get('/api/gamerewards/v1/pending', (c) => c.json([])) - // The player's room keys. The C# returns "[]". + // The player's room keys. Returns "[]". .get('/api/roomkeys/v1/mine', (c) => c.json([])) // Room keys for a given room (client calls this on the econ host). [] with no DB. .get('/api/roomkeys/v1/room', (c) => c.json([])) - // Subscription lookup. The C# returns both fields null with no auth. + // Subscription lookup. Returns both fields null with no auth. .post('/api/CampusCard/v1/UpdateAndGetSubscription', (c) => c.json({ subscription: null, platformAccountSubscribedPlayerId: null }) ) diff --git a/apps/econ/src/jwt.ts b/apps/econ/src/jwt.ts index d3c5d2d..b105025 100644 --- a/apps/econ/src/jwt.ts +++ b/apps/econ/src/jwt.ts @@ -1,5 +1,5 @@ /** - * Minimal HS256 JWT validation, mirroring the C# `JwtTokenService.ValidateAndGetAccountId`. + * Minimal HS256 JWT validation. * * Uses the same placeholder dev secret as the `auth` worker (`apps/auth/src/jwt.ts`). * Swap both for a shared secret binding before this is used for anything real. diff --git a/apps/econ/src/test/integration/api.test.ts b/apps/econ/src/test/integration/api.test.ts index 9436e91..0a11d89 100644 --- a/apps/econ/src/test/integration/api.test.ts +++ b/apps/econ/src/test/integration/api.test.ts @@ -3,7 +3,7 @@ import { describe, expect, test } from 'vitest' import '../../econ.app' -const ORIGIN = 'https://econ.rec.djdevin.net' +const ORIGIN = 'https://example.com' // Mint a token the way the `auth` worker does, using the same dev secret. const DEV_SECRET = 'dev-insecure-signing-key-change-me' diff --git a/apps/econ/wrangler.jsonc b/apps/econ/wrangler.jsonc index 6b918ef..3022c7a 100644 --- a/apps/econ/wrangler.jsonc +++ b/apps/econ/wrangler.jsonc @@ -6,7 +6,7 @@ "compatibility_flags": ["nodejs_compat"], "routes": [ { - "pattern": "econ.rec.djdevin.net", + "pattern": "econ.rec.example.com", "custom_domain": true } ], diff --git a/apps/img/README.md b/apps/img/README.md index ab4256f..ce9bc61 100644 --- a/apps/img/README.md +++ b/apps/img/README.md @@ -1,6 +1,6 @@ # img -Image-delivery worker served at `img.rec.djdevin.net`. +Image-delivery worker served on the `img` subdomain. Images are stored as objects in the **`rec-img` R2 bucket** and streamed back by key: @@ -12,8 +12,8 @@ key: conditional requests via `If-None-Match` (returns `304`). Missing keys `404`. - `GET /?sig=p1` — same, but the response body is RSA-SHA1 signed and the signature returned in a `Content-Signature: key-id=KEY:RSA:p1.rec.net; data=` - header (mirrors the C# `ImageController` / `Signatures`). The client uses this - to verify image integrity. Signing buffers the whole object. + header. The client uses this to verify image integrity. Signing buffers the + whole object. The bucket is bound in the Worker as `env.IMAGES` (see `wrangler.jsonc`). diff --git a/apps/img/src/img.app.ts b/apps/img/src/img.app.ts index 3375332..8f600a3 100644 --- a/apps/img/src/img.app.ts +++ b/apps/img/src/img.app.ts @@ -29,7 +29,7 @@ function getSigningKey(env: Env): Promise { return signingKey } -/** RSA-SHA1 sign the bytes, base64-encoded — matches the C# `Signatures.Sign`. */ +/** RSA-SHA1 sign the bytes, base64-encoded. */ async function signImage(env: Env, bytes: ArrayBuffer): Promise { const key = await getSigningKey(env) if (!key) return null @@ -60,8 +60,8 @@ const app = new Hono() // objects. Supports conditional requests via If-None-Match. // // When the client appends `?sig=p1`, the response body is RSA-SHA1 signed and - // the signature returned in a `Content-Signature` header (mirrors the C# - // ImageController). Signing requires the full body, so the object is buffered. + // the signature returned in a `Content-Signature` header. Signing requires the + // full body, so the object is buffered. .get('/:key{.+}', async (c) => { const key = c.req.param('key') if (key.includes('..')) return c.body(null, 400) diff --git a/apps/img/src/test/integration/api.test.ts b/apps/img/src/test/integration/api.test.ts index 435f548..affae3e 100644 --- a/apps/img/src/test/integration/api.test.ts +++ b/apps/img/src/test/integration/api.test.ts @@ -9,7 +9,7 @@ declare module 'cloudflare:test' { interface ProvidedEnv extends Env {} } -const ORIGIN = 'https://img.rec.djdevin.net' +const ORIGIN = 'https://example.com' // A tiny valid JPEG magic-number blob — enough to assert round-tripping. const IMAGE_BYTES = new Uint8Array([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46]) diff --git a/apps/img/wrangler.jsonc b/apps/img/wrangler.jsonc index 12b4fb2..308b828 100644 --- a/apps/img/wrangler.jsonc +++ b/apps/img/wrangler.jsonc @@ -6,7 +6,7 @@ "compatibility_flags": ["nodejs_compat"], "routes": [ { - "pattern": "img.rec.djdevin.net", + "pattern": "img.rec.example.com", "custom_domain": true } ], diff --git a/apps/match/README.md b/apps/match/README.md index 99a38e9..c27ee29 100644 --- a/apps/match/README.md +++ b/apps/match/README.md @@ -1,8 +1,7 @@ # match -Matchmaking Worker served at `match.rec.djdevin.net`. A Hono app ported from the -C# `MatchmakingController`. EF Core (`AppDbContext`) queries are stubbed for now -— no real bindings yet. +Matchmaking Worker served on the `match` subdomain. A Hono app for matchmaking. +Database queries are stubbed for now — no real bindings yet. ## Behavior @@ -10,7 +9,7 @@ C# `MatchmakingController`. EF Core (`AppDbContext`) queries are stubbed for now Bearer JWT issued by the `auth` worker (same dev secret, see `src/jwt.ts`) and 401 when it's missing/invalid. - **`GET /player`** always returns the inlined `JSON/getplayer.json` default - (the C# fell back to that file when the account/room instance wasn't found). + (falls back to that file when the account/room instance isn't found). - **`POST /goto/none`** returns the static offline-dorm instance with a fresh `photonRoomId`. - **`POST /goto/room/:room`** synthesizes the room-instance response (no Rooms @@ -19,7 +18,7 @@ C# `MatchmakingController`. EF Core (`AppDbContext`) queries are stubbed for now - **`POST /player/heartbeat`** echoes the posted heartbeat fields; `roomInstance` is always null and `isOnline` false until there's a DB binding. - **`/player/login`, `/player/statusvisibility`, `/roominstance/:id/reportjoinresult`** - return empty 200s, as in the source. + return empty 200s. ## TODO before production diff --git a/apps/match/src/jwt.ts b/apps/match/src/jwt.ts index d3c5d2d..b105025 100644 --- a/apps/match/src/jwt.ts +++ b/apps/match/src/jwt.ts @@ -1,5 +1,5 @@ /** - * Minimal HS256 JWT validation, mirroring the C# `JwtTokenService.ValidateAndGetAccountId`. + * Minimal HS256 JWT validation. * * Uses the same placeholder dev secret as the `auth` worker (`apps/auth/src/jwt.ts`). * Swap both for a shared secret binding before this is used for anything real. diff --git a/apps/match/src/match.app.ts b/apps/match/src/match.app.ts index 7bd9e51..8f9a0f7 100644 --- a/apps/match/src/match.app.ts +++ b/apps/match/src/match.app.ts @@ -11,17 +11,16 @@ import type { App } from './context' import type { Room } from './rooms-db' /** - * Ported from the C# `MatchmakingController`. Endpoints the C# backs with EF Core - * (`AppDbContext`) are stubbed here — there's no DB binding yet, so room/player - * lookups fall back to the same defaults the C# uses when nothing is found. + * The matchmaking surface. Database-backed endpoints are stubbed here — there's + * no DB binding yet, so room/player lookups fall back to default values when + * nothing is found. * * Auth-gated routes still validate the Bearer JWT issued by the `auth` worker. */ /** - * Default `/player` payload. The C# serves this from `JSON/getplayer.json` - * whenever the `id` is missing/invalid or the account isn't found; Workers have - * no filesystem so it's inlined here. + * Default `/player` payload, served whenever the `id` is missing/invalid or the + * account isn't found. Inlined here (Workers have no filesystem). */ const DEFAULT_GET_PLAYER = [ { @@ -48,7 +47,7 @@ interface HeartbeatRequest { /** * Resolve the account id from a Bearer token, mirroring the repeated - * auth-header check in the C#. Returns `null` when the header is missing, + * auth-header check. Returns `null` when the header is missing, * the token is invalid, or the `sub` claim isn't an integer. */ async function authedId(c: Context): Promise { @@ -89,10 +88,9 @@ interface Presence { const PRESENCE_TTL = 900 /** - * Game build version reported in presence. Like the reference servers (FemRec - * `ServerConfig.GameVersion`, 2025 `HeartbeatDB`), this is a server-side - * constant — the client doesn't supply it, and an empty value breaks the - * client's presence/version handling. Matches our target 2023 client build. + * Game build version reported in presence. This is a server-side constant — the + * client doesn't supply it, and an empty value breaks the client's + * presence/version handling. Matches our target 2023 client build. */ const GAME_VERSION = '20230302' @@ -256,8 +254,8 @@ const app = new Hono() .post('/player/logout', (c) => c.body(null, 200)) .get('/player', async (c) => { - // Returns each requested player's presence. The C# reads the `id` query - // param(s); with none it serves the static getplayer.json default. + // Returns each requested player's presence. Reads the `id` query param(s); + // with none it serves the static getplayer.json default. const ids = c.req .queries('id') ?.flatMap((v) => v.split(',')) @@ -363,8 +361,7 @@ const app = new Hono() // isn't swallowed by the auth-gated matchmake handler. .post('/matchmake/none', async (c) => { const id = await authedId(c) - // FemRec (our 2023-client target) returns the player's *current* heartbeat - // here rather than forcing the dorm (the 2025 server's behavior we'd copied). + // Return the player's *current* heartbeat here rather than forcing the dorm. // Orientation is a solo room the client establishes via matchmake/none; if we // force the dorm, the new player is warped out of Orientation within seconds. // So: preserve existing presence; only fall back to the offline dorm when the @@ -396,7 +393,7 @@ const app = new Hono() const room = c.req.param('room') const joinMode = await readJoinMode(c) - // The C# dorm check here is "dorm" (goto/room uses "dormroom"). + // The dorm check here is "dorm" (goto/room uses "dormroom"). const instance = room.toLowerCase() === 'dorm' ? dormRoomInstance() diff --git a/apps/match/src/test/integration/api.test.ts b/apps/match/src/test/integration/api.test.ts index 7cb49d0..11b010d 100644 --- a/apps/match/src/test/integration/api.test.ts +++ b/apps/match/src/test/integration/api.test.ts @@ -10,7 +10,7 @@ declare module 'cloudflare:test' { interface ProvidedEnv extends Env {} } -const ORIGIN = 'https://match.rec.djdevin.net' +const ORIGIN = 'https://example.com' // Matchmaking into a room resolves its real scene from the shared rec-rooms D1. // Seed the schema + a couple of rooms (matching the rooms worker's migration). diff --git a/apps/match/wrangler.jsonc b/apps/match/wrangler.jsonc index e206681..f3d454d 100644 --- a/apps/match/wrangler.jsonc +++ b/apps/match/wrangler.jsonc @@ -6,7 +6,7 @@ "compatibility_flags": ["nodejs_compat"], "routes": [ { - "pattern": "match.rec.djdevin.net", + "pattern": "match.rec.example.com", "custom_domain": true } ], diff --git a/apps/notify/README.md b/apps/notify/README.md index afc4fa4..6f10e87 100644 --- a/apps/notify/README.md +++ b/apps/notify/README.md @@ -1,13 +1,11 @@ # notify -Notifications Worker served at `notify.rec.djdevin.net`. Ported from the C# -`NotifyController` / `NotificationsHub` / `NotificationService`, which host a -SignalR hub at `/hub/v1`. +Notifications Worker served on the `notify` subdomain. Hosts a SignalR hub at +`/hub/v1`. The hub is implemented as a **Durable Object** (`NotificationsHub`) speaking the SignalR JSON Hub Protocol over a hibernatable WebSocket. A single global DO -instance plays the role of the C# static dictionaries (one shared process across -all connections). +instance holds the shared hub state (one process across all connections). ## Endpoints @@ -27,8 +25,7 @@ all connections). 1. Client `POST /hub/v1/negotiate`, then opens a WebSocket to `/hub/v1?id=`. 2. Handshake: client sends `{"protocol":"json","version":1}␞`, server replies `{}␞` (`␞` = record separator `0x1e`). -3. Server immediately sends the `OnConnect` invocation (mirrors the C# - `OnConnectedAsync`). +3. Server immediately sends the `OnConnect` invocation on connect. 4. Client→server invocations: - `SubscribeToPlayers({ playerIds })` — replaces this connection's subscriptions and flushes any queued notifications for those players. @@ -42,11 +39,11 @@ all connections). Held in the DO's SQLite so it survives hibernation: - `subscriptions(connectionId, playerId)` — serves both the connection→players - and player→connections lookups from the C#. + and player→connections lookups. - `pending(id, playerId, payload)` — per-player queue delivered once the player subscribes. -A connection's rows are removed on `webSocketClose` (the C# `OnDisconnected`). +A connection's rows are removed on `webSocketClose`. ## TODO before production diff --git a/apps/notify/src/notifications-hub.ts b/apps/notify/src/notifications-hub.ts index 26beb9c..531b11f 100644 --- a/apps/notify/src/notifications-hub.ts +++ b/apps/notify/src/notifications-hub.ts @@ -3,9 +3,8 @@ import { DurableObject } from 'cloudflare:workers' import type { Env } from './context' /** - * Durable Object hosting the SignalR notifications hub, ported from the C# - * `NotificationsHub` + `NotificationService`. A single global instance plays the - * role of the C# static dictionaries (one process, shared across connections). + * Durable Object hosting the SignalR notifications hub. A single global instance + * holds the shared hub state (one process, shared across connections). * * It speaks the SignalR JSON Hub Protocol over a hibernatable WebSocket: * 1. negotiate happens in the worker; the client then opens a WS to `/hub/v1`. @@ -15,7 +14,7 @@ import type { Env } from './context' * * Connection/subscription state lives in SQLite so it survives hibernation: * - `subscriptions(connectionId, playerId)` — both the connection→players and - * (queried the other way) the player→connections maps from the C#. + * (queried the other way) the player→connections maps. * - `pending(id, playerId, payload)` — the per-player queue delivered once a * player is subscribed. */ @@ -136,7 +135,7 @@ export class NotificationsHub extends DurableObject { state.handshakeDone = true ws.serializeAttachment(state) - // C# OnConnectedAsync sends "OnConnect" to the caller after connecting. + // Send "OnConnect" to the caller after connecting. ws.send(this.invocation('OnConnect', [])) } @@ -275,8 +274,8 @@ export class NotificationsHub extends DurableObject { // ---- Helpers ------------------------------------------------------------- /** - * Build the `Notification` argument: a JSON string `{ Id, Msg }`, matching the - * C# `SendToConnection` (null values are dropped from `Msg`). + * Build the `Notification` argument: a JSON string `{ Id, Msg }` + * (null values are dropped from `Msg`). */ private buildNotificationPayload( notificationType: number, diff --git a/apps/notify/src/notify.app.ts b/apps/notify/src/notify.app.ts index 8ed4ddc..9a1e3bb 100644 --- a/apps/notify/src/notify.app.ts +++ b/apps/notify/src/notify.app.ts @@ -8,15 +8,14 @@ import { NotificationsHub } from './notifications-hub' import type { App } from './context' /** - * Ported from the C# `NotifyController`, which maps a SignalR hub at `/hub/v1` - * (see `NotificationsHub` / `NotificationService`). The hub itself — WebSocket - * transport, the SignalR JSON Hub Protocol, and the shared connection state — + * Maps a SignalR hub at `/hub/v1`. The hub itself — WebSocket transport, the + * SignalR JSON Hub Protocol, and the shared connection state — * lives in the `NotificationsHub` Durable Object; this worker handles the * SignalR negotiate handshake, forwards the WebSocket upgrade to the DO, and * exposes internal send/broadcast endpoints for other workers. */ -/** The hub state is global in the C# (static dictionaries) → one DO instance. */ +/** The hub state is global → one DO instance. */ const HUB_INSTANCE = 'global' const app = new Hono() @@ -57,8 +56,8 @@ const app = new Hono() }) // ---- Internal service-to-service send/broadcast -------------------------- - // Lets other workers push notifications, the way the C# controllers called - // the shared NotificationService. TODO: protect these before production. + // Lets other workers push notifications through the shared hub. + // TODO: protect these before production. .post('/internal/notify', async (c) => { const body = await c.req .json<{ playerId?: number; notificationType?: number; data?: Record }>() diff --git a/apps/notify/src/test/integration/api.test.ts b/apps/notify/src/test/integration/api.test.ts index 6f90eca..14ba66f 100644 --- a/apps/notify/src/test/integration/api.test.ts +++ b/apps/notify/src/test/integration/api.test.ts @@ -3,7 +3,7 @@ import { describe, expect, test } from 'vitest' import '../../notify.app' -const ORIGIN = 'https://notify.rec.djdevin.net' +const ORIGIN = 'https://example.com' const RS = '\u001e' interface HubRecord { @@ -58,7 +58,7 @@ async function connect( }) } - // Handshake, then the C# OnConnect callback. + // Handshake, then the OnConnect callback. ws.send(`{"protocol":"json","version":1}${RS}`) await waitFor((r) => r.type === 1 && r.target === 'OnConnect') diff --git a/apps/notify/wrangler.jsonc b/apps/notify/wrangler.jsonc index fc5677c..ba9b3b0 100644 --- a/apps/notify/wrangler.jsonc +++ b/apps/notify/wrangler.jsonc @@ -6,7 +6,7 @@ "compatibility_flags": ["nodejs_compat"], "routes": [ { - "pattern": "notify.rec.djdevin.net", + "pattern": "notify.rec.example.com", "custom_domain": true } ], diff --git a/apps/ns/README.md b/apps/ns/README.md index 4805fef..b4a6855 100644 --- a/apps/ns/README.md +++ b/apps/ns/README.md @@ -1,20 +1,22 @@ # ns -Name-server / service-discovery worker served at `ns.rec.djdevin.net`. +Name-server / service-discovery worker served on the `ns` subdomain. `GET /` returns the endpoints document the game client fetches on startup to discover every service host (Accounts, API, Auth, Econ, Matchmaking, Notifications, …). -The document is served from `static/endpoints.json` (a snapshot downloaded from -the live host). **It will be generated dynamically eventually** — e.g. per -environment / from the deployed routes — at which point the static file goes -away. +The document is served from `static/endpoints.json`, whose hosts are generated +from the repo-root `env.json` by `runx sync` — every entry is derived from the +configured base `domain`. -## Updating the snapshot +## Updating endpoints + +Change `domain` in `env.json` (or edit the host map in `static/endpoints.json`), +then regenerate: ```sh -curl -sS https://rec.djdevin.net/ -o apps/ns/static/endpoints.json +just sync ``` (Bundled at build time, so a redeploy is required for changes to take effect.) diff --git a/apps/ns/src/ns.app.ts b/apps/ns/src/ns.app.ts index 622f72b..42c43ad 100644 --- a/apps/ns/src/ns.app.ts +++ b/apps/ns/src/ns.app.ts @@ -8,9 +8,10 @@ import endpoints from '../static/endpoints.json' import type { App } from './context' /** - * Name-server / service-discovery worker served at the apex `rec.djdevin.net`. + * Name-server / service-discovery worker served at the apex domain. * Returns the endpoints document the game client fetches to discover every - * service host. Static for now — this will be generated dynamically later. + * service host. Generated from `env.json` by `runx sync` — run it after + * changing the domain (see `apps/ns/static/endpoints.json`). */ const app = new Hono() .use( diff --git a/apps/ns/src/test/integration/api.test.ts b/apps/ns/src/test/integration/api.test.ts index 1e2406d..fdd0584 100644 --- a/apps/ns/src/test/integration/api.test.ts +++ b/apps/ns/src/test/integration/api.test.ts @@ -2,17 +2,16 @@ import { exports } from 'cloudflare:workers' import { describe, expect, test } from 'vitest' import '../../ns.app' +import endpoints from '../../../static/endpoints.json' -const ORIGIN = 'https://ns.rec.djdevin.net' +const ORIGIN = 'https://example.com' describe('ns endpoints', () => { test('GET / returns the endpoints document', async () => { const res = await exports.default.fetch(`${ORIGIN}/`) expect(res.status).toBe(200) - const body = (await res.json()) as Record - expect(body.API).toBe('https://api.rec.djdevin.net') - expect(body.Notifications).toBe('https://notify.rec.djdevin.net') - expect(body.Econ).toBe('https://econ.rec.djdevin.net') + const body = await res.json() + expect(body).toEqual(endpoints) }) test('unknown path returns 404', async () => { diff --git a/apps/ns/static/endpoints.json b/apps/ns/static/endpoints.json index d955111..da68746 100644 --- a/apps/ns/static/endpoints.json +++ b/apps/ns/static/endpoints.json @@ -1,38 +1,38 @@ { - "Accounts": "https://accounts.rec.djdevin.net", - "AI": "https://ai.rec.djdevin.net", - "API": "https://api.rec.djdevin.net", - "Auth": "https://auth.rec.djdevin.net", - "BugReporting": "https://bugreporting.rec.djdevin.net", - "Cards": "https://cards.rec.djdevin.net", - "CDN": "https://cdn.rec.djdevin.net", - "Chat": "https://chat.rec.djdevin.net", - "Clubs": "https://clubs.rec.djdevin.net", - "CMS": "https://cms.rec.djdevin.net", - "Commerce": "https://commerce.rec.djdevin.net", - "Data": "https://data.rec.djdevin.net", - "DataCollection": "https://datacollection.rec.djdevin.net", - "Discovery": "https://discovery.rec.djdevin.net", - "Econ": "https://econ.rec.djdevin.net", - "GameLogs": "https://gamelogs.rec.djdevin.net", - "Geo": "https://geo.rec.djdevin.net", - "Images": "https://img.rec.djdevin.net", - "Leaderboard": "https://leaderboard.rec.djdevin.net", - "Link": "https://link.rec.djdevin.net", - "Lists": "https://lists.rec.djdevin.net", - "Matchmaking": "https://match.rec.djdevin.net", - "Moderation": "https://api.rec.djdevin.net", - "Notifications": "https://notify.rec.djdevin.net", - "PlatformNotifications": "https://platformnotifications.rec.djdevin.net", - "PlayerSettings": "https://playersettings.rec.djdevin.net", - "RoomComments": "https://roomcomments.rec.djdevin.net", - "RoomieIntegrations": "https://roomieintegrations.rec.djdevin.net", - "Rooms": "https://rooms.rec.djdevin.net", - "Storage": "https://storage.rec.djdevin.net", - "Strings": "https://strings.rec.djdevin.net", - "StringsCDN": "https://strings-cdn.rec.djdevin.net", - "Studio": "https://studio.rec.djdevin.net", - "Thorn": "https://thorn.rec.djdevin.net", - "Videos": "https://videos.rec.djdevin.net", - "WWW": "https://www.rec.djdevin.net" + "Accounts": "https://accounts.rec.example.com", + "AI": "https://ai.rec.example.com", + "API": "https://api.rec.example.com", + "Auth": "https://auth.rec.example.com", + "BugReporting": "https://bugreporting.rec.example.com", + "Cards": "https://cards.rec.example.com", + "CDN": "https://cdn.rec.example.com", + "Chat": "https://chat.rec.example.com", + "Clubs": "https://clubs.rec.example.com", + "CMS": "https://cms.rec.example.com", + "Commerce": "https://commerce.rec.example.com", + "Data": "https://data.rec.example.com", + "DataCollection": "https://datacollection.rec.example.com", + "Discovery": "https://discovery.rec.example.com", + "Econ": "https://econ.rec.example.com", + "GameLogs": "https://gamelogs.rec.example.com", + "Geo": "https://geo.rec.example.com", + "Images": "https://img.rec.example.com", + "Leaderboard": "https://leaderboard.rec.example.com", + "Link": "https://link.rec.example.com", + "Lists": "https://lists.rec.example.com", + "Matchmaking": "https://match.rec.example.com", + "Moderation": "https://api.rec.example.com", + "Notifications": "https://notify.rec.example.com", + "PlatformNotifications": "https://platformnotifications.rec.example.com", + "PlayerSettings": "https://playersettings.rec.example.com", + "RoomComments": "https://roomcomments.rec.example.com", + "RoomieIntegrations": "https://roomieintegrations.rec.example.com", + "Rooms": "https://rooms.rec.example.com", + "Storage": "https://storage.rec.example.com", + "Strings": "https://strings.rec.example.com", + "StringsCDN": "https://strings-cdn.rec.example.com", + "Studio": "https://studio.rec.example.com", + "Thorn": "https://thorn.rec.example.com", + "Videos": "https://videos.rec.example.com", + "WWW": "https://www.rec.example.com" } diff --git a/apps/ns/wrangler.jsonc b/apps/ns/wrangler.jsonc index f0b0d7c..2c8e612 100644 --- a/apps/ns/wrangler.jsonc +++ b/apps/ns/wrangler.jsonc @@ -6,7 +6,7 @@ "compatibility_flags": ["nodejs_compat"], "routes": [ { - "pattern": "ns.rec.djdevin.net", + "pattern": "ns.rec.example.com", "custom_domain": true } ], diff --git a/apps/playersettings/README.md b/apps/playersettings/README.md index 6d4a015..e363ac4 100644 --- a/apps/playersettings/README.md +++ b/apps/playersettings/README.md @@ -1,17 +1,17 @@ # playersettings -Player-settings worker served at `playersettings.rec.djdevin.net`. +Player-settings worker served on the `playersettings` subdomain. - `GET /` — service status `{ "service": "playersettings", "status": "ok" }`. - `GET /playersettings` — `[Authorize]`. The player's settings as `{ PlayerId, Key, Value }`, read from the per-player KV map. On a player's - first read it seeds (and persists) the C# default settings. + first read it seeds (and persists) the default settings. - `PUT /playersettings` — `[Authorize]`. Accepts a form-urlencoded `key=…&value=…` (or a JSON `{key,value}` / array) and **upserts** it into the player's settings, keyed by the `sub` claim of the Bearer JWT. Returns `200`. Persisted in Workers KV (`PLAYER_SETTINGS`, key `player:`). -> The C# `PutPlayerSettings` replaces the player's _entire_ settings set on each +> A full settings PUT would replace the player's _entire_ settings set on each > call; we merge instead, so a single-key PUT (e.g. `key=PlayerSessionCount`) > doesn't wipe the others. diff --git a/apps/playersettings/src/jwt.ts b/apps/playersettings/src/jwt.ts index d3c5d2d..b105025 100644 --- a/apps/playersettings/src/jwt.ts +++ b/apps/playersettings/src/jwt.ts @@ -1,5 +1,5 @@ /** - * Minimal HS256 JWT validation, mirroring the C# `JwtTokenService.ValidateAndGetAccountId`. + * Minimal HS256 JWT validation. * * Uses the same placeholder dev secret as the `auth` worker (`apps/auth/src/jwt.ts`). * Swap both for a shared secret binding before this is used for anything real. diff --git a/apps/playersettings/src/playersettings.app.ts b/apps/playersettings/src/playersettings.app.ts index 3f0134e..676c22a 100644 --- a/apps/playersettings/src/playersettings.app.ts +++ b/apps/playersettings/src/playersettings.app.ts @@ -10,7 +10,7 @@ import type { Context } from 'hono' import type { App } from './context' /** - * Resolve the account id from a Bearer token (the C# action is `[Authorize]`). + * Resolve the account id from a Bearer token (the route is auth-gated). * Returns `null` when the header is missing, the token is invalid, or the `sub` * claim isn't an integer. */ @@ -32,9 +32,8 @@ function unauthorized(c: Context) { } /** - * Pull `{ key, value }` pairs out of a PUT body. Mirrors the C#: a - * form-urlencoded `key`/`value`, or a JSON body (single object or array). - * Entries with an empty key are dropped. + * Pull `{ key, value }` pairs out of a PUT body: a form-urlencoded `key`/`value`, + * or a JSON body (single object or array). Entries with an empty key are dropped. */ async function parseSettings(c: Context): Promise> { const contentType = c.req.header('content-type') ?? '' @@ -84,7 +83,7 @@ const app = new Hono() .get('/', (c) => c.json({ service: 'playersettings', status: 'ok' })) // The authenticated player's settings as `{ PlayerId, Key, Value }`. Reads - // the per-player KV map; seeds (and persists) the C# defaults on first read. + // the per-player KV map; seeds (and persists) the defaults on first read. .get('/playersettings', async (c) => { const id = await authedId(c) if (id === null) return unauthorized(c) @@ -100,7 +99,7 @@ const app = new Hono() }) // Upsert player settings into KV, keyed by the authenticated player id. - // The C# replaces the player's entire set; we merge so individual key PUTs + // A full replace would overwrite the player's entire set; we merge so individual key PUTs // (e.g. `key=PlayerSessionCount&value=1`) don't wipe the rest. .put('/playersettings', async (c) => { const id = await authedId(c) diff --git a/apps/playersettings/src/test/integration/api.test.ts b/apps/playersettings/src/test/integration/api.test.ts index b9d36e2..880a641 100644 --- a/apps/playersettings/src/test/integration/api.test.ts +++ b/apps/playersettings/src/test/integration/api.test.ts @@ -9,7 +9,7 @@ declare module 'cloudflare:test' { interface ProvidedEnv extends Env {} } -const ORIGIN = 'https://playersettings.rec.djdevin.net' +const ORIGIN = 'https://example.com' // Mint a token the way the `auth` worker does, using the same dev secret. const DEV_SECRET = 'dev-insecure-signing-key-change-me' diff --git a/apps/playersettings/wrangler.jsonc b/apps/playersettings/wrangler.jsonc index 7916f43..51cf3ea 100644 --- a/apps/playersettings/wrangler.jsonc +++ b/apps/playersettings/wrangler.jsonc @@ -6,7 +6,7 @@ "compatibility_flags": ["nodejs_compat"], "routes": [ { - "pattern": "playersettings.rec.djdevin.net", + "pattern": "playersettings.rec.example.com", "custom_domain": true } ], diff --git a/apps/rooms/src/jwt.ts b/apps/rooms/src/jwt.ts index d3c5d2d..b105025 100644 --- a/apps/rooms/src/jwt.ts +++ b/apps/rooms/src/jwt.ts @@ -1,5 +1,5 @@ /** - * Minimal HS256 JWT validation, mirroring the C# `JwtTokenService.ValidateAndGetAccountId`. + * Minimal HS256 JWT validation. * * Uses the same placeholder dev secret as the `auth` worker (`apps/auth/src/jwt.ts`). * Swap both for a shared secret binding before this is used for anything real. diff --git a/apps/rooms/src/rooms.app.ts b/apps/rooms/src/rooms.app.ts index 2ff7017..5ea3399 100644 --- a/apps/rooms/src/rooms.app.ts +++ b/apps/rooms/src/rooms.app.ts @@ -23,7 +23,7 @@ import type { App } from './context' * querying (see rooms-db.ts); the dorm (RoomId 1) is seeded by the migration. * Responses are the stored JSON verbatim (PascalCase, client-facing shape). * - * The C# `[Route("rooms")]` prefix maps to this worker's subdomain, so method + * The `rooms` prefix maps to this worker's subdomain, so method * routes are served bare. The 2023 client also hits several of these without the * `/roomserver` prefix, so both forms are registered. */ @@ -100,8 +100,8 @@ const app = new Hono() .get('/', (c) => c.json({ service: 'rooms', status: 'ok' })) - // Room lookup by `id` (first match wins) or `name`. The C# 400s when neither - // is supplied and returns `{}` when nothing matches. + // Room lookup by `id` (first match wins) or `name`. 400s when neither is + // supplied and returns `{}` when nothing matches. .get('/rooms', async (c) => { const idParam = c.req.query('id') const nameParam = c.req.query('name') @@ -162,7 +162,7 @@ const app = new Hono() }) // Toggle the player's cheer/favorite on a room. Both are PUTs that flip the - // stored flag and return the updated interaction (matches the C#). + // stored flag and return the updated interaction. .put('/rooms/:roomId{[0-9]+}/interactionby/me/cheer', async (c) => { const interaction = await toggleCheer( c.env.DB, @@ -180,8 +180,8 @@ const app = new Hono() return c.json({ ...interaction, LastVisitedAt: new Date().toISOString() }) }) - // Single room by id. 404 when the room isn't in D1 (matches the C#). Ignores - // the include/unityAsset* query params, same as the C#. + // Single room by id. 404 when the room isn't in D1. Ignores the + // include/unityAsset* query params. .get('/rooms/:roomId{[0-9]+}', async (c) => { const room = await getRoomById(c.env.DB, Number.parseInt(c.req.param('roomId'), 10)) return room ? c.json(room) : c.notFound() diff --git a/apps/rooms/src/test/integration/api.test.ts b/apps/rooms/src/test/integration/api.test.ts index 6a50b4a..6164efb 100644 --- a/apps/rooms/src/test/integration/api.test.ts +++ b/apps/rooms/src/test/integration/api.test.ts @@ -12,7 +12,7 @@ declare module 'cloudflare:test' { interface ProvidedEnv extends Env {} } -const ORIGIN = 'https://rooms.rec.djdevin.net' +const ORIGIN = 'https://example.com' // Mint a token the way the `auth` worker does, using the same dev secret. const DEV_SECRET = 'dev-insecure-signing-key-change-me' diff --git a/apps/rooms/wrangler.jsonc b/apps/rooms/wrangler.jsonc index a0728e1..af9390d 100644 --- a/apps/rooms/wrangler.jsonc +++ b/apps/rooms/wrangler.jsonc @@ -6,7 +6,7 @@ "compatibility_flags": ["nodejs_compat"], "routes": [ { - "pattern": "rooms.rec.djdevin.net", + "pattern": "rooms.rec.example.com", "custom_domain": true } ], diff --git a/env.example.json b/env.example.json new file mode 100644 index 0000000..a67d27b --- /dev/null +++ b/env.example.json @@ -0,0 +1,6 @@ +{ + "domain": "rec.example.com", + "subdomains": { + "playersettings": "settings" + } +} diff --git a/packages/tools/src/bin/runx.cmd.ts b/packages/tools/src/bin/runx.cmd.ts index c4def41..c2230c7 100644 --- a/packages/tools/src/bin/runx.cmd.ts +++ b/packages/tools/src/bin/runx.cmd.ts @@ -9,6 +9,7 @@ import { ciCmd } from '../cmd/ci.cmd' import { devCmd } from '../cmd/dev.cmd' import { fixCmd } from '../cmd/fix.cmd' import { shfmtCmd } from '../cmd/shfmt.cmd' +import { syncCmd } from '../cmd/sync.cmd' import { updateCmd } from '../cmd/update.cmd' program @@ -25,6 +26,7 @@ program .addCommand(ciCmd) .addCommand(updateCmd) .addCommand(shfmtCmd) + .addCommand(syncCmd) // Don't hang for unresolved promises .hook('postAction', () => process.exit(0)) diff --git a/packages/tools/src/cmd/sync.cmd.ts b/packages/tools/src/cmd/sync.cmd.ts new file mode 100644 index 0000000..6c284f7 --- /dev/null +++ b/packages/tools/src/cmd/sync.cmd.ts @@ -0,0 +1,92 @@ +import { Command } from '@commander-js/extra-typings' +import { z } from 'zod' + +import { getRepoRoot } from '../path' + +const Env = z.object({ + /** Base domain that all service hosts are derived from, e.g. `rec.example.com`. */ + domain: z.string().min(1), + /** + * Optional per-app subdomain overrides, keyed by the app's directory name. + * Defaults to the directory name when not set. + */ + subdomains: z.record(z.string(), z.string()).optional(), +}) + +type Edit = { + label: string + file: string + /** Rewrites the file's derived values; a no-op when nothing needs changing. */ + transform: (text: string) => string +} + +export const syncCmd = new Command('sync') + .description('Sync generated config (wrangler routes, ns endpoints, etc.) from env.json') + .option('--check', `Exit non-zero if any file is out of sync (don't write changes)`, false) + .action(async ({ check }) => { + const repoRoot = getRepoRoot() + const env = Env.parse(await fs.readJson(path.join(repoRoot, 'env.json'))) + + const edits: Edit[] = [] + + // Worker custom-domain routes: `.`, derived from the app dir name. + const wranglerConfigs = await glob('apps/*/wrangler.jsonc', { cwd: repoRoot, absolute: true }) + for (const file of wranglerConfigs.sort()) { + const dir = path.basename(path.dirname(file)) + const subdomain = env.subdomains?.[dir] ?? dir + const pattern = `${subdomain}.${env.domain}` + edits.push({ + label: `apps/${dir}/wrangler.jsonc → ${pattern}`, + file, + // Only matches when the file has a route; otherwise leaves the file untouched. + transform: (t) => t.replace(/("pattern":\s*")[^"]*(")/, `$1${pattern}$2`), + }) + } + + // ns service-discovery document — every host is one of our own subdomains, so swap + // the base domain while preserving each entry's subdomain label. + const endpointsFile = path.join(repoRoot, 'apps/ns/static/endpoints.json') + if (await fs.pathExists(endpointsFile)) { + edits.push({ + label: 'apps/ns/static/endpoints.json', + file: endpointsFile, + transform: (t) => + t.replace(/("https:\/\/[a-z0-9-]+\.)[a-z0-9.-]+(")/g, `$1${env.domain}$2`), + }) + } + + // Share-link base URL in the api worker's static config (only this one field is a + // host of ours; other URLs in the file are third-party and must be left alone). + const apiConfig = path.join(repoRoot, 'apps/api/static/api-config-v2.json') + if (await fs.pathExists(apiConfig)) { + edits.push({ + label: 'apps/api/static/api-config-v2.json (ShareBaseUrl)', + file: apiConfig, + transform: (t) => + t.replace(/("ShareBaseUrl":\s*"https:\/\/[a-z0-9-]+\.)[a-z0-9.-]+(\/)/, `$1${env.domain}$2`), + }) + } + + const outOfSync: string[] = [] + for (const { label, file, transform } of edits) { + const text = await fs.readFile(file, 'utf8') + const next = transform(text) + if (next === text) continue + outOfSync.push(label) + if (!check) await fs.writeFile(file, next) + } + + if (outOfSync.length === 0) { + echo(chalk.green('✓ generated config in sync')) + return + } + + if (check) { + echo(chalk.red('✗ generated config out of sync. Run `just sync` to fix:')) + for (const line of outOfSync) echo(` ${line}`) + process.exit(1) + } + + echo(chalk.green(`✓ synced ${outOfSync.length} file(s):`)) + for (const line of outOfSync) echo(` ${line}`) + })