mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 14:41:28 -07:00
update api docs
This commit is contained in:
@@ -11,7 +11,7 @@ import {
|
||||
searchAccounts,
|
||||
updateAccount,
|
||||
} from '@repo/domain'
|
||||
import { logger, withNotFound, withOnError } from '@repo/hono-helpers'
|
||||
import { logger, withCleanSpec, withNotFound, withOnError } from '@repo/hono-helpers'
|
||||
import { validateAndGetAccountId } from '@repo/jwt'
|
||||
|
||||
import {
|
||||
@@ -638,36 +638,38 @@ const app = new Hono<App>()
|
||||
app.get(
|
||||
'/openapi.json',
|
||||
describeRoute({ hide: true }),
|
||||
openAPIRouteHandler(app, {
|
||||
documentation: {
|
||||
info: {
|
||||
title: 'recflare accounts',
|
||||
version: '1.0.0',
|
||||
description: [
|
||||
'Account reads, profile mutations and lookups for recflare, a private-server',
|
||||
'reimplementation of the Rec Room backend. Accounts live in the shared `recflare`',
|
||||
'D1 database, whose `account` schema is owned by the `auth` worker.',
|
||||
'',
|
||||
'The shapes here are **reverse-engineered from the game client**, which is the only',
|
||||
'real consumer. They record observed behaviour, not a designed contract; the handlers',
|
||||
'are lenient and reads fall back to a synthesized default account rather than 404.',
|
||||
'Nothing in this spec is enforced at runtime — treat a field marked required as "the',
|
||||
'client always sends it", not "the server rejects it if absent".',
|
||||
].join('\n'),
|
||||
},
|
||||
servers: [{ url: 'https://accounts.recflare.net', description: 'Production' }],
|
||||
components: {
|
||||
securitySchemes: {
|
||||
bearerAuth: {
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
bearerFormat: 'JWT',
|
||||
description: 'An `access_token` from the auth worker’s `POST /connect/token`.',
|
||||
withCleanSpec(
|
||||
openAPIRouteHandler(app, {
|
||||
documentation: {
|
||||
info: {
|
||||
title: 'recflare accounts',
|
||||
version: '1.0.0',
|
||||
description: [
|
||||
'Account reads, profile mutations and lookups for recflare, a private-server',
|
||||
'reimplementation of the Rec Room backend. Accounts live in the shared `recflare`',
|
||||
'D1 database, whose `account` schema is owned by the `auth` worker.',
|
||||
'',
|
||||
'The shapes here are **reverse-engineered from the game client**, which is the only',
|
||||
'real consumer. They record observed behaviour, not a designed contract; the handlers',
|
||||
'are lenient and reads fall back to a synthesized default account rather than 404.',
|
||||
'Nothing in this spec is enforced at runtime — treat a field marked required as "the',
|
||||
'client always sends it", not "the server rejects it if absent".',
|
||||
].join('\n'),
|
||||
},
|
||||
servers: [{ url: 'https://accounts.recflare.net', description: 'Production' }],
|
||||
components: {
|
||||
securitySchemes: {
|
||||
bearerAuth: {
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
bearerFormat: 'JWT',
|
||||
description: 'An `access_token` from the auth worker’s `POST /connect/token`.',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
export default app
|
||||
|
||||
+37
-35
@@ -2,7 +2,7 @@ import { Hono } from 'hono'
|
||||
import { describeRoute, openAPIRouteHandler } from 'hono-openapi'
|
||||
import { useWorkersLogger } from 'workers-tagged-logger'
|
||||
|
||||
import { withNotFound, withOnError } from '@repo/hono-helpers'
|
||||
import { withCleanSpec, withNotFound, withOnError } from '@repo/hono-helpers'
|
||||
|
||||
import { avatarRoutes } from './routes/avatar'
|
||||
import { configRoutes } from './routes/config'
|
||||
@@ -58,43 +58,45 @@ const app = new Hono<App>({ strict: false })
|
||||
app.get(
|
||||
'/openapi.json',
|
||||
describeRoute({ hide: true }),
|
||||
openAPIRouteHandler(app, {
|
||||
documentation: {
|
||||
info: {
|
||||
title: 'recflare api',
|
||||
version: '1.0.0',
|
||||
description: [
|
||||
'The catch-all Game API for recflare, a private-server reimplementation of the Rec',
|
||||
'Room backend: everything the client calls that has not been split out into its own',
|
||||
'worker yet. Today that is config, the friend graph, inventions, saved photos,',
|
||||
'reputation and the assorted sinks the client hits while loading. Relationships,',
|
||||
'inventions and images are D1-backed; several endpoints are still stubs, noted per',
|
||||
'route.',
|
||||
'',
|
||||
'Expect this surface to shrink. Paths that also exist on a dedicated worker (avatar,',
|
||||
'equipment, consumables and objectives on `econ`) are already served there — the',
|
||||
'client calls that host and the copy here is a stub, which each route says.',
|
||||
'',
|
||||
'The shapes are **reverse-engineered from the game client**, which is the only real',
|
||||
'consumer. They record observed behaviour, not a designed contract; the handlers are',
|
||||
'lenient and parse bodies defensively. Nothing in this spec is enforced at runtime —',
|
||||
'treat a field marked required as "the client always sends it", not "the server',
|
||||
'rejects it if absent".',
|
||||
].join('\n'),
|
||||
},
|
||||
servers: [{ url: 'https://api.recflare.net', description: 'Production' }],
|
||||
components: {
|
||||
securitySchemes: {
|
||||
bearerAuth: {
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
bearerFormat: 'JWT',
|
||||
description: 'An `access_token` from the auth worker’s `POST /connect/token`.',
|
||||
withCleanSpec(
|
||||
openAPIRouteHandler(app, {
|
||||
documentation: {
|
||||
info: {
|
||||
title: 'recflare api',
|
||||
version: '1.0.0',
|
||||
description: [
|
||||
'The catch-all Game API for recflare, a private-server reimplementation of the Rec',
|
||||
'Room backend: everything the client calls that has not been split out into its own',
|
||||
'worker yet. Today that is config, the friend graph, inventions, saved photos,',
|
||||
'reputation and the assorted sinks the client hits while loading. Relationships,',
|
||||
'inventions and images are D1-backed; several endpoints are still stubs, noted per',
|
||||
'route.',
|
||||
'',
|
||||
'Expect this surface to shrink. Paths that also exist on a dedicated worker (avatar,',
|
||||
'equipment, consumables and objectives on `econ`) are already served there — the',
|
||||
'client calls that host and the copy here is a stub, which each route says.',
|
||||
'',
|
||||
'The shapes are **reverse-engineered from the game client**, which is the only real',
|
||||
'consumer. They record observed behaviour, not a designed contract; the handlers are',
|
||||
'lenient and parse bodies defensively. Nothing in this spec is enforced at runtime —',
|
||||
'treat a field marked required as "the client always sends it", not "the server',
|
||||
'rejects it if absent".',
|
||||
].join('\n'),
|
||||
},
|
||||
servers: [{ url: 'https://api.recflare.net', description: 'Production' }],
|
||||
components: {
|
||||
securitySchemes: {
|
||||
bearerAuth: {
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
bearerFormat: 'JWT',
|
||||
description: 'An `access_token` from the auth worker’s `POST /connect/token`.',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
export default app
|
||||
|
||||
@@ -1990,4 +1990,16 @@ describe('openapi', () => {
|
||||
const raw = await res.text()
|
||||
expect(raw.match(/\$ref/g)).toBeNull()
|
||||
})
|
||||
|
||||
// `z.int()` carries the safe-integer range as its bounds, which Scalar would
|
||||
// otherwise show as the example value for every integer field (-9007199254740991).
|
||||
// withCleanSpec() supplies a placeholder instead; this guards the wrapper staying
|
||||
// wired up.
|
||||
test('integer fields carry a placeholder example', async () => {
|
||||
const res = await exports.default.fetch(`${ORIGIN}/openapi.json`)
|
||||
const raw = await res.text()
|
||||
const integers = raw.match(/"type":"integer"/g) ?? []
|
||||
expect(integers.length).toBeGreaterThan(0)
|
||||
expect(raw.match(/"example":12345/g)?.length).toBe(integers.length)
|
||||
})
|
||||
})
|
||||
|
||||
+30
-28
@@ -18,7 +18,7 @@ import {
|
||||
setPresence,
|
||||
verifyPassword,
|
||||
} from '@repo/domain'
|
||||
import { intVar, logger, withNotFound, withOnError } from '@repo/hono-helpers'
|
||||
import { intVar, logger, withCleanSpec, withNotFound, withOnError } from '@repo/hono-helpers'
|
||||
import { generateToken, TOKEN_TTL_SECONDS, validateAndGetAccountId } from '@repo/jwt'
|
||||
|
||||
import {
|
||||
@@ -720,36 +720,38 @@ const app = new Hono<App>()
|
||||
app.get(
|
||||
'/openapi.json',
|
||||
describeRoute({ hide: true }),
|
||||
openAPIRouteHandler(app, {
|
||||
documentation: {
|
||||
info: {
|
||||
title: 'recflare auth',
|
||||
version: '1.0.0',
|
||||
description: [
|
||||
'Authentication and token issuance for recflare, a private-server reimplementation',
|
||||
'of the Rec Room backend.',
|
||||
'',
|
||||
'The shapes here are **reverse-engineered from the game client**, which is the only',
|
||||
'real consumer. They record observed behaviour rather than a designed contract, and',
|
||||
'the handlers are deliberately lenient: missing or malformed fields generally fall',
|
||||
'through to a graceful path instead of erroring. Nothing in this spec is enforced at',
|
||||
'runtime, so treat a field marked required as "the client always sends it", not "the',
|
||||
'server rejects it if absent".',
|
||||
].join('\n'),
|
||||
},
|
||||
servers: [{ url: 'https://auth.recflare.net', description: 'Production' }],
|
||||
components: {
|
||||
securitySchemes: {
|
||||
bearerAuth: {
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
bearerFormat: 'JWT',
|
||||
description: 'An `access_token` from `POST /connect/token`.',
|
||||
withCleanSpec(
|
||||
openAPIRouteHandler(app, {
|
||||
documentation: {
|
||||
info: {
|
||||
title: 'recflare auth',
|
||||
version: '1.0.0',
|
||||
description: [
|
||||
'Authentication and token issuance for recflare, a private-server reimplementation',
|
||||
'of the Rec Room backend.',
|
||||
'',
|
||||
'The shapes here are **reverse-engineered from the game client**, which is the only',
|
||||
'real consumer. They record observed behaviour rather than a designed contract, and',
|
||||
'the handlers are deliberately lenient: missing or malformed fields generally fall',
|
||||
'through to a graceful path instead of erroring. Nothing in this spec is enforced at',
|
||||
'runtime, so treat a field marked required as "the client always sends it", not "the',
|
||||
'server rejects it if absent".',
|
||||
].join('\n'),
|
||||
},
|
||||
servers: [{ url: 'https://auth.recflare.net', description: 'Production' }],
|
||||
components: {
|
||||
securitySchemes: {
|
||||
bearerAuth: {
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
bearerFormat: 'JWT',
|
||||
description: 'An `access_token` from `POST /connect/token`.',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
export default app
|
||||
|
||||
+31
-29
@@ -3,7 +3,7 @@ import { describeRoute, openAPIRouteHandler } from 'hono-openapi'
|
||||
import { useWorkersLogger } from 'workers-tagged-logger'
|
||||
|
||||
import { consumeGift, createGift, getGift, getPendingGifts } from '@repo/domain'
|
||||
import { intVar, logger, withNotFound, withOnError } from '@repo/hono-helpers'
|
||||
import { intVar, logger, withCleanSpec, withNotFound, withOnError } from '@repo/hono-helpers'
|
||||
import { validateAndGetAccountId } from '@repo/jwt'
|
||||
|
||||
// The notification-type ids the hub carries (owned by the `notify` worker). Imported
|
||||
@@ -1200,37 +1200,39 @@ const app = new Hono<App>({ strict: false })
|
||||
app.get(
|
||||
'/openapi.json',
|
||||
describeRoute({ hide: true }),
|
||||
openAPIRouteHandler(app, {
|
||||
documentation: {
|
||||
info: {
|
||||
title: 'recflare econ',
|
||||
version: '1.0.0',
|
||||
description: [
|
||||
'Avatar and economy endpoints for recflare, a private-server reimplementation of the',
|
||||
'Rec Room backend. The client calls these on the `econ` host; many are also served by',
|
||||
'the `api` worker. Storefront catalogs are static assets (`sf{N}.json`); balances,',
|
||||
'inventory, consumables, saved outfits and gift boxes are D1-backed.',
|
||||
'',
|
||||
'The shapes here are **reverse-engineered from the game client**, which is the only',
|
||||
'real consumer. They record observed behaviour, not a designed contract; the handlers',
|
||||
'are lenient and parse bodies defensively. Nothing in this spec is enforced at',
|
||||
'runtime — treat a field marked required as "the client always sends it", not "the',
|
||||
'server rejects it if absent".',
|
||||
].join('\n'),
|
||||
},
|
||||
servers: [{ url: 'https://econ.recflare.net', description: 'Production' }],
|
||||
components: {
|
||||
securitySchemes: {
|
||||
bearerAuth: {
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
bearerFormat: 'JWT',
|
||||
description: 'An `access_token` from the auth worker’s `POST /connect/token`.',
|
||||
withCleanSpec(
|
||||
openAPIRouteHandler(app, {
|
||||
documentation: {
|
||||
info: {
|
||||
title: 'recflare econ',
|
||||
version: '1.0.0',
|
||||
description: [
|
||||
'Avatar and economy endpoints for recflare, a private-server reimplementation of the',
|
||||
'Rec Room backend. The client calls these on the `econ` host; many are also served by',
|
||||
'the `api` worker. Storefront catalogs are static assets (`sf{N}.json`); balances,',
|
||||
'inventory, consumables, saved outfits and gift boxes are D1-backed.',
|
||||
'',
|
||||
'The shapes here are **reverse-engineered from the game client**, which is the only',
|
||||
'real consumer. They record observed behaviour, not a designed contract; the handlers',
|
||||
'are lenient and parse bodies defensively. Nothing in this spec is enforced at',
|
||||
'runtime — treat a field marked required as "the client always sends it", not "the',
|
||||
'server rejects it if absent".',
|
||||
].join('\n'),
|
||||
},
|
||||
servers: [{ url: 'https://econ.recflare.net', description: 'Production' }],
|
||||
components: {
|
||||
securitySchemes: {
|
||||
bearerAuth: {
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
bearerFormat: 'JWT',
|
||||
description: 'An `access_token` from the auth worker’s `POST /connect/token`.',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
export default app
|
||||
|
||||
+32
-30
@@ -23,7 +23,7 @@ import {
|
||||
setPresence,
|
||||
setRoomInstanceInProgress,
|
||||
} from '@repo/domain'
|
||||
import { withNotFound, withOnError } from '@repo/hono-helpers'
|
||||
import { withCleanSpec, withNotFound, withOnError } from '@repo/hono-helpers'
|
||||
import { validateAndGetAccountId } from '@repo/jwt'
|
||||
|
||||
import {
|
||||
@@ -1084,38 +1084,40 @@ async function sweepExpiredPresence(env: Env): Promise<void> {
|
||||
app.get(
|
||||
'/openapi.json',
|
||||
describeRoute({ hide: true }),
|
||||
openAPIRouteHandler(app, {
|
||||
documentation: {
|
||||
info: {
|
||||
title: 'recflare match',
|
||||
version: '1.0.0',
|
||||
description: [
|
||||
'Matchmaking and presence for recflare, a private-server reimplementation of the Rec',
|
||||
'Room backend. Rooms and room instances are D1-backed (matchmaking finds or creates a',
|
||||
'`room_instance` per session); presence — the instance each player is currently in —',
|
||||
'lives in the shared `presence` table and expires on a TTL. A cron sweep clears',
|
||||
'expired presence and frees up instances a crashed player never left.',
|
||||
'',
|
||||
'The shapes here are **reverse-engineered from the game client**, which is the only',
|
||||
'real consumer. They record observed behaviour, not a designed contract; the handlers',
|
||||
'are lenient and parse bodies defensively. Nothing in this spec is enforced at',
|
||||
'runtime — treat a field marked required as "the client always sends it", not "the',
|
||||
'server rejects it if absent".',
|
||||
].join('\n'),
|
||||
},
|
||||
servers: [{ url: 'https://match.recflare.net', description: 'Production' }],
|
||||
components: {
|
||||
securitySchemes: {
|
||||
bearerAuth: {
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
bearerFormat: 'JWT',
|
||||
description: 'An `access_token` from the auth worker’s `POST /connect/token`.',
|
||||
withCleanSpec(
|
||||
openAPIRouteHandler(app, {
|
||||
documentation: {
|
||||
info: {
|
||||
title: 'recflare match',
|
||||
version: '1.0.0',
|
||||
description: [
|
||||
'Matchmaking and presence for recflare, a private-server reimplementation of the Rec',
|
||||
'Room backend. Rooms and room instances are D1-backed (matchmaking finds or creates a',
|
||||
'`room_instance` per session); presence — the instance each player is currently in —',
|
||||
'lives in the shared `presence` table and expires on a TTL. A cron sweep clears',
|
||||
'expired presence and frees up instances a crashed player never left.',
|
||||
'',
|
||||
'The shapes here are **reverse-engineered from the game client**, which is the only',
|
||||
'real consumer. They record observed behaviour, not a designed contract; the handlers',
|
||||
'are lenient and parse bodies defensively. Nothing in this spec is enforced at',
|
||||
'runtime — treat a field marked required as "the client always sends it", not "the',
|
||||
'server rejects it if absent".',
|
||||
].join('\n'),
|
||||
},
|
||||
servers: [{ url: 'https://match.recflare.net', description: 'Production' }],
|
||||
components: {
|
||||
securitySchemes: {
|
||||
bearerAuth: {
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
bearerFormat: 'JWT',
|
||||
description: 'An `access_token` from the auth worker’s `POST /connect/token`.',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
// The HTTP surface is a standard Hono app, exported by name so it can be mounted
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { Handler, MiddlewareHandler } from 'hono'
|
||||
|
||||
/**
|
||||
* Zod 4 encodes `z.int()` as `{ type: 'integer', minimum: -9007199254740991, maximum:
|
||||
* 9007199254740991 }` — the safe-integer range. That is accurate, but Scalar (and most
|
||||
* spec viewers) derive the displayed example from `minimum` when a schema carries no
|
||||
* `example` of its own, so every integer field in the docs rendered as
|
||||
* `-9007199254740991`.
|
||||
*
|
||||
* The bounds are left alone; we just supply a neutral placeholder so the viewer has
|
||||
* something better to show.
|
||||
*/
|
||||
const PLACEHOLDER_INTEGER = 12345
|
||||
|
||||
/** Recursively add a placeholder example to integer schemas that lack one. */
|
||||
function addIntegerExamples(node: unknown): void {
|
||||
if (Array.isArray(node)) {
|
||||
for (const item of node) addIntegerExamples(item)
|
||||
return
|
||||
}
|
||||
if (node === null || typeof node !== 'object') return
|
||||
|
||||
const obj = node as Record<string, unknown>
|
||||
if (obj.type === 'integer' && obj.example === undefined && obj.examples === undefined) {
|
||||
// Don't contradict a schema that really is narrow (`z.int().max(10)`, an enum-ish
|
||||
// range) — the placeholder only goes in where it's a legal value.
|
||||
const min = obj.minimum
|
||||
const max = obj.maximum
|
||||
const tooLow = typeof min === 'number' && PLACEHOLDER_INTEGER < min
|
||||
const tooHigh = typeof max === 'number' && PLACEHOLDER_INTEGER > max
|
||||
if (!tooLow && !tooHigh) obj.example = PLACEHOLDER_INTEGER
|
||||
}
|
||||
for (const value of Object.values(obj)) addIntegerExamples(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap `openAPIRouteHandler(...)` so the generated document gets example values for its
|
||||
* integer fields. Purely cosmetic — nothing about the documented shapes changes.
|
||||
*
|
||||
* ```ts
|
||||
* app.get('/openapi.json', describeRoute({ hide: true }), withCleanSpec(openAPIRouteHandler(app, { ... })))
|
||||
* ```
|
||||
*/
|
||||
export function withCleanSpec(handler: Handler | MiddlewareHandler): Handler {
|
||||
return async (c, next) => {
|
||||
const res = await (handler as Handler)(c, next)
|
||||
if (!(res instanceof Response)) return res as never
|
||||
const spec: unknown = await res.json()
|
||||
addIntegerExamples(spec)
|
||||
return c.json(spec as Record<string, unknown>)
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ export * from './helpers/env'
|
||||
export { logger } from './helpers/logger'
|
||||
export { getRequestLogData, type LogDataRequest } from './helpers/request'
|
||||
export * from './helpers/errors'
|
||||
export * from './helpers/openapi'
|
||||
export * from './helpers/url'
|
||||
export * from './middleware/withCache'
|
||||
export * from './middleware/withDefaultCors'
|
||||
|
||||
Reference in New Issue
Block a user