OpenAPI
permdock/openapi emits OpenAPI 3.2 security schemes, per-operation security and x-permdock-permissions from permission references, hooks into hono-openapi, @hono/zod-openapi, oRPC and trpc-to-openapi, hands an Overlay to next-openapi-gen and other producers, and imports OpenAPI documents into a catalog.
Status: planned Phase: 2
permdock/openapi works in both directions. Outbound, it derives securitySchemes, per-operation security and an x-permdock-permissions extension from the permission references attached to routes, targeting OpenAPI 3.2 with registered x-oai-* fallbacks (and x-permdock-oauth2MetadataUrl) for 3.1 documents, either in-process through a framework's generator hook or as an Overlay applied to a document another tool produced. Inbound, permdock openapi import reads an OpenAPI document and generates a definePermissions() file from its operations and scopes.
Purpose
The 3.3 target is experimental and emits the pinned OpenAPI 3.3 Security Profile draft next to the stable x-permdock-securityProfile extension (OpenAPI 3.3, ADR 0025).
No OpenAPI generator in the TypeScript ecosystem is fed by an authorization library; every one of them (@hono/zod-openapi, hono-openapi, @orpc/openapi, trpc-to-openapi, @elysia/openapi, @fastify/swagger, @nestjs/swagger, next-openapi-gen) expects security to be attached by hand per route. PermDock already knows the permission for each protected route (protect(permissions.post.delete, ...)), and each permission carries a scope. Emitting the security metadata from that knowledge keeps the document honest: a route cannot document a scope it does not enforce. OpenAPI 3.2 (release) adds the device authorization flow, oauth2MetadataUrl, deprecated on security schemes and security schemes referenced by URI, which is why 3.2 is the primary target (decision 0014).
PermDock is one stage of a pipeline it does not own: a producer writes the description, PermDock adds authorization, an applier merges it, and SDK generators, docs UIs and bridges consume the result. It composes with those tools through standard fields and an Overlay rather than wrapping any of them (ADR 0023, OpenAPI ecosystem).
Works with
| Stage | Tools | How PermDock connects |
|---|---|---|
| Producers with an in-process hook | hono-openapi, @hono/zod-openapi, @orpc/openapi, trpc-to-openapi, @elysia/openapi, @fastify/swagger, @nestjs/swagger | describe(), security(), spec() passed into the generator next to protect |
| Producers without a hook | next-openapi-gen (Next.js, TanStack Start, React Router, Remix, SvelteKit, Nuxt, Astro), TypeSpec, tsoa, ts-rest, Effect HttpApi, hand-written YAML or JSON | overlay() or permdock openapi emit --format overlay, applied by the producer or a CLI |
| Appliers | next-openapi-gen overlay.apply, Redocly CLI join --overlay, Bump.sh bump overlay, Speakeasy CLI, Zuplo CLI, overlays-js, openapi-format | Consume the Overlay; PermDock ships none |
| SDK generators | Hey API (@hey-api/openapi-ts), Orval, Kubb, openapi-typescript, OpenAPI Generator, Kiota, Fern, Speakeasy, Redocly generate-client | Read standard security; no PermDock code |
| Docs UIs and hosts | Scalar, Swagger UI, Redoc, Stoplight Elements, fumadocs-openapi, Zudoku; hosted: Mintlify, Bump.sh, Fern docs, Redocly Realm, GitBook, Postman | Read standard security; Scalar optionally renders the x-badges approval hint; PermDock never writes a host's own extensions |
| OpenAPI-to-MCP bridges | Orval and Kubb MCP output, Scalar, Speakeasy, Fern, Mintlify, Postman MCP Generator, Zuplo, Kong openapi2mcp, AWS AgentCore Gateway | Bind each generated tool's permission from x-permdock-permissions (MCP adapter) |
| Linters | Spectral, Redocly lint, vacuum | A PermDock ruleset file (Phase 2) checks the document invariants |
| Testing and diff | Schemathesis ignored_auth, oasdiff, Prism, Microcks | Read security; turn PermDock's output into a runtime parity test and a breaking-change gate (CLI: openapi) |
| Workflows | Arazzo runners, permdock.simulate | Read operationId and x-permdock-permissions from the applied description (Arazzo) |
The full producer, applier and consumer matrix with notes per tool is on the OpenAPI ecosystem research page.
API
import { createPermDock } from 'permdock/openapi'
const { securitySchemes, security, describe, spec, overlay } = createPermDock(policy, {
scheme: {
name: 'oauth',
type: 'oauth2',
oauth2MetadataUrl: 'https://auth.example.com/.well-known/oauth-authorization-server',
flows: { authorizationCode: {}, deviceAuthorization: {} }, // scopes filled from the catalog
},
target: '3.2', // '3.1' | '3.2' | '3.3' ('3.1' emits the registered fallbacks; '3.3' is experimental and
// emits the pinned OpenAPI 3.3 Security Profile draft next to x-permdock-securityProfile)
securityProfile: 'fapi2', // optional; on '3.3' also emits the type: profile scheme
profileScheme: 'permdockFapi2', // optional; name of that scheme
})
// hono-openapi
app.delete('/posts/:id', describeRoute({ ...describe(permissions.post.delete) }), protect(permissions.post.delete, loadPost), handler)
// @hono/zod-openapi
createRoute({ method: 'delete', path: '/posts/{id}', security: security(permissions.post.delete), ... })
// oRPC: middleware-attached spec extension
const protectedProc = os.use(protect(permissions.post.delete)).use(oo.spec(spec(permissions.post.delete)))
// trpc-to-openapi: `protect: true` plus x-permdock-permissions in meta
t.procedure.meta({ openapi: { method: 'DELETE', path: '/posts/{id}', protect: true, ...describe(permissions.post.delete) } })
// components.securitySchemes for the document
generateSpecs(app, { components: { securitySchemes: securitySchemes() } })securitySchemes()returns the scheme with every permissionscopefromlistPermissions(permissions)listed under the configured flows, plusdescriptionfrom action metadata. Schemes can be referenced by URI in 3.2 ($refto a shared security document) whenscheme.refis set.security(permission | permission[])returns the per-operationsecurityarray:[{ oauth: ['post:delete'] }]; multiple permissions produce one requirement object (AND) unlessanyOfis passed (OR across objects).describe(permission)returnssecurityplusx-permdock-permissions: ['post.delete']and, when the permission has conditions,x-permdock-conditionsin the portable JSON format so clients can explain what a scope allows.spec(permission)is the oRPCoo.speccallback form: it receives the operation object and returns it extended.deprecated: trueon an action's metadata marks the scopedeprecatedin 3.2 and emitsx-oai-deprecatedfor 3.1.securityProfile: 'fapi2'in the adapter options writesx-permdock-securityProfile: "fapi2"on the scheme and on every described operation, so consumers know the resource server follows the FAPI 2.0 Security Profile (bearer or DPoP tokens in the header only, sender-constrained tokens verified). The CLI equivalent is--profile fapi2.target: '3.3'withsecurityProfileset additionally emits the pinned OpenAPI 3.3 Security Profile draft:securitySchemes()returns a second,type: profilescheme (profileScheme, defaultpermdockFapi2) withprofileMetadata.name: fapi-20-security-profile,supportedParametersSchemaandserversfromscheme.oauth2MetadataUrl;describe()andspec()are unchanged (operations keepsecurityand the extension twin);securityProfileRequirements()returns thecomponents.securityProfileRequirementsmap, one entry per distinct scope set, for generators that accept extra components;overlay()includes it. The rootx-permdock-catalog.draftsrecords the pin. The target is experimental: the shape follows the pinned draft until 3.3.0 is released, then switches to the released construct (ADR 0025).scheme.type: 'gnap'is a reserved value that throws today with a pointer to the GNAP page; no GNAP scheme is emitted until the OpenAPI Initiative defines one.
Registry and namespace
Every PermDock-specific field in a document is an OpenAPI extension under the x-permdock- prefix: x-permdock-permissions, x-permdock-conditions, x-permdock-securityProfile, x-permdock-approval, x-permdock-oauth2MetadataUrl and the root-level x-permdock-catalog. Registering the permdock namespace in the OAI Namespace Registry is a Phase 2 action item. When the OpenAPI Initiative has already registered an extension for the same purpose (x-oai-deprecated, x-oai-deviceAuthorization, x-oai-deviceAuthorizationUrl for 3.1 targets, x-agent-trust for agent-authenticated apiKey schemes) the adapter emits the registered name instead of inventing one; it never coins an x-oai-* name. The full extension table and the rules are on OpenAPI registries.
Overlay output
The adapter can describe its changes as an OpenAPI Overlay instead of returning fragments to be spliced into the document. overlay() returns an Overlay 1.1.0 document whose actions target operations by operationId and update them with security and x-permdock-* fields, plus one action for components.securitySchemes. overlay({ version: '1.2' }) returns the pinned Overlay 1.2 draft instead: the per-operation bodies become reusable actions under components.actions, one per distinct permission set, and each operation is a $ref reference with its own target; x-permdock-catalog.drafts.overlay names the pin (OpenAPI Overlay, ADR 0025). The source document stays untouched and owned by the API team; the Overlay is reviewed and applied in the build. permdock openapi emit --format overlay (with --overlay 1.2 for the draft) produces the same output from the CLI.
const doc = overlay({ extends: './openapi.json' })
// { overlay: '1.1.0', info: {...}, extends: './openapi.json', actions: [...] }
const draft = overlay({ extends: './openapi.json', version: '1.2' })
// { overlay: '1.2.0', info: {...}, extends: './openapi.json', components: { actions: {...} }, actions: [...] }Recipe: a producer without a hook (Next.js and next-openapi-gen)
Next.js route handlers have no built-in spec generator, so permdock/next has no in-process hook. next-openapi-gen scans the route files, generates operationIds, applies Overlay files before writing the spec, and scaffolds Scalar. PermDock's contribution is the Overlay:
// openapi-gen.config.ts
export default defineConfig({
openapi: '3.2.0',
overlay: { apply: ['./permdock.overlay.json'] },
})permdock openapi emit --doc public/openapi.json --format overlay --out permdock.overlay.json
pnpm exec openapi-gen generate # applies the Overlay, writes the spec, scaffolds ScalarTwo rules keep the recipe honest. operationId is the join key: next-openapi-gen generates one per route, the Overlay targets it, and --check fails when it is missing. And there is one owner per field: on operations PermDock covers, do not also use next-openapi-gen's @auth JSDoc tag or authPresets; PermDock owns security, the producer owns paths and schemas. The same recipe applies to any producer that cannot call describe(), including hand-written descriptions. The Next.js adapter page repeats it in context.
Recipe: SDK generators and docs UIs
Nothing to wire. Point @hey-api/openapi-ts, Orval or Redocly generate-client at the applied description; the generated SDK attaches credentials from securitySchemes and the right scopes per operation. Scalar, Swagger UI and Redoc render security as they render any description. A denial arrives as the documented 403 Problem Details body, which generated SDKs surface as the typed error.
Optional, off by default: docsHints: { badges: true } (name illustrative) adds x-badges: [{ name: 'Approval required' }] to operations whose permission carries approval, so portals that render x-badges (Scalar does) show the hint without understanding PermDock. It is a rendering hint with no semantics and the only field PermDock writes outside x-permdock-* and the registered set. PermDock never writes x-scalar-* (registries).
Recipe: linters
A ruleset file for Spectral, Redocly and vacuum ships with the CLI in Phase 2. It checks that every operation carrying x-permdock-permissions also carries security, that no x-oai-* name outside the registered three appears, and that an Overlay has no remove on security. Teams that already lint descriptions in CI get PermDock's invariants for free; permdock openapi emit --check stays the authoritative gate.
Request lifecycle
There is no request path: this adapter runs at document generation time and at import time.
Generation:
- The framework hook (
describeRoute,createRoute,oo.spec,meta.openapi) receives the object returned bydescribe/security/specnext to theprotectmiddleware for the same permission. - The document generator collects operations;
securitySchemes()supplies the component with all scopes. permdock openapi --check(CLI) compares the generated document against the routes registered withprotectand fails when a protected route has no security entry or a documented scope has no enforcing route.
Import (permdock openapi import ./openapi.json --out src/permissions.generated.ts):
- Operations are grouped by
tagsor path segments into resources;operationIdor method and path derive action names (GET /posts/{id}becomespost.read,POST /postsbecomespost.create). securityscopes andx-permdock-permissionsare read back into keys and scopes; request and response schemas become resource schemas for the chosen validator (--schema zod|valibot|arktype).- A deterministic
definePermissions()file with a// @generatedheader is written; it merges with hand-written definitions throughmergePermissionslike any feature file (larger apps).
What it validates
- Every permission referenced in
describe/securityexists in the catalog (type error at compile time; runtime throw for strings fromfindPermission). - Scopes are unique across the catalog and match the
resource:actionconvention; collisions aftermergePermissionsare reported. - 3.1 target: 3.2-only fields are moved to extensions so the document stays valid 3.1:
deviceAuthorizationtox-oai-deviceAuthorization(withx-oai-deviceAuthorizationUrl),deprecatedon schemes tox-oai-deprecated,oauth2MetadataUrltox-permdock-oauth2MetadataUrl(nox-oai-*extension is registered for it), and URI-referenced schemes are inlined. - Extension names: only registered
x-oai-*names andx-permdock-*names appear in output; any other prefix is a bug. - 3.3 target: the
type: profilescheme andsecurityProfileRequirementsvalidate against a PermDock-maintained patch of the 3.2 JSON Schema keyed by the draft pin, since no official 3.3 schema exists; thex-permdock-securityProfiletwin is present on every scheme and operation that carries the native construct;x-permdock-catalog.draftsnames the pin. - Overlay 1.2:
overlay({ version: '1.2' })output validates against theschemas/v1.2-devJSON Schema at the pinned commit; references carry only$ref,targetanddescription, andcomponents.actionskeys are RFC 6901-escaped in the$ref. - Import: documents are validated against the OpenAPI 3.1 or 3.2 schema before generation; unknown security scheme types produce
opaquescope entries rather than failing.
How denials surface
The adapter emits metadata; enforcement and denials belong to the server adapters. What it contributes to the denial story:
- The
403application/problem+jsonbody produced byprotectnames the samepermissionkey andscopethat the document advertises, so a client can match a denial to a documented requirement. - Documented response
403entries are added to each protected operation with the Problem Details schema (type,title,permission,denials,alternatives), including the.../approval-requiredtype variant. WWW-Authenticatestep-up hints emitted by HTTP adapters use the same scope strings assecuritySchemes.
Example app
No dedicated app. apps/examples/hono generates a 3.2 document with hono-openapi, apps/examples/orpc uses oo.spec, apps/examples/trpc uses trpc-to-openapi protect, apps/examples/next runs next-openapi-gen with overlay.apply and serves the result in Scalar, and apps/examples/monorepo runs permdock openapi import on a third-party document, merges the generated permissions, and generates a client with @hey-api/openapi-ts from the applied description.
Related standards
- OpenAPI 3.2: security scheme additions and the 3.1 fallback table.
- OpenAPI registries: the
x-permdock-*namespace and registered names. - OpenAPI Overlay: the
--format overlayoutput and its threat model. - OpenAPI 3.3: the experimental
3.3target and the pinned Security Profile draft it emits. - FAPI 2.0: what
securityProfile: 'fapi2'declares. - Standard Schema: request and response schemas via Standard JSON Schema.
- Problem Details: documented
403body. - CLI: openapi:
permdock openapiemit, import,--target,--formatand--check. - ADR 0019: extensions and Overlay.
- ADR 0025: why
target: '3.3'emits a pinned draft. - ADR 0023 and OpenAPI ecosystem: how the adapter composes with producers, appliers and consumers.
Open questions
- Whether
describeshould also emitx-permdock-conditionsby default or only with an option, given conditions can leak model details to API consumers. - How to map several permissions on one route (AND versus OR) in
securitywithout inventing a vendor extension. - Decided by ADR 0023: the fragment API stays for producers with an in-process hook; the Overlay is the documented default for producers without one. Whether the fragment API becomes a compatibility layer is revisited only if generators start consuming Overlays directly.
- Import heuristics for action names when
operationIdis absent and paths are irregular; a mapping file may be needed. - Whether to emit
securitySchemesper document or as a shared URI-referenced document in monorepos with several APIs. trpc-to-openapisupports only a booleanprotect; per-scope requirements may need an upstream change or post-processing of the generated document.
Shared Signals (SSF / CAEP)
permdock/ssf receives Shared Signals Framework security event tokens (push and poll) and maps CAEP events to snapshot and cache invalidation so permissions go stale when the IdP says so, not on a timer.
OpenTelemetry
permdock/otel records a span and a counter per permission check with decision attributes, behind a structural logger interface, with @opentelemetry/api as an optional dependency that is never required.