@rotorsoft/act-http 1.8.7 → 1.8.9

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.
@@ -94,8 +94,11 @@ function build_operation(action_name, body_schema, options) {
94
94
  parameters.push({
95
95
  name: "Idempotency-Key",
96
96
  in: "header",
97
- required: false,
98
- description: "Optional idempotency token. Duplicate values produce a 409 Conflict; first-claim result is not cached on the server.",
97
+ // Required: the generated route fail-closes with 400 when idempotency is
98
+ // enabled and the header is absent, so the doc must not advertise it as
99
+ // optional (#1287).
100
+ required: true,
101
+ description: "Required idempotency token. Duplicate values produce a 409 Conflict; first-claim result is not cached on the server.",
99
102
  schema: { type: "string" }
100
103
  });
101
104
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/openapi/index.ts"],"sourcesContent":["/**\n * @packageDocumentation\n * @module act-http/openapi\n *\n * OpenAPI subpath for the auto-generated API epic (#835). Walks the\n * registry of a built `Act` once at function call and returns a\n * valid OpenAPI 3.1 document describing the same action surface the\n * Hono sibling at `@rotorsoft/act-http/hono` serves. The doc is\n * suitable for cross-language client codegen (Go, Python, Swift,\n * Kotlin, .NET), for serving over `/openapi.json` alongside a live\n * API, and for the broader OpenAPI tooling ecosystem (Postman,\n * Swagger UI, Redoc, schemathesis, API gateways).\n *\n * Usage:\n *\n * ```ts\n * import { openapi } from \"@rotorsoft/act-http/openapi\";\n *\n * const doc = openapi(app, {\n * info: { title: \"Wolfdesk API\", version: \"1.0.0\" },\n * servers: [{ url: \"https://api.example.com\" }],\n * });\n * // doc: OpenAPIDocument — valid OpenAPI 3.1 object\n * ```\n *\n * **Pure data emit.** No runtime dep on Hono or tRPC. The subpath is\n * consumable standalone for hosts that serve their API some other\n * way (Fastify, Lambda, edge) or that only want the doc for client\n * codegen. The emit walks the registry once at function call; cost\n * is linear in `N actions × M Zod fields each`, single-digit\n * milliseconds for a 100-endpoint registry.\n *\n * **Determinism.** Output is stable across runs given the same\n * registry — entries land in `Object.entries(app.registry.actions)`\n * iteration order, schemas come from the action's Zod definition,\n * and the rest of the doc is fixed content. CI can snapshot the\n * result to catch unintended API surface changes.\n *\n * The same shared utilities at `@rotorsoft/act-http/api` underwrite\n * cross-transport consistency: the `ApiError` envelope is referenced\n * once from `components.schemas`, every error response points at it,\n * and the same shape ships from the tRPC and Hono siblings at\n * runtime. A client speaking the doc and the live REST API sees one\n * envelope per framework error.\n *\n * **The doc describes the Hono surface, not tRPC.** tRPC's URL\n * shape (`POST /trpc/<procedure>`, JSON-RPC-style body framing,\n * batching) doesn't model cleanly as OpenAPI operations — and tRPC\n * consumers already share types directly via `typeof router`, so\n * OpenAPI buys them nothing. The path shape this emitter produces\n * (`POST <basePath>/actions/<name>`) matches the\n * `@rotorsoft/act-http/hono` sibling exactly: same default\n * `basePath`, same request/response shapes, same envelope. If the\n * operator overrides `basePath` on the Hono adapter, they pass the\n * same override here so the doc keeps matching the live routes by\n * construction. tRPC and Hono can run side-by-side at different\n * mount points on the same Act instance; the OpenAPI doc covers\n * the REST half only.\n */\nimport { pii_fields } from \"@rotorsoft/act\";\nimport { z } from \"zod\";\n\n/**\n * Minimal OpenAPI 3.1 document type. The emitter targets the 3.1\n * spec; the type is intentionally a structural subset rather than\n * the full surface from `openapi-types` so callers can post-process\n * without fighting the type system. Cast to a fuller type from\n * `openapi-types` when downstream consumers need it.\n */\nexport type OpenAPIDocument = {\n readonly openapi: \"3.1.0\";\n readonly info: OpenAPIInfo;\n readonly servers?: ReadonlyArray<OpenAPIServer>;\n readonly paths: Record<string, OpenAPIPathItem>;\n readonly components: OpenAPIComponents;\n};\n\n/**\n * Per-call options for {@link openapi}. The host supplies the\n * document-shape fields the registry can't derive (title, version,\n * servers) and toggles for the cross-cutting headers that the live\n * REST API may accept.\n *\n * - `info` — required. `title` and `version` must be non-empty;\n * `info` may carry any other top-level OpenAPI info fields.\n * - `servers` — optional. Each entry's `url` may contain\n * `{variable}` template syntax; bare URLs are validated through\n * `URL`'s parser and reject malformed inputs.\n * - `basePath` — optional, default `/api`. Mirrors the Hono\n * sibling's default so the doc describes the same paths the\n * generated REST surface serves.\n * - `idempotency` — optional, default `false`. When `true`, every\n * mutation operation documents an optional `Idempotency-Key`\n * request header.\n * - `expectedVersion` — optional, default `false`. When `true`,\n * every mutation operation documents an optional `If-Match`\n * request header carrying the expected stream version.\n */\nexport type OpenAPIOptions = {\n readonly info: OpenAPIInfo;\n readonly servers?: ReadonlyArray<OpenAPIServer>;\n readonly basePath?: string;\n readonly idempotency?: boolean;\n readonly expectedVersion?: boolean;\n};\n\nexport type OpenAPIInfo = {\n readonly title: string;\n readonly version: string;\n readonly description?: string;\n readonly summary?: string;\n readonly termsOfService?: string;\n readonly contact?: {\n readonly name?: string;\n readonly url?: string;\n readonly email?: string;\n };\n readonly license?: {\n readonly name: string;\n readonly identifier?: string;\n readonly url?: string;\n };\n};\n\nexport type OpenAPIServer = {\n readonly url: string;\n readonly description?: string;\n readonly variables?: Record<\n string,\n {\n readonly default: string;\n readonly enum?: ReadonlyArray<string>;\n readonly description?: string;\n }\n >;\n};\n\nexport type OpenAPIPathItem = {\n readonly post?: OpenAPIOperation;\n};\n\nexport type OpenAPIOperation = {\n readonly operationId: string;\n readonly summary: string;\n readonly tags: ReadonlyArray<string>;\n readonly parameters?: ReadonlyArray<OpenAPIParameter>;\n readonly requestBody?: OpenAPIRequestBody;\n readonly responses: Record<string, OpenAPIResponse>;\n};\n\nexport type OpenAPIParameter = {\n readonly name: string;\n readonly in: \"header\" | \"query\" | \"path\" | \"cookie\";\n readonly required?: boolean;\n readonly description?: string;\n readonly schema?: Record<string, unknown>;\n};\n\nexport type OpenAPIRequestBody = {\n readonly required?: boolean;\n readonly content: Record<\n string,\n { readonly schema: Record<string, unknown> }\n >;\n};\n\nexport type OpenAPIResponse =\n | {\n readonly description: string;\n readonly content?: Record<\n string,\n { readonly schema: Record<string, unknown> }\n >;\n }\n | { readonly $ref: string };\n\nexport type OpenAPIComponents = {\n readonly schemas: Record<string, Record<string, unknown>>;\n readonly responses: Record<string, OpenAPIResponse>;\n};\n\nconst DEFAULT_BASE_PATH = \"/api\";\n\n/**\n * The shared `ApiError` envelope schema, ref'd by every error\n * response. Matches the runtime envelope at\n * `@rotorsoft/act-http/api`'s `toApiError` exactly — keep in sync if\n * the envelope shape evolves.\n */\nconst API_ERROR_SCHEMA: Record<string, unknown> = {\n type: \"object\",\n properties: {\n error: { type: \"string\" },\n detail: { type: \"string\" },\n code: { type: \"string\" },\n },\n required: [\"error\"],\n};\n\n/**\n * The generic snapshot-array response schema returned on a\n * successful mutation. Each action commits one or more events; the\n * shape here is `Snapshot[]` with the per-action state and event\n * data left as open `object` (deeper per-action typing would need\n * to walk `state.events[*]` and emit a discriminated union — a\n * future refinement once the doc consumers ask for it).\n */\nconst SNAPSHOT_ARRAY_SCHEMA: Record<string, unknown> = {\n type: \"array\",\n items: {\n type: \"object\",\n properties: {\n state: { type: \"object\" },\n event: { type: \"object\" },\n version: { type: \"integer\" },\n patches: { type: \"integer\" },\n snaps: { type: \"integer\" },\n cache_hit: { type: \"boolean\" },\n replayed: { type: \"integer\" },\n },\n required: [\"state\", \"version\", \"patches\", \"snaps\", \"cache_hit\", \"replayed\"],\n },\n};\n\n/**\n * The error-response shape every operation references via\n * `$ref: \"#/components/responses/ApiError\"`. Single source of\n * truth — bug fixes to the envelope flow to every operation\n * automatically.\n */\nconst API_ERROR_RESPONSE: OpenAPIResponse = {\n description: \"Error envelope per @rotorsoft/act-http/api's toApiError.\",\n content: {\n \"application/json\": {\n schema: { $ref: \"#/components/schemas/ApiError\" },\n },\n },\n};\n\n/**\n * Server URL refinement: OpenAPI permits `{variable}` template syntax\n * inside server URLs, so we substitute each capture with `x` before\n * parsing. The character class forbids both `{` and `}` inside the\n * template, matching OpenAPI's variable-name grammar exactly and\n * eliminating the catastrophic-backtracking surface CodeQL flagged\n * on `[^}]+`.\n *\n * @internal\n */\nfunction is_valid_server_url(url: string): boolean {\n try {\n new URL(url.replace(/\\{[^{}]+\\}/g, \"x\"));\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Zod schema for the minimum required shape of {@link OpenAPIOptions}.\n * Same declarative-validation pattern the rest of the framework uses;\n * the OpenAPI surface is mostly free-form (`description`, `contact`,\n * `license` — the spec lets through anything that JSON-stringifies),\n * so only the load-bearing fields land here. Hosts overriding into\n * unusual shapes still get the rest of the doc emitted; this schema\n * only fails the construction call when `info.title` / `info.version`\n * is missing-or-empty or a `servers[].url` won't parse.\n *\n * @internal\n */\nconst OpenAPIOptionsSchema = z.object({\n info: z.object({\n title: z\n .string({ message: \"openapi: info.title is required (non-empty string)\" })\n .trim()\n .min(1, \"openapi: info.title is required (non-empty string)\"),\n version: z\n .string({\n message: \"openapi: info.version is required (non-empty string)\",\n })\n .trim()\n .min(1, \"openapi: info.version is required (non-empty string)\"),\n }),\n servers: z\n .array(\n z.object({\n url: z.string().refine(is_valid_server_url, {\n message: \"openapi: invalid server url\",\n }),\n })\n )\n .optional(),\n});\n\nfunction validate_options(options: OpenAPIOptions): void {\n OpenAPIOptionsSchema.parse({\n info: { title: options.info?.title, version: options.info?.version },\n servers: options.servers,\n });\n}\n\nfunction strip_json_schema_meta(\n schema: Record<string, unknown>\n): Record<string, unknown> {\n // `$schema` at the top of an inline schema is harmless in OpenAPI\n // 3.1 but adds noise to the doc. Drop it.\n const { $schema: _, ...rest } = schema;\n return rest;\n}\n\n/**\n * Annotate the emitted request-body schema's sensitive properties so\n * codegen / Swagger UI treat them as secrets rather than echoing them\n * freely. `z.toJSONSchema` has no knowledge of the sensitive registry\n * (`sensitive()` marks schemas out-of-band in a WeakMap), so we walk the\n * action's declared sensitive fields — via `pii_fields`, the same lookup\n * the orchestrator uses — and mark each matching property `writeOnly:\n * true` + `format: password`. Non-object schemas and actions with no\n * sensitive fields pass through untouched (the zero-cost common path).\n *\n * `pii_fields` only reports top-level keys of a `z.object`, and Zod's\n * JSON Schema emit always renders those as members of `properties` — so\n * once `fields` is non-empty, `body_schema.properties[field]` is\n * guaranteed present. No defensive fallback needed.\n *\n * @internal\n */\nfunction mark_sensitive_fields(\n body_schema: Record<string, unknown>,\n zod_schema: z.ZodType\n): Record<string, unknown> {\n const fields = pii_fields(zod_schema);\n if (fields.length === 0) return body_schema;\n const properties = body_schema.properties as Record<\n string,\n Record<string, unknown>\n >;\n const next_properties: Record<string, Record<string, unknown>> = {\n ...properties,\n };\n for (const field of fields) {\n next_properties[field] = {\n ...properties[field],\n writeOnly: true,\n format: \"password\",\n };\n }\n return { ...body_schema, properties: next_properties };\n}\n\nfunction build_operation(\n action_name: string,\n body_schema: Record<string, unknown>,\n options: OpenAPIOptions\n): OpenAPIOperation {\n const parameters: OpenAPIParameter[] = [];\n if (options.idempotency) {\n parameters.push({\n name: \"Idempotency-Key\",\n in: \"header\",\n required: false,\n description:\n \"Optional idempotency token. Duplicate values produce a 409 Conflict; first-claim result is not cached on the server.\",\n schema: { type: \"string\" },\n });\n }\n if (options.expectedVersion) {\n parameters.push({\n name: \"If-Match\",\n in: \"header\",\n required: false,\n description:\n \"Expected stream version for optimistic concurrency. Mismatch produces a 412 Precondition Failed via ConcurrencyError.\",\n schema: { type: \"string\" },\n });\n }\n\n return {\n operationId: action_name,\n summary: `Commit the ${action_name} action`,\n tags: [\"Actions\"],\n ...(parameters.length > 0 ? { parameters } : {}),\n requestBody: {\n required: true,\n content: {\n \"application/json\": { schema: body_schema },\n },\n },\n responses: {\n \"200\": {\n description: \"Action committed; returns the snapshot array.\",\n content: {\n \"application/json\": {\n schema: { $ref: \"#/components/schemas/SnapshotArray\" },\n },\n },\n },\n \"400\": { $ref: \"#/components/responses/ApiError\" },\n \"409\": { $ref: \"#/components/responses/ApiError\" },\n \"410\": { $ref: \"#/components/responses/ApiError\" },\n \"412\": { $ref: \"#/components/responses/ApiError\" },\n \"422\": { $ref: \"#/components/responses/ApiError\" },\n \"500\": { $ref: \"#/components/responses/ApiError\" },\n },\n };\n}\n\n/**\n * Build an OpenAPI 3.1 document describing a built `Act` instance's\n * action surface.\n *\n * Walks `app.registry.actions` once and emits a `POST\n * <basePath>/actions/<actionName>` operation per action, deriving\n * the request-body schema from each action's Zod definition via\n * `z.toJSONSchema` (Zod 4's native JSON Schema 2020-12 emitter,\n * which is the OpenAPI 3.1 schema dialect — no conversion layer\n * needed). Error responses reference the shared\n * `#/components/responses/ApiError` so a single envelope shape\n * covers every error path.\n *\n * @param app A built `Act` orchestrator. Required.\n * @param options Document-shape fields (`info.title`, `info.version`,\n * servers, base path) and toggles for the cross-cutting headers\n * the live REST API may accept (`Idempotency-Key`, `If-Match`).\n * @returns A valid OpenAPI 3.1 document object — serve as\n * `/openapi.json`, ship to a CDN, write to disk during CI, or\n * pipe to a codegen tool.\n *\n * @throws Error if `info.title` / `info.version` is missing or\n * empty, or if any `servers[].url` fails URL parsing after\n * `{variable}` substitution.\n */\n/**\n * Structural shape of the Act surface this emitter walks. Letting\n * TApp infer to the caller's concrete `Act<TSchemaReg, ...>` instead\n * of forcing it to fit a narrow framework-typed upper bound keeps\n * the caller's variance from leaking — and avoids `any` in the\n * signature.\n *\n * @internal\n */\ntype ActRegistryView = {\n readonly registry: {\n actions: Record<\n string,\n {\n readonly name: string;\n readonly actions: Record<string, unknown>;\n }\n >;\n };\n};\n\nexport function openapi<TApp extends ActRegistryView>(\n app: TApp,\n options: OpenAPIOptions\n): OpenAPIDocument {\n validate_options(options);\n const base_path = options.basePath ?? DEFAULT_BASE_PATH;\n const paths: Record<string, OpenAPIPathItem> = {};\n\n for (const [action_name, state] of Object.entries(app.registry.actions)) {\n const zod_schema = state.actions[action_name] as z.ZodType;\n const body_schema = mark_sensitive_fields(\n strip_json_schema_meta(\n z.toJSONSchema(zod_schema) as Record<string, unknown>\n ),\n zod_schema\n );\n paths[`${base_path}/actions/${action_name}`] = {\n post: build_operation(action_name, body_schema, options),\n };\n }\n\n return {\n openapi: \"3.1.0\",\n info: options.info,\n ...(options.servers ? { servers: options.servers } : {}),\n paths,\n components: {\n schemas: {\n ApiError: API_ERROR_SCHEMA,\n SnapshotArray: SNAPSHOT_ARRAY_SCHEMA,\n },\n responses: {\n ApiError: API_ERROR_RESPONSE,\n },\n },\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/openapi/index.ts"],"sourcesContent":["/**\n * @packageDocumentation\n * @module act-http/openapi\n *\n * OpenAPI subpath for the auto-generated API epic (#835). Walks the\n * registry of a built `Act` once at function call and returns a\n * valid OpenAPI 3.1 document describing the same action surface the\n * Hono sibling at `@rotorsoft/act-http/hono` serves. The doc is\n * suitable for cross-language client codegen (Go, Python, Swift,\n * Kotlin, .NET), for serving over `/openapi.json` alongside a live\n * API, and for the broader OpenAPI tooling ecosystem (Postman,\n * Swagger UI, Redoc, schemathesis, API gateways).\n *\n * Usage:\n *\n * ```ts\n * import { openapi } from \"@rotorsoft/act-http/openapi\";\n *\n * const doc = openapi(app, {\n * info: { title: \"Wolfdesk API\", version: \"1.0.0\" },\n * servers: [{ url: \"https://api.example.com\" }],\n * });\n * // doc: OpenAPIDocument — valid OpenAPI 3.1 object\n * ```\n *\n * **Pure data emit.** No runtime dep on Hono or tRPC. The subpath is\n * consumable standalone for hosts that serve their API some other\n * way (Fastify, Lambda, edge) or that only want the doc for client\n * codegen. The emit walks the registry once at function call; cost\n * is linear in `N actions × M Zod fields each`, single-digit\n * milliseconds for a 100-endpoint registry.\n *\n * **Determinism.** Output is stable across runs given the same\n * registry — entries land in `Object.entries(app.registry.actions)`\n * iteration order, schemas come from the action's Zod definition,\n * and the rest of the doc is fixed content. CI can snapshot the\n * result to catch unintended API surface changes.\n *\n * The same shared utilities at `@rotorsoft/act-http/api` underwrite\n * cross-transport consistency: the `ApiError` envelope is referenced\n * once from `components.schemas`, every error response points at it,\n * and the same shape ships from the tRPC and Hono siblings at\n * runtime. A client speaking the doc and the live REST API sees one\n * envelope per framework error.\n *\n * **The doc describes the Hono surface, not tRPC.** tRPC's URL\n * shape (`POST /trpc/<procedure>`, JSON-RPC-style body framing,\n * batching) doesn't model cleanly as OpenAPI operations — and tRPC\n * consumers already share types directly via `typeof router`, so\n * OpenAPI buys them nothing. The path shape this emitter produces\n * (`POST <basePath>/actions/<name>`) matches the\n * `@rotorsoft/act-http/hono` sibling exactly: same default\n * `basePath`, same request/response shapes, same envelope. If the\n * operator overrides `basePath` on the Hono adapter, they pass the\n * same override here so the doc keeps matching the live routes by\n * construction. tRPC and Hono can run side-by-side at different\n * mount points on the same Act instance; the OpenAPI doc covers\n * the REST half only.\n */\nimport { pii_fields } from \"@rotorsoft/act\";\nimport { z } from \"zod\";\n\n/**\n * Minimal OpenAPI 3.1 document type. The emitter targets the 3.1\n * spec; the type is intentionally a structural subset rather than\n * the full surface from `openapi-types` so callers can post-process\n * without fighting the type system. Cast to a fuller type from\n * `openapi-types` when downstream consumers need it.\n */\nexport type OpenAPIDocument = {\n readonly openapi: \"3.1.0\";\n readonly info: OpenAPIInfo;\n readonly servers?: ReadonlyArray<OpenAPIServer>;\n readonly paths: Record<string, OpenAPIPathItem>;\n readonly components: OpenAPIComponents;\n};\n\n/**\n * Per-call options for {@link openapi}. The host supplies the\n * document-shape fields the registry can't derive (title, version,\n * servers) and toggles for the cross-cutting headers that the live\n * REST API may accept.\n *\n * - `info` — required. `title` and `version` must be non-empty;\n * `info` may carry any other top-level OpenAPI info fields.\n * - `servers` — optional. Each entry's `url` may contain\n * `{variable}` template syntax; bare URLs are validated through\n * `URL`'s parser and reject malformed inputs.\n * - `basePath` — optional, default `/api`. Mirrors the Hono\n * sibling's default so the doc describes the same paths the\n * generated REST surface serves.\n * - `idempotency` — optional, default `false`. When `true`, every\n * mutation operation documents a **required** `Idempotency-Key`\n * request header (the generated route fail-closes with 400 when the\n * header is absent, so the doc marks it required, not optional).\n * - `expectedVersion` — optional, default `false`. When `true`,\n * every mutation operation documents an optional `If-Match`\n * request header carrying the expected stream version.\n */\nexport type OpenAPIOptions = {\n readonly info: OpenAPIInfo;\n readonly servers?: ReadonlyArray<OpenAPIServer>;\n readonly basePath?: string;\n readonly idempotency?: boolean;\n readonly expectedVersion?: boolean;\n};\n\nexport type OpenAPIInfo = {\n readonly title: string;\n readonly version: string;\n readonly description?: string;\n readonly summary?: string;\n readonly termsOfService?: string;\n readonly contact?: {\n readonly name?: string;\n readonly url?: string;\n readonly email?: string;\n };\n readonly license?: {\n readonly name: string;\n readonly identifier?: string;\n readonly url?: string;\n };\n};\n\nexport type OpenAPIServer = {\n readonly url: string;\n readonly description?: string;\n readonly variables?: Record<\n string,\n {\n readonly default: string;\n readonly enum?: ReadonlyArray<string>;\n readonly description?: string;\n }\n >;\n};\n\nexport type OpenAPIPathItem = {\n readonly post?: OpenAPIOperation;\n};\n\nexport type OpenAPIOperation = {\n readonly operationId: string;\n readonly summary: string;\n readonly tags: ReadonlyArray<string>;\n readonly parameters?: ReadonlyArray<OpenAPIParameter>;\n readonly requestBody?: OpenAPIRequestBody;\n readonly responses: Record<string, OpenAPIResponse>;\n};\n\nexport type OpenAPIParameter = {\n readonly name: string;\n readonly in: \"header\" | \"query\" | \"path\" | \"cookie\";\n readonly required?: boolean;\n readonly description?: string;\n readonly schema?: Record<string, unknown>;\n};\n\nexport type OpenAPIRequestBody = {\n readonly required?: boolean;\n readonly content: Record<\n string,\n { readonly schema: Record<string, unknown> }\n >;\n};\n\nexport type OpenAPIResponse =\n | {\n readonly description: string;\n readonly content?: Record<\n string,\n { readonly schema: Record<string, unknown> }\n >;\n }\n | { readonly $ref: string };\n\nexport type OpenAPIComponents = {\n readonly schemas: Record<string, Record<string, unknown>>;\n readonly responses: Record<string, OpenAPIResponse>;\n};\n\nconst DEFAULT_BASE_PATH = \"/api\";\n\n/**\n * The shared `ApiError` envelope schema, ref'd by every error\n * response. Matches the runtime envelope at\n * `@rotorsoft/act-http/api`'s `toApiError` exactly — keep in sync if\n * the envelope shape evolves.\n */\nconst API_ERROR_SCHEMA: Record<string, unknown> = {\n type: \"object\",\n properties: {\n error: { type: \"string\" },\n detail: { type: \"string\" },\n code: { type: \"string\" },\n },\n required: [\"error\"],\n};\n\n/**\n * The generic snapshot-array response schema returned on a\n * successful mutation. Each action commits one or more events; the\n * shape here is `Snapshot[]` with the per-action state and event\n * data left as open `object` (deeper per-action typing would need\n * to walk `state.events[*]` and emit a discriminated union — a\n * future refinement once the doc consumers ask for it).\n */\nconst SNAPSHOT_ARRAY_SCHEMA: Record<string, unknown> = {\n type: \"array\",\n items: {\n type: \"object\",\n properties: {\n state: { type: \"object\" },\n event: { type: \"object\" },\n version: { type: \"integer\" },\n patches: { type: \"integer\" },\n snaps: { type: \"integer\" },\n cache_hit: { type: \"boolean\" },\n replayed: { type: \"integer\" },\n },\n required: [\"state\", \"version\", \"patches\", \"snaps\", \"cache_hit\", \"replayed\"],\n },\n};\n\n/**\n * The error-response shape every operation references via\n * `$ref: \"#/components/responses/ApiError\"`. Single source of\n * truth — bug fixes to the envelope flow to every operation\n * automatically.\n */\nconst API_ERROR_RESPONSE: OpenAPIResponse = {\n description: \"Error envelope per @rotorsoft/act-http/api's toApiError.\",\n content: {\n \"application/json\": {\n schema: { $ref: \"#/components/schemas/ApiError\" },\n },\n },\n};\n\n/**\n * Server URL refinement: OpenAPI permits `{variable}` template syntax\n * inside server URLs, so we substitute each capture with `x` before\n * parsing. The character class forbids both `{` and `}` inside the\n * template, matching OpenAPI's variable-name grammar exactly and\n * eliminating the catastrophic-backtracking surface CodeQL flagged\n * on `[^}]+`.\n *\n * @internal\n */\nfunction is_valid_server_url(url: string): boolean {\n try {\n new URL(url.replace(/\\{[^{}]+\\}/g, \"x\"));\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Zod schema for the minimum required shape of {@link OpenAPIOptions}.\n * Same declarative-validation pattern the rest of the framework uses;\n * the OpenAPI surface is mostly free-form (`description`, `contact`,\n * `license` — the spec lets through anything that JSON-stringifies),\n * so only the load-bearing fields land here. Hosts overriding into\n * unusual shapes still get the rest of the doc emitted; this schema\n * only fails the construction call when `info.title` / `info.version`\n * is missing-or-empty or a `servers[].url` won't parse.\n *\n * @internal\n */\nconst OpenAPIOptionsSchema = z.object({\n info: z.object({\n title: z\n .string({ message: \"openapi: info.title is required (non-empty string)\" })\n .trim()\n .min(1, \"openapi: info.title is required (non-empty string)\"),\n version: z\n .string({\n message: \"openapi: info.version is required (non-empty string)\",\n })\n .trim()\n .min(1, \"openapi: info.version is required (non-empty string)\"),\n }),\n servers: z\n .array(\n z.object({\n url: z.string().refine(is_valid_server_url, {\n message: \"openapi: invalid server url\",\n }),\n })\n )\n .optional(),\n});\n\nfunction validate_options(options: OpenAPIOptions): void {\n OpenAPIOptionsSchema.parse({\n info: { title: options.info?.title, version: options.info?.version },\n servers: options.servers,\n });\n}\n\nfunction strip_json_schema_meta(\n schema: Record<string, unknown>\n): Record<string, unknown> {\n // `$schema` at the top of an inline schema is harmless in OpenAPI\n // 3.1 but adds noise to the doc. Drop it.\n const { $schema: _, ...rest } = schema;\n return rest;\n}\n\n/**\n * Annotate the emitted request-body schema's sensitive properties so\n * codegen / Swagger UI treat them as secrets rather than echoing them\n * freely. `z.toJSONSchema` has no knowledge of the sensitive registry\n * (`sensitive()` marks schemas out-of-band in a WeakMap), so we walk the\n * action's declared sensitive fields — via `pii_fields`, the same lookup\n * the orchestrator uses — and mark each matching property `writeOnly:\n * true` + `format: password`. Non-object schemas and actions with no\n * sensitive fields pass through untouched (the zero-cost common path).\n *\n * `pii_fields` only reports top-level keys of a `z.object`, and Zod's\n * JSON Schema emit always renders those as members of `properties` — so\n * once `fields` is non-empty, `body_schema.properties[field]` is\n * guaranteed present. No defensive fallback needed.\n *\n * @internal\n */\nfunction mark_sensitive_fields(\n body_schema: Record<string, unknown>,\n zod_schema: z.ZodType\n): Record<string, unknown> {\n const fields = pii_fields(zod_schema);\n if (fields.length === 0) return body_schema;\n const properties = body_schema.properties as Record<\n string,\n Record<string, unknown>\n >;\n const next_properties: Record<string, Record<string, unknown>> = {\n ...properties,\n };\n for (const field of fields) {\n next_properties[field] = {\n ...properties[field],\n writeOnly: true,\n format: \"password\",\n };\n }\n return { ...body_schema, properties: next_properties };\n}\n\nfunction build_operation(\n action_name: string,\n body_schema: Record<string, unknown>,\n options: OpenAPIOptions\n): OpenAPIOperation {\n const parameters: OpenAPIParameter[] = [];\n if (options.idempotency) {\n parameters.push({\n name: \"Idempotency-Key\",\n in: \"header\",\n // Required: the generated route fail-closes with 400 when idempotency is\n // enabled and the header is absent, so the doc must not advertise it as\n // optional (#1287).\n required: true,\n description:\n \"Required idempotency token. Duplicate values produce a 409 Conflict; first-claim result is not cached on the server.\",\n schema: { type: \"string\" },\n });\n }\n if (options.expectedVersion) {\n parameters.push({\n name: \"If-Match\",\n in: \"header\",\n required: false,\n description:\n \"Expected stream version for optimistic concurrency. Mismatch produces a 412 Precondition Failed via ConcurrencyError.\",\n schema: { type: \"string\" },\n });\n }\n\n return {\n operationId: action_name,\n summary: `Commit the ${action_name} action`,\n tags: [\"Actions\"],\n ...(parameters.length > 0 ? { parameters } : {}),\n requestBody: {\n required: true,\n content: {\n \"application/json\": { schema: body_schema },\n },\n },\n responses: {\n \"200\": {\n description: \"Action committed; returns the snapshot array.\",\n content: {\n \"application/json\": {\n schema: { $ref: \"#/components/schemas/SnapshotArray\" },\n },\n },\n },\n \"400\": { $ref: \"#/components/responses/ApiError\" },\n \"409\": { $ref: \"#/components/responses/ApiError\" },\n \"410\": { $ref: \"#/components/responses/ApiError\" },\n \"412\": { $ref: \"#/components/responses/ApiError\" },\n \"422\": { $ref: \"#/components/responses/ApiError\" },\n \"500\": { $ref: \"#/components/responses/ApiError\" },\n },\n };\n}\n\n/**\n * Build an OpenAPI 3.1 document describing a built `Act` instance's\n * action surface.\n *\n * Walks `app.registry.actions` once and emits a `POST\n * <basePath>/actions/<actionName>` operation per action, deriving\n * the request-body schema from each action's Zod definition via\n * `z.toJSONSchema` (Zod 4's native JSON Schema 2020-12 emitter,\n * which is the OpenAPI 3.1 schema dialect — no conversion layer\n * needed). Error responses reference the shared\n * `#/components/responses/ApiError` so a single envelope shape\n * covers every error path.\n *\n * @param app A built `Act` orchestrator. Required.\n * @param options Document-shape fields (`info.title`, `info.version`,\n * servers, base path) and toggles for the cross-cutting headers\n * the live REST API may accept (`Idempotency-Key`, `If-Match`).\n * @returns A valid OpenAPI 3.1 document object — serve as\n * `/openapi.json`, ship to a CDN, write to disk during CI, or\n * pipe to a codegen tool.\n *\n * @throws Error if `info.title` / `info.version` is missing or\n * empty, or if any `servers[].url` fails URL parsing after\n * `{variable}` substitution.\n */\n/**\n * Structural shape of the Act surface this emitter walks. Letting\n * TApp infer to the caller's concrete `Act<TSchemaReg, ...>` instead\n * of forcing it to fit a narrow framework-typed upper bound keeps\n * the caller's variance from leaking — and avoids `any` in the\n * signature.\n *\n * @internal\n */\ntype ActRegistryView = {\n readonly registry: {\n actions: Record<\n string,\n {\n readonly name: string;\n readonly actions: Record<string, unknown>;\n }\n >;\n };\n};\n\nexport function openapi<TApp extends ActRegistryView>(\n app: TApp,\n options: OpenAPIOptions\n): OpenAPIDocument {\n validate_options(options);\n const base_path = options.basePath ?? DEFAULT_BASE_PATH;\n const paths: Record<string, OpenAPIPathItem> = {};\n\n for (const [action_name, state] of Object.entries(app.registry.actions)) {\n const zod_schema = state.actions[action_name] as z.ZodType;\n const body_schema = mark_sensitive_fields(\n strip_json_schema_meta(\n z.toJSONSchema(zod_schema) as Record<string, unknown>\n ),\n zod_schema\n );\n paths[`${base_path}/actions/${action_name}`] = {\n post: build_operation(action_name, body_schema, options),\n };\n }\n\n return {\n openapi: \"3.1.0\",\n info: options.info,\n ...(options.servers ? { servers: options.servers } : {}),\n paths,\n components: {\n schemas: {\n ApiError: API_ERROR_SCHEMA,\n SnapshotArray: SNAPSHOT_ARRAY_SCHEMA,\n },\n responses: {\n ApiError: API_ERROR_RESPONSE,\n },\n },\n };\n}\n"],"mappings":"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","names":[]}
@@ -14732,8 +14732,17 @@ function trpc(app, options) {
14732
14732
  for (const [action_name, state] of Object.entries(app.registry.actions)) {
14733
14733
  handlers[action_name] = t.procedure.input(state.actions[action_name]).mutation(async ({ input, ctx }) => {
14734
14734
  const host_ctx = ctx;
14735
+ let actor;
14736
+ try {
14737
+ actor = await options.actor(host_ctx);
14738
+ } catch (err) {
14739
+ throw new import_server.TRPCError({
14740
+ code: "UNAUTHORIZED",
14741
+ message: err instanceof Error ? err.message : void 0,
14742
+ cause: err instanceof Error ? err : void 0
14743
+ });
14744
+ }
14735
14745
  try {
14736
- const actor = await options.actor(host_ctx);
14737
14746
  const stream = await options.stream(action_name, input, host_ctx);
14738
14747
  const expected_version = options.expectedVersion ? await options.expectedVersion(action_name, input, host_ctx) : void 0;
14739
14748
  const target = expected_version === void 0 ? { stream, actor } : { stream, actor, expectedVersion: expected_version };