@znight/openapi 0.0.0-stage → 0.1.1
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 +572 -2
- package/dist/build-BTUC96mY.d.mts +174 -0
- package/dist/build-DXunOzlI.mjs +733 -0
- package/dist/build-DXunOzlI.mjs.map +1 -0
- package/dist/compiler.d.mts +16 -0
- package/dist/compiler.mjs +2639 -0
- package/dist/compiler.mjs.map +1 -0
- package/dist/emit-child.d.mts +1 -0
- package/dist/emit-child.mjs +124 -0
- package/dist/emit-child.mjs.map +1 -0
- package/dist/emit.d.mts +2 -0
- package/dist/emit.mjs +252 -0
- package/dist/emit.mjs.map +1 -0
- package/dist/index-CqQ6IRnN.d.mts +60 -0
- package/dist/index.d.mts +260 -0
- package/dist/index.mjs +777 -0
- package/dist/index.mjs.map +1 -0
- package/dist/openapi-CQk5dHqc.mjs +39 -0
- package/dist/openapi-CQk5dHqc.mjs.map +1 -0
- package/dist/shared-BZxtyaB0.mjs +147 -0
- package/dist/shared-BZxtyaB0.mjs.map +1 -0
- package/dist/testing.d.mts +39 -0
- package/dist/testing.mjs +155 -0
- package/dist/testing.mjs.map +1 -0
- package/dist/types-VzBexMKj.d.mts +213 -0
- package/dist/vite.d.mts +24 -0
- package/dist/vite.mjs +64 -0
- package/dist/vite.mjs.map +1 -0
- package/package.json +115 -4
- package/src/build.ts +898 -0
- package/src/compiler/analyze.ts +81 -0
- package/src/compiler/document.ts +612 -0
- package/src/compiler/generate.ts +79 -0
- package/src/compiler/index.ts +47 -0
- package/src/compiler/model.ts +141 -0
- package/src/compiler/operations.ts +1475 -0
- package/src/compiler/schemas.ts +706 -0
- package/src/compiler/validate.ts +102 -0
- package/src/compiler/values.ts +81 -0
- package/src/converters.ts +137 -0
- package/src/decorators.ts +187 -0
- package/src/emit/child.ts +144 -0
- package/src/emit/index.ts +377 -0
- package/src/emit/protocol.ts +35 -0
- package/src/index.ts +94 -0
- package/src/metadata.ts +112 -0
- package/src/openapi.ts +55 -0
- package/src/plugin.ts +810 -0
- package/src/shared.ts +154 -0
- package/src/testing.ts +224 -0
- package/src/types.ts +240 -0
- package/src/ui.ts +215 -0
- package/src/vite.ts +126 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/decorators.ts","../src/ui.ts","../src/plugin.ts"],"sourcesContent":["/**\n * Decorators of `@znight/openapi`. Like every Znight decorator they do nothing at runtime: the\n * compiler extension (`@znight/openapi/compiler`) reads them, type arguments included\n * (`@ApiResponse<Book[]>(200)`), and the Vite plugin strips them from the bundle.\n */\nimport type { ClassMarker, MethodMarker } from '@znight/core'\nimport type { RouteDecorator, StandardSchemaV1 } from '@znight/http'\nimport type {\n ExternalDocumentationObject,\n HeaderObject,\n InfoObject,\n JsonValue,\n SecuritySchemeObject,\n ServerObject,\n TagObject,\n} from './types.ts'\n\nfunction marker(): void {\n // intentionally empty: decorators are compile-time markers\n}\n\nconst classMarker: ClassMarker = marker\nconst methodMarker: MethodMarker = marker\nconst classOrMethodMarker: ClassMarker & MethodMarker = marker\n\n/** A security scheme declared with {@link securityScheme}. */\nexport interface SecurityScheme {\n /** Key of the scheme in `components.securitySchemes`. */\n readonly name: string\n readonly definition: SecuritySchemeObject\n}\n\n/** One scheme, several schemes that all apply (AND), or a scheme with scopes. */\nexport type SecurityRequirementInput =\n | SecurityScheme\n | readonly SecurityScheme[]\n | { readonly scheme: SecurityScheme; readonly scopes: readonly string[] }\n\n/** Security of the routes carrying a route decorator (§5.5 of RFC 0002). */\nexport interface SecurityDecision {\n /** A route decorator (`createRouteDecorator`) found in the route's effective metadata. */\n // biome-ignore lint/suspicious/noExplicitAny: any route decorator value type is accepted\n readonly when: RouteDecorator<any>\n /** Requirements of the matching routes; `[]` means public. */\n readonly security: readonly SecurityRequirementInput[]\n /** `'value'`: the decorator value (a static string array) becomes the scopes of every requirement. */\n readonly scopes?: 'value'\n}\n\n/** Style of the default operation ids. */\nexport type OperationIdStyle = 'controller.handler' | 'handler' | 'Controller_handler'\n\n/** Options of {@link OpenApiDocument}. */\nexport interface OpenApiDocumentOptions {\n /** Title, version, description... (defaults: the JSDoc of the class, then `package.json`). */\n readonly info?: Partial<InfoObject>\n readonly servers?: readonly ServerObject[]\n /** Declared tags first, in this order; used tags are added, sorted. */\n readonly tags?: readonly TagObject[]\n readonly externalDocs?: ExternalDocumentationObject\n /** Default requirements of every operation: entries are alternatives (OR). Default: none. */\n readonly security?: readonly SecurityRequirementInput[]\n /** Security derived from route decorators; the first matching entry wins. */\n readonly decisions?: readonly SecurityDecision[]\n /** Ids of operations without `@ApiOperation({ operationId })`. Default `'controller.handler'`. */\n readonly operationIds?: OperationIdStyle\n /** Standard error responses (400, 413, 415, declared 4XX/5XX). Default `true`. */\n readonly errors?: boolean | { readonly schema: StandardSchemaV1; readonly contentType?: string }\n /** ZNIGHT031 and ZNIGHT032 become errors. Default `false`. */\n readonly strict?: boolean\n /** Read the JSDoc of controllers, handlers and types. Default `true`. */\n readonly jsdoc?: boolean\n /** `x-*` fields of the root object. */\n readonly extensions?: Readonly<Record<`x-${string}`, JsonValue>>\n}\n\n/**\n * Options of the document (one per application, optional). The class is never instantiated nor\n * registered as a provider; its JSDoc gives the title (first line) and the description (the rest).\n *\n * ```ts\n * /** Bookshelf API *\\/\n * @OpenApiDocument({ servers: [{ url: '/api' }], security: [ApiKey], decisions: [{ when: Public, security: [] }] })\n * export class BookshelfApi {}\n * ```\n */\nexport function OpenApiDocument(_options?: OpenApiDocumentOptions): ClassMarker {\n return classMarker\n}\n\n/**\n * Declares a security scheme, read statically by the compiler. At runtime it returns a frozen\n * `{ name, definition }`.\n *\n * ```ts\n * export const ApiKey = securityScheme('apiKey', { type: 'apiKey', in: 'header', name: 'x-api-key' })\n * ```\n */\nexport function securityScheme(name: string, definition: SecuritySchemeObject): SecurityScheme {\n return Object.freeze({ name, definition })\n}\n\n/** Options of {@link ApiOperation}. */\nexport interface ApiOperationOptions {\n readonly summary?: string\n readonly description?: string\n readonly operationId?: string\n readonly deprecated?: boolean\n readonly externalDocs?: ExternalDocumentationObject\n readonly extensions?: Readonly<Record<`x-${string}`, JsonValue>>\n}\n\n/** Overrides the JSDoc and the defaults of an operation. */\nexport function ApiOperation(_options: ApiOperationOptions): MethodMarker {\n return methodMarker\n}\n\n/** Tags of the operations (class and method tags are merged, class tags first). */\nexport function ApiTags(..._names: [string, ...string[]]): ClassMarker & MethodMarker {\n return classOrMethodMarker\n}\n\n/** A response status: a code, `'2XX'`-like ranges, or `'default'`. */\nexport type ApiResponseStatus = number | `${1 | 2 | 3 | 4 | 5}XX` | 'default'\n\n/** Options of {@link ApiResponse}. */\nexport interface ApiResponseOptions {\n readonly description?: string\n /** Body schema (a Standard Schema, documented with its output JSON Schema). */\n readonly schema?: StandardSchemaV1\n /** Media type(s) of the body. Default `application/json` (when the body has a schema). */\n readonly contentType?: string | readonly string[]\n readonly headers?: Readonly<Record<string, Partial<HeaderObject>>>\n readonly example?: JsonValue\n}\n\n/**\n * Declares or overrides a response (repeatable). The body type comes from `T`\n * (`@ApiResponse<Book>(200)`) or from a Standard Schema in `schema`.\n */\nexport function ApiResponse<_T = never>(_status: ApiResponseStatus, _options?: ApiResponseOptions): MethodMarker {\n return methodMarker\n}\n\n/** Options of {@link ApiBody}. */\nexport interface ApiBodyOptions {\n readonly description?: string\n readonly required?: boolean\n readonly contentType?: string | readonly string[]\n readonly schema?: StandardSchemaV1\n}\n\n/** The body of a route that reads it itself (`ctx.parseBody()`, `ctx.bodyStream()`). */\nexport function ApiBody<_T = never>(_options?: ApiBodyOptions): MethodMarker {\n return methodMarker\n}\n\n/** Options of {@link ApiQuery} and {@link ApiHeader}. */\nexport interface ApiParameterOptions {\n readonly description?: string\n readonly required?: boolean\n readonly deprecated?: boolean\n readonly example?: JsonValue\n}\n\n/** A query parameter read outside the signature (`ctx.query`); its type is `T` (default `string`). */\nexport function ApiQuery<_T = string>(_name: string, _options?: ApiParameterOptions): MethodMarker {\n return methodMarker\n}\n\n/** A header read by a guard or an interceptor (`x-tenant`); its type is `T` (default `string`). */\nexport function ApiHeader<_T = string>(_name: string, _options?: ApiParameterOptions): ClassMarker & MethodMarker {\n return classOrMethodMarker\n}\n\n/**\n * Security of the operations: the route's wins over the class's. Several arguments are\n * alternatives (OR); no argument means public (`security: []`).\n */\nexport function ApiSecurity(..._requirements: SecurityRequirementInput[]): ClassMarker & MethodMarker {\n return classOrMethodMarker\n}\n\n/** The controller or route is not documented. */\nexport function ApiExclude(): ClassMarker & MethodMarker {\n return classOrMethodMarker\n}\n","/**\n * API reference pages (Scalar by default, Swagger UI as an option, §8.3 of RFC 0002): an HTML page\n * whose inline script is fixed at boot (allowed by hash in its own Content-Security-Policy), with\n * the UI assets served from `node_modules` or from a pinned CDN with Subresource Integrity.\n */\nimport type { JsonValue } from './types.ts'\n\nimport { createHash } from 'node:crypto'\nimport { existsSync, readFileSync } from 'node:fs'\nimport { createRequire } from 'node:module'\nimport { dirname, join } from 'node:path'\nimport { pathToFileURL } from 'node:url'\n\n/** The supported UIs. */\nexport type UiKind = 'scalar' | 'swagger'\n\n/** An asset of a UI package. */\nexport interface UiAsset {\n /** Path in the package. */\n readonly file: string\n /** SRI of the pinned CDN copy. */\n readonly integrity: string\n readonly kind: 'script' | 'style'\n}\n\n/** A UI package pinned by this release (bumped by Renovate; checked by a network test). */\nexport interface UiPackage {\n readonly name: string\n readonly version: string\n readonly assets: readonly UiAsset[]\n}\n\n/** jsDelivr. */\nexport const CDN_ORIGIN = 'https://cdn.jsdelivr.net'\n\n/** Pinned versions and SRI hashes of the UI assets. */\nexport const UI_PACKAGES: Readonly<Record<UiKind, UiPackage>> = Object.freeze({\n scalar: Object.freeze({\n name: '@scalar/api-reference',\n version: '1.75.1',\n assets: Object.freeze([\n {\n file: 'dist/browser/standalone.js',\n integrity: 'sha384-G+2Q2i9l3PPfmYkk0b4BAIUrs7xb/DNaTL1wCYYab4c9dIKlCyiqzC0Qx3eq9Wf6',\n kind: 'script' as const,\n },\n ]),\n }),\n swagger: Object.freeze({\n name: 'swagger-ui-dist',\n version: '5.33.1',\n assets: Object.freeze([\n {\n file: 'swagger-ui.css',\n integrity: 'sha384-Ov4/wv3j2bmct8cDc5X4ngJZohVPzEmc6uDPH8WeljUxO5vtoykvMEfbu9Vh6RaW',\n kind: 'style' as const,\n },\n {\n file: 'swagger-ui-bundle.js',\n integrity: 'sha384-ZPehFMQommnnuaZ4rpxgkgTT2DKFVp4hZC/7pLit+9Lek9T1YGSo23eHFbvNkXkw',\n kind: 'script' as const,\n },\n ]),\n }),\n})\n\n/** URL of a pinned asset on the CDN. */\nexport function cdnUrl(pkg: UiPackage, asset: UiAsset): string {\n return `${CDN_ORIGIN}/npm/${pkg.name}@${pkg.version}/${asset.file}`\n}\n\n/** An installed UI package. */\nexport interface LocalPackage {\n readonly dir: string\n readonly version: string\n}\n\n/**\n * Finds an installed UI package: from `@znight/openapi` (it is an optional peer), then from the\n * working directory. `undefined` when it is not installed or misses an asset. The same on Node.js and\n * Bun, whatever the exports map of the package says (`@scalar/api-reference` does not export its\n * `package.json`).\n */\nexport function findLocalPackage(pkg: UiPackage, from: readonly string[] = defaultBases()): LocalPackage | undefined {\n for (const base of from) {\n for (const manifest of manifestCandidates(pkg.name, base)) {\n const found = readLocalPackage(pkg, manifest)\n if (found !== undefined) return found\n }\n }\n return undefined\n}\n\n/**\n * Where the manifest of `name` may be, seen from `base`: the `package.json` subpath when it resolves\n * (Bun always resolves it, Node.js only when the exports map exports it), then `<dir>/<name>/package.json`\n * in the `node_modules` directories the resolution searches.\n */\nfunction manifestCandidates(name: string, base: string): string[] {\n let require: ReturnType<typeof createRequire>\n try {\n require = createRequire(base)\n } catch {\n return []\n }\n const candidates: string[] = []\n try {\n candidates.push(require.resolve(`${name}/package.json`))\n } catch {\n // not installed, or an exports map without \"./package.json\"\n }\n for (const dir of require.resolve.paths(name) ?? []) candidates.push(join(dir, name, 'package.json'))\n return [...new Set(candidates)]\n}\n\nfunction readLocalPackage(pkg: UiPackage, manifest: string): LocalPackage | undefined {\n try {\n if (!existsSync(manifest)) return undefined\n const { name, version } = JSON.parse(readFileSync(manifest, 'utf8')) as { name?: unknown; version?: unknown }\n if (name !== pkg.name || typeof version !== 'string') return undefined\n const dir = dirname(manifest)\n return pkg.assets.every(asset => existsSync(join(dir, asset.file))) ? { dir, version } : undefined\n } catch {\n // unreadable manifest: try the next candidate\n return undefined\n }\n}\n\nfunction defaultBases(): string[] {\n return [import.meta.url, pathToFileURL(join(process.cwd(), 'package.json')).href]\n}\n\n/** What the page needs. */\nexport interface UiPageInput {\n readonly kind: UiKind\n /** URL of the JSON document, prefix included (`/api/openapi.json`). */\n readonly documentUrl: string\n /** Base URL of the local assets (`/api/docs/_assets/scalar@1.75.1`), `undefined` for the CDN. */\n readonly assetsBase: string | undefined\n readonly title: string\n /** User configuration (JSON-serialisable), merged over the defaults. */\n readonly config: Readonly<Record<string, JsonValue>>\n /** Origins the UI may call (\"try it\"): the absolute servers of the document. */\n readonly connect: readonly string[]\n}\n\n/** A rendered page. */\nexport interface UiPage {\n readonly html: string\n readonly csp: string\n}\n\n/** The HTML page of the UI and its Content-Security-Policy. */\nexport function renderUiPage(input: UiPageInput): UiPage {\n const pkg = UI_PACKAGES[input.kind]\n const local = input.assetsBase !== undefined\n const tags = pkg.assets.map(asset => {\n const url = local ? `${input.assetsBase}/${asset.file.slice(asset.file.lastIndexOf('/') + 1)}` : cdnUrl(pkg, asset)\n const integrity = local ? '' : ` integrity=\"${asset.integrity}\" crossorigin=\"anonymous\"`\n return asset.kind === 'style'\n ? `<link rel=\"stylesheet\" href=\"${escapeHtml(url)}\"${integrity}>`\n : `<script src=\"${escapeHtml(url)}\"${integrity}></script>`\n })\n const styles = tags.filter(tag => tag.startsWith('<link'))\n const scripts = tags.filter(tag => tag.startsWith('<script'))\n const config =\n input.kind === 'scalar'\n ? { url: input.documentUrl, withDefaultFonts: false, telemetry: false, ...input.config }\n : { url: input.documentUrl, dom_id: '#swagger-ui', validatorUrl: null, ...input.config }\n const json = scriptJson(config)\n const inline =\n input.kind === 'scalar' ? `Scalar.createApiReference('#app', ${json})` : `window.ui = SwaggerUIBundle(${json})`\n const hash = `'sha256-${createHash('sha256').update(inline).digest('base64')}'`\n const container = input.kind === 'scalar' ? '<div id=\"app\"></div>' : '<div id=\"swagger-ui\"></div>'\n const html = [\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>${escapeHtml(input.title)}</title>`,\n ...styles,\n '</head>',\n '<body>',\n container,\n ...scripts,\n `<script>${inline}</script>`,\n '</body>',\n '</html>',\n '',\n ].join('\\n')\n const cdn = local ? '' : ` ${CDN_ORIGIN}`\n const connect = [\"'self'\", ...input.connect].join(' ')\n const csp = [\n \"default-src 'none'\",\n `script-src 'self'${cdn} ${hash}`,\n `style-src 'self' 'unsafe-inline'${cdn}`,\n \"img-src 'self' data: https:\",\n `font-src 'self' data:${cdn}`,\n `connect-src ${connect}`,\n \"frame-ancestors 'none'\",\n \"base-uri 'none'\",\n \"form-action 'none'\",\n ].join('; ')\n return { html, csp }\n}\n\n/** JSON safe inside an inline `<script>` (`<` escaped, line separators escaped). */\nfunction scriptJson(value: unknown): string {\n return JSON.stringify(value).replaceAll('<', '\\\\u003c').replaceAll('
', '\\\\u2028').replaceAll('
', '\\\\u2029')\n}\n\nfunction escapeHtml(text: string): string {\n return text.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('\"', '"')\n}\n","/**\n * `OpenApiPlugin` (§5.6, §8 of RFC 0002): serves the document (`/openapi.json`, optional YAML) and\n * the API reference UI through the plugin routes of `@znight/http`, and provides the `OpenApi`\n * external.\n */\nimport type {\n Container,\n CoreLogger,\n Plugin,\n PluginContext,\n PluginOptions,\n PluginRootsContext,\n ProviderKey,\n ZnightMetadata,\n} from '@znight/core'\nimport type {\n GuardRef,\n HttpContext,\n HttpPlugin,\n PluginHttpRoutes,\n PluginHttpRoutesContext,\n PluginRoute,\n RouteMetaEntry,\n} from '@znight/http'\nimport type { BuildOpenApiOptions, OpenApiOperationView } from './build.ts'\nimport type { SchemaConverter } from './converters.ts'\nimport type { InfoObject, JsonValue, OpenApiObject, ServerObject } from './types.ts'\nimport type { LocalPackage, UiKind } from './ui.ts'\n\nimport { createHash } from 'node:crypto'\nimport { readFile } from 'node:fs/promises'\nimport { fileURLToPath } from 'node:url'\nimport { ErrorCode, resolveOptions, ZnightError } from '@znight/core'\nimport { HttpResponse, NotFoundException } from '@znight/http'\n\nimport { applyDocumentOptions, buildOpenApiDocument, openApiSection, operationViews } from './build.ts'\nimport { OpenApi, OpenApiTokens, toYaml } from './openapi.ts'\nimport { findLocalPackage, renderUiPage, UI_PACKAGES } from './ui.ts'\n\n/** The UI option: a kind, or a kind with its JSON-serialisable configuration. */\nexport type OpenApiUiOption =\n | UiKind\n | false\n | { readonly scalar: Readonly<Record<string, JsonValue>> }\n | { readonly swagger: Readonly<Record<string, JsonValue>> }\n\n/** Options of {@link OpenApiPlugin.configure}. */\nexport interface OpenApiPluginOptions {\n /** `false` serves nothing and builds nothing (the `OpenApi` external still works). Default `true`. */\n readonly enabled?: boolean\n /** Path of the JSON document, relative to the HTTP prefix. Default `'/openapi.json'`. */\n readonly path?: string\n /** A path (`'/openapi.yaml'`) also serves YAML (`application/yaml`, optional peer `yaml`). Default `false`. */\n readonly yaml?: string | false\n /** `'scalar'` (default), `'swagger'`, `false`, or `{ scalar: config }` / `{ swagger: config }`. */\n readonly ui?: OpenApiUiOption\n /** Path of the UI page. Local assets are under `${uiPath}/_assets/`. Default `'/docs'`. */\n readonly uiPath?: string\n /**\n * `'local'` serves the UI package from `node_modules`, `'cdn'` the jsDelivr copy pinned by this\n * release (with SRI), `'auto'` (default) local when the package is installed.\n */\n readonly assets?: 'auto' | 'local' | 'cdn'\n /** `'build'` (default) assembles the document from the metadata; `{ file }` serves a prebuilt `openapi.json`. */\n readonly document?: 'build' | { readonly file: string | URL }\n /** `'boot'` (default): built in `onBootstrap` (a failure fails the boot); `'first-request'`: built lazily. */\n readonly build?: 'boot' | 'first-request'\n /** Merged over the static `info`. */\n readonly info?: Partial<InfoObject>\n /** Replaces the declared `servers`. */\n readonly servers?: readonly ServerObject[]\n readonly filter?: (operation: OpenApiOperationView) => boolean\n readonly transform?: (document: OpenApiObject) => OpenApiObject\n /** Extra converters, tried before the built-in ones. */\n readonly converters?: readonly SchemaConverter[]\n /** `'warn'`: a schema that cannot be converted becomes `{}` (ZNIGHT037 warning). Default `'error'`. */\n readonly unrepresentable?: 'error' | 'warn'\n /** Route metadata of the docs routes (`[Public.meta()]`): seen by guards and by the route decision check. */\n readonly meta?: readonly RouteMetaEntry[]\n /** Extra guards of the docs routes (classes or instances), after the global guards. */\n readonly guards?: readonly GuardRef[]\n /** `cache-control` of the document (revalidated with the ETag). Default `'no-cache'`. */\n readonly cacheControl?: string\n}\n\ninterface Resolved {\n readonly enabled: boolean\n readonly path: string\n readonly yaml: string | undefined\n readonly ui: { readonly kind: UiKind; readonly config: Readonly<Record<string, JsonValue>> } | undefined\n readonly uiPath: string\n readonly assets: 'auto' | 'local' | 'cdn'\n readonly file: string | undefined\n readonly lazy: boolean\n readonly build: BuildOpenApiOptions\n readonly meta: readonly RouteMetaEntry[]\n readonly guards: readonly GuardRef[]\n readonly cacheControl: string\n}\n\n/** A precomputed response body. */\ninterface Body {\n readonly bytes: Uint8Array\n readonly etag: string\n readonly contentType: string\n}\n\n/** Everything served, computed once. */\ninterface Served {\n readonly document: OpenApiObject\n readonly json: Body\n readonly yaml: Body | undefined\n}\n\nconst ENCODER = new TextEncoder()\nconst ASSET_CACHE = 'public, max-age=31536000, immutable'\nconst PATH = /^\\/[A-Za-z0-9\\-._~!$&'()*+,;=:@/]*$/\n\n/** The OpenAPI plugin: `OpenApiPlugin.configure()` (name `openapi`). */\nexport class OpenApiPlugin implements Plugin, PluginHttpRoutes {\n readonly name = 'openapi'\n readonly #options: PluginOptions<OpenApiPluginOptions>\n #resolved: Resolved | undefined\n #container: Container | undefined\n #metadata: ZnightMetadata | undefined\n #logger: CoreLogger | undefined\n #http: HttpPlugin | undefined\n #served: Served | undefined\n #building: Promise<Served> | undefined\n #ui: { html: Uint8Array; csp: string; etag: string } | undefined\n #local: LocalPackage | undefined\n #operations: readonly OpenApiOperationView[] = []\n #active = false\n\n private constructor(options: PluginOptions<OpenApiPluginOptions>) {\n this.#options = options\n }\n\n /**\n * Creates the plugin. `options` may be a function of the container, evaluated once the container\n * is ready: `OpenApiPlugin.configure(c => ({ enabled: c.get(ServerConfig).docs }))`.\n */\n static configure(options: PluginOptions<OpenApiPluginOptions> = {}): OpenApiPlugin {\n return new OpenApiPlugin(options)\n }\n\n /** The classes of the `guards` option (instances are left out): `TestApp` creates them at boot. */\n initRoots(_context?: PluginRootsContext): readonly ProviderKey[] {\n const options: unknown = this.#options\n if (options === null || typeof options !== 'object') return []\n try {\n const guards: unknown = (options as OpenApiPluginOptions).guards\n return Array.isArray(guards) ? guards.filter((guard): guard is ProviderKey => typeof guard === 'function') : []\n } catch {\n return []\n }\n }\n\n register(ctx: PluginContext): void {\n if (this.#active) {\n throw new ZnightError(ErrorCode.DuplicateProvider, 'This OpenApiPlugin is already used by another application', {\n hints: ['Call OpenApiPlugin.configure() once per application'],\n })\n }\n openApiSection(ctx.metadata)\n if (ctx.getPlugin('http') === undefined) {\n throw new ZnightError(\n ErrorCode.InvalidOpenApiDefinition,\n 'OpenApiPlugin serves its routes through HttpPlugin, which is not registered',\n {\n hints: ['Add HttpPlugin.configure() to the plugins of the application'],\n },\n )\n }\n this.#active = true\n this.#resolved = undefined\n this.#served = undefined\n this.#building = undefined\n this.#ui = undefined\n this.#local = undefined\n this.#container = ctx.container\n this.#metadata = ctx.metadata\n this.#logger = ctx.logger\n this.#http = ctx.getPlugin<HttpPlugin>('http')\n this.#operations = operationViews(ctx.metadata)\n ctx.container.registerValue(OpenApiTokens.OpenApi, new OpenApiService(this))\n }\n\n httpRoutes(context: PluginHttpRoutesContext): readonly PluginRoute[] {\n this.#container ??= context.container\n const options = this.#resolve()\n if (!options.enabled) return []\n const common = {\n ...(options.meta.length > 0 ? { meta: options.meta } : {}),\n ...(options.guards.length > 0 ? { guards: options.guards } : {}),\n }\n const routes: PluginRoute[] = [\n {\n method: 'GET',\n path: options.path,\n name: 'document',\n handler: ctx => this.#serveDocument(ctx, 'json'),\n ...common,\n },\n ]\n if (options.yaml !== undefined) {\n routes.push({\n method: 'GET',\n path: options.yaml,\n name: 'yaml',\n handler: ctx => this.#serveDocument(ctx, 'yaml'),\n ...common,\n })\n }\n if (options.ui !== undefined) {\n routes.push({\n method: 'GET',\n path: options.uiPath,\n name: 'ui',\n handler: ctx => this.#serveUi(ctx, context.prefix),\n ...common,\n })\n if (this.#localPackage(options) !== undefined) {\n routes.push({\n method: 'GET',\n path: `${options.uiPath === '/' ? '' : options.uiPath}/_assets/*`,\n name: 'assets',\n handler: ctx => this.#serveAsset(ctx),\n ...common,\n })\n }\n }\n return routes\n }\n\n async onBootstrap(ctx: PluginContext): Promise<void> {\n this.#container = ctx.container\n const options = this.#resolve()\n if (!options.enabled) return\n this.#checkServers(options)\n if (options.lazy && options.file === undefined) return\n await this.served()\n }\n\n onReady(ctx: PluginContext): void {\n const options = this.#resolved\n const address = this.#http?.address\n if (options === undefined || !options.enabled || options.ui === undefined || address === undefined) return\n ctx.logger.info(`API reference: ${String(address.url).replace(/\\/$/, '')}${this.#prefix()}${options.uiPath}`)\n }\n\n onDispose(): void {\n this.#active = false\n this.#served = undefined\n this.#building = undefined\n this.#ui = undefined\n this.#resolved = undefined\n }\n\n /** The views of the operations (no schema conversion). */\n get operations(): readonly OpenApiOperationView[] {\n return this.#operations\n }\n\n /** The served document and bodies, built once (shared promise; a failure is retried on the next call). */\n served(): Promise<Served> {\n if (this.#served !== undefined) return Promise.resolve(this.#served)\n if (this.#building === undefined) {\n this.#building = this.#build().then(\n served => {\n this.#served = served\n return served\n },\n (error: unknown) => {\n this.#building = undefined\n throw error\n },\n )\n }\n return this.#building\n }\n\n async #build(): Promise<Served> {\n const options = this.#resolve()\n const metadata = this.#metadata\n if (metadata === undefined) {\n throw new ZnightError(\n ErrorCode.LifecycleFailed,\n 'OpenApiPlugin: the document is only available once the plugin is registered',\n )\n }\n let document: OpenApiObject\n let json: string\n if (options.file === undefined) {\n const result = await buildOpenApiDocument(metadata, { ...options.build, prefix: this.#prefix() })\n for (const warning of result.warnings) this.#logger?.warn(`${warning.code}: ${warning.message}`)\n this.#logger?.debug('OpenAPI document built', {\n operations: result.stats.operations,\n schemas: result.stats.schemas,\n ms: Math.round(result.stats.durationMs * 100) / 100,\n })\n document = result.document\n json = result.json\n } else {\n json = await readFile(options.file, 'utf8')\n document = parseDocumentFile(json, options.file)\n const expected = openApiSection(metadata).fingerprint\n const actual = (document as { 'x-znight-fingerprint'?: unknown })['x-znight-fingerprint']\n if (actual !== expected) {\n this.#logger?.warn(\n `ZNIGHT030: ${options.file} is stale: it was not emitted from this build of the application (x-znight-fingerprint differs)`,\n { hints: ['Regenerate it: znight openapi (or znight build --openapi)'] },\n )\n }\n const prefix = this.#prefix()\n if (options.build.servers === undefined) this.#checkFileServers(options.file, document.servers, prefix)\n // the runtime options apply to the file as to a build (filter, info, servers, prefix, transform)\n const applied = applyDocumentOptions(json, metadata, { ...options.build, prefix })\n if (applied !== undefined) {\n document = applied.document\n json = applied.json\n }\n }\n const yaml = options.yaml === undefined ? undefined : body(await toYaml(document), 'application/yaml')\n return { document, json: body(json, 'application/json; charset=utf-8'), yaml }\n }\n\n // ------------------------------------------------------------------------------------------------\n // handlers\n\n async #serveDocument(ctx: HttpContext, format: 'json' | 'yaml'): Promise<HttpResponse<Uint8Array | undefined>> {\n const options = this.#resolve()\n let served: Served\n try {\n served = await this.served()\n } catch (error) {\n this.#logger?.error('OpenApiPlugin: the document cannot be built', {\n error: error instanceof Error ? error.message : String(error),\n })\n throw error\n }\n const selected = format === 'json' ? served.json : served.yaml\n if (selected === undefined) throw new NotFoundException()\n return conditional(ctx, selected, options.cacheControl)\n }\n\n async #serveUi(ctx: HttpContext, prefix: string): Promise<HttpResponse<Uint8Array | undefined>> {\n const options = this.#resolve()\n if (this.#ui === undefined) {\n const ui = options.ui\n if (ui === undefined) throw new NotFoundException()\n // the CSP allows the servers of the served document, whatever the build mode\n let document: Pick<OpenApiObject, 'info' | 'servers'> | undefined\n try {\n document = (await this.served()).document\n } catch {\n // the document cannot be built: the declared servers, the page is not cached\n document = this.#declared(options)\n }\n const local = this.#localPackage(options)\n const base = prefix === '/' ? '' : prefix\n const uiPath = options.uiPath === '/' ? '' : options.uiPath\n const page = renderUiPage({\n kind: ui.kind,\n documentUrl: `${base}${options.path}`,\n assetsBase: local === undefined ? undefined : `${base}${uiPath}/_assets/${ui.kind}@${local.version}`,\n title: document?.info.title === undefined ? 'API reference' : `${document.info.title} — API reference`,\n config: ui.config,\n connect: serverOrigins(document?.servers ?? []),\n })\n const bytes = ENCODER.encode(page.html)\n const rendered = { html: bytes, csp: page.csp, etag: etagOf(bytes) }\n if (this.#served === undefined) return uiResponse(ctx, rendered)\n this.#ui = rendered\n }\n return uiResponse(ctx, this.#ui)\n }\n\n /** `info` and `servers` as declared (static template and runtime options), without building. */\n #declared(options: Resolved): Pick<OpenApiObject, 'info' | 'servers'> | undefined {\n const metadata = this.#metadata\n if (metadata === undefined) return undefined\n let template: OpenApiObject['info'] | undefined\n let servers: readonly ServerObject[] | undefined\n try {\n const section = openApiSection(metadata)\n template = section.document.info\n servers = section.document.servers\n } catch {\n return undefined\n }\n const info = { ...template, ...options.build.info } as OpenApiObject['info']\n const resolved = options.build.servers ?? servers\n return resolved === undefined ? { info } : { info, servers: resolved }\n }\n\n #serveAsset(ctx: HttpContext): HttpResponse {\n const options = this.#resolve()\n const ui = options.ui\n const local = this.#localPackage(options)\n if (ui === undefined || local === undefined) throw new NotFoundException()\n const requested = ctx.params['*'] ?? ''\n const prefix = `${ui.kind}@${local.version}/`\n const asset = requested.startsWith(prefix)\n ? UI_PACKAGES[ui.kind].assets.find(\n candidate =>\n candidate.file.endsWith(`/${requested.slice(prefix.length)}`) ||\n candidate.file === requested.slice(prefix.length),\n )\n : undefined\n if (asset === undefined) throw new NotFoundException()\n return HttpResponse.file(asset.file, { root: local.dir }).withHeader('cache-control', ASSET_CACHE)\n }\n\n // ------------------------------------------------------------------------------------------------\n // options\n\n #localPackage(options: Resolved): LocalPackage | undefined {\n if (options.ui === undefined || options.assets === 'cdn') return undefined\n if (this.#local === undefined) {\n this.#local = findLocalPackage(UI_PACKAGES[options.ui.kind])\n if (this.#local === undefined && options.assets === 'local') {\n throw new ZnightError(\n ErrorCode.InvalidOpenApiDefinition,\n `OpenApiPlugin: assets: 'local' needs ${UI_PACKAGES[options.ui.kind].name}, which is not installed`,\n {\n hints: [`npm install ${UI_PACKAGES[options.ui.kind].name}`, \"Or serve the pinned CDN copy: assets: 'cdn'\"],\n },\n )\n }\n }\n return this.#local\n }\n\n #prefix(): string {\n try {\n return this.#http?.prefix ?? ''\n } catch {\n return ''\n }\n }\n\n #checkServers(options: Resolved): void {\n const prefix = this.#prefix()\n // a prebuilt file is checked once read (#checkFileServers), unless `servers` replaces its servers\n if (prefix === '' || (options.file !== undefined && options.build.servers === undefined)) return\n const metadata = this.#metadata\n const declared =\n options.build.servers ?? (metadata === undefined ? undefined : openApiSection(metadata).document.servers)\n this.#checkPrefix(declared ?? [], prefix)\n }\n\n /**\n * The servers of a prebuilt file against the HTTP prefix (the file may be emitted without the\n * prefix, or with servers of another deployment): ZNIGHT030 when it declares no servers, when a\n * relative server does not end with the prefix, or when no server ends with it (`servers: []`,\n * absolute servers of another path). Runtime `servers` replace the file's and skip this check.\n */\n #checkFileServers(file: string, servers: readonly ServerObject[] | undefined, prefix: string): void {\n if (prefix === '' || prefix === '/') return\n if (servers === undefined) {\n this.#logger?.warn(\n `ZNIGHT030: ${file} declares no servers, but the HTTP prefix is \"${prefix}\": it is served with the server \"${prefix}\", and clients generated from the file would call the wrong paths`,\n {\n hints: [\n `Emit it with the prefix: znight openapi --prefix ${prefix}, znight build --openapi --openapi-prefix ${prefix}, or openapi({ prefix: '${prefix}' }) in vite.config`,\n ],\n },\n )\n return\n }\n // a hand-edited file may hold anything: only well-formed servers count\n const declared = Array.isArray(servers) ? servers.filter(server => typeof server?.url === 'string') : []\n // a relative server not ending with the prefix is reported on its own\n if (this.#checkPrefix(declared, prefix) > 0) return\n if (declared.some(server => reflectsPrefix(server.url, prefix))) return\n const listed = declared.length === 0 ? '[]' : declared.map(server => `\"${server.url}\"`).join(', ')\n this.#logger?.warn(\n `ZNIGHT030: ${file} declares no server ending with the HTTP prefix \"${prefix}\" (servers: ${listed}): clients generated from the file would call the wrong paths`,\n {\n hints: [\n `Declare a server ending with \"${prefix}\" (@OpenApiDocument({ servers: [{ url: '${prefix}' }] })) and emit the file again`,\n 'Behind a gateway that rewrites the paths, replace the servers at runtime: OpenApiPlugin.configure({ servers })',\n ],\n },\n )\n }\n\n /** Warns about each relative server that does not end with the HTTP prefix; returns how many. */\n #checkPrefix(servers: readonly ServerObject[], prefix: string): number {\n let mismatched = 0\n for (const server of servers) {\n if (server.url.startsWith('/') && !server.url.replace(/\\/$/, '').endsWith(prefix)) {\n mismatched++\n this.#logger?.warn(\n `ZNIGHT030: the OpenAPI server \"${server.url}\" does not end with the HTTP prefix \"${prefix}\": generated clients would call the wrong paths`,\n )\n }\n }\n return mismatched\n }\n\n #resolve(): Resolved {\n if (this.#resolved !== undefined) return this.#resolved\n let raw: OpenApiPluginOptions\n if (typeof this.#options === 'function') {\n const container = this.#container\n if (container === undefined) {\n throw new ZnightError(\n ErrorCode.LifecycleFailed,\n 'OpenApiPlugin: its options are a function of the container, which is not ready',\n )\n }\n raw = resolveOptions(this.#options, container)\n } else {\n raw = this.#options\n }\n this.#resolved = resolvePluginOptions(raw)\n return this.#resolved\n }\n}\n\n/** Implementation of the {@link OpenApi} external. */\nclass OpenApiService extends OpenApi {\n readonly #plugin: OpenApiPlugin\n\n constructor(plugin: OpenApiPlugin) {\n super()\n this.#plugin = plugin\n }\n\n get operations(): readonly OpenApiOperationView[] {\n return this.#plugin.operations\n }\n\n async document(): Promise<OpenApiObject> {\n return (await this.#plugin.served()).document\n }\n\n async json(): Promise<string> {\n return new TextDecoder().decode((await this.#plugin.served()).json.bytes)\n }\n\n async yaml(): Promise<string> {\n const served = await this.#plugin.served()\n return served.yaml === undefined ? toYaml(served.document) : new TextDecoder().decode(served.yaml.bytes)\n }\n}\n\n/** The UI page: HTML, its Content-Security-Policy, revalidated with its ETag. */\nfunction uiResponse(\n ctx: HttpContext,\n page: { readonly html: Uint8Array; readonly csp: string; readonly etag: string },\n): HttpResponse<Uint8Array | undefined> {\n const response = conditional(\n ctx,\n { bytes: page.html, etag: page.etag, contentType: 'text/html; charset=utf-8' },\n 'no-cache',\n )\n return response.withHeader('content-security-policy', page.csp).withHeader('x-content-type-options', 'nosniff')\n}\n\nfunction body(text: string, contentType: string): Body {\n const bytes = ENCODER.encode(text)\n return { bytes, etag: etagOf(bytes), contentType }\n}\n\n/** Strong ETag: base64url SHA-256, 27 characters. */\nfunction etagOf(bytes: Uint8Array): string {\n return `\"${createHash('sha256').update(bytes).digest('base64url').slice(0, 27)}\"`\n}\n\nfunction conditional(ctx: HttpContext, served: Body, cacheControl: string): HttpResponse<Uint8Array | undefined> {\n const headers = { etag: served.etag, 'cache-control': cacheControl }\n if (matches(ctx.header('if-none-match'), served.etag))\n return new HttpResponse<Uint8Array | undefined>(undefined, { status: 304, headers })\n return new HttpResponse<Uint8Array | undefined>(served.bytes, {\n status: 200,\n headers: { ...headers, 'content-type': served.contentType },\n })\n}\n\nfunction matches(header: string | undefined, etag: string): boolean {\n if (header === undefined) return false\n return header.split(',').some(candidate => {\n const tag = candidate.trim()\n return tag === '*' || tag === etag || tag === `W/${etag}`\n })\n}\n\n/** Origins of the absolute server URLs (`connect-src` of the UI). */\nfunction serverOrigins(servers: readonly ServerObject[]): string[] {\n const origins = new Set<string>()\n for (const server of servers) {\n try {\n const url = new URL(server.url)\n if (url.protocol === 'http:' || url.protocol === 'https:') origins.add(url.origin)\n } catch {\n // relative server: 'self'\n }\n }\n return [...origins].sort()\n}\n\nfunction parseDocumentFile(text: string, file: string): OpenApiObject {\n let document: unknown\n try {\n document = JSON.parse(text)\n } catch (error) {\n throw new ZnightError(ErrorCode.InvalidOpenApiDefinition, `OpenApiPlugin: ${file} is not valid JSON`, {\n cause: error,\n })\n }\n const version = (document as { openapi?: unknown } | null)?.openapi\n if (typeof version !== 'string' || !version.startsWith('3.1.')) {\n throw new ZnightError(ErrorCode.InvalidOpenApiDefinition, `OpenApiPlugin: ${file} is not an OpenAPI 3.1 document`, {\n hints: ['Emit it with znight openapi (or znight build --openapi)'],\n })\n }\n return deepFreeze(document as OpenApiObject)\n}\n\nfunction deepFreeze<T>(value: T): T {\n if (typeof value === 'object' && value !== null && !Object.isFrozen(value)) {\n Object.freeze(value)\n for (const item of Object.values(value)) deepFreeze(item)\n }\n return value\n}\n\nfunction invalid(option: string, expected: string, value: unknown): ZnightError {\n let shown: string\n try {\n shown = typeof value === 'function' ? 'a function' : (JSON.stringify(value) ?? String(value))\n } catch {\n shown = typeof value\n }\n return new ZnightError(\n ErrorCode.InvalidOpenApiDefinition,\n `Invalid OpenApiPlugin option \"${option}\": expected ${expected}, received ${shown}`,\n )\n}\n\nconst OPTION_KEYS = new Set([\n 'enabled',\n 'path',\n 'yaml',\n 'ui',\n 'uiPath',\n 'assets',\n 'document',\n 'build',\n 'info',\n 'servers',\n 'filter',\n 'transform',\n 'converters',\n 'unrepresentable',\n 'meta',\n 'guards',\n 'cacheControl',\n])\n\n/** Checks and defaults the options (ZNIGHT030). */\nexport function resolvePluginOptions(options: OpenApiPluginOptions): Resolved {\n if (options === null || typeof options !== 'object') throw invalid('options', 'an object', options)\n for (const key of Object.keys(options)) {\n if (!OPTION_KEYS.has(key)) throw invalid(key, `one of ${[...OPTION_KEYS].join(', ')} (unknown option)`, key)\n }\n const enabled = options.enabled ?? true\n if (typeof enabled !== 'boolean') throw invalid('enabled', 'true or false', enabled)\n const route = (key: 'path' | 'uiPath', fallback: string): string => {\n const value = options[key] ?? fallback\n if (\n typeof value !== 'string' ||\n !PATH.test(value) ||\n value.includes('//') ||\n (value.length > 1 && value.endsWith('/'))\n ) {\n throw invalid(key, \"a static path starting with '/' ('/openapi.json')\", value)\n }\n return value\n }\n const path = route('path', '/openapi.json')\n const uiPath = route('uiPath', '/docs')\n const yamlOption = options.yaml ?? false\n if (yamlOption !== false && (typeof yamlOption !== 'string' || !PATH.test(yamlOption))) {\n throw invalid('yaml', \"false or a path ('/openapi.yaml')\", yamlOption)\n }\n const yaml = yamlOption === false ? undefined : yamlOption\n const uiOption = options.ui ?? 'scalar'\n let ui: Resolved['ui']\n if (uiOption === 'scalar' || uiOption === 'swagger') ui = { kind: uiOption, config: {} }\n else if (uiOption === false) ui = undefined\n else if (typeof uiOption === 'object' && uiOption !== null && Object.keys(uiOption).length === 1) {\n const kind = Object.keys(uiOption)[0] as UiKind\n const config = (uiOption as Record<string, unknown>)[kind]\n if ((kind !== 'scalar' && kind !== 'swagger') || typeof config !== 'object' || config === null || !isJson(config)) {\n throw invalid(\n 'ui',\n \"'scalar', 'swagger', false, { scalar: config } or { swagger: config } (JSON config)\",\n uiOption,\n )\n }\n ui = { kind, config: config as Readonly<Record<string, JsonValue>> }\n } else throw invalid('ui', \"'scalar', 'swagger', false, { scalar: config } or { swagger: config }\", uiOption)\n const used = [path, ...(yaml === undefined ? [] : [yaml]), ...(ui === undefined ? [] : [uiPath])]\n if (new Set(used).size !== used.length)\n throw invalid('path', 'distinct paths for the document, the YAML and the UI', used)\n const assets = options.assets ?? 'auto'\n if (assets !== 'auto' && assets !== 'local' && assets !== 'cdn')\n throw invalid('assets', \"'auto', 'local' or 'cdn'\", assets)\n const documentOption = options.document ?? 'build'\n let file: string | undefined\n if (documentOption !== 'build') {\n const target = (documentOption as { file?: unknown } | null)?.file\n if (typeof target === 'string' && target !== '') file = target\n else if (target instanceof URL) file = fileURLToPath(target)\n else throw invalid('document', \"'build' or { file: string | URL }\", documentOption)\n }\n const build = options.build ?? 'boot'\n if (build !== 'boot' && build !== 'first-request') throw invalid('build', \"'boot' or 'first-request'\", build)\n for (const key of ['filter', 'transform'] as const) {\n if (options[key] !== undefined && typeof options[key] !== 'function') throw invalid(key, 'a function', options[key])\n }\n if (options.converters !== undefined) {\n if (\n !Array.isArray(options.converters) ||\n options.converters.some(converter => typeof converter?.convert !== 'function')\n ) {\n throw invalid('converters', 'an array of { name, convert(schema, context) }', options.converters)\n }\n }\n if (\n options.unrepresentable !== undefined &&\n options.unrepresentable !== 'error' &&\n options.unrepresentable !== 'warn'\n ) {\n throw invalid('unrepresentable', \"'error' or 'warn'\", options.unrepresentable)\n }\n if (options.info !== undefined && (typeof options.info !== 'object' || options.info === null))\n throw invalid('info', 'an object', options.info)\n if (\n options.servers !== undefined &&\n (!Array.isArray(options.servers) || options.servers.some(server => typeof server?.url !== 'string'))\n ) {\n throw invalid('servers', 'an array of { url, description? }', options.servers)\n }\n const meta = options.meta ?? []\n if (!Array.isArray(meta)) throw invalid('meta', 'an array of route metadata entries (Public.meta())', meta)\n const guards = options.guards ?? []\n if (!Array.isArray(guards)) throw invalid('guards', 'an array of guards (classes or instances)', guards)\n const cacheControl = options.cacheControl ?? 'no-cache'\n if (typeof cacheControl !== 'string' || cacheControl === '')\n throw invalid('cacheControl', 'a cache-control value', cacheControl)\n const buildOptions: BuildOpenApiOptions = {\n ...(options.info === undefined ? {} : { info: options.info }),\n ...(options.servers === undefined ? {} : { servers: options.servers }),\n ...(options.filter === undefined ? {} : { filter: options.filter }),\n ...(options.transform === undefined ? {} : { transform: options.transform }),\n ...(options.converters === undefined ? {} : { converters: options.converters }),\n ...(options.unrepresentable === undefined ? {} : { unrepresentable: options.unrepresentable }),\n }\n return {\n enabled,\n path,\n yaml,\n ui,\n uiPath,\n assets,\n file,\n lazy: build === 'first-request',\n build: buildOptions,\n meta,\n guards,\n cacheControl,\n }\n}\n\n/**\n * Whether a server URL ends with the HTTP prefix: a relative URL (`/api`, `/v2/api/`), or the path\n * of an absolute one (`https://api.example.com/api`). A URL with variables (`{basePath}`) or that\n * cannot be parsed is not judged.\n */\nfunction reflectsPrefix(url: string, prefix: string): boolean {\n if (url.includes('{')) return true\n let path = url\n if (/^[a-z][a-z\\d+.-]*:/i.test(url)) {\n try {\n path = new URL(url).pathname\n } catch {\n return true\n }\n }\n return path\n .replace(/[?#].*$/, '')\n .replace(/\\/+$/, '')\n .endsWith(prefix.replace(/\\/+$/, ''))\n}\n\nfunction isJson(value: unknown): boolean {\n if (value === null || typeof value === 'string' || typeof value === 'boolean') return true\n if (typeof value === 'number') return Number.isFinite(value)\n if (Array.isArray(value)) return value.every(isJson)\n if (typeof value === 'object') {\n const prototype = Object.getPrototypeOf(value)\n return (prototype === Object.prototype || prototype === null) && Object.values(value).every(isJson)\n }\n return false\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ErrorCode, ZnightError } from "@znight/core";
|
|
2
|
+
// src/openapi.ts
|
|
3
|
+
/**
|
|
4
|
+
* The document of the application, provided by `OpenApiPlugin` (a singleton external declared in
|
|
5
|
+
* the manifest of `@znight/openapi`).
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* @Injectable()
|
|
9
|
+
* export class GatewayRegistration implements OnReady {
|
|
10
|
+
* constructor(private readonly openapi: OpenApi, private readonly gateway: GatewayClient) {}
|
|
11
|
+
* async onReady(): Promise<void> {
|
|
12
|
+
* await this.gateway.publish(await this.openapi.json())
|
|
13
|
+
* }
|
|
14
|
+
* }
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
var OpenApi = class {};
|
|
18
|
+
/** Tokens of the externals provided by `OpenApiPlugin` (declared in the package manifest). */
|
|
19
|
+
const OpenApiTokens = Object.freeze({ OpenApi: "@znight/openapi#OpenApi" });
|
|
20
|
+
/** YAML text of a document (`yaml` is an optional peer, loaded lazily). */
|
|
21
|
+
async function toYaml(document) {
|
|
22
|
+
let module;
|
|
23
|
+
try {
|
|
24
|
+
module = await import("yaml");
|
|
25
|
+
} catch (error) {
|
|
26
|
+
throw new ZnightError(ErrorCode.InvalidOpenApiDefinition, "YAML output needs the optional peer dependency \"yaml\"", {
|
|
27
|
+
cause: error,
|
|
28
|
+
hints: ["npm install yaml"]
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
if (typeof module.stringify !== "function") throw new ZnightError(ErrorCode.InvalidOpenApiDefinition, "The installed \"yaml\" package has no stringify()", { hints: ["npm install yaml@2"] });
|
|
32
|
+
return module.stringify(document, {
|
|
33
|
+
aliasDuplicateObjects: false,
|
|
34
|
+
lineWidth: 0
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
export { OpenApiTokens as n, toYaml as r, OpenApi as t };
|
|
38
|
+
|
|
39
|
+
//# sourceMappingURL=openapi-CQk5dHqc.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openapi-CQk5dHqc.mjs","names":[],"sources":["../src/openapi.ts"],"sourcesContent":["/** The injectable `OpenApi` external and the YAML serialisation. */\nimport type { OpenApiOperationView } from './build.ts'\nimport type { OpenApiObject } from './types.ts'\n\nimport { ErrorCode, type TypedToken, ZnightError } from '@znight/core'\n\n/**\n * The document of the application, provided by `OpenApiPlugin` (a singleton external declared in\n * the manifest of `@znight/openapi`).\n *\n * ```ts\n * @Injectable()\n * export class GatewayRegistration implements OnReady {\n * constructor(private readonly openapi: OpenApi, private readonly gateway: GatewayClient) {}\n * async onReady(): Promise<void> {\n * await this.gateway.publish(await this.openapi.json())\n * }\n * }\n * ```\n */\nexport abstract class OpenApi {\n /** The frozen document (built on the first call in `'first-request'` mode, or when the plugin is disabled). */\n abstract document(): Promise<OpenApiObject>\n /** The served JSON text (2 spaces, final newline). */\n abstract json(): Promise<string>\n /** The YAML text (optional peer `yaml`). */\n abstract yaml(): Promise<string>\n /** Static view of the operations, without any schema conversion. */\n abstract readonly operations: readonly OpenApiOperationView[]\n}\n\n/** Tokens of the externals provided by `OpenApiPlugin` (declared in the package manifest). */\nexport const OpenApiTokens: {\n /** The {@link OpenApi} singleton. */\n readonly OpenApi: TypedToken<OpenApi>\n} = Object.freeze({ OpenApi: '@znight/openapi#OpenApi' })\n\n/** YAML text of a document (`yaml` is an optional peer, loaded lazily). */\nexport async function toYaml(document: OpenApiObject): Promise<string> {\n let module: { stringify?: (value: unknown, options?: Record<string, unknown>) => string }\n try {\n module = (await import('yaml')) as typeof module\n } catch (error) {\n throw new ZnightError(ErrorCode.InvalidOpenApiDefinition, 'YAML output needs the optional peer dependency \"yaml\"', {\n cause: error,\n hints: ['npm install yaml'],\n })\n }\n if (typeof module.stringify !== 'function') {\n throw new ZnightError(ErrorCode.InvalidOpenApiDefinition, 'The installed \"yaml\" package has no stringify()', {\n hints: ['npm install yaml@2'],\n })\n }\n return module.stringify(document, { aliasDuplicateObjects: false, lineWidth: 0 })\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAoBA,IAAsB,UAAtB,MAA8B,CAS9B;;AAGA,MAAa,gBAGT,OAAO,OAAO,EAAE,SAAS,0BAA0B,CAAC;;AAGxD,eAAsB,OAAO,UAA0C;CACrE,IAAI;CACJ,IAAI;EACF,SAAU,MAAM,OAAO;CACzB,SAAS,OAAO;EACd,MAAM,IAAI,YAAY,UAAU,0BAA0B,2DAAyD;GACjH,OAAO;GACP,OAAO,CAAC,kBAAkB;EAC5B,CAAC;CACH;CACA,IAAI,OAAO,OAAO,cAAc,YAC9B,MAAM,IAAI,YAAY,UAAU,0BAA0B,qDAAmD,EAC3G,OAAO,CAAC,oBAAoB,EAC9B,CAAC;CAEH,OAAO,OAAO,UAAU,UAAU;EAAE,uBAAuB;EAAO,WAAW;CAAE,CAAC;AAClF"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// src/shared.ts
|
|
2
|
+
/**
|
|
3
|
+
* Import-free helpers shared by the compiler extension and the runtime (the compiler entry may
|
|
4
|
+
* only reach modules of the package that import nothing at runtime).
|
|
5
|
+
*/
|
|
6
|
+
/** Grammar of operation ids. */
|
|
7
|
+
const OPERATION_ID = /^[A-Za-z_][\w.-]*$/;
|
|
8
|
+
/** Grammar of HTTP header names (RFC 9110 token). */
|
|
9
|
+
const HEADER_NAME = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
|
|
10
|
+
/** A media type or a range (`application/json`, `image/*`, `*\/*`), parameters allowed. */
|
|
11
|
+
const MEDIA_TYPE = /^(?:\*\/\*|[\w!#$&^.+-]+\/(?:\*|[\w!#$&^.+-]+))(?:\s*;\s*[\w!#$&^.+-]+=(?:[\w!#$&^.+-]+|"[^"]*"))*$/;
|
|
12
|
+
/** OpenAPI methods, in document order. */
|
|
13
|
+
const METHOD_ORDER = [
|
|
14
|
+
"get",
|
|
15
|
+
"put",
|
|
16
|
+
"post",
|
|
17
|
+
"delete",
|
|
18
|
+
"options",
|
|
19
|
+
"head",
|
|
20
|
+
"patch"
|
|
21
|
+
];
|
|
22
|
+
/** Reserved names of the standard error components. */
|
|
23
|
+
const ERROR_COMPONENTS = Object.freeze({
|
|
24
|
+
body: "HttpErrorBody",
|
|
25
|
+
validation: "ValidationErrorBody"
|
|
26
|
+
});
|
|
27
|
+
/** Standard responses referenced by operations (`#/components/responses/<name>`). */
|
|
28
|
+
const ERROR_RESPONSES = Object.freeze({
|
|
29
|
+
badRequest: "BadRequest",
|
|
30
|
+
payloadTooLarge: "PayloadTooLarge",
|
|
31
|
+
unsupportedMediaType: "UnsupportedMediaType"
|
|
32
|
+
});
|
|
33
|
+
/** The OpenAPI version of the documents. */
|
|
34
|
+
const OPENAPI_VERSION = "3.1.1";
|
|
35
|
+
/** A component name: `[A-Za-z0-9._-]`, other characters replaced with `_`. */
|
|
36
|
+
function componentName(name) {
|
|
37
|
+
const cleaned = name.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
38
|
+
return cleaned === "" ? "_" : cleaned;
|
|
39
|
+
}
|
|
40
|
+
/** `#/components/schemas/<name>`. */
|
|
41
|
+
function schemaRef(name) {
|
|
42
|
+
return `#/components/schemas/${name}`;
|
|
43
|
+
}
|
|
44
|
+
/** Code-point comparison of strings (deterministic across runtimes and locales). */
|
|
45
|
+
function compareStrings(a, b) {
|
|
46
|
+
if (a === b) return 0;
|
|
47
|
+
return a < b ? -1 : 1;
|
|
48
|
+
}
|
|
49
|
+
/** Deep equality of JSON-like values (key order ignored). */
|
|
50
|
+
function jsonEquals(a, b) {
|
|
51
|
+
if (a === b) return true;
|
|
52
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
53
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
54
|
+
if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((item, index) => jsonEquals(item, b[index]));
|
|
55
|
+
const keysA = Object.keys(a).filter((key) => a[key] !== void 0);
|
|
56
|
+
const keysB = Object.keys(b).filter((key) => b[key] !== void 0);
|
|
57
|
+
if (keysA.length !== keysB.length) return false;
|
|
58
|
+
return keysA.every((key) => jsonEquals(a[key], b[key]));
|
|
59
|
+
}
|
|
60
|
+
/** JSON text with sorted object keys (fingerprints). */
|
|
61
|
+
function canonicalJson(value) {
|
|
62
|
+
return JSON.stringify(sortKeys(value));
|
|
63
|
+
}
|
|
64
|
+
function sortKeys(value) {
|
|
65
|
+
if (Array.isArray(value)) return value.map(sortKeys);
|
|
66
|
+
if (typeof value !== "object" || value === null) return value;
|
|
67
|
+
const sorted = {};
|
|
68
|
+
for (const key of Object.keys(value).sort(compareStrings)) {
|
|
69
|
+
const item = value[key];
|
|
70
|
+
if (item !== void 0) sorted[key] = sortKeys(item);
|
|
71
|
+
}
|
|
72
|
+
return sorted;
|
|
73
|
+
}
|
|
74
|
+
/** Reason phrases of the statuses documented by default (`description` of responses). */
|
|
75
|
+
const REASONS = {
|
|
76
|
+
100: "Continue",
|
|
77
|
+
101: "Switching Protocols",
|
|
78
|
+
200: "OK",
|
|
79
|
+
201: "Created",
|
|
80
|
+
202: "Accepted",
|
|
81
|
+
203: "Non-Authoritative Information",
|
|
82
|
+
204: "No Content",
|
|
83
|
+
205: "Reset Content",
|
|
84
|
+
206: "Partial Content",
|
|
85
|
+
207: "Multi-Status",
|
|
86
|
+
300: "Multiple Choices",
|
|
87
|
+
301: "Moved Permanently",
|
|
88
|
+
302: "Found",
|
|
89
|
+
303: "See Other",
|
|
90
|
+
304: "Not Modified",
|
|
91
|
+
307: "Temporary Redirect",
|
|
92
|
+
308: "Permanent Redirect",
|
|
93
|
+
400: "Bad Request",
|
|
94
|
+
401: "Unauthorized",
|
|
95
|
+
402: "Payment Required",
|
|
96
|
+
403: "Forbidden",
|
|
97
|
+
404: "Not Found",
|
|
98
|
+
405: "Method Not Allowed",
|
|
99
|
+
406: "Not Acceptable",
|
|
100
|
+
408: "Request Timeout",
|
|
101
|
+
409: "Conflict",
|
|
102
|
+
410: "Gone",
|
|
103
|
+
411: "Length Required",
|
|
104
|
+
412: "Precondition Failed",
|
|
105
|
+
413: "Content Too Large",
|
|
106
|
+
414: "URI Too Long",
|
|
107
|
+
415: "Unsupported Media Type",
|
|
108
|
+
416: "Range Not Satisfiable",
|
|
109
|
+
417: "Expectation Failed",
|
|
110
|
+
418: "I'm a teapot",
|
|
111
|
+
421: "Misdirected Request",
|
|
112
|
+
422: "Unprocessable Content",
|
|
113
|
+
423: "Locked",
|
|
114
|
+
424: "Failed Dependency",
|
|
115
|
+
425: "Too Early",
|
|
116
|
+
426: "Upgrade Required",
|
|
117
|
+
428: "Precondition Required",
|
|
118
|
+
429: "Too Many Requests",
|
|
119
|
+
431: "Request Header Fields Too Large",
|
|
120
|
+
451: "Unavailable For Legal Reasons",
|
|
121
|
+
500: "Internal Server Error",
|
|
122
|
+
501: "Not Implemented",
|
|
123
|
+
502: "Bad Gateway",
|
|
124
|
+
503: "Service Unavailable",
|
|
125
|
+
504: "Gateway Timeout",
|
|
126
|
+
505: "HTTP Version Not Supported"
|
|
127
|
+
};
|
|
128
|
+
/** Description of a response without one: its reason phrase (`'2XX'` → `Success`). */
|
|
129
|
+
function reasonPhrase(status) {
|
|
130
|
+
if (status === "default") return "Unexpected response";
|
|
131
|
+
const range = /^([1-5])XX$/.exec(status);
|
|
132
|
+
if (range) return {
|
|
133
|
+
"1": "Informational",
|
|
134
|
+
"2": "Success",
|
|
135
|
+
"3": "Redirection",
|
|
136
|
+
"4": "Client error",
|
|
137
|
+
"5": "Server error"
|
|
138
|
+
}[range[1]];
|
|
139
|
+
return REASONS[Number(status)] ?? `Status ${status}`;
|
|
140
|
+
}
|
|
141
|
+
/** `200`-`599` codes, `1XX`-`5XX` ranges, or `default`. */
|
|
142
|
+
function isResponseStatus(status) {
|
|
143
|
+
return status === "default" || /^[1-5]XX$/.test(status) || /^[1-5]\d\d$/.test(status) && Number(status) >= 100;
|
|
144
|
+
}
|
|
145
|
+
export { METHOD_ORDER as a, canonicalJson as c, isResponseStatus as d, jsonEquals as f, MEDIA_TYPE as i, compareStrings as l, schemaRef as m, ERROR_RESPONSES as n, OPENAPI_VERSION as o, reasonPhrase as p, HEADER_NAME as r, OPERATION_ID as s, ERROR_COMPONENTS as t, componentName as u };
|
|
146
|
+
|
|
147
|
+
//# sourceMappingURL=shared-BZxtyaB0.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared-BZxtyaB0.mjs","names":[],"sources":["../src/shared.ts"],"sourcesContent":["/**\n * Import-free helpers shared by the compiler extension and the runtime (the compiler entry may\n * only reach modules of the package that import nothing at runtime).\n */\n\n/** Grammar of operation ids. */\nexport const OPERATION_ID: RegExp = /^[A-Za-z_][\\w.-]*$/\n\n/** Grammar of HTTP header names (RFC 9110 token). */\nexport const HEADER_NAME: RegExp = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/\n\n/** A media type or a range (`application/json`, `image/*`, `*\\/*`), parameters allowed. */\nexport const MEDIA_TYPE: RegExp =\n /^(?:\\*\\/\\*|[\\w!#$&^.+-]+\\/(?:\\*|[\\w!#$&^.+-]+))(?:\\s*;\\s*[\\w!#$&^.+-]+=(?:[\\w!#$&^.+-]+|\"[^\"]*\"))*$/\n\n/** OpenAPI methods, in document order. */\nexport const METHOD_ORDER: readonly string[] = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch']\n\n/** Reserved names of the standard error components. */\nexport const ERROR_COMPONENTS: { readonly body: 'HttpErrorBody'; readonly validation: 'ValidationErrorBody' } =\n Object.freeze({ body: 'HttpErrorBody', validation: 'ValidationErrorBody' })\n\n/** Standard responses referenced by operations (`#/components/responses/<name>`). */\nexport const ERROR_RESPONSES: {\n readonly badRequest: 'BadRequest'\n readonly payloadTooLarge: 'PayloadTooLarge'\n readonly unsupportedMediaType: 'UnsupportedMediaType'\n} = Object.freeze({\n badRequest: 'BadRequest',\n payloadTooLarge: 'PayloadTooLarge',\n unsupportedMediaType: 'UnsupportedMediaType',\n})\n\n/** The OpenAPI version of the documents. */\nexport const OPENAPI_VERSION = '3.1.1'\n\n/** A component name: `[A-Za-z0-9._-]`, other characters replaced with `_`. */\nexport function componentName(name: string): string {\n const cleaned = name.replace(/[^A-Za-z0-9._-]/g, '_')\n return cleaned === '' ? '_' : cleaned\n}\n\n/** `#/components/schemas/<name>`. */\nexport function schemaRef(name: string): string {\n return `#/components/schemas/${name}`\n}\n\n/** Code-point comparison of strings (deterministic across runtimes and locales). */\nexport function compareStrings(a: string, b: string): number {\n if (a === b) return 0\n return a < b ? -1 : 1\n}\n\n/** Deep equality of JSON-like values (key order ignored). */\nexport function jsonEquals(a: unknown, b: unknown): boolean {\n if (a === b) return true\n if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return false\n if (Array.isArray(a) !== Array.isArray(b)) return false\n if (Array.isArray(a) && Array.isArray(b)) {\n return a.length === b.length && a.every((item, index) => jsonEquals(item, b[index]))\n }\n const keysA = Object.keys(a).filter(key => (a as Record<string, unknown>)[key] !== undefined)\n const keysB = Object.keys(b).filter(key => (b as Record<string, unknown>)[key] !== undefined)\n if (keysA.length !== keysB.length) return false\n return keysA.every(key => jsonEquals((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key]))\n}\n\n/** JSON text with sorted object keys (fingerprints). */\nexport function canonicalJson(value: unknown): string {\n return JSON.stringify(sortKeys(value))\n}\n\nfunction sortKeys(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(sortKeys)\n if (typeof value !== 'object' || value === null) return value\n const sorted: Record<string, unknown> = {}\n for (const key of Object.keys(value).sort(compareStrings)) {\n const item = (value as Record<string, unknown>)[key]\n if (item !== undefined) sorted[key] = sortKeys(item)\n }\n return sorted\n}\n\n/** Reason phrases of the statuses documented by default (`description` of responses). */\nconst REASONS: Readonly<Record<number, string>> = {\n 100: 'Continue',\n 101: 'Switching Protocols',\n 200: 'OK',\n 201: 'Created',\n 202: 'Accepted',\n 203: 'Non-Authoritative Information',\n 204: 'No Content',\n 205: 'Reset Content',\n 206: 'Partial Content',\n 207: 'Multi-Status',\n 300: 'Multiple Choices',\n 301: 'Moved Permanently',\n 302: 'Found',\n 303: 'See Other',\n 304: 'Not Modified',\n 307: 'Temporary Redirect',\n 308: 'Permanent Redirect',\n 400: 'Bad Request',\n 401: 'Unauthorized',\n 402: 'Payment Required',\n 403: 'Forbidden',\n 404: 'Not Found',\n 405: 'Method Not Allowed',\n 406: 'Not Acceptable',\n 408: 'Request Timeout',\n 409: 'Conflict',\n 410: 'Gone',\n 411: 'Length Required',\n 412: 'Precondition Failed',\n 413: 'Content Too Large',\n 414: 'URI Too Long',\n 415: 'Unsupported Media Type',\n 416: 'Range Not Satisfiable',\n 417: 'Expectation Failed',\n 418: \"I'm a teapot\",\n 421: 'Misdirected Request',\n 422: 'Unprocessable Content',\n 423: 'Locked',\n 424: 'Failed Dependency',\n 425: 'Too Early',\n 426: 'Upgrade Required',\n 428: 'Precondition Required',\n 429: 'Too Many Requests',\n 431: 'Request Header Fields Too Large',\n 451: 'Unavailable For Legal Reasons',\n 500: 'Internal Server Error',\n 501: 'Not Implemented',\n 502: 'Bad Gateway',\n 503: 'Service Unavailable',\n 504: 'Gateway Timeout',\n 505: 'HTTP Version Not Supported',\n}\n\n/** Description of a response without one: its reason phrase (`'2XX'` → `Success`). */\nexport function reasonPhrase(status: string): string {\n if (status === 'default') return 'Unexpected response'\n const range = /^([1-5])XX$/.exec(status)\n if (range) {\n return { '1': 'Informational', '2': 'Success', '3': 'Redirection', '4': 'Client error', '5': 'Server error' }[\n range[1] as '1' | '2' | '3' | '4' | '5'\n ]\n }\n return REASONS[Number(status)] ?? `Status ${status}`\n}\n\n/** `200`-`599` codes, `1XX`-`5XX` ranges, or `default`. */\nexport function isResponseStatus(status: string): boolean {\n return status === 'default' || /^[1-5]XX$/.test(status) || (/^[1-5]\\d\\d$/.test(status) && Number(status) >= 100)\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { _ as OpenApiObject } from "./types-VzBexMKj.mjs";
|
|
2
|
+
/** A problem found by {@link validateResponse}. */
|
|
3
|
+
export interface ResponseIssue {
|
|
4
|
+
/** `status`, `headers.location`, `body.items[0].id`... */
|
|
5
|
+
readonly path: string;
|
|
6
|
+
readonly message: string;
|
|
7
|
+
/** `operation`, `status`, `media-type`, `header`, `json`, or the failing JSON Schema keyword (`type`, `required`...). */
|
|
8
|
+
readonly keyword: string;
|
|
9
|
+
}
|
|
10
|
+
/** A response: an `InjectResponse` of `@znight/http` / `@znight/testing`, or a plain object. */
|
|
11
|
+
export interface ResponseLike {
|
|
12
|
+
readonly status: number;
|
|
13
|
+
readonly headers: Readonly<Record<string, string | readonly string[] | undefined>>;
|
|
14
|
+
readonly body: string;
|
|
15
|
+
}
|
|
16
|
+
/** What to check. */
|
|
17
|
+
export interface ValidateResponseInput {
|
|
18
|
+
/** HTTP method (`'GET'`). */
|
|
19
|
+
readonly method: string;
|
|
20
|
+
/** Path template of the document (`/books/{id}`). */
|
|
21
|
+
readonly path: string;
|
|
22
|
+
readonly response: ResponseLike;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Checks `response` against the documentation of `method path` in `document`. Returns `[]` when it
|
|
26
|
+
* matches.
|
|
27
|
+
*
|
|
28
|
+
* ```ts
|
|
29
|
+
* const response = await app.inject({ method: 'GET', url: '/books/1' })
|
|
30
|
+
* expect(validateResponse(document, { method: 'GET', path: '/books/{id}', response })).toEqual([])
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* The status is looked up as an exact code, then its range (`4XX`), then `default`; the media type
|
|
34
|
+
* against the documented ones (ranges such as `image/*` included); documented headers marked
|
|
35
|
+
* `required` must be present; a JSON body is validated against its schema (`$ref`s to the
|
|
36
|
+
* components followed).
|
|
37
|
+
*/
|
|
38
|
+
export declare function validateResponse(document: OpenApiObject, input: ValidateResponseInput): ResponseIssue[];
|
|
39
|
+
//# sourceMappingURL=testing.d.mts.map
|
package/dist/testing.mjs
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
// src/testing.ts
|
|
3
|
+
let validatorClass;
|
|
4
|
+
function loadValidator() {
|
|
5
|
+
if (validatorClass !== void 0) return validatorClass;
|
|
6
|
+
try {
|
|
7
|
+
const module = createRequire(import.meta.url)("@cfworker/json-schema");
|
|
8
|
+
if (typeof module.Validator !== "function") throw new Error("no Validator export");
|
|
9
|
+
validatorClass = module.Validator;
|
|
10
|
+
return validatorClass;
|
|
11
|
+
} catch (error) {
|
|
12
|
+
throw new Error("validateResponse() needs the optional peer dependency @cfworker/json-schema (npm install -D @cfworker/json-schema)", { cause: error });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const COMPOSITE = /* @__PURE__ */ new Set([
|
|
16
|
+
"properties",
|
|
17
|
+
"items",
|
|
18
|
+
"prefixItems",
|
|
19
|
+
"$ref",
|
|
20
|
+
"allOf",
|
|
21
|
+
"patternProperties",
|
|
22
|
+
"dependentSchemas",
|
|
23
|
+
"$dynamicRef"
|
|
24
|
+
]);
|
|
25
|
+
/**
|
|
26
|
+
* Checks `response` against the documentation of `method path` in `document`. Returns `[]` when it
|
|
27
|
+
* matches.
|
|
28
|
+
*
|
|
29
|
+
* ```ts
|
|
30
|
+
* const response = await app.inject({ method: 'GET', url: '/books/1' })
|
|
31
|
+
* expect(validateResponse(document, { method: 'GET', path: '/books/{id}', response })).toEqual([])
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* The status is looked up as an exact code, then its range (`4XX`), then `default`; the media type
|
|
35
|
+
* against the documented ones (ranges such as `image/*` included); documented headers marked
|
|
36
|
+
* `required` must be present; a JSON body is validated against its schema (`$ref`s to the
|
|
37
|
+
* components followed).
|
|
38
|
+
*/
|
|
39
|
+
function validateResponse(document, input) {
|
|
40
|
+
const issues = [];
|
|
41
|
+
const method = input.method.toLowerCase();
|
|
42
|
+
const operation = document.paths?.[input.path]?.[method];
|
|
43
|
+
if (operation === void 0) return [{
|
|
44
|
+
path: "operation",
|
|
45
|
+
keyword: "operation",
|
|
46
|
+
message: `${input.method.toUpperCase()} ${input.path} is not documented`
|
|
47
|
+
}];
|
|
48
|
+
const status = String(input.response.status);
|
|
49
|
+
const responses = operation.responses ?? {};
|
|
50
|
+
const entry = responses[status] ?? responses[`${status.charAt(0)}XX`] ?? responses["default"];
|
|
51
|
+
if (entry === void 0) return [{
|
|
52
|
+
path: "status",
|
|
53
|
+
keyword: "status",
|
|
54
|
+
message: `The status ${status} is not documented (documented: ${Object.keys(responses).join(", ") || "none"})`
|
|
55
|
+
}];
|
|
56
|
+
const documented = resolveResponse(document, entry);
|
|
57
|
+
if (documented === void 0) return [{
|
|
58
|
+
path: "status",
|
|
59
|
+
keyword: "$ref",
|
|
60
|
+
message: `Cannot resolve the response ${JSON.stringify(entry.$ref)}`
|
|
61
|
+
}];
|
|
62
|
+
const headers = lowerCase(input.response.headers);
|
|
63
|
+
for (const [name, header] of Object.entries(documented.headers ?? {})) if (header.required === true && headers[name.toLowerCase()] === void 0) issues.push({
|
|
64
|
+
path: `headers.${name.toLowerCase()}`,
|
|
65
|
+
keyword: "header",
|
|
66
|
+
message: `The documented header "${name}" is missing`
|
|
67
|
+
});
|
|
68
|
+
const content = documented.content;
|
|
69
|
+
const body = input.response.body;
|
|
70
|
+
const contentType = headers["content-type"];
|
|
71
|
+
const mediaType = (Array.isArray(contentType) ? contentType[0] : contentType)?.split(";")[0]?.trim().toLowerCase();
|
|
72
|
+
if (content === void 0 || Object.keys(content).length === 0) {
|
|
73
|
+
if (body !== "") issues.push({
|
|
74
|
+
path: "body",
|
|
75
|
+
keyword: "content",
|
|
76
|
+
message: `The ${status} response is documented without body, but it has one`
|
|
77
|
+
});
|
|
78
|
+
return issues;
|
|
79
|
+
}
|
|
80
|
+
if (mediaType === void 0) {
|
|
81
|
+
if (body !== "") issues.push({
|
|
82
|
+
path: "headers.content-type",
|
|
83
|
+
keyword: "media-type",
|
|
84
|
+
message: "The response has a body but no content-type"
|
|
85
|
+
});
|
|
86
|
+
return issues;
|
|
87
|
+
}
|
|
88
|
+
const key = Object.keys(content).find((candidate) => mediaMatches(candidate, mediaType));
|
|
89
|
+
if (key === void 0) {
|
|
90
|
+
issues.push({
|
|
91
|
+
path: "headers.content-type",
|
|
92
|
+
keyword: "media-type",
|
|
93
|
+
message: `The media type ${mediaType} is not documented (documented: ${Object.keys(content).join(", ")})`
|
|
94
|
+
});
|
|
95
|
+
return issues;
|
|
96
|
+
}
|
|
97
|
+
const schema = content[key]?.schema;
|
|
98
|
+
if (schema === void 0 || !isJsonMediaType(mediaType)) return issues;
|
|
99
|
+
let value;
|
|
100
|
+
try {
|
|
101
|
+
value = JSON.parse(body);
|
|
102
|
+
} catch {
|
|
103
|
+
issues.push({
|
|
104
|
+
path: "body",
|
|
105
|
+
keyword: "json",
|
|
106
|
+
message: "The body is not valid JSON"
|
|
107
|
+
});
|
|
108
|
+
return issues;
|
|
109
|
+
}
|
|
110
|
+
const result = new (loadValidator())(structuredClone({
|
|
111
|
+
...schema,
|
|
112
|
+
components: document.components ?? {}
|
|
113
|
+
}), "2020-12", false).validate(value);
|
|
114
|
+
if (result.valid) return issues;
|
|
115
|
+
const leaves = result.errors.filter((error) => !COMPOSITE.has(error.keyword));
|
|
116
|
+
for (const error of leaves.length > 0 ? leaves : result.errors) issues.push({
|
|
117
|
+
path: readablePath(error.instanceLocation),
|
|
118
|
+
keyword: error.keyword,
|
|
119
|
+
message: error.error
|
|
120
|
+
});
|
|
121
|
+
return issues;
|
|
122
|
+
}
|
|
123
|
+
function resolveResponse(document, entry) {
|
|
124
|
+
let current = entry;
|
|
125
|
+
for (let depth = 0; depth < 8 && current !== void 0 && "$ref" in current; depth++) {
|
|
126
|
+
const name = current.$ref.replace(/^#\/components\/responses\//, "");
|
|
127
|
+
current = document.components?.responses?.[name];
|
|
128
|
+
}
|
|
129
|
+
return current === void 0 || "$ref" in current ? void 0 : current;
|
|
130
|
+
}
|
|
131
|
+
function lowerCase(headers) {
|
|
132
|
+
const result = {};
|
|
133
|
+
for (const [name, value] of Object.entries(headers)) result[name.toLowerCase()] = value;
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
function mediaMatches(documented, actual) {
|
|
137
|
+
const pattern = documented.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
138
|
+
if (pattern === "*/*" || pattern === actual) return true;
|
|
139
|
+
return pattern.endsWith("/*") && actual.startsWith(pattern.slice(0, -1));
|
|
140
|
+
}
|
|
141
|
+
function isJsonMediaType(mediaType) {
|
|
142
|
+
return mediaType === "application/json" || mediaType.endsWith("+json");
|
|
143
|
+
}
|
|
144
|
+
/** `#/items/0/id` → `body.items[0].id`. */
|
|
145
|
+
function readablePath(pointer) {
|
|
146
|
+
const segments = pointer.replace(/^#\/?/, "").split("/").filter((segment) => segment !== "").map((segment) => segment.replaceAll("~1", "/").replaceAll("~0", "~"));
|
|
147
|
+
let path = "body";
|
|
148
|
+
for (const segment of segments) if (/^\d+$/.test(segment)) path += `[${segment}]`;
|
|
149
|
+
else if (/^[A-Za-z_$][\w$]*$/.test(segment)) path += `.${segment}`;
|
|
150
|
+
else path += `[${JSON.stringify(segment)}]`;
|
|
151
|
+
return path;
|
|
152
|
+
}
|
|
153
|
+
export { validateResponse };
|
|
154
|
+
|
|
155
|
+
//# sourceMappingURL=testing.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing.mjs","names":[],"sources":["../src/testing.ts"],"sourcesContent":["/**\n * `@znight/openapi/testing`: checks that a response matches its documentation (status, media type,\n * documented headers, JSON body against the resolved schema). JSON Schema 2020-12 validation uses\n * the optional peer `@cfworker/json-schema` (no code generation: Bun, Node.js and edge runtimes).\n */\nimport type { JsonSchema, OpenApiObject, ReferenceObject, ResponseObject } from './types.ts'\n\nimport { createRequire } from 'node:module'\n\n/** A problem found by {@link validateResponse}. */\nexport interface ResponseIssue {\n /** `status`, `headers.location`, `body.items[0].id`... */\n readonly path: string\n readonly message: string\n /** `operation`, `status`, `media-type`, `header`, `json`, or the failing JSON Schema keyword (`type`, `required`...). */\n readonly keyword: string\n}\n\n/** A response: an `InjectResponse` of `@znight/http` / `@znight/testing`, or a plain object. */\nexport interface ResponseLike {\n readonly status: number\n readonly headers: Readonly<Record<string, string | readonly string[] | undefined>>\n readonly body: string\n}\n\n/** What to check. */\nexport interface ValidateResponseInput {\n /** HTTP method (`'GET'`). */\n readonly method: string\n /** Path template of the document (`/books/{id}`). */\n readonly path: string\n readonly response: ResponseLike\n}\n\ninterface CfValidator {\n validate(instance: unknown): {\n valid: boolean\n errors: { keyword: string; instanceLocation: string; error: string }[]\n }\n}\ntype CfValidatorClass = new (schema: object, draft?: string, shortCircuit?: boolean) => CfValidator\n\nlet validatorClass: CfValidatorClass | undefined\n\nfunction loadValidator(): CfValidatorClass {\n if (validatorClass !== undefined) return validatorClass\n try {\n const module = createRequire(import.meta.url)('@cfworker/json-schema') as { Validator?: CfValidatorClass }\n if (typeof module.Validator !== 'function') throw new Error('no Validator export')\n validatorClass = module.Validator\n return validatorClass\n } catch (error) {\n throw new Error(\n 'validateResponse() needs the optional peer dependency @cfworker/json-schema (npm install -D @cfworker/json-schema)',\n {\n cause: error,\n },\n )\n }\n}\n\nconst COMPOSITE = new Set([\n 'properties',\n 'items',\n 'prefixItems',\n '$ref',\n 'allOf',\n 'patternProperties',\n 'dependentSchemas',\n '$dynamicRef',\n])\n\n/**\n * Checks `response` against the documentation of `method path` in `document`. Returns `[]` when it\n * matches.\n *\n * ```ts\n * const response = await app.inject({ method: 'GET', url: '/books/1' })\n * expect(validateResponse(document, { method: 'GET', path: '/books/{id}', response })).toEqual([])\n * ```\n *\n * The status is looked up as an exact code, then its range (`4XX`), then `default`; the media type\n * against the documented ones (ranges such as `image/*` included); documented headers marked\n * `required` must be present; a JSON body is validated against its schema (`$ref`s to the\n * components followed).\n */\nexport function validateResponse(document: OpenApiObject, input: ValidateResponseInput): ResponseIssue[] {\n const issues: ResponseIssue[] = []\n const method = input.method.toLowerCase() as 'get'\n const operation = document.paths?.[input.path]?.[method]\n if (operation === undefined) {\n return [\n {\n path: 'operation',\n keyword: 'operation',\n message: `${input.method.toUpperCase()} ${input.path} is not documented`,\n },\n ]\n }\n const status = String(input.response.status)\n const responses = operation.responses ?? {}\n const entry = responses[status] ?? responses[`${status.charAt(0)}XX`] ?? responses['default']\n if (entry === undefined) {\n return [\n {\n path: 'status',\n keyword: 'status',\n message: `The status ${status} is not documented (documented: ${Object.keys(responses).join(', ') || 'none'})`,\n },\n ]\n }\n const documented = resolveResponse(document, entry)\n if (documented === undefined) {\n return [\n {\n path: 'status',\n keyword: '$ref',\n message: `Cannot resolve the response ${JSON.stringify((entry as ReferenceObject).$ref)}`,\n },\n ]\n }\n // headers\n const headers = lowerCase(input.response.headers)\n for (const [name, header] of Object.entries(documented.headers ?? {})) {\n if ((header as { required?: boolean }).required === true && headers[name.toLowerCase()] === undefined) {\n issues.push({\n path: `headers.${name.toLowerCase()}`,\n keyword: 'header',\n message: `The documented header \"${name}\" is missing`,\n })\n }\n }\n // media type\n const content = documented.content\n const body = input.response.body\n const contentType = headers['content-type']\n const mediaType = (Array.isArray(contentType) ? contentType[0] : contentType)?.split(';')[0]?.trim().toLowerCase()\n if (content === undefined || Object.keys(content).length === 0) {\n if (body !== '')\n issues.push({\n path: 'body',\n keyword: 'content',\n message: `The ${status} response is documented without body, but it has one`,\n })\n return issues\n }\n if (mediaType === undefined) {\n if (body !== '')\n issues.push({\n path: 'headers.content-type',\n keyword: 'media-type',\n message: 'The response has a body but no content-type',\n })\n return issues\n }\n const key = Object.keys(content).find(candidate => mediaMatches(candidate, mediaType))\n if (key === undefined) {\n issues.push({\n path: 'headers.content-type',\n keyword: 'media-type',\n message: `The media type ${mediaType} is not documented (documented: ${Object.keys(content).join(', ')})`,\n })\n return issues\n }\n const schema = content[key]?.schema\n if (schema === undefined || !isJsonMediaType(mediaType)) return issues\n let value: unknown\n try {\n value = JSON.parse(body)\n } catch {\n issues.push({ path: 'body', keyword: 'json', message: 'The body is not valid JSON' })\n return issues\n }\n const Validator = loadValidator()\n // the validator annotates the schemas it reads: work on a copy of the (frozen) document parts\n const root = structuredClone({ ...(schema as JsonSchema), components: document.components ?? {} })\n const result = new Validator(root, '2020-12', false).validate(value)\n if (result.valid) return issues\n const leaves = result.errors.filter(error => !COMPOSITE.has(error.keyword))\n for (const error of leaves.length > 0 ? leaves : result.errors) {\n issues.push({ path: readablePath(error.instanceLocation), keyword: error.keyword, message: error.error })\n }\n return issues\n}\n\nfunction resolveResponse(document: OpenApiObject, entry: ResponseObject | ReferenceObject): ResponseObject | undefined {\n let current: ResponseObject | ReferenceObject | undefined = entry\n for (let depth = 0; depth < 8 && current !== undefined && '$ref' in current; depth++) {\n const name: string = current.$ref.replace(/^#\\/components\\/responses\\//, '')\n current = document.components?.responses?.[name]\n }\n return current === undefined || '$ref' in current ? undefined : current\n}\n\nfunction lowerCase(headers: ResponseLike['headers']): Record<string, string | readonly string[] | undefined> {\n const result: Record<string, string | readonly string[] | undefined> = {}\n for (const [name, value] of Object.entries(headers)) result[name.toLowerCase()] = value\n return result\n}\n\nfunction mediaMatches(documented: string, actual: string): boolean {\n const pattern = documented.split(';')[0]?.trim().toLowerCase() ?? ''\n if (pattern === '*/*' || pattern === actual) return true\n return pattern.endsWith('/*') && actual.startsWith(pattern.slice(0, -1))\n}\n\nfunction isJsonMediaType(mediaType: string): boolean {\n return mediaType === 'application/json' || mediaType.endsWith('+json')\n}\n\n/** `#/items/0/id` → `body.items[0].id`. */\nfunction readablePath(pointer: string): string {\n const segments = pointer\n .replace(/^#\\/?/, '')\n .split('/')\n .filter(segment => segment !== '')\n .map(segment => segment.replaceAll('~1', '/').replaceAll('~0', '~'))\n let path = 'body'\n for (const segment of segments)\n if (/^\\d+$/.test(segment)) path += `[${segment}]`\n else if (/^[A-Za-z_$][\\w$]*$/.test(segment)) path += `.${segment}`\n else path += `[${JSON.stringify(segment)}]`\n return path\n}\n"],"mappings":"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"}
|