@tetsujs/openapi 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +146 -0
- package/dist/src/annotations.d.ts +141 -0
- package/dist/src/annotations.d.ts.map +1 -0
- package/dist/src/components.d.ts +45 -0
- package/dist/src/components.d.ts.map +1 -0
- package/dist/src/controller.d.ts +91 -0
- package/dist/src/controller.d.ts.map +1 -0
- package/dist/src/document.d.ts +80 -0
- package/dist/src/document.d.ts.map +1 -0
- package/dist/src/emit.d.ts +40 -0
- package/dist/src/emit.d.ts.map +1 -0
- package/dist/src/index.d.ts +75 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +616 -0
- package/dist/src/index.js.map +17 -0
- package/dist/src/operation.d.ts +39 -0
- package/dist/src/operation.d.ts.map +1 -0
- package/dist/src/page.d.ts +54 -0
- package/dist/src/page.d.ts.map +1 -0
- package/dist/src/parameters.d.ts +47 -0
- package/dist/src/parameters.d.ts.map +1 -0
- package/package.json +52 -0
- package/src/annotations.ts +225 -0
- package/src/components.ts +90 -0
- package/src/controller.ts +175 -0
- package/src/document.ts +90 -0
- package/src/emit.ts +98 -0
- package/src/index.ts +203 -0
- package/src/operation.ts +583 -0
- package/src/page.ts +139 -0
- package/src/parameters.ts +145 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/annotations.ts", "../src/components.ts", "../src/emit.ts", "../src/parameters.ts", "../src/operation.ts", "../src/controller.ts", "../src/page.ts", "../src/index.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * What hooks contribute to the documentation.\n *\n * What a route is protected by, or answers with when a guard refuses, is\n * not in its config: a hook is a function in a slot, and the route that\n * mounts it says nothing about tokens or rate limits. The contribution is\n * therefore annotated onto the hook — from this package, not from\n * `hook.*` — so the core keeps knowing nothing about OpenAPI and the\n * factories stay free of documentation options.\n *\n * A hook annotated once is documented everywhere it runs: application and\n * group chains are merged into every route's chains at startup, so a guard\n * mounted on a zone documents the zone's operations without repeating\n * itself.\n *\n * @module\n */\n\nimport type { AnyHook, AnySchema } from \"@tetsujs/core\";\n\n/**\n * The definition of a scheme, as OpenAPI spells it.\n *\n * Typed as loosely as the spec is wide — `http`, `apiKey`, `oauth2` and\n * `openIdConnect` carry different fields, and narrowing them here would\n * be a second, poorer copy of the specification.\n */\nexport interface SecurityScheme {\n readonly type: \"http\" | \"apiKey\" | \"oauth2\" | \"openIdConnect\";\n readonly [key: string]: unknown;\n}\n\n/** What a hook demands of a request, and what it answers when refused. */\nexport interface SecurityRequirement {\n /** The name the scheme is registered under in `components`. */\n readonly name: string;\n\n /** The scheme itself, registered once per name. */\n readonly scheme: SecurityScheme;\n\n /** OAuth2 scopes; empty for every other type. */\n readonly scopes?: readonly string[];\n\n /** The status an unauthenticated request gets. Defaults to `401`. */\n readonly status?: number;\n\n /** How the refusal is described in the document. */\n readonly description?: string;\n\n /**\n * The `error` code the refusal carries, when the guard answers with the\n * framework's envelope — `\"UNAUTHORIZED\"`, `\"SESSION_EXPIRED\"`.\n *\n * Documented as a `const` for the same reason as on a\n * {@link DocumentedResponse}: only the guard knows its own code.\n */\n readonly error?: string;\n\n /** An example of the refusal's `message`, for the same reason. */\n readonly message?: string;\n}\n\n/**\n * One response a hook can produce, which the route's own schema knows\n * nothing about — a rate limiter's `429`, a guard's `401`.\n */\nexport interface DocumentedResponse {\n readonly status: number;\n readonly description: string;\n\n /** The body, when the hook answers with one. */\n readonly schema?: AnySchema;\n\n /**\n * The `error` code of the failure envelope, when the hook answers with\n * the framework's shape — `\"RATE_LIMITED\"`, `\"SESSION_EXPIRED\"`.\n *\n * Documented as a `const`, so a client can discriminate on it. Only the\n * hook knows its own code; without one the document says the field is a\n * string and no more.\n */\n readonly error?: string;\n\n /** An example of the envelope's `message`, for the same reason. */\n readonly message?: string;\n}\n\n/** Everything a hook tells the generator about itself. */\nexport interface HookDocs {\n /** What the hook demands of a request. */\n readonly security?: SecurityRequirement;\n\n /** What the hook can answer with. */\n readonly responses?: readonly DocumentedResponse[];\n}\n\nconst docsKey = \"~tetsu/hook-docs\";\n\n/**\n * Annotates a hook with what it contributes to the documentation.\n *\n * Returns a copy: what the caller exports is what carries the annotation,\n * and a hook used elsewhere is not changed behind its author's back. The\n * type is preserved exactly, so an annotated hook goes into a stack like\n * any other.\n *\n * @example\n * ```ts\n * export const limiter = documented(hook.beforeParse(check), {\n * responses: [\n * { status: 429, description: \"Rate limit exceeded\", schema: TooMany },\n * ],\n * });\n * ```\n */\nexport function documented<H extends AnyHook>(hook: H, docs: HookDocs): H {\n const annotated = { ...hook };\n\n Object.defineProperty(annotated, docsKey, {\n enumerable: false,\n value: { ...docsOf(hook), ...docs },\n });\n\n return annotated as H;\n}\n\n/**\n * Annotates a hook with the security it enforces — {@link documented} for\n * the case that comes up most.\n *\n * @example\n * ```ts\n * export const auth = secured(\n * hook.beforeParse((ctx) => {\n * const user = verify(ctx.req.headers.get(\"authorization\"));\n * if (!user) throw new HttpError(401);\n * return { user };\n * }),\n * {\n * name: \"bearerAuth\",\n * scheme: { type: \"http\", scheme: \"bearer\", bearerFormat: \"JWT\" },\n * },\n * );\n * ```\n */\nexport function secured<H extends AnyHook>(\n hook: H,\n requirement: SecurityRequirement,\n): H {\n return documented(hook, { security: requirement });\n}\n\n/** Reads everything a hook was annotated with. */\nexport function docsOf(hook: AnyHook): HookDocs | undefined {\n return (hook as unknown as Record<string, HookDocs | undefined>)[docsKey];\n}\n\n/** Reads the security requirement a hook carries, if it carries one. */\nexport function securityOf(hook: AnyHook): SecurityRequirement | undefined {\n return docsOf(hook)?.security;\n}\n\n/** What the hooks of one route contribute, in execution order. */\nexport interface HookContributions {\n readonly security: readonly SecurityRequirement[];\n readonly responses: readonly DocumentedResponse[];\n}\n\n/**\n * Collects the contributions of a route's chains.\n *\n * A scheme is registered once per name and a response once per status and\n * description: the same guard on the application and on a group is one\n * requirement, not two.\n *\n * Deduplicating by name means two hooks claiming one name with different\n * schemes lose one of them, and that is not a duplicate being collapsed —\n * it is a requirement the route really has going undescribed. Passing\n * `warn` is how the caller hears about it; the generator does.\n */\nexport function contributionsOf(\n chains: Record<string, readonly AnyHook[]>,\n warn?: (message: string) => void,\n): HookContributions {\n const security = new Map<string, SecurityRequirement>();\n const responses = new Map<string, DocumentedResponse>();\n\n for (const slot of Object.values(chains)) {\n for (const hook of slot) {\n const docs = docsOf(hook);\n\n if (!docs) {\n continue;\n }\n\n if (docs.security) {\n const claimed = security.get(docs.security.name);\n\n if (!claimed) {\n security.set(docs.security.name, docs.security);\n } else if (\n JSON.stringify(claimed.scheme) !==\n JSON.stringify(docs.security.scheme)\n ) {\n warn?.(\n `two hooks claim the security scheme \"${docs.security.name}\" with different definitions; one of them is not described`,\n );\n }\n }\n\n for (const response of docs.responses ?? []) {\n const key = `${response.status} ${response.description}`;\n\n if (!responses.has(key)) {\n responses.set(key, response);\n }\n }\n }\n }\n\n return {\n security: [...security.values()],\n responses: [...responses.values()],\n };\n}\n",
|
|
6
|
+
"/**\n * The document's `components/schemas` section.\n *\n * The failures the framework fills in repeat across every operation — a\n * `500` on all of them, a `413` on every route with a body — and inlining\n * the same envelope each time makes a document that is mostly boilerplate.\n * They are defined once here and referenced with `$ref`, which is also how\n * a client generator ends up with one `MalformedJson` type instead of one\n * per endpoint.\n *\n * Only the framework's own shapes go here. A schema that came from a\n * validator is still inlined: it is the author's, it has no name the\n * generator could give it, and `$ref` would invent one.\n *\n * @module\n */\n\n/** A reference to a definition in `components/schemas`. */\nexport interface SchemaRef {\n readonly $ref: string;\n}\n\n/** The definitions collected while generating a document. */\nexport interface SchemaComponents {\n /**\n * Registers a schema and returns the reference to it.\n *\n * Identical schemas share one definition, whatever name each caller\n * asked for; different schemas that want the same name get a numbered\n * one, so a document is never silently wrong about what a `$ref` points\n * at.\n */\n ref(preferred: string, schema: Record<string, unknown>): SchemaRef;\n\n /** What was registered, in registration order. */\n readonly schemas: Record<string, unknown>;\n}\n\nexport function schemaComponents(): SchemaComponents {\n const schemas: Record<string, unknown> = {};\n const registered = new Map<string, string>();\n\n return {\n schemas,\n\n ref(preferred: string, schema: Record<string, unknown>): SchemaRef {\n const fingerprint = JSON.stringify(schema);\n const already = registered.get(fingerprint);\n\n if (already) {\n return { $ref: `#/components/schemas/${already}` };\n }\n\n let name = preferred;\n\n for (let attempt = 2; name in schemas; attempt += 1) {\n name = `${preferred}${attempt}`;\n }\n\n schemas[name] = schema;\n registered.set(fingerprint, name);\n\n return { $ref: `#/components/schemas/${name}` };\n },\n };\n}\n\n/**\n * The name a failure is defined under: its `error` code in PascalCase —\n * `MALFORMED_JSON` becomes `MalformedJson` — so the definition and the code\n * a client branches on read as the same thing.\n *\n * A failure whose code only the hook knows, and which the hook did not\n * declare, is named after its status instead.\n */\nexport function failureName(status: number, error?: string): string {\n const name = (error ?? \"\")\n .toLowerCase()\n .split(/[^a-z0-9]+/)\n .filter(Boolean)\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join(\"\");\n\n // A code with nothing ASCII in it sanitizes to nothing — a code in\n // Cyrillic or Japanese, or one that is all punctuation. That is the same\n // situation as no code at all, and gets the same name: the alternative\n // was a component keyed `\"\"`, which the specification does not allow, and\n // a second such code keyed `\"2\"` by the collision numbering below it.\n return name || `Failure${status}`;\n}\n",
|
|
7
|
+
"/**\n * Asking a schema to describe itself, without letting it stop the document.\n *\n * Internal to the package.\n *\n * @module\n */\n\nimport { toJsonSchema } from \"@tetsujs/core\";\n\n/**\n * The JSON Schema dialect a schema is asked for.\n *\n * OpenAPI 3.1 takes JSON Schema 2020-12 as it is, and `draft-2020-12` is a\n * target the Standard JSON Schema specification names — so every library\n * that implements it answers. This used to be `openapi-3.1`, which only\n * Zod accepts: ArkType and Valibot refused it, and their bodies went into\n * the document without a shape and with a warning each.\n */\nconst target = { target: \"draft-2020-12\" } as const;\n\n/**\n * A JSON Schema as the generator reads it: any keyword, with the two it\n * looks into named, so reading them does not depend on the reader's\n * `noPropertyAccessFromIndexSignature`.\n */\nexport interface JsonSchemaObject {\n properties?: unknown;\n required?: unknown;\n [keyword: string]: unknown;\n}\n\n/** Which shape of a schema to describe: what arrives, or what leaves. */\ntype Direction = \"input\" | \"output\";\n\n/**\n * The JSON Schema of a declaration, or `undefined` and a warning saying why.\n *\n * A schema can fail to describe itself in two ways, and the generator owes\n * the same answer to both: an adapter without JSON Schema support returns\n * nothing, and an adapter that has it can still throw — a converter meeting\n * a shape it cannot express, a structure too deep to walk, a `$ref` that\n * leads back to where it started.\n *\n * Only the first was handled, and the difference between them is not the\n * document: it is whether the application starts at all. `docs()` builds\n * the document from `onMount`, which `createApp` calls while it is still\n * assembling itself, so an escaping error is not a gap in a description —\n * it is a server that never listens, over a route that would have worked.\n *\n * `subject` names the declaration for the message, as the reader of a\n * warning would name it: `\"the body\"`, `\"a response\"`.\n */\nexport function emitted(\n schema: unknown,\n subject: string,\n warn: (message: string) => void,\n direction: Direction = \"output\",\n): JsonSchemaObject | undefined {\n let described: JsonSchemaObject | undefined;\n\n try {\n described = toJsonSchema(schema, target, direction);\n } catch (error) {\n warn(`${subject} schema failed to emit JSON Schema: ${reason(error)}`);\n\n return undefined;\n }\n\n if (!described) {\n warn(`${subject} schema does not emit JSON Schema`);\n\n return undefined;\n }\n\n return withoutDialect(described);\n}\n\n/**\n * A schema as the document embeds it: without its own `$schema`.\n *\n * Asked for draft 2020-12, Zod and ArkType name the dialect at the root of\n * every schema they emit. The document declares it once for all of them,\n * and the same line on every body and response is noise.\n */\nfunction withoutDialect(schema: JsonSchemaObject): JsonSchemaObject {\n if (!(\"$schema\" in schema)) {\n return schema;\n }\n\n const { $schema: _, ...rest } = schema;\n\n return rest;\n}\n\nfunction reason(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n",
|
|
8
|
+
"/**\n * Path templates and request parameters.\n *\n * The framework's path syntax is Bun's — `:param` and a trailing `*` —\n * while OpenAPI templates variables as `{param}`. The conversion happens\n * here, together with the parameter objects the template then requires:\n * OpenAPI demands that every variable in a path be declared.\n *\n * @module\n */\n\nimport type { AnySchema } from \"@tetsujs/core\";\nimport type { ParameterObject } from \"./document.ts\";\nimport { emitted } from \"./emit.ts\";\n\n/** The name a wildcard segment is documented under. */\nexport const wildcardName = \"wildcard\";\n\n/**\n * Rewrites a route path as an OpenAPI path template.\n *\n * A wildcard becomes an ordinary variable: OpenAPI has no syntax for\n * \"everything below here\", so the closest honest template is a named\n * segment, documented as such.\n */\nexport function toTemplate(path: string): string {\n return path\n .split(\"/\")\n .map((segment) => {\n if (segment.startsWith(\":\")) {\n return `{${segment.slice(1)}}`;\n }\n\n return segment === \"*\" ? `{${wildcardName}}` : segment;\n })\n .join(\"/\");\n}\n\n/** The variables a path template declares, in order. */\nexport function templateNames(path: string): string[] {\n const names: string[] = [];\n\n for (const segment of path.split(\"/\")) {\n if (segment.startsWith(\":\")) {\n names.push(segment.slice(1));\n } else if (segment === \"*\") {\n names.push(wildcardName);\n }\n }\n\n return names;\n}\n\n/**\n * Describes the path parameters of a route.\n *\n * A `params` schema describes the whole object, so each variable takes the\n * property of the same name; without one — or without a property for a\n * given variable — the value is what the router captured: a string.\n */\nexport function pathParameters(\n path: string,\n schema: AnySchema | undefined,\n warn: (message: string) => void,\n): ParameterObject[] {\n const properties = objectProperties(schema, \"params\", warn);\n\n return templateNames(path).map((name) => ({\n name,\n in: \"path\" as const,\n required: true,\n ...(name === wildcardName\n ? {\n description:\n \"Matches the rest of the path, including further segments.\",\n }\n : {}),\n schema: properties[name] ?? { type: \"string\" },\n }));\n}\n\n/**\n * Describes the query, header or cookie parameters of a route.\n *\n * All three are objects at the schema level and lists of parameters in a\n * document, so each declared property becomes one parameter, required when\n * the schema says so.\n *\n * A session cookie is the exception the specification makes itself: a\n * credential belongs in `securitySchemes` as `apiKey in: cookie`, not\n * among the parameters. Nothing here can tell one cookie from the other —\n * that is what an annotation on the hook reading it is for, the same way\n * `secured()` marks an authenticating hook.\n */\nexport function partParameters(\n schema: AnySchema | undefined,\n where: \"query\" | \"header\" | \"cookie\",\n warn: (message: string) => void,\n): ParameterObject[] {\n if (!schema) {\n return [];\n }\n\n const described = emitted(schema, `the ${where}`, warn, \"input\");\n\n if (!described) {\n return [];\n }\n\n const properties = (described.properties ?? {}) as Record<\n string,\n Record<string, unknown>\n >;\n const required = new Set(\n Array.isArray(described.required) ? (described.required as string[]) : [],\n );\n\n return Object.entries(properties).map(([name, property]) => ({\n name,\n in: where,\n required: required.has(name),\n schema: property,\n }));\n}\n\nfunction objectProperties(\n schema: AnySchema | undefined,\n where: string,\n warn: (message: string) => void,\n): Record<string, Record<string, unknown>> {\n if (!schema) {\n return {};\n }\n\n const described = emitted(schema, `the ${where}`, warn, \"input\");\n\n if (!described) {\n return {};\n }\n\n return (described.properties ?? {}) as Record<\n string,\n Record<string, unknown>\n >;\n}\n",
|
|
9
|
+
"/**\n * One route table entry, described as an OpenAPI operation.\n *\n * Bodies and responses live here; parameters are in `parameters.ts`. The\n * error responses a route can produce without the author writing anything\n * — a rejected schema, an unparsable body, a body over the limit — are\n * derived from what the route declares and from the options the\n * application actually runs with.\n *\n * @module\n */\n\nimport type {\n AnySchema,\n AppOptions,\n BodyType,\n RouteTableEntry,\n} from \"@tetsujs/core\";\nimport type { HookContributions } from \"./annotations.ts\";\nimport { contributionsOf } from \"./annotations.ts\";\nimport type { SchemaComponents } from \"./components.ts\";\nimport { failureName } from \"./components.ts\";\nimport type {\n ContentMap,\n OperationObject,\n ResponseObject,\n} from \"./document.ts\";\nimport type { JsonSchemaObject } from \"./emit.ts\";\nimport { emitted } from \"./emit.ts\";\nimport { partParameters, pathParameters } from \"./parameters.ts\";\n\n/** Describes one route. */\nexport function operationOf(\n entry: RouteTableEntry,\n options: AppOptions,\n components: SchemaComponents,\n ids: OperationIds,\n warn: (message: string) => void,\n): OperationObject {\n const schema = entry.def.schema;\n\n const parameters = [\n ...pathParameters(entry.path, schema?.params, warn),\n ...partParameters(schema?.query, \"query\", warn),\n ...partParameters(schema?.headers, \"header\", warn),\n ...partParameters(schema?.cookies, \"cookie\", warn),\n ];\n\n const body = requestBody(entry, warn);\n const contributed = contributionsOf(entry.hooks, warn);\n\n return {\n operationId: ids.take(entry),\n ...(entry.def.docs?.summary === undefined\n ? {}\n : { summary: entry.def.docs.summary }),\n ...(entry.def.docs?.description === undefined\n ? {}\n : { description: entry.def.docs.description }),\n ...(entry.def.docs?.tags === undefined\n ? {}\n : { tags: entry.def.docs.tags }),\n ...(entry.def.docs?.deprecated === undefined\n ? {}\n : { deprecated: entry.def.docs.deprecated }),\n ...(parameters.length > 0 ? { parameters } : {}),\n ...(body ? { requestBody: body } : {}),\n ...(contributed.security.length > 0\n ? {\n security: contributed.security.map((requirement) => ({\n [requirement.name]: requirement.scopes ?? [],\n })),\n }\n : {}),\n responses: responses(entry, options, contributed, components, warn),\n };\n}\n\n/**\n * Hands out the `operationId` of every route in one document.\n *\n * Uniqueness is a requirement of the specification and cannot be decided\n * one route at a time: the pair a name is built from — the controller and\n * the field — is not unique on its own. One instance mounted under two\n * prefixes is a documented pattern (`/v1` and `/v2` from the same\n * controller), and two controller classes in different modules may simply\n * share a class name. Either way the pair repeats while the routes differ.\n *\n * A repeat falls back to what the route is named by when it has no field\n * at all — its method and path — because that is derived from the route\n * rather than from the order routes were visited in. A numbered name is\n * the last resort only, since it would move between documents as routes\n * are added, and an `operationId` is what a generated client calls its\n * method.\n */\nexport interface OperationIds {\n /** The id for this route, unique within the document being built. */\n take(entry: RouteTableEntry): string;\n}\n\nexport function operationIds(): OperationIds {\n const taken = new Set<string>();\n\n return {\n take(entry: RouteTableEntry): string {\n const preferred = declaredId(entry) ?? derivedId(entry);\n\n if (!taken.has(preferred)) {\n taken.add(preferred);\n\n return preferred;\n }\n\n const derived = derivedId(entry);\n\n if (!taken.has(derived)) {\n taken.add(derived);\n\n return derived;\n }\n\n let name = derived;\n\n for (let attempt = 2; taken.has(name); attempt += 1) {\n name = `${derived}${attempt}`;\n }\n\n taken.add(name);\n\n return name;\n },\n };\n}\n\n/**\n * The name the controller gave it: the field, qualified by the controller\n * — `usersSetAvatar` for `setAvatar` on `UsersController`.\n */\nfunction declaredId(entry: RouteTableEntry): string | undefined {\n if (entry.name === undefined) {\n return undefined;\n }\n\n const controller = entry.controller.replace(/Controller$/, \"\");\n\n return `${lowerFirst(controller)}${capitalize(entry.name)}`;\n}\n\n/**\n * What identifies a route when its field does not: its method and path.\n *\n * Also the name of a route mounted standalone, which has no field to be\n * called after.\n */\nfunction derivedId(entry: RouteTableEntry): string {\n return `${entry.method.toLowerCase()}${entry.path\n .split(/[/:*]/)\n .filter(Boolean)\n .map(capitalize)\n .join(\"\")}`;\n}\n\n/**\n * Describes the request body, when the route reads one.\n *\n * The media type follows the declaration, not a guess: `\"form\"` accepts\n * both multipart and urlencoded at runtime, so both are listed — unless\n * the schema declares a binary field, which only multipart can carry.\n */\nfunction requestBody(\n entry: RouteTableEntry,\n warn: (message: string) => void,\n): { required: boolean; content: ContentMap } | undefined {\n const declared = entry.def.schema?.body;\n const bodyType = entry.def.bodyType ?? (declared ? \"json\" : undefined);\n\n if (!bodyType) {\n return undefined;\n }\n\n const described = declared\n ? emitted(declared, \"the body\", warn, \"input\")\n : undefined;\n\n const schema = described ? { schema: described } : {};\n\n // Whether a body must be sent is a question about the wire, not about\n // the schema: `json` and `form` parse it before anything is validated,\n // and an empty one fails that parse with a `400` — so the client has to\n // send something whether or not a shape was declared. `text` has no such\n // step, because an absent body reads as `\"\"` just like an empty one; if\n // that string is unacceptable, the schema is where it says so. Neither\n // has `stream`: an absent body is handed over as an empty stream.\n const required = bodyType !== \"text\" && bodyType !== \"stream\";\n\n if (bodyType === \"stream\") {\n /**\n * Bytes with no shape to describe: the route takes the body unread, so\n * the document says what it is rather than what is in it. A `stream`\n * route cannot carry a body schema at all — the compiler refuses the\n * pair — so there is never a `schema` to attach here.\n */\n return { required, content: { \"application/octet-stream\": {} } };\n }\n\n if (bodyType === \"text\") {\n return { required, content: { \"text/plain\": schema } };\n }\n\n if (bodyType === \"json\") {\n return { required, content: { \"application/json\": schema } };\n }\n\n const content: ContentMap = { \"multipart/form-data\": schema };\n\n if (!described || !carriesBinary(described)) {\n content[\"application/x-www-form-urlencoded\"] = schema;\n }\n\n return { required, content };\n}\n\n/**\n * Whether any part of an emitted schema describes binary content.\n *\n * Walked with a stack rather than by recursion, and remembering what it has\n * already seen: the shape comes from an adapter, so its depth is the\n * author's to choose and nothing says it is a tree. Recursion would answer\n * a deep one by overflowing the stack, and a self-referential one by never\n * returning — both while `createApp` is still assembling the application.\n */\nfunction carriesBinary(root: unknown): boolean {\n const pending: unknown[] = [root];\n const seen = new Set<unknown>();\n\n while (pending.length > 0) {\n const node = pending.pop();\n\n if (node === null || typeof node !== \"object\" || seen.has(node)) {\n continue;\n }\n\n seen.add(node);\n\n if ((node as { contentEncoding?: unknown }).contentEncoding === \"binary\") {\n return true;\n }\n\n for (const value of Object.values(node)) {\n pending.push(value);\n }\n }\n\n return false;\n}\n\n/**\n * Describes what the route answers with.\n *\n * The declared responses come first; the framework's own failures fill in\n * around them, and never over them — an author who documented `422`\n * himself has said something more specific than this could.\n */\nfunction responses(\n entry: RouteTableEntry,\n options: AppOptions,\n contributed: HookContributions,\n components: SchemaComponents,\n warn: (message: string) => void,\n): Record<string, ResponseObject> {\n const declared: Record<string, ResponseObject> = {};\n const response = entry.def.schema?.response;\n\n if (response && \"~standard\" in response) {\n declared[\"200\"] = jsonResponse(\n response as AnySchema,\n \"Successful response\",\n warn,\n );\n } else if (response) {\n for (const [status, schema] of Object.entries(response)) {\n declared[status] =\n schema === null\n ? { description: describeStatus(status) }\n : jsonResponse(schema, describeStatus(status), warn);\n }\n }\n\n if (!declared[\"200\"] && !hasSuccess(declared)) {\n declared[\"200\"] = { description: \"Successful response\" };\n }\n\n return {\n ...declared,\n ...frameworkFailures(\n entry,\n options,\n contributed,\n declared,\n components,\n warn,\n ),\n };\n}\n\nfunction frameworkFailures(\n entry: RouteTableEntry,\n options: AppOptions,\n contributed: HookContributions,\n declared: Record<string, ResponseObject>,\n components: SchemaComponents,\n warn: (message: string) => void,\n): Record<string, ResponseObject> {\n const schema = entry.def.schema;\n const failures = new Map<string, Failure[]>();\n\n const add = (status: number, failure: Failure): void => {\n const key = String(status);\n\n failures.set(key, [...(failures.get(key) ?? []), failure]);\n };\n\n /** Defines the envelope of one failure once, and refers to it. */\n const envelopeRef = (\n status: number,\n error?: string,\n message?: string,\n ): Record<string, unknown> =>\n components.ref(\n failureName(status, error),\n envelope(status, error, message),\n ) as unknown as Record<string, unknown>;\n\n if (schema?.params || schema?.query || schema?.headers || schema?.body) {\n add(options.validationStatus, {\n description: \"Request failed schema validation\",\n schema: components.ref(\n failureName(options.validationStatus, \"VALIDATION_FAILED\"),\n validationFailed(options.validationStatus),\n ) as unknown as Record<string, unknown>,\n });\n }\n\n for (const requirement of contributed.security) {\n const status = requirement.status ?? 401;\n\n add(status, {\n description:\n requirement.description ??\n `Request did not satisfy ${requirement.name}`,\n schema: envelopeRef(status, requirement.error, requirement.message),\n });\n }\n\n for (const response of contributed.responses) {\n const described = response.schema\n ? emitted(response.schema, \"a hook's response\", warn)\n : undefined;\n\n add(response.status, {\n description: response.description,\n schema:\n described ??\n envelopeRef(response.status, response.error, response.message),\n });\n }\n\n if (schema?.body || entry.def.bodyType) {\n const unparsable = parseFailure(entry.def.bodyType);\n\n if (unparsable) {\n add(400, {\n description: \"Body could not be parsed in the declared shape\",\n schema: envelopeRef(400, unparsable.error, unparsable.message),\n });\n }\n\n add(413, {\n description: \"Body exceeded the configured size limit\",\n schema: envelopeRef(\n 413,\n \"BODY_TOO_LARGE\",\n \"Body exceeds the configured limit\",\n ),\n });\n }\n\n add(500, {\n description: \"The request failed and nothing mapped the failure\",\n schema: envelopeRef(500, \"INTERNAL_SERVER_ERROR\", \"Internal Server Error\"),\n });\n\n const responses: Record<string, ResponseObject> = {};\n\n for (const [status, list] of failures) {\n responses[status] = merge(declared[status], list);\n }\n\n return responses;\n}\n\n/** One way the framework itself can answer. */\ninterface Failure {\n readonly description: string;\n readonly schema: Record<string, unknown>;\n}\n\n/**\n * Folds everything that answers with one status into one response.\n *\n * Three things can meet on a status: what the route declared, and any\n * number of failures the framework fills in — a rejected schema and an\n * unparsable body collide on `400` as soon as an application configures\n * `validation: { status: 400 }`, and a route that answers `404` itself\n * shares that status with nothing while sharing `400` with the parser.\n *\n * All of them are true at once, so the document says so with an `anyOf`\n * rather than picking a winner. The route's own body comes first: it is\n * the answer the endpoint is about, and the failures are what can happen\n * to it. Identical bodies are folded together — a `$ref` repeated is one\n * alternative, not two.\n */\nfunction merge(\n declared: ResponseObject | undefined,\n list: readonly Failure[],\n): ResponseObject {\n const descriptions = [\n ...(declared ? [declared.description] : []),\n ...list.map((failure) => failure.description),\n ];\n\n const schemas = distinct([\n ...declaredSchemas(declared),\n ...list.map((failure) => failure.schema),\n ]);\n\n const [only] = schemas;\n\n return {\n description: descriptions.join(\"; \"),\n ...(schemas.length === 1 && only\n ? { content: { \"application/json\": { schema: only } } }\n : { content: { \"application/json\": { schema: { anyOf: schemas } } } }),\n };\n}\n\n/**\n * The body a declared response describes, if it describes one: a response\n * whose schema could not be emitted contributes its description alone.\n */\nfunction declaredSchemas(\n declared: ResponseObject | undefined,\n): Record<string, unknown>[] {\n const schema = declared?.content?.[\"application/json\"]?.schema;\n\n return schema ? [schema] : [];\n}\n\nfunction distinct(\n schemas: readonly Record<string, unknown>[],\n): Record<string, unknown>[] {\n const seen = new Set<string>();\n\n return schemas.filter((schema) => {\n const fingerprint = JSON.stringify(schema);\n\n if (seen.has(fingerprint)) {\n return false;\n }\n\n seen.add(fingerprint);\n\n return true;\n });\n}\n\nfunction jsonResponse(\n schema: unknown,\n description: string,\n warn: (message: string) => void,\n): ResponseObject {\n const described = emitted(schema, \"a response\", warn);\n\n if (!described) {\n return { description };\n }\n\n return {\n description,\n content: { \"application/json\": { schema: described } },\n };\n}\n\nfunction hasSuccess(declared: Record<string, ResponseObject>): boolean {\n return Object.keys(declared).some((status) => status.startsWith(\"2\"));\n}\n\nfunction describeStatus(status: string): string {\n if (status === \"204\") {\n return \"No content\";\n }\n\n return status.startsWith(\"2\") ? \"Successful response\" : `Response ${status}`;\n}\n\n/**\n * The body of one framework-produced failure, as JSON Schema.\n *\n * The status is a `const`, and so is the code whenever the framework knows\n * which one it will be: a generated client can then discriminate on\n * `error` instead of receiving \"an object with three strings\", and a\n * reader of the document sees `\"BODY_TOO_LARGE\"` rather than `string`.\n *\n * The message is documented by example, never as a `const` — it is wording\n * meant for a human, the one field a client must not match on, and the one\n * this project reserves the right to reword.\n */\nfunction envelope(\n status: number,\n error?: string,\n message?: string,\n): JsonSchemaObject {\n return {\n type: \"object\",\n required: [\"status\", \"message\", \"error\"],\n properties: {\n status: { type: \"integer\", const: status },\n message: message\n ? { type: \"string\", examples: [message] }\n : { type: \"string\" },\n error: error ? { type: \"string\", const: error } : { type: \"string\" },\n },\n };\n}\n\n/** The validation envelope: the standard three fields, plus the issues. */\nfunction validationFailed(status: number): Record<string, unknown> {\n const base = envelope(status, \"VALIDATION_FAILED\", \"Validation failed\");\n\n return {\n ...base,\n required: [\"status\", \"message\", \"error\", \"issues\"],\n properties: {\n ...(base.properties as Record<string, unknown>),\n issues: {\n type: \"array\",\n items: {\n type: \"object\",\n required: [\"message\", \"path\"],\n properties: {\n message: { type: \"string\" },\n path: { type: \"array\", items: { type: [\"string\", \"number\"] } },\n },\n },\n },\n },\n };\n}\n\n/**\n * How a body of this type fails to parse — or nothing, for a text or a\n * stream body: one is whatever bytes arrived, the other is handed over\n * unread, and neither can be malformed.\n */\nfunction parseFailure(\n bodyType: BodyType | undefined,\n): { error: string; message: string } | undefined {\n if (bodyType === \"text\" || bodyType === \"stream\") {\n return undefined;\n }\n\n return bodyType === \"form\"\n ? { error: \"MALFORMED_FORM\", message: \"Body is not a valid form\" }\n : { error: \"MALFORMED_JSON\", message: \"Body is not valid JSON\" };\n}\n\nfunction capitalize(value: string): string {\n return value.charAt(0).toUpperCase() + value.slice(1);\n}\n\nfunction lowerFirst(value: string): string {\n return value.charAt(0).toLowerCase() + value.slice(1);\n}\n",
|
|
10
|
+
"/**\n * The documentation endpoints as a mountable controller.\n *\n * `openapi()` returns data and mounts nothing, which is the right default\n * for a generator but a chore in the ordinary case: every application ends\n * up writing the same controller, holding the same application, to serve\n * the same two routes. {@link docs} is that controller, written once.\n *\n * It is a controller like any other — mounted in `routes`, groupable,\n * hookable — that receives the application through `onMount` and generates\n * the document from it at startup.\n *\n * @module\n */\n\nimport type { App, RouteDef, SchemaConfig, ValidatePath } from \"@tetsujs/core\";\nimport { onMount, route } from \"@tetsujs/core\";\nimport type { OpenApiDocument } from \"./document.ts\";\nimport type { GeneratorWarning } from \"./index.ts\";\nimport { openapi } from \"./index.ts\";\nimport type { DocsAssets, DocsUi } from \"./page.ts\";\nimport { docsPage } from \"./page.ts\";\n\n/** What the documentation controller serves, and from where. */\nexport interface DocsOptions<Path extends string, UiPath extends string> {\n /** Title, version and the rest of the document's `info` block. */\n readonly info: OpenApiDocument[\"info\"];\n\n /** Servers the API is reachable at. */\n readonly servers?: OpenApiDocument[\"servers\"];\n\n /** Where the document is served. Defaults to `/openapi.json`. */\n readonly path?: Path & ValidatePath<Path>;\n\n /** Where the page is served. Defaults to `/docs`. */\n readonly uiPath?: UiPath & ValidatePath<UiPath>;\n\n /** Which renderer the page bootstraps. Defaults to `\"scalar\"`. */\n readonly ui?: DocsUi;\n\n /** Browser title of the page. Defaults to the document's own title. */\n readonly title?: string;\n\n /** Overrides the renderer's CDN URLs, for a pinned or self-hosted copy. */\n readonly assets?: DocsAssets;\n\n /**\n * Documents the two endpoints this controller serves.\n *\n * Off by default: a reader who is looking at the document has already\n * found the document, and an operation for \"the page you are reading\"\n * is one a generated client would happily produce a method for.\n */\n readonly documentSelf?: boolean;\n\n /**\n * What to do with what the generator could not describe.\n *\n * Defaults to one `console.warn` per warning, at startup — a document\n * that silently omits a body is worse than a noisy one.\n */\n readonly onWarning?: (warning: GeneratorWarning) => void;\n}\n\n/**\n * The controller {@link docs} returns: two routes and the `onMount` that\n * fills them.\n *\n * Instantiated through the factory, never directly — the routes carry the\n * configured paths in their types, which only the factory can infer.\n */\nexport class DocsController<Path extends string, UiPath extends string> {\n /** The document itself, as `application/json`. */\n readonly json: RouteDef<Path, SchemaConfig, never, undefined, \"GET\">;\n\n /** The page that renders it. */\n readonly ui: RouteDef<UiPath, SchemaConfig, never, undefined, \"GET\">;\n\n private document: OpenApiDocument | undefined;\n\n constructor(\n private readonly options: DocsOptions<Path, UiPath>,\n documentPath: string,\n uiPath: string,\n page: string,\n ) {\n const hidden = !options.documentSelf;\n\n const json = route({\n method: \"GET\",\n path: \"/openapi.json\",\n docs: { summary: \"This document\", tags: [\"docs\"], hidden },\n handler: () => this.document,\n });\n\n const ui = route({\n method: \"GET\",\n path: \"/docs\",\n docs: { summary: \"Rendered documentation\", tags: [\"docs\"], hidden },\n handler: () =>\n new Response(page, {\n headers: { \"content-type\": \"text/html; charset=utf-8\" },\n }),\n });\n\n /**\n * The configured paths, put back on the routes.\n *\n * `route()` validates its path at the type level, and that check\n * cannot be satisfied by a type variable — `ValidatePath<Path>` stays\n * unresolved while `Path` is generic. The routes are therefore built\n * with the default literals and re-typed here, which is also where the\n * configured path lands at runtime.\n */\n this.json = { ...json, path: documentPath } as unknown as this[\"json\"];\n this.ui = { ...ui, path: uiPath } as unknown as this[\"ui\"];\n }\n\n /** Generates the document of the application this was mounted in. */\n [onMount](app: App): void {\n const generated = openapi(app, {\n info: this.options.info,\n ...(this.options.servers ? { servers: this.options.servers } : {}),\n });\n\n this.document = generated.document;\n\n const report = this.options.onWarning ?? warn;\n\n for (const warning of generated.warnings) {\n report(warning);\n }\n }\n}\n\n/**\n * The documentation of an application, as a controller to mount.\n *\n * @example\n * ```ts\n * createApp({\n * routes: [\n * group(\"/api\", { children: [new UsersController(users)] }),\n * docs({ info: { title: \"Users API\", version: \"1.0.0\" } }),\n * ],\n * });\n * ```\n *\n * The document is generated once, at startup, from the application the\n * controller was mounted in. Its own two routes stay out of it — pass\n * `documentSelf` to put them in.\n *\n * The page loads its renderer from a CDN; see {@link docsPage} for the\n * trade and for how to point it at a self-hosted copy.\n */\nexport function docs<\n const Path extends string = \"/openapi.json\",\n const UiPath extends string = \"/docs\",\n>(options: DocsOptions<Path, UiPath>): DocsController<Path, UiPath> {\n const documentPath = options.path ?? \"/openapi.json\";\n const uiPath = options.uiPath ?? \"/docs\";\n\n const page = docsPage({\n ui: options.ui ?? \"scalar\",\n documentUrl: documentPath,\n title: options.title ?? options.info.title,\n ...(options.assets ? { assets: options.assets } : {}),\n });\n\n return new DocsController(options, documentPath, uiPath, page);\n}\n\nfunction warn(warning: GeneratorWarning): void {\n console.warn(`[openapi] ${warning.route}: ${warning.message}`);\n}\n",
|
|
11
|
+
"/**\n * The HTML page that renders a document.\n *\n * Returns a string and mounts nothing: which path serves the page, and\n * whether one is served at all, is the application's decision like every\n * other route.\n *\n * The renderers themselves are loaded from a CDN. Vendoring Scalar or\n * Swagger UI would put megabytes of somebody else's bundle into a\n * dependency-free repository, so the trade is stated instead of hidden:\n * the documentation page — and only it — needs the network, and the URL is\n * a parameter, so a self-hosted copy is one option away. Nothing is\n * fetched unless this function is called.\n *\n * @module\n */\n\n/** The renderers this package knows how to bootstrap. */\nexport type DocsUi = \"scalar\" | \"swagger-ui\" | \"redoc\";\n\n/** Where a renderer is loaded from. */\nexport interface DocsAssets {\n readonly script: string;\n readonly style?: string;\n}\n\n/** What the page needs to know. */\nexport interface DocsPageOptions {\n /** Which renderer to bootstrap. */\n readonly ui: DocsUi;\n\n /** Where the document is served — the page fetches it from there. */\n readonly documentUrl: string;\n\n /** Browser title; defaults to `\"API documentation\"`. */\n readonly title?: string;\n\n /** Overrides the CDN URLs, for a pinned version or a self-hosted copy. */\n readonly assets?: DocsAssets;\n}\n\nconst defaults: Record<DocsUi, DocsAssets> = {\n scalar: {\n script: \"https://cdn.jsdelivr.net/npm/@scalar/api-reference\",\n },\n \"swagger-ui\": {\n script:\n \"https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js\",\n style: \"https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css\",\n },\n redoc: {\n script: \"https://cdn.jsdelivr.net/npm/redoc@2/bundles/redoc.standalone.js\",\n },\n};\n\n/**\n * Builds the documentation page.\n *\n * Called by `docs()` for the page it serves; call it directly to serve one\n * from somewhere else.\n *\n * @example\n * ```ts\n * const page = docsPage({ ui: \"scalar\", documentUrl: \"/openapi.json\" });\n *\n * const ui = route({\n * method: \"GET\",\n * path: \"/docs\",\n * handler: () =>\n * new Response(page, { headers: { \"content-type\": \"text/html\" } }),\n * });\n * ```\n */\nexport function docsPage(options: DocsPageOptions): string {\n const assets = options.assets ?? defaults[options.ui];\n const title = escapeHtml(options.title ?? \"API documentation\");\n const url = escapeHtml(options.documentUrl);\n\n return [\n \"<!doctype html>\",\n '<html lang=\"en\">',\n \"<head>\",\n '<meta charset=\"utf-8\" />',\n '<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />',\n `<title>${title}</title>`,\n assets.style\n ? `<link rel=\"stylesheet\" href=\"${escapeHtml(assets.style)}\" />`\n : \"\",\n \"</head>\",\n \"<body>\",\n body(options.ui, url),\n `<script src=\"${escapeHtml(assets.script)}\"></script>`,\n bootstrap(options.ui, url),\n \"</body>\",\n \"</html>\",\n ]\n .filter(Boolean)\n .join(\"\\n\");\n}\n\nfunction body(ui: DocsUi, url: string): string {\n if (ui === \"scalar\") {\n return `<script id=\"api-reference\" data-url=\"${url}\"></script>`;\n }\n\n if (ui === \"redoc\") {\n return `<redoc spec-url=\"${url}\"></redoc>`;\n }\n\n return '<div id=\"swagger-ui\"></div>';\n}\n\nfunction bootstrap(ui: DocsUi, url: string): string {\n if (ui !== \"swagger-ui\") {\n return \"\";\n }\n\n return [\n \"<script>\",\n `SwaggerUIBundle({ url: \"${url}\", dom_id: \"#swagger-ui\" });`,\n \"</script>\",\n ].join(\"\\n\");\n}\n\n/**\n * Escapes the values that reach the markup.\n *\n * The document URL and the title come from the application, but they are\n * still values interpolated into HTML — a quote in either would break out\n * of the attribute it sits in.\n */\nfunction escapeHtml(value: string): string {\n return value\n .replaceAll(\"&\", \"&\")\n .replaceAll(\"<\", \"<\")\n .replaceAll(\">\", \">\")\n .replaceAll('\"', \""\")\n .replaceAll(\"'\", \"'\");\n}\n",
|
|
12
|
+
"/**\n * OpenAPI generation for the framework.\n *\n * Reads a compiled application — `app.entries` and the options it actually\n * runs with — and returns an OpenAPI 3.1 document. Nothing is registered,\n * served or mutated: the document is data, and serving it is a route the\n * application declares like any other.\n *\n * Serving it is a route the application declares — or `docs()`, the\n * controller that declares those routes for you.\n *\n * @example\n * ```ts\n * const { document, warnings } = openapi(app, {\n * info: { title: \"Users API\", version: \"1.0.0\" },\n * });\n * ```\n *\n * @module\n */\n\nimport type { App } from \"@tetsujs/core\";\nimport { contributionsOf } from \"./annotations.ts\";\nimport { schemaComponents } from \"./components.ts\";\nimport type { OpenApiDocument, PathItemObject } from \"./document.ts\";\nimport { operationIds, operationOf } from \"./operation.ts\";\nimport { toTemplate } from \"./parameters.ts\";\n\nexport type {\n DocumentedResponse,\n HookContributions,\n HookDocs,\n SecurityRequirement,\n SecurityScheme,\n} from \"./annotations.ts\";\nexport { documented, secured } from \"./annotations.ts\";\nexport type { DocsOptions } from \"./controller.ts\";\nexport { DocsController, docs } from \"./controller.ts\";\nexport type {\n ContentMap,\n DocumentInfo,\n DocumentServer,\n OpenApiDocument,\n OperationObject,\n ParameterObject,\n PathItemObject,\n ResponseObject,\n} from \"./document.ts\";\nexport type { DocsAssets, DocsPageOptions, DocsUi } from \"./page.ts\";\nexport { docsPage } from \"./page.ts\";\n\n/**\n * What the generator cannot read off the application.\n */\nexport interface OpenApiOptions {\n /** Title, version and the rest of the document's `info` block. */\n readonly info: OpenApiDocument[\"info\"];\n\n /** Servers the API is reachable at. */\n readonly servers?: OpenApiDocument[\"servers\"];\n}\n\n/**\n * What the generator could not describe faithfully.\n *\n * Each entry names a route and what was missing — almost always a schema\n * from a validator that emits no JSON Schema. Documentation that silently\n * omits a body is worse than documentation that says it does not know, so\n * these are returned rather than swallowed.\n */\nexport interface GeneratorWarning {\n readonly route: string;\n readonly message: string;\n}\n\n/** A generated document together with what could not be described. */\nexport interface GeneratorResult {\n readonly document: OpenApiDocument;\n readonly warnings: readonly GeneratorWarning[];\n}\n\n/**\n * Generates the OpenAPI document of an application.\n *\n * Socket endpoints are skipped: a handshake is a `GET` in the table, but\n * OpenAPI has no way to describe what happens after it, and documenting\n * the upgrade as an ordinary operation would describe a request that\n * clients must not make. AsyncAPI is that format, and a separate document.\n *\n * So are routes marked `docs: { hidden: true }`, and everything they would\n * have contributed with them: a security scheme only a hidden route\n * requires has nothing left to describe.\n *\n * Every route of the table becomes an operation under its path template,\n * with the parameters, body and responses its schemas describe. What the\n * framework itself can answer — a validation failure, an unparsable body,\n * a body over the limit — is filled in from the options the application\n * runs with, so the document states the status actually configured.\n */\nexport function openapi(app: App, options: OpenApiOptions): GeneratorResult {\n const paths: Record<string, PathItemObject> = {};\n const warnings: GeneratorWarning[] = [];\n const schemes: Record<string, unknown> = {};\n const components = schemaComponents();\n const ids = operationIds();\n\n // Which route already occupies each slot of the document. Two routes the\n // router keeps apart can still meet here, because a template says less\n // than a path does: `/files/*` and `/files/:wildcard` both become\n // `/files/{wildcard}`, and a literal `/users/{id}` — which Bun matches as\n // the string it is — is spelled the same as `/users/:id`. The second one\n // then replaces the first, and the document quietly loses an endpoint\n // that works. It still loses it; it no longer does so without saying.\n const occupied = new Map<string, string>();\n\n for (const entry of app.entries) {\n if (entry.ws || entry.def.docs?.hidden) {\n continue;\n }\n\n const template = toTemplate(entry.path);\n const route = `${entry.method} ${entry.path}`;\n\n const operation = operationOf(\n entry,\n app.options,\n components,\n ids,\n (message) => {\n warnings.push({ route, message });\n },\n );\n\n // First definition wins, as it does among the hooks of one route: a\n // name is the identity of a scheme, and the operations that reference\n // it say only the name. A second, different scheme under that name\n // cannot be described without renaming it out from under everything\n // already pointing at it, so it is refused rather than silently\n // swapped in — which used to make every earlier operation describe an\n // authentication its route does not perform.\n for (const requirement of contributionsOf(entry.hooks).security) {\n const defined = schemes[requirement.name];\n\n if (defined === undefined) {\n schemes[requirement.name] = requirement.scheme;\n\n continue;\n }\n\n if (JSON.stringify(defined) !== JSON.stringify(requirement.scheme)) {\n warnings.push({\n route,\n message: `the security scheme \"${requirement.name}\" is already defined differently; this definition is ignored`,\n });\n }\n }\n\n const slot = `${entry.method.toLowerCase()} ${template}`;\n const taken = occupied.get(slot);\n\n if (taken) {\n warnings.push({\n route,\n message: `${template} is already described by ${taken}, whose operation this replaces`,\n });\n }\n\n occupied.set(slot, route);\n\n paths[template] = {\n ...paths[template],\n [entry.method.toLowerCase()]: operation,\n };\n }\n\n return {\n document: {\n openapi: \"3.1.0\",\n info: options.info,\n ...(options.servers ? { servers: options.servers } : {}),\n paths,\n ...componentsOf(schemes, components.schemas),\n },\n warnings,\n };\n}\n\n/**\n * The `components` block, present only when something goes in it: the\n * security schemes the guards registered, and the failure envelopes the\n * operations refer to.\n */\nfunction componentsOf(\n securitySchemes: Record<string, unknown>,\n schemas: Record<string, unknown>,\n): { components?: Record<string, unknown> } {\n const components = {\n ...(Object.keys(securitySchemes).length > 0 ? { securitySchemes } : {}),\n ...(Object.keys(schemas).length > 0 ? { schemas } : {}),\n };\n\n return Object.keys(components).length > 0 ? { components } : {};\n}\n"
|
|
13
|
+
],
|
|
14
|
+
"mappings": "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",
|
|
15
|
+
"debugId": "698CDE5A6513C23964756E2164756E21",
|
|
16
|
+
"names": []
|
|
17
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One route table entry, described as an OpenAPI operation.
|
|
3
|
+
*
|
|
4
|
+
* Bodies and responses live here; parameters are in `parameters.ts`. The
|
|
5
|
+
* error responses a route can produce without the author writing anything
|
|
6
|
+
* — a rejected schema, an unparsable body, a body over the limit — are
|
|
7
|
+
* derived from what the route declares and from the options the
|
|
8
|
+
* application actually runs with.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
import type { AppOptions, RouteTableEntry } from "@tetsujs/core";
|
|
13
|
+
import type { SchemaComponents } from "./components.ts";
|
|
14
|
+
import type { OperationObject } from "./document.ts";
|
|
15
|
+
/** Describes one route. */
|
|
16
|
+
export declare function operationOf(entry: RouteTableEntry, options: AppOptions, components: SchemaComponents, ids: OperationIds, warn: (message: string) => void): OperationObject;
|
|
17
|
+
/**
|
|
18
|
+
* Hands out the `operationId` of every route in one document.
|
|
19
|
+
*
|
|
20
|
+
* Uniqueness is a requirement of the specification and cannot be decided
|
|
21
|
+
* one route at a time: the pair a name is built from — the controller and
|
|
22
|
+
* the field — is not unique on its own. One instance mounted under two
|
|
23
|
+
* prefixes is a documented pattern (`/v1` and `/v2` from the same
|
|
24
|
+
* controller), and two controller classes in different modules may simply
|
|
25
|
+
* share a class name. Either way the pair repeats while the routes differ.
|
|
26
|
+
*
|
|
27
|
+
* A repeat falls back to what the route is named by when it has no field
|
|
28
|
+
* at all — its method and path — because that is derived from the route
|
|
29
|
+
* rather than from the order routes were visited in. A numbered name is
|
|
30
|
+
* the last resort only, since it would move between documents as routes
|
|
31
|
+
* are added, and an `operationId` is what a generated client calls its
|
|
32
|
+
* method.
|
|
33
|
+
*/
|
|
34
|
+
export interface OperationIds {
|
|
35
|
+
/** The id for this route, unique within the document being built. */
|
|
36
|
+
take(entry: RouteTableEntry): string;
|
|
37
|
+
}
|
|
38
|
+
export declare function operationIds(): OperationIds;
|
|
39
|
+
//# sourceMappingURL=operation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"operation.d.ts","sourceRoot":"","sources":["../../src/operation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAEV,UAAU,EAEV,eAAe,EAChB,MAAM,eAAe,CAAC;AAGvB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAExD,OAAO,KAAK,EAEV,eAAe,EAEhB,MAAM,eAAe,CAAC;AAKvB,2BAA2B;AAC3B,wBAAgB,WAAW,CACzB,KAAK,EAAE,eAAe,EACtB,OAAO,EAAE,UAAU,EACnB,UAAU,EAAE,gBAAgB,EAC5B,GAAG,EAAE,YAAY,EACjB,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAC9B,eAAe,CAsCjB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,YAAY;IAC3B,qEAAqE;IACrE,IAAI,CAAC,KAAK,EAAE,eAAe,GAAG,MAAM,CAAC;CACtC;AAED,wBAAgB,YAAY,IAAI,YAAY,CAgC3C"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The HTML page that renders a document.
|
|
3
|
+
*
|
|
4
|
+
* Returns a string and mounts nothing: which path serves the page, and
|
|
5
|
+
* whether one is served at all, is the application's decision like every
|
|
6
|
+
* other route.
|
|
7
|
+
*
|
|
8
|
+
* The renderers themselves are loaded from a CDN. Vendoring Scalar or
|
|
9
|
+
* Swagger UI would put megabytes of somebody else's bundle into a
|
|
10
|
+
* dependency-free repository, so the trade is stated instead of hidden:
|
|
11
|
+
* the documentation page — and only it — needs the network, and the URL is
|
|
12
|
+
* a parameter, so a self-hosted copy is one option away. Nothing is
|
|
13
|
+
* fetched unless this function is called.
|
|
14
|
+
*
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
/** The renderers this package knows how to bootstrap. */
|
|
18
|
+
export type DocsUi = "scalar" | "swagger-ui" | "redoc";
|
|
19
|
+
/** Where a renderer is loaded from. */
|
|
20
|
+
export interface DocsAssets {
|
|
21
|
+
readonly script: string;
|
|
22
|
+
readonly style?: string;
|
|
23
|
+
}
|
|
24
|
+
/** What the page needs to know. */
|
|
25
|
+
export interface DocsPageOptions {
|
|
26
|
+
/** Which renderer to bootstrap. */
|
|
27
|
+
readonly ui: DocsUi;
|
|
28
|
+
/** Where the document is served — the page fetches it from there. */
|
|
29
|
+
readonly documentUrl: string;
|
|
30
|
+
/** Browser title; defaults to `"API documentation"`. */
|
|
31
|
+
readonly title?: string;
|
|
32
|
+
/** Overrides the CDN URLs, for a pinned version or a self-hosted copy. */
|
|
33
|
+
readonly assets?: DocsAssets;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Builds the documentation page.
|
|
37
|
+
*
|
|
38
|
+
* Called by `docs()` for the page it serves; call it directly to serve one
|
|
39
|
+
* from somewhere else.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* const page = docsPage({ ui: "scalar", documentUrl: "/openapi.json" });
|
|
44
|
+
*
|
|
45
|
+
* const ui = route({
|
|
46
|
+
* method: "GET",
|
|
47
|
+
* path: "/docs",
|
|
48
|
+
* handler: () =>
|
|
49
|
+
* new Response(page, { headers: { "content-type": "text/html" } }),
|
|
50
|
+
* });
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export declare function docsPage(options: DocsPageOptions): string;
|
|
54
|
+
//# sourceMappingURL=page.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page.d.ts","sourceRoot":"","sources":["../../src/page.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,yDAAyD;AACzD,MAAM,MAAM,MAAM,GAAG,QAAQ,GAAG,YAAY,GAAG,OAAO,CAAC;AAEvD,uCAAuC;AACvC,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,mCAAmC;AACnC,MAAM,WAAW,eAAe;IAC9B,mCAAmC;IACnC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IAEpB,qEAAqE;IACrE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAE7B,wDAAwD;IACxD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAExB,0EAA0E;IAC1E,QAAQ,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC;CAC9B;AAgBD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,CAyBzD"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path templates and request parameters.
|
|
3
|
+
*
|
|
4
|
+
* The framework's path syntax is Bun's — `:param` and a trailing `*` —
|
|
5
|
+
* while OpenAPI templates variables as `{param}`. The conversion happens
|
|
6
|
+
* here, together with the parameter objects the template then requires:
|
|
7
|
+
* OpenAPI demands that every variable in a path be declared.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
import type { AnySchema } from "@tetsujs/core";
|
|
12
|
+
import type { ParameterObject } from "./document.ts";
|
|
13
|
+
/** The name a wildcard segment is documented under. */
|
|
14
|
+
export declare const wildcardName = "wildcard";
|
|
15
|
+
/**
|
|
16
|
+
* Rewrites a route path as an OpenAPI path template.
|
|
17
|
+
*
|
|
18
|
+
* A wildcard becomes an ordinary variable: OpenAPI has no syntax for
|
|
19
|
+
* "everything below here", so the closest honest template is a named
|
|
20
|
+
* segment, documented as such.
|
|
21
|
+
*/
|
|
22
|
+
export declare function toTemplate(path: string): string;
|
|
23
|
+
/** The variables a path template declares, in order. */
|
|
24
|
+
export declare function templateNames(path: string): string[];
|
|
25
|
+
/**
|
|
26
|
+
* Describes the path parameters of a route.
|
|
27
|
+
*
|
|
28
|
+
* A `params` schema describes the whole object, so each variable takes the
|
|
29
|
+
* property of the same name; without one — or without a property for a
|
|
30
|
+
* given variable — the value is what the router captured: a string.
|
|
31
|
+
*/
|
|
32
|
+
export declare function pathParameters(path: string, schema: AnySchema | undefined, warn: (message: string) => void): ParameterObject[];
|
|
33
|
+
/**
|
|
34
|
+
* Describes the query, header or cookie parameters of a route.
|
|
35
|
+
*
|
|
36
|
+
* All three are objects at the schema level and lists of parameters in a
|
|
37
|
+
* document, so each declared property becomes one parameter, required when
|
|
38
|
+
* the schema says so.
|
|
39
|
+
*
|
|
40
|
+
* A session cookie is the exception the specification makes itself: a
|
|
41
|
+
* credential belongs in `securitySchemes` as `apiKey in: cookie`, not
|
|
42
|
+
* among the parameters. Nothing here can tell one cookie from the other —
|
|
43
|
+
* that is what an annotation on the hook reading it is for, the same way
|
|
44
|
+
* `secured()` marks an authenticating hook.
|
|
45
|
+
*/
|
|
46
|
+
export declare function partParameters(schema: AnySchema | undefined, where: "query" | "header" | "cookie", warn: (message: string) => void): ParameterObject[];
|
|
47
|
+
//# sourceMappingURL=parameters.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parameters.d.ts","sourceRoot":"","sources":["../../src/parameters.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAGrD,uDAAuD;AACvD,eAAO,MAAM,YAAY,aAAa,CAAC;AAEvC;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAW/C;AAED,wDAAwD;AACxD,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAYpD;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,SAAS,GAAG,SAAS,EAC7B,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAC9B,eAAe,EAAE,CAenB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,SAAS,GAAG,SAAS,EAC7B,KAAK,EAAE,OAAO,GAAG,QAAQ,GAAG,QAAQ,EACpC,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAC9B,eAAe,EAAE,CAyBnB"}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tetsujs/openapi",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "An OpenAPI 3.1 document and docs page generated from Tetsu routes",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"bun",
|
|
7
|
+
"tetsu",
|
|
8
|
+
"openapi",
|
|
9
|
+
"swagger",
|
|
10
|
+
"api-docs"
|
|
11
|
+
],
|
|
12
|
+
"author": "tetsuodev",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"homepage": "https://github.com/tetsujs/tetsu/tree/main/packages/openapi#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/tetsujs/tetsu/issues"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/tetsujs/tetsu.git",
|
|
21
|
+
"directory": "packages/openapi"
|
|
22
|
+
},
|
|
23
|
+
"type": "module",
|
|
24
|
+
"engines": {
|
|
25
|
+
"bun": ">=1.4"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"prepack": "bun run --cwd ../.. build"
|
|
29
|
+
},
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/src/index.d.ts",
|
|
33
|
+
"default": "./dist/src/index.js"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@tetsujs/core": "^0.1.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@tetsujs/core": "0.1.0"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"files": [
|
|
46
|
+
"dist",
|
|
47
|
+
"!dist/**/*.tsbuildinfo",
|
|
48
|
+
"src",
|
|
49
|
+
"!**/*.test.ts",
|
|
50
|
+
"!**/*.test-d.ts"
|
|
51
|
+
]
|
|
52
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What hooks contribute to the documentation.
|
|
3
|
+
*
|
|
4
|
+
* What a route is protected by, or answers with when a guard refuses, is
|
|
5
|
+
* not in its config: a hook is a function in a slot, and the route that
|
|
6
|
+
* mounts it says nothing about tokens or rate limits. The contribution is
|
|
7
|
+
* therefore annotated onto the hook — from this package, not from
|
|
8
|
+
* `hook.*` — so the core keeps knowing nothing about OpenAPI and the
|
|
9
|
+
* factories stay free of documentation options.
|
|
10
|
+
*
|
|
11
|
+
* A hook annotated once is documented everywhere it runs: application and
|
|
12
|
+
* group chains are merged into every route's chains at startup, so a guard
|
|
13
|
+
* mounted on a zone documents the zone's operations without repeating
|
|
14
|
+
* itself.
|
|
15
|
+
*
|
|
16
|
+
* @module
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { AnyHook, AnySchema } from "@tetsujs/core";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The definition of a scheme, as OpenAPI spells it.
|
|
23
|
+
*
|
|
24
|
+
* Typed as loosely as the spec is wide — `http`, `apiKey`, `oauth2` and
|
|
25
|
+
* `openIdConnect` carry different fields, and narrowing them here would
|
|
26
|
+
* be a second, poorer copy of the specification.
|
|
27
|
+
*/
|
|
28
|
+
export interface SecurityScheme {
|
|
29
|
+
readonly type: "http" | "apiKey" | "oauth2" | "openIdConnect";
|
|
30
|
+
readonly [key: string]: unknown;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** What a hook demands of a request, and what it answers when refused. */
|
|
34
|
+
export interface SecurityRequirement {
|
|
35
|
+
/** The name the scheme is registered under in `components`. */
|
|
36
|
+
readonly name: string;
|
|
37
|
+
|
|
38
|
+
/** The scheme itself, registered once per name. */
|
|
39
|
+
readonly scheme: SecurityScheme;
|
|
40
|
+
|
|
41
|
+
/** OAuth2 scopes; empty for every other type. */
|
|
42
|
+
readonly scopes?: readonly string[];
|
|
43
|
+
|
|
44
|
+
/** The status an unauthenticated request gets. Defaults to `401`. */
|
|
45
|
+
readonly status?: number;
|
|
46
|
+
|
|
47
|
+
/** How the refusal is described in the document. */
|
|
48
|
+
readonly description?: string;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The `error` code the refusal carries, when the guard answers with the
|
|
52
|
+
* framework's envelope — `"UNAUTHORIZED"`, `"SESSION_EXPIRED"`.
|
|
53
|
+
*
|
|
54
|
+
* Documented as a `const` for the same reason as on a
|
|
55
|
+
* {@link DocumentedResponse}: only the guard knows its own code.
|
|
56
|
+
*/
|
|
57
|
+
readonly error?: string;
|
|
58
|
+
|
|
59
|
+
/** An example of the refusal's `message`, for the same reason. */
|
|
60
|
+
readonly message?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* One response a hook can produce, which the route's own schema knows
|
|
65
|
+
* nothing about — a rate limiter's `429`, a guard's `401`.
|
|
66
|
+
*/
|
|
67
|
+
export interface DocumentedResponse {
|
|
68
|
+
readonly status: number;
|
|
69
|
+
readonly description: string;
|
|
70
|
+
|
|
71
|
+
/** The body, when the hook answers with one. */
|
|
72
|
+
readonly schema?: AnySchema;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The `error` code of the failure envelope, when the hook answers with
|
|
76
|
+
* the framework's shape — `"RATE_LIMITED"`, `"SESSION_EXPIRED"`.
|
|
77
|
+
*
|
|
78
|
+
* Documented as a `const`, so a client can discriminate on it. Only the
|
|
79
|
+
* hook knows its own code; without one the document says the field is a
|
|
80
|
+
* string and no more.
|
|
81
|
+
*/
|
|
82
|
+
readonly error?: string;
|
|
83
|
+
|
|
84
|
+
/** An example of the envelope's `message`, for the same reason. */
|
|
85
|
+
readonly message?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Everything a hook tells the generator about itself. */
|
|
89
|
+
export interface HookDocs {
|
|
90
|
+
/** What the hook demands of a request. */
|
|
91
|
+
readonly security?: SecurityRequirement;
|
|
92
|
+
|
|
93
|
+
/** What the hook can answer with. */
|
|
94
|
+
readonly responses?: readonly DocumentedResponse[];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const docsKey = "~tetsu/hook-docs";
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Annotates a hook with what it contributes to the documentation.
|
|
101
|
+
*
|
|
102
|
+
* Returns a copy: what the caller exports is what carries the annotation,
|
|
103
|
+
* and a hook used elsewhere is not changed behind its author's back. The
|
|
104
|
+
* type is preserved exactly, so an annotated hook goes into a stack like
|
|
105
|
+
* any other.
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```ts
|
|
109
|
+
* export const limiter = documented(hook.beforeParse(check), {
|
|
110
|
+
* responses: [
|
|
111
|
+
* { status: 429, description: "Rate limit exceeded", schema: TooMany },
|
|
112
|
+
* ],
|
|
113
|
+
* });
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
export function documented<H extends AnyHook>(hook: H, docs: HookDocs): H {
|
|
117
|
+
const annotated = { ...hook };
|
|
118
|
+
|
|
119
|
+
Object.defineProperty(annotated, docsKey, {
|
|
120
|
+
enumerable: false,
|
|
121
|
+
value: { ...docsOf(hook), ...docs },
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return annotated as H;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Annotates a hook with the security it enforces — {@link documented} for
|
|
129
|
+
* the case that comes up most.
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* ```ts
|
|
133
|
+
* export const auth = secured(
|
|
134
|
+
* hook.beforeParse((ctx) => {
|
|
135
|
+
* const user = verify(ctx.req.headers.get("authorization"));
|
|
136
|
+
* if (!user) throw new HttpError(401);
|
|
137
|
+
* return { user };
|
|
138
|
+
* }),
|
|
139
|
+
* {
|
|
140
|
+
* name: "bearerAuth",
|
|
141
|
+
* scheme: { type: "http", scheme: "bearer", bearerFormat: "JWT" },
|
|
142
|
+
* },
|
|
143
|
+
* );
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
export function secured<H extends AnyHook>(
|
|
147
|
+
hook: H,
|
|
148
|
+
requirement: SecurityRequirement,
|
|
149
|
+
): H {
|
|
150
|
+
return documented(hook, { security: requirement });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Reads everything a hook was annotated with. */
|
|
154
|
+
export function docsOf(hook: AnyHook): HookDocs | undefined {
|
|
155
|
+
return (hook as unknown as Record<string, HookDocs | undefined>)[docsKey];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Reads the security requirement a hook carries, if it carries one. */
|
|
159
|
+
export function securityOf(hook: AnyHook): SecurityRequirement | undefined {
|
|
160
|
+
return docsOf(hook)?.security;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** What the hooks of one route contribute, in execution order. */
|
|
164
|
+
export interface HookContributions {
|
|
165
|
+
readonly security: readonly SecurityRequirement[];
|
|
166
|
+
readonly responses: readonly DocumentedResponse[];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Collects the contributions of a route's chains.
|
|
171
|
+
*
|
|
172
|
+
* A scheme is registered once per name and a response once per status and
|
|
173
|
+
* description: the same guard on the application and on a group is one
|
|
174
|
+
* requirement, not two.
|
|
175
|
+
*
|
|
176
|
+
* Deduplicating by name means two hooks claiming one name with different
|
|
177
|
+
* schemes lose one of them, and that is not a duplicate being collapsed —
|
|
178
|
+
* it is a requirement the route really has going undescribed. Passing
|
|
179
|
+
* `warn` is how the caller hears about it; the generator does.
|
|
180
|
+
*/
|
|
181
|
+
export function contributionsOf(
|
|
182
|
+
chains: Record<string, readonly AnyHook[]>,
|
|
183
|
+
warn?: (message: string) => void,
|
|
184
|
+
): HookContributions {
|
|
185
|
+
const security = new Map<string, SecurityRequirement>();
|
|
186
|
+
const responses = new Map<string, DocumentedResponse>();
|
|
187
|
+
|
|
188
|
+
for (const slot of Object.values(chains)) {
|
|
189
|
+
for (const hook of slot) {
|
|
190
|
+
const docs = docsOf(hook);
|
|
191
|
+
|
|
192
|
+
if (!docs) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (docs.security) {
|
|
197
|
+
const claimed = security.get(docs.security.name);
|
|
198
|
+
|
|
199
|
+
if (!claimed) {
|
|
200
|
+
security.set(docs.security.name, docs.security);
|
|
201
|
+
} else if (
|
|
202
|
+
JSON.stringify(claimed.scheme) !==
|
|
203
|
+
JSON.stringify(docs.security.scheme)
|
|
204
|
+
) {
|
|
205
|
+
warn?.(
|
|
206
|
+
`two hooks claim the security scheme "${docs.security.name}" with different definitions; one of them is not described`,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
for (const response of docs.responses ?? []) {
|
|
212
|
+
const key = `${response.status} ${response.description}`;
|
|
213
|
+
|
|
214
|
+
if (!responses.has(key)) {
|
|
215
|
+
responses.set(key, response);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
security: [...security.values()],
|
|
223
|
+
responses: [...responses.values()],
|
|
224
|
+
};
|
|
225
|
+
}
|