[econ] fix some storefronts

This commit is contained in:
Devin Zuczek
2026-10-19 12:27:05 -04:00
parent be1b275a0a
commit d918936795
11 changed files with 305 additions and 25 deletions
+41 -1
View File
@@ -59,7 +59,7 @@ import {
isSpendable,
spendCurrency,
} from './balance-db'
import { getCatalogItem, toCatalogAvatarItem } from './catalog-db'
import { getCatalogItem, getStoreListings, toCatalogAvatarItem } from './catalog-db'
// `LEGACY_CLIENT_BUILD` is shared with the storefront generator rather than restated: it picks
// which store FILE a caller is served here, and is what the generator's help names.
import { CatalogKind, LEGACY_CLIENT_BUILD } from './catalog-load'
@@ -130,6 +130,7 @@ import {
OPTIONAL_AUTHED,
OwnsRoomKeysRequest,
OwnsRoomKeysResponse,
PurchasableItemsRequest,
PurchaseRoomCurrencyEnvelope,
PurchaseRoomCurrencyRequest,
PurchaseRoomKeyWithCurrencyRequest,
@@ -5061,6 +5062,45 @@ const app = new Hono<App>({ strict: false })
}
)
// Store listings by `PurchasableItemId` — `{ PurchasableItemIds: [2594], GiftDropIds: null }`.
// Answers a bare array of the listings exactly as the served 2025 store (`sf3-2025.json`)
// lists them, read from the `catalog` table's `listing` column rather than the 4 MB file
// (a row's `catalog_id` IS its `PurchasableItemId`). Every item kind resolves; unknown ids
// are dropped, so the answer is in request order but not positional. `GiftDropIds` has only
// been seen null and is ignored. No auth: a listing is what the public store shows anyone.
.post(
'/api/storefronts/v1/purchasableitems',
describeRoute({
tags: ['Storefront'],
summary: 'Store listings by PurchasableItemId',
description: [
'Resolves `PurchasableItemIds[]` against the `catalog` table and answers each listing',
'verbatim as the served 2025 store (`sf3-2025.json`) lists it — `GiftDrop`, `Prices`,',
'`SubscriberPrices` and the rest. Avatar items, consumables, skins and custom avatar',
'items all resolve. Unknown ids (and repeats) are dropped, so the response follows',
'request order but is never a positional match. `GiftDropIds` is ignored.',
].join(' '),
requestBody: jsonBody(PurchasableItemsRequest, 'The listings to resolve'),
responses: {
200: json(JsonArray, 'The resolved listings (unknown ids omitted)'),
400: json(ErrorResponse, 'Malformed body'),
},
}),
async (c) => {
const body = (await c.req.json().catch(() => null)) as Record<string, unknown> | null
if (!body || typeof body !== 'object')
return c.json({ error: 'A JSON body is required' }, 400)
const raw = body.PurchasableItemIds
if (raw !== undefined && raw !== null && !Array.isArray(raw)) {
return c.json({ error: 'PurchasableItemIds must be an array' }, 400)
}
const ids = ((raw as unknown[] | null | undefined) ?? []).filter((id): id is number =>
Number.isInteger(id)
)
return c.json(await getStoreListings(c.env.DB, ids))
}
)
// The consumable DEFINITIONS the client knows how to use (`?supportedVersion=2`), as
// opposed to the ones a player owns. Served verbatim from `static/consumables.json`, the
// live game's list; `supportedVersion` is accepted and unused. A row's `ConfigurationData`