stub the checklist endpoint

This commit is contained in:
Devin Zuczek
2026-08-01 12:38:31 -04:00
parent 1a59ab42bf
commit 79aab56c9e
13 changed files with 364 additions and 41 deletions
+46
View File
@@ -352,6 +352,52 @@ export const CustomAvatarItemsPage = z.object({
TotalResults: z.int(),
})
/**
* One custom-item save — the rebuilt version of a legacy avatar item. This is the
* official shape, recorded for documentation: nothing stores custom items yet, so we
* never actually emit one of these.
*/
export const CustomAvatarItemSave = z.object({
customAvatarItemSaveId: z.int().describe('The saves id'),
customAvatarItemId: z.string().describe('Guid of the custom item this save belongs to'),
unityAssetId: z.string().describe('Guid of the built Unity asset'),
createdAt: z.string().describe('ISO 8601 timestamp'),
thumbnailFileName: z.string(),
additionalConfiguration: z.string(),
unityAsset: z.string(),
unityAssetHash: z.string(),
})
/**
* The custom-item saves that replace a set of legacy avatar items, keyed by the legacy
* item's `AvatarItemDesc`. Nothing stores custom items yet, so the map is always empty —
* the value shape is documented rather than served.
*/
export const LegacyAvatarItemSaves = z.object({
customAvatarItemSavesByAvatarItemDesc: z.record(z.string(), CustomAvatarItemSave),
})
/**
* `GET /outfits/me` — the empty-outfit envelope. Stubbed, so every field that would
* carry a stored outfit is null/empty; `DataVersion` 9 is what the client parses against.
*/
export const OutfitsMeResponse = z.object({
LegacyData: z.object({
SelectionsV1: z.null(),
SelectionsV2: z.null(),
FaceFeatures: z.null(),
SkinColor: z.null(),
HairColor: z.null(),
}),
Selections: JsonArray,
DataVersion: z.int(),
CustomizationSettings: z.null(),
ThumbnailFileName: z.null(),
Name: z.null(),
Accessibility: z.int(),
Slot: z.int(),
})
/** The `{ success, value }` envelope `isCreationAllowedForAccount` wraps its answer in. */
export const SuccessValueEnvelope = z.object({ success: z.boolean(), value: z.null() })
+63
View File
@@ -39,6 +39,8 @@ import {
json,
JsonArray,
jsonBody,
LegacyAvatarItemSaves,
OutfitsMeResponse,
pageParams,
SaveInventionRequest,
SetTagsRequest,
@@ -213,6 +215,67 @@ export const avatarRoutes = new Hono<App>({ strict: false })
(c) => c.json({ Results: [], TotalResults: 0 })
)
// The client asks which legacy avatar items have been rebuilt as custom items, so it
// can render the custom version instead. Nothing stores custom items yet, so nothing
// has a save — an empty list means "use the legacy items as-is".
.post(
'/api/customAvatarItems/GetCustomAvatarItemCurrentSavesForLegacyAvatarItems',
describeRoute({
tags: ['Avatar'],
summary: 'Custom-item saves for legacy avatar items',
description:
'Given a set of legacy avatar items, the custom-item saves that replace them, keyed ' +
'by the legacy items `AvatarItemDesc`. Nothing stores custom items yet, so the map ' +
'is always empty — which the client reads as “render the legacy items as-is”. The ' +
'request body is ignored.\n\n' +
'The value shape is the official one, recorded here for documentation; we never ' +
'emit one until custom items are stored.',
responses: { 200: json(LegacyAvatarItemSaves, 'An empty map') },
}),
(c) => c.json({ customAvatarItemSavesByAvatarItemDesc: {} })
)
// The newer outfit read, on a bare (un-prefixed) path. Auth-gated. Stubbed: every
// caller gets the brand-new-account envelope — all-null LegacyData, no selections —
// rather than their saved outfit, which lives on the `econ` worker.
.get(
'/outfits/me',
describeRoute({
tags: ['Avatar'],
summary: 'The callers outfit (stub)',
description:
'The newer outfit read, on a bare un-prefixed path. Stubbed for now: every caller ' +
'gets the brand-new-account envelope — all-null `LegacyData`, no `Selections` — ' +
'regardless of what they have saved (saved outfits live on the `econ` worker). ' +
'`DataVersion` 9 is the version the client expects to parse.',
security: AUTHED,
responses: {
200: json(OutfitsMeResponse, 'The empty-outfit envelope'),
401: UNAUTHORIZED_RESPONSE,
},
}),
async (c) => {
const id = await authedId(c)
if (id === null) return unauthorized(c)
return c.json({
LegacyData: {
SelectionsV1: null,
SelectionsV2: null,
FaceFeatures: null,
SkinColor: null,
HairColor: null,
},
Selections: [],
DataVersion: 9,
CustomizationSettings: null,
ThumbnailFileName: null,
Name: null,
Accessibility: 0,
Slot: 0,
})
}
)
// A single invention by id (`?inventionId=…`). Returns the stored RRInvention,
// or 404 when there's no such invention.
.get(
+3 -3
View File
@@ -19,10 +19,10 @@ import type { App } from '../context'
/**
* Client builds the version check answers as current. `GAME_VERSION` is the build the
* rest of the stack targets; `20230616` is a later client that talks the same protocol,
* so we let it through rather than telling it to update.
* rest of the stack targets; `20230616` and `20231207` are later clients that talk the
* same protocol, so we let them through rather than telling them to update.
*/
const ACCEPTED_GAME_VERSIONS = new Set([GAME_VERSION, '20230616'])
const ACCEPTED_GAME_VERSIONS = new Set([GAME_VERSION, '20230616', '20231207'])
// ---- Config / version ------------------------------------------------------
export const configRoutes = new Hono<App>({ strict: false })
+38
View File
@@ -350,6 +350,42 @@ describe('public endpoints', () => {
expect(await res.json()).toEqual({ Results: [], TotalResults: 0 })
})
test('POST /api/customAvatarItems/GetCustomAvatarItemCurrentSavesForLegacyAvatarItems returns an empty map', async () => {
const res = await exports.default.fetch(
`${ORIGIN}/api/customAvatarItems/GetCustomAvatarItemCurrentSavesForLegacyAvatarItems`,
{
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ AvatarItemIds: [1, 2, 3] }),
}
)
expect(res.status).toBe(200)
expect(await res.json()).toEqual({ customAvatarItemSavesByAvatarItemDesc: {} })
})
test('GET /outfits/me 401s without a token, serves the empty envelope with one', async () => {
const anon = await exports.default.fetch(`${ORIGIN}/outfits/me`)
expect(anon.status).toBe(401)
const res = await exports.default.fetch(`${ORIGIN}/outfits/me`, { headers: await bearer() })
expect(res.status).toBe(200)
expect(await res.json()).toEqual({
LegacyData: {
SelectionsV1: null,
SelectionsV2: null,
FaceFeatures: null,
SkinColor: null,
HairColor: null,
},
Selections: [],
DataVersion: 9,
CustomizationSettings: null,
ThumbnailFileName: null,
Name: null,
Accessibility: 0,
Slot: 0,
})
})
test('GET /api/rooms/v1/filters returns an object with filter arrays', async () => {
const res = await exports.default.fetch(`${ORIGIN}/api/rooms/v1/filters`)
expect(res.status).toBe(200)
@@ -1972,12 +2008,14 @@ describe('openapi', () => {
'GET /api/roomkeys/v1/room',
'GET /api/rooms/v1/filters',
'GET /api/versioncheck/v4',
'GET /outfits/me',
'GET /voice/config',
'POST /api/CampusCard/v1/UpdateAndGetSubscription',
'POST /api/PlayerReporting/v1/deviceId',
'POST /api/PlayerReporting/v1/hile',
'POST /api/PlayerReporting/v1/moderationBlockDetails',
'POST /api/avatar/v2/gifts/generate',
'POST /api/customAvatarItems/GetCustomAvatarItemCurrentSavesForLegacyAvatarItems',
'POST /api/gamesight/event',
'POST /api/images/v1/cheer',
'POST /api/images/v4/uploadsaved',