From 8ea0caa1e54831ba75762b2c433ec27ea5fb6f72 Mon Sep 17 00:00:00 2001 From: Devin Zuczek Date: Fri, 14 Aug 2026 01:01:19 -0400 Subject: [PATCH] more stubs --- apps/rooms/src/openapi.ts | 21 +++++++++++++ apps/rooms/src/rooms.app.ts | 33 +++++++++++++++++++++ apps/rooms/src/test/integration/api.test.ts | 20 +++++++++++++ 3 files changed, 74 insertions(+) diff --git a/apps/rooms/src/openapi.ts b/apps/rooms/src/openapi.ts index 34fb817..0a2c321 100644 --- a/apps/rooms/src/openapi.ts +++ b/apps/rooms/src/openapi.ts @@ -683,3 +683,24 @@ export const PlayerDataDto = z.object({ export const RoomExperiencePlayer = z .array(z.unknown()) .describe('Always empty — no per-room experience is tracked') + +/** + * `GET /publishState/configs` — the limits the client enforces on republishing a room: + * how many updates are allowed in the rolling window, and the cooldown/expiry around + * them. Served as fixed values from the reference server. + * + * The envelope is NOT the `{ success, error, value }` one the room mutations use: `error` + * is null rather than `""`, and there's an extra `error_id`. Kept as-is — the client + * reads both keys. + */ +export const PublishStateConfigsEnvelope = z.object({ + value: z.object({ + UpdateMaxCount: z.int().describe('Updates allowed per rolling window'), + UpdateRollingWindowInDays: z.int().describe('Length of that window, in days'), + UpdateExpirationInDays: z.int().describe('Days before an update expires'), + UpdateCooldownInDays: z.int().describe('Days between updates'), + }), + success: z.literal(true), + error_id: z.null(), + error: z.null(), +}) diff --git a/apps/rooms/src/rooms.app.ts b/apps/rooms/src/rooms.app.ts index 45cd70c..53729c2 100644 --- a/apps/rooms/src/rooms.app.ts +++ b/apps/rooms/src/rooms.app.ts @@ -91,6 +91,7 @@ import { PlayerDataDto, playerIdParam, PublishSaveRequest, + PublishStateConfigsEnvelope, RestrictionsRequest, RoleRequest, RoomBanEntryDto, @@ -2682,6 +2683,38 @@ const app = new Hono() } ) + // The republish limits, verbatim from the reference server. Fixed values, no auth: + // the client reads them to render its publish UI, before any room is in play. + .get( + '/publishState/configs', + describeRoute({ + tags: ['Rooms'], + summary: 'Room republish limits', + description: [ + 'The limits the client enforces around republishing a room — how many updates are', + 'allowed per rolling window, and the cooldown and expiry around them. Fixed values', + 'from the reference server; nothing here enforces them server-side yet, so this is', + 'what the client shows and gates its own UI on.', + '', + 'Note the envelope differs from the room mutations’: `error` is null (not `""`) and', + 'there is an extra `error_id`.', + ].join(' '), + responses: { 200: json(PublishStateConfigsEnvelope, 'The republish limits') }, + }), + (c) => + c.json({ + value: { + UpdateMaxCount: 3, + UpdateRollingWindowInDays: 365, + UpdateExpirationInDays: 30, + UpdateCooldownInDays: 45, + }, + success: true, + error_id: null, + error: null, + }) + ) + // Photon access token + room permissions the client needs to spawn into a room. .get( '/photon_access_token', diff --git a/apps/rooms/src/test/integration/api.test.ts b/apps/rooms/src/test/integration/api.test.ts index 84c352e..ea9c737 100644 --- a/apps/rooms/src/test/integration/api.test.ts +++ b/apps/rooms/src/test/integration/api.test.ts @@ -141,6 +141,25 @@ describe('rooms endpoints', () => { expect(body.SubRooms[0].UnitySceneId).toBe('76d98498-60a1-430c-ab76-b54a29b7a163') }) + // Pinned whole: these are the numbers the client's publish UI counts against, and + // `error: null` / `error_id` is a different envelope from the room mutations' — a + // "cleanup" that unified the two would break the client silently. + it('GET /publishState/configs returns the republish limits', async () => { + const res = await SELF.fetch(`${ORIGIN}/publishState/configs`) + expect(res.status).toBe(200) + expect(await res.json()).toEqual({ + value: { + UpdateMaxCount: 3, + UpdateRollingWindowInDays: 365, + UpdateExpirationInDays: 30, + UpdateCooldownInDays: 45, + }, + success: true, + error_id: null, + error: null, + }) + }) + // Stub. Registered (not 404) matters more than the body: the client asks for this on // room entry, and an unregistered path stalls the load rather than erroring visibly. it('GET /rooms/:id/experience/player returns [] for any room', async () => { @@ -2966,6 +2985,7 @@ describe('rooms endpoints', () => { 'GET /XXXfeaturedrooms/current', 'GET /dormroom/me', 'GET /photon_access_token', + 'GET /publishState/configs', 'GET /rooms', 'GET /rooms/base', 'GET /rooms/bulk',