mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 14:41:28 -07:00
support for 202507 endpoints (#37)
* [auth][api] accept the 20250424.01 client * [2025] unstable * 20250718.0 * correct one this time * stubs * more stubs * more stubs * [lists] add worker * [ai] route stubs * [api] player photo setting * [econ] add roomEconConfig route * [infra] update worker generators * [worker] add cards/moderation/platformnotification workers * [lists] updates to some endpoints * [clubs] stub out announcement endpoint, for now * [econ] stub out season endpoints for now * [chat] apps/chat stub out party endpoint not sure the shape yet * [api] stub out statsig and lockeditems * [doc] new services * [lists] stub the bulk endpoint * [datacollection] add placeholder service until we can kill it * [api] set gifting to lvl5 * update lock * [cdn] enable cache * [match] matchmake v2 * [lists] stub some lists * [ai] stubs * [rooms] new subroom save endpoint * [econ] add bulk purchase endpoint * [discovery] update featured creator to 1 for fun * [api] add photo settings flag * [chat] fixup chat permissions (sorta) * [auth] restrictions endpoint * [rooms] contributed endpoint * [api] fix outfit endpoint * [discovery] attempt to fix store * [chat] privacy endpoints * [api] cheered images * [rooms] add xp endpoint (disbaled) * [rooms] add xp endpoint (disabled) * update images-db for cheers * [rooms] add autocomplete endpoint * [cdn/img] increase cache ttl for statics * [api] bulk route for images * [accounts] add banner image * [api] add misc missing endpoints * [discovery] remove AI tab * [platformnotifications] stub some endpoints * [lists] add some more lists * [rooms] additional endpoints * [chat] stub a few privacy endpoints * [econ] stub some endpoints * misc db fixes * [api] tweak shape for images v6 * [rooms] dont show trending RROs
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
import { resolver } from 'hono-openapi'
|
||||
import { z } from 'zod'
|
||||
|
||||
import type { OpenAPIV3_1 } from 'openapi-types'
|
||||
|
||||
/**
|
||||
* OpenAPI schemas for the ai worker.
|
||||
*
|
||||
* IMPORTANT: these are DESCRIPTIVE ONLY. They are passed to `describeRoute` to generate
|
||||
* the spec and are never wired into `hono-openapi`'s `validator()`. Same rationale as the
|
||||
* auth/accounts/econ/match/playersettings workers: a reverse-engineered protocol, lenient
|
||||
* handlers, no runtime validation.
|
||||
*
|
||||
* Do NOT add `.meta({ id })` to these schemas — with this hono-openapi + zod v4 setup a
|
||||
* meta'd schema used in a response emits a `$ref` the framework doesn't always hoist into
|
||||
* `components.schemas`, leaving a dangling reference. Leaving meta off makes every schema
|
||||
* inline, which renders correctly in any tool.
|
||||
*/
|
||||
|
||||
/** Emit a zod schema as an `application/json` response body. */
|
||||
export function json(schema: z.ZodType, description: string) {
|
||||
return { description, content: { 'application/json': { schema: resolver(schema) } } }
|
||||
}
|
||||
|
||||
/** The empty-body 401 the auth-gated routes return. */
|
||||
export const UNAUTHORIZED_RESPONSE = { description: 'Missing or invalid bearer token (empty body)' }
|
||||
|
||||
/** Bearer-JWT security requirement, for the auth-gated routes. */
|
||||
export const AUTHED = [{ bearerAuth: [] }]
|
||||
|
||||
/** An optional integer query parameter. */
|
||||
export function intQuery(name: string, description: string): OpenAPIV3_1.ParameterObject {
|
||||
return { name, in: 'query', required: false, description, schema: { type: 'integer' } }
|
||||
}
|
||||
|
||||
/**
|
||||
* An optional boolean query parameter. The client spells these .NET-style (`False`, not
|
||||
* `false`), which is worth recording even where the value is ignored.
|
||||
*/
|
||||
export function boolQuery(name: string, description: string): OpenAPIV3_1.ParameterObject {
|
||||
return { name, in: 'query', required: false, description, schema: { type: 'boolean' } }
|
||||
}
|
||||
|
||||
/** An integer path parameter (ids are constrained to `[0-9]+` by the route pattern). */
|
||||
export function idParam(name: string, description: string): OpenAPIV3_1.ParameterObject {
|
||||
return { name, in: 'path', required: true, description, schema: { type: 'integer' } }
|
||||
}
|
||||
|
||||
/**
|
||||
* An `application/json` request body.
|
||||
*
|
||||
* The schema is emitted directly rather than through `resolver()` — zod's `$schema` key
|
||||
* and `additionalProperties: false` are dropped, since the handler reads the fields it
|
||||
* knows and ignores the rest, so a closed object would misreport it as stricter than it is.
|
||||
*/
|
||||
export function jsonBody(schema: z.ZodType, description: string): OpenAPIV3_1.RequestBodyObject {
|
||||
const { $schema: _$schema, additionalProperties: _extra, ...jsonSchema } = z.toJSONSchema(schema)
|
||||
return {
|
||||
description,
|
||||
content: { 'application/json': { schema: jsonSchema as OpenAPIV3_1.SchemaObject } },
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Response schemas ------------------------------------------------------
|
||||
|
||||
/** `GET /` — the root health check. */
|
||||
export const HealthResponse = z.object({
|
||||
service: z.literal('ai'),
|
||||
status: z.literal('ok'),
|
||||
})
|
||||
|
||||
/**
|
||||
* The Roomie AI access envelope — the `{ success, error_id, error, value }` shape, unlike
|
||||
* the flat Game AI refusal above. Roomie is granted here, with its energy budget pinned at
|
||||
* the maximum a signed 32-bit int holds (see INT32_MAX).
|
||||
*/
|
||||
export const RoomieAiAccess = z.object({
|
||||
success: z.literal(true),
|
||||
error_id: z.null(),
|
||||
error: z.null(),
|
||||
value: z.object({
|
||||
MaxEnergyFromSubscriptions: z
|
||||
.int()
|
||||
.describe('The energy ceiling a subscription buys — pinned to int32 max'),
|
||||
EnergyLeft: z.int().describe('Energy remaining. Never spent here, so also int32 max'),
|
||||
NextSubscriptionEnergyRechargeAt: z
|
||||
.string()
|
||||
.nullable()
|
||||
.describe('When the budget refills. Null — nothing depletes, so nothing recharges'),
|
||||
OutputAudioEnabled: z.boolean().describe('Whether Roomie may speak its replies'),
|
||||
}),
|
||||
})
|
||||
|
||||
/**
|
||||
* The refusal every Game AI read answers with. It is a 200 carrying `success: false`, not
|
||||
* an HTTP error — the client branches on the body, and an error status would surface as a
|
||||
* failed request rather than the "not available here" state it is meant to show.
|
||||
*/
|
||||
export const GameAiAccessDenied = z.object({
|
||||
success: z.literal(false),
|
||||
error_id: z.string().describe('Machine-readable reason, e.g. `AI.RoomDoesNotSupportGameAI`'),
|
||||
error: z.string().describe('The message shown to the player'),
|
||||
})
|
||||
|
||||
/**
|
||||
* The same refusal, plus an explicit `value: null`. The spend summary carries the key
|
||||
* where the access check omits it — the access check answers a yes/no and has nothing to
|
||||
* carry, while this one's payload slot exists and is simply empty. Reproduced as the
|
||||
* reference server sends it; don't unify the two.
|
||||
*/
|
||||
export const GameAiSpendSummaryDenied = GameAiAccessDenied.extend({
|
||||
value: z.null().describe('The spend summary. Null — there is no Game AI spend to report'),
|
||||
})
|
||||
|
||||
/**
|
||||
* `GET /makerai/user/access` — a BARE JSON boolean, not an envelope and not a `{ value }`
|
||||
* wrapper. The whole body is the answer.
|
||||
*/
|
||||
export const MakerAiAccessResponse = z
|
||||
.boolean()
|
||||
.describe('Whether the caller may use Maker AI; always false — no model runs here')
|
||||
|
||||
/**
|
||||
* Maker AI's dollar balances. A FLAT body — no `{ success, error, value }` envelope — and
|
||||
* every figure zero, since nothing here bills for model usage.
|
||||
*/
|
||||
export const MakerAiBalances = z.object({
|
||||
UsageDollars: z.number().describe('Dollars of model usage spent this period. Always 0'),
|
||||
UsersMaxUsageDollars: z.number().describe('The caller’s usage ceiling. Always 0'),
|
||||
RRPlusUsageDollars: z.number().describe('Usage spent against the RR+ allowance. Always 0'),
|
||||
UsersMaxRRPlusUsageDollars: z.number().describe('The RR+ allowance ceiling. Always 0'),
|
||||
TimeBalanceStatus: z.string().describe('Time-balance bucket state, e.g. `Empty`'),
|
||||
TimeExpiresAt: z
|
||||
.string()
|
||||
.describe('When the time balance lapses. `DateTime.MinValue` — there is none'),
|
||||
UsageBalanceStatus: z.string().describe('Usage-balance bucket state, e.g. `Good`'),
|
||||
UsagePercent: z.number().describe('Share of the usage ceiling consumed. Always 0'),
|
||||
RRPlusUsageBalanceStatus: z.string().describe('RR+ usage bucket state, e.g. `Good`'),
|
||||
RRPlusUsagePercent: z.number().describe('Share of the RR+ allowance consumed. Always 0'),
|
||||
})
|
||||
|
||||
/** The body the client posts to open a realtime session. Read for documentation only. */
|
||||
export const RealtimeSessionCreateBody = z.object({
|
||||
AIType: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Which assistant the client is opening a session for, e.g. `Roomie`'),
|
||||
})
|
||||
|
||||
/**
|
||||
* The realtime-session refusal. `{ success, error, error_id, value }` — note `error_id` is
|
||||
* an empty string rather than a machine-readable code, and `value` (which would carry the
|
||||
* session id and its client secret) is null.
|
||||
*/
|
||||
export const RealtimeSessionDenied = z.object({
|
||||
success: z.literal(false),
|
||||
error: z.string().describe('The message shown to the player'),
|
||||
error_id: z.string().describe('Empty — the reference server sends no code for this refusal'),
|
||||
value: z.null().describe('The session credentials. Null: no session is created'),
|
||||
})
|
||||
|
||||
/**
|
||||
* What Roomie knows about the caller: a prose profile it is primed with, and the discrete
|
||||
* facts behind it. Both empty here — nothing observes the player to build them.
|
||||
*/
|
||||
export const RoomieUserFacts = z.object({
|
||||
UserContext: z.string().describe('A prose profile Roomie is primed with. Empty'),
|
||||
UserFacts: z
|
||||
.array(
|
||||
z.object({
|
||||
Id: z.string().describe('GUID identifying the fact'),
|
||||
CreatedAt: z.string().describe('When the fact was recorded'),
|
||||
Emotion: z.string().describe('Sentiment attached to the fact, e.g. `neutral`'),
|
||||
Predicate: z.string().describe('The relation, e.g. `identifies as`'),
|
||||
Object: z.string().describe('The value the predicate points at'),
|
||||
})
|
||||
)
|
||||
.describe('The recorded facts. Always empty — nothing here observes the player'),
|
||||
})
|
||||
Reference in New Issue
Block a user