feat(server): REST API, auth, sources, changes, export
createApp() serves /api/v1 from a typed route table (zod bodies and queries), with session-cookie and Bearer app-token auth, a CSRF header for cookie writes, login/register rate limiting, body caps, JSON error mapping, OpenAPI generation and a 404 /ingest mount. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
7b0a6738dd
commit
6963e297b0
@@ -0,0 +1,64 @@
|
||||
import { z } from "zod";
|
||||
import type { RouteDef } from "./http.ts";
|
||||
|
||||
type JsonSchema = Record<string, unknown>;
|
||||
|
||||
const toSchema = (s: z.ZodType): JsonSchema => {
|
||||
const { $schema: _, ...rest } = z.toJSONSchema(s, { io: "input", unrepresentable: "any" }) as JsonSchema;
|
||||
return rest;
|
||||
};
|
||||
|
||||
const SECURITY: Record<RouteDef["auth"], { [k: string]: string[] }[]> = {
|
||||
none: [],
|
||||
session: [{ session: [] }],
|
||||
"token:read": [{ appToken: ["read"] }],
|
||||
"token:write": [{ appToken: ["write"] }],
|
||||
"any:read": [{ session: [] }, { appToken: ["read"] }],
|
||||
"any:write": [{ session: [] }, { appToken: ["write"] }],
|
||||
};
|
||||
|
||||
/** OpenAPI 3.1 document generated from the route table (paths, summaries, request schemas). */
|
||||
export function buildOpenApi(routes: RouteDef[], prefix = "/api/v1"): JsonSchema {
|
||||
const paths: Record<string, Record<string, JsonSchema>> = {};
|
||||
for (const r of routes) {
|
||||
const path = `${prefix}/${r.path.replace(/:(\w+)/g, "{$1}")}`;
|
||||
const parameters: JsonSchema[] = [...r.path.matchAll(/:(\w+)/g)].map((m) => ({
|
||||
name: m[1],
|
||||
in: "path",
|
||||
required: true,
|
||||
schema: { type: "string" },
|
||||
}));
|
||||
if (r.query) {
|
||||
const q = toSchema(r.query) as { properties?: Record<string, JsonSchema>; required?: string[] };
|
||||
for (const [name, schema] of Object.entries(q.properties ?? {})) {
|
||||
parameters.push({ name, in: "query", required: q.required?.includes(name) ?? false, schema });
|
||||
}
|
||||
}
|
||||
const op: JsonSchema = {
|
||||
operationId: `${r.method.toLowerCase()}_${r.path.replace(/[^\w]+/g, "_")}`,
|
||||
summary: r.summary,
|
||||
security: SECURITY[r.auth],
|
||||
responses: {
|
||||
default: {
|
||||
description: "Success, or an error: `{ error: { code, message } }`.",
|
||||
},
|
||||
},
|
||||
};
|
||||
if (parameters.length > 0) op.parameters = parameters;
|
||||
if (r.body) op.requestBody = { required: !r.body.safeParse(undefined).success, content: { "application/json": { schema: toSchema(r.body) } } };
|
||||
if (r.multipart) op.requestBody = { required: true, content: { "multipart/form-data": { schema: { type: "object" } } } };
|
||||
(paths[path] ??= {})[r.method.toLowerCase()] = op;
|
||||
}
|
||||
return {
|
||||
openapi: "3.1.0",
|
||||
info: { title: "STTracker API", version: "1" },
|
||||
servers: [{ url: "/" }],
|
||||
components: {
|
||||
securitySchemes: {
|
||||
session: { type: "apiKey", in: "cookie", name: "stt_session", description: "Non-GET requests also need X-Requested-With: sttracker." },
|
||||
appToken: { type: "http", scheme: "bearer", description: "App token with scopes read, write or ingest." },
|
||||
},
|
||||
},
|
||||
paths,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user