Files
recflare/apps/cdn/src/openapi.ts
T
Devin Zuczek a5136d2bfa cdn openapi
2026-07-24 21:48:10 -04:00

115 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { resolver } from 'hono-openapi'
import { z } from 'zod'
import type { OpenAPIV3_1 } from 'openapi-types'
/**
* OpenAPI schemas for the cdn 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/img 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.
*
* Most of this worker's surface is opaque BYTES, not JSON, so those responses are
* described with a binary content type rather than a zod schema (the same way the `img`
* worker describes image bytes).
*/
/** 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) } } }
}
/**
* A binary asset response. Everything streamed out of the bucket is served as
* `application/octet-stream` regardless of what it actually is — the client downloads
* these blobs, it never sniffs their type.
*/
export function assetBytes(description: string): OpenAPIV3_1.ResponseObject {
return {
description,
content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } },
}
}
/** The shared responses of every asset route: the byte-serving ones plus its failures. */
export function assetResponses(description: string): OpenAPIV3_1.ResponsesObject {
return {
200: assetBytes(description),
206: assetBytes('A byte range, when the request carried a `Range` header'),
304: { description: '`If-None-Match` matched the stored etag (no body)' },
400: { description: 'The key contains `..` (no body)' },
404: { description: 'No such object in the bucket' },
}
}
/** The `Range` / `If-None-Match` headers every asset route honours. */
export const CONDITIONAL_HEADERS: OpenAPIV3_1.ParameterObject[] = [
{
name: 'Range',
in: 'header',
required: false,
description:
'A single byte range (`bytes=start-end`, `bytes=start-`, `bytes=-suffix`). Honoured with a 206; a malformed or multi-range value is ignored and the whole object served.',
schema: { type: 'string', example: 'bytes=0-1023' },
},
{
name: 'If-None-Match',
in: 'header',
required: false,
description: 'The etag of a previously fetched copy; a match answers 304 with no body.',
schema: { type: 'string' },
},
]
/** A path parameter naming an object in the bucket. */
export function keyParam(
name: string,
description: string,
slashes: boolean
): OpenAPIV3_1.ParameterObject {
return {
name,
in: 'path',
required: true,
description: slashes ? `${description} May contain slashes.` : description,
schema: { type: 'string' },
}
}
// ---- Response schemas ------------------------------------------------------
/** `GET /` — the liveness probe body. */
export const ServiceStatus = z.object({
service: z.literal('cdn'),
status: z.literal('ok'),
})
/**
* One loading-screen tip. `Context`/`InputType`/`Visibility` are client-side enums that
* decide where a tip may appear, and `PlatformMask` is a bit field of the platforms it
* shows on — every tip in the bundled set is left at whatever the 2019 capture had.
*/
export const LoadingScreenTip = z.object({
Name: z.string().describe('A GUID (no dashes) — the tips id, not a display name'),
Title: z.string(),
Message: z.string(),
RoomNames: z
.array(z.string())
.describe('Rooms to restrict the tip to; empty everywhere in the bundled set'),
Context: z.int(),
InputType: z.int(),
Visibility: z.int(),
AllowCycling: z.boolean(),
RestrictToNewUsers: z.boolean(),
ImageName: z.string().describe('An image key the client resolves against the img worker'),
PlatformMask: z.int().describe('Bit field of the platforms the tip shows on'),
CreatedAt: z.string(),
})