From 525e6ee24e45884983cd78cbc0ed008d0a7e68d4 Mon Sep 17 00:00:00 2001 From: Devin Zuczek Date: Tue, 6 Oct 2026 14:00:25 -0400 Subject: [PATCH] [econ] stub active consumable endpoint (?) --- apps/econ/README.md | 1 + apps/econ/src/econ.app.ts | 49 ++++++++++++++++++++++ apps/econ/src/openapi.ts | 13 ++++++ apps/econ/src/test/integration/api.test.ts | 25 +++++++++++ 4 files changed, 88 insertions(+) diff --git a/apps/econ/README.md b/apps/econ/README.md index 8292eb6..5a8bcca 100644 --- a/apps/econ/README.md +++ b/apps/econ/README.md @@ -43,6 +43,7 @@ missing/invalid). `~` = optional auth: served to anyone, personalised for a vali | POST | `/api/settings/v2/set` | ✓ | Persist settings (accept-and-ack) | | GET | `/api/consumables/v2/getUnlocked` | ✓ | Unlocked consumables | | POST | `/api/consumables/v1/consume` | ✓ | Consume an owned consumable | +| POST | `/api/consumables/v1/updateActive` | ✓ | Mark a consumable active (stub, empty) | | GET | `/api/storefronts/v4/balance/:currencyType` | ✓ | Currency balance | | GET | `/api/storefronts/v3/giftdropstore/:id` | | Gift-drop storefront catalog | | POST | `/api/storefronts/v2/buyItem` | ✓ | Buy a storefront item | diff --git a/apps/econ/src/econ.app.ts b/apps/econ/src/econ.app.ts index f92ec29..fbe4789 100644 --- a/apps/econ/src/econ.app.ts +++ b/apps/econ/src/econ.app.ts @@ -149,6 +149,7 @@ import { UgcPurchasableBulkRequest, UgcPurchasableItemList, UNAUTHORIZED_RESPONSE, + UpdateActiveConsumableRequest, UpdateObjectiveRequest, UpdateObjectiveResponse, UpdateRoomCurrencyRequest, @@ -4859,6 +4860,54 @@ const app = new Hono({ strict: false }) } ) + // STUB. The client posts `{ Id, IsActive, ActivatedByRoomie }` when a consumable starts + // or stops being used — `Id` is the consumable row id, as on `/consume`. Nothing is + // stored: the body is read only to log it, and the client ignores the response entirely, + // so none is sent. + // + // What this most likely means: the consumable is IN USE, and marking it so is what keeps + // a player from reusing one before it is over (a second Confetti Cannon while the first + // is still firing), and `ActivatedByRoomie` says a roommate set it off rather than its + // owner. That is a guess from the field names, not observed behaviour, so no row flips + // and `/consume` is unchanged. When the client's reading of it is known, this is where + // an `is_active` flag on the consumable row (and a refusal on `/consume`) would go. + .post( + '/api/consumables/v1/updateActive', + describeRoute({ + tags: ['Consumables'], + summary: 'Mark a consumable active or inactive (stub)', + description: [ + 'STUB. Accepts `{ Id, IsActive, ActivatedByRoomie }` — the client posting that one', + 'of its consumables has started or stopped being used — and stores nothing. The', + 'client ignores the response entirely, so this answers an empty 200.', + '', + 'Believed to mark the consumable as IN USE so it cannot be reused before it is over,', + 'with `ActivatedByRoomie` saying a roommate set it off; that is a guess from the field', + 'names, and nothing acts on it yet. `/consume` is unaffected.', + ].join(' '), + security: AUTHED, + requestBody: jsonBody(UpdateActiveConsumableRequest, 'The consumable and its new state'), + responses: { + 200: { description: 'Empty body — the client ignores the response entirely' }, + 401: UNAUTHORIZED_RESPONSE, + }, + }), + async (c) => { + const id = await authedId(c) + if (id === null) return unauthorized(c) + const body = await c.req + .json>() + .catch(() => ({}) as Record) + logger.info('consumable updateActive (stub, not stored)', { + accountId: id, + consumableId: body.Id, + isActive: body.IsActive, + activatedByRoomie: body.ActivatedByRoomie, + }) + return c.body(null, 200) + } + ) + // Currency balance. [Authorize]. The trailing int is a CurrencyType — the client // fetches `/balance/2` (RecCenterTokens) on load. Backed by the `balance` table; a // player who has never been granted gets their starting balance on this first read. diff --git a/apps/econ/src/openapi.ts b/apps/econ/src/openapi.ts index dc270a4..dd9d764 100644 --- a/apps/econ/src/openapi.ts +++ b/apps/econ/src/openapi.ts @@ -1257,6 +1257,19 @@ export const ConsumeConsumableRequest = z.object({ DeltaCount: z.int().optional().describe('How many to spend; defaults to 1'), }) +/** + * `POST /api/consumables/v1/updateActive` JSON body — the client marking one of its + * consumables active or not. Accepted and NOT acted on yet; see the route. + */ +export const UpdateActiveConsumableRequest = z.object({ + Id: z.int().describe('The consumable row id'), + IsActive: z.boolean().describe('Whether the consumable is now in use'), + ActivatedByRoomie: z + .boolean() + .optional() + .describe('Whether a roommate, rather than the owner, activated it'), +}) + /** `POST /api/avatar/v2/gifts/consume` form body (posted with a trailing slash). */ export const ConsumeGiftRequest = z.object({ Id: z.string().describe('The gift-box id to open'), diff --git a/apps/econ/src/test/integration/api.test.ts b/apps/econ/src/test/integration/api.test.ts index 272393f..4b9b985 100644 --- a/apps/econ/src/test/integration/api.test.ts +++ b/apps/econ/src/test/integration/api.test.ts @@ -3578,6 +3578,30 @@ describe('econ endpoints', () => { expect(await res.json()).toEqual([]) }) + test('POST /api/consumables/v1/updateActive is a stub: auth-gated, stores nothing, answers nothing', async () => { + const before = await env.DB.prepare('SELECT * FROM consumable ORDER BY id').all() + const body = { Id: 7550, IsActive: true, ActivatedByRoomie: false } + + const anon = await exports.default.fetch(`${ORIGIN}/api/consumables/v1/updateActive`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }) + expect(anon.status).toBe(401) + + const res = await exports.default.fetch(`${ORIGIN}/api/consumables/v1/updateActive`, { + method: 'POST', + headers: { ...(await bearer('314')), 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }) + expect(res.status).toBe(200) + // No body at all: the client ignores the response entirely. + expect(await res.text()).toBe('') + // Nothing is stored yet. + const after = await env.DB.prepare('SELECT * FROM consumable ORDER BY id').all() + expect(after.results).toEqual(before.results) + }) + test('POST /api/consumables/v1/consume reduces the count and deletes the row at zero', async () => { // Seed account 313 with two Supreme Pizza instances (counts 3 and 1). await grantConsumable(env.DB, 313, 'Supreme Pizza', 3) @@ -6991,6 +7015,7 @@ describe('econ endpoints', () => { 'POST /api/checklist/v1/complete', 'POST /api/checklist/v2/complete', 'POST /api/consumables/v1/consume', + 'POST /api/consumables/v1/updateActive', 'POST /api/equipment/v1/update', 'POST /api/gamerewards/v1/request', 'POST /api/itemWishlists/v1/isonwishlist/bulk',