openapi
Emit security and securitySchemes into an existing OpenAPI document (or as an Overlay) from the permission catalog, or import a document into a generated definition.
permdock openapi works in both directions between a permission catalog and an OpenAPI document. It targets OpenAPI 3.2 by default, can write 3.1 documents using the registered x-oai-* fallbacks plus x-permdock-oauth2MetadataUrl, and can write experimental 3.3 documents that carry the pinned OpenAPI 3.3 Security Profile draft. The runtime hooks that attach security while a framework generates its document live in permdock/openapi (OpenAPI adapter); the CLI is for documents that already exist as files.
Flags
| Flag | Values | Default | Applies to | Purpose |
|---|---|---|---|---|
--doc <path or URL> | file path, http(s) URL | required | emit, import | The OpenAPI document to read |
--out <path> | file path | --doc for emit; required for import | emit, import | Where to write the result |
--from <module> | module path | policy from the config | emit | Module exporting the policy, instead of the one the config names |
--target | 3.1, 3.2, 3.3 | 3.2 | emit | OpenAPI version of the output. 3.1 writes the fallback extensions; 3.3 is experimental: it writes the 3.2 shape plus the pinned Security Profile draft (type: profile scheme, securityProfileRequirements) and records the pin in x-permdock-catalog.drafts (OpenAPI 3.3) |
--format | document, overlay | document | emit | Write the mutated document, or an Overlay that describes the changes without touching the source |
--overlay | 1.1, 1.2 | 1.1 | emit (--format overlay only) | Overlay specification version of the output. 1.1 writes Overlay 1.1.0; 1.2 is experimental: it writes the pinned Overlay 1.2 draft with one reusable action under components.actions per permission set and $ref references per operation, and records the pin in x-permdock-catalog.drafts.overlay (OpenAPI Overlay) |
--check | flag | off | emit | Do not write; exit 1 when the output would differ from what is on disk (drift), including a drafts pin older than the one this CLI emits |
--profile | fapi2 | none | emit | Declare a security profile: writes x-permdock-securityProfile on every target, plus the native profile scheme and requirements on 3.3, and validates the scheme against the profile's resource-server rules (FAPI 2.0) |
--profile-scheme <name> | scheme name | permdock<Profile> (permdockFapi2) | emit (--target 3.3 only) | Name of the type: profile scheme under components.securitySchemes |
--scheme <name> | scheme name | permdockOAuth | emit | Name of the oauth2 scheme under components.securitySchemes |
--metadata-url <URL> or <name>=<URL> (repeatable) | URL | none | emit | Value for oauth2MetadataUrl (3.2, 3.3) or x-permdock-oauth2MetadataUrl (3.1); on 3.3 each entry also becomes a profileMetadata.servers item |
--arity | flag | off | emit (--format document) | Add x-permdock-arity to every covered operation: { kind: 'collection' }, or { kind: 'instance', parameter } with the path parameter that carries the resource id |
--device-flow | flag | off | emit | Add a deviceAuthorization flow (3.2, 3.3) or x-oai-deviceAuthorization with x-oai-deviceAuthorizationUrl (3.1) |
--authorization-url <URL>, --token-url <URL> | URL | the document's own values | emit | authorizationUrl and tokenUrl of the authorizationCode flow (and tokenUrl of the device flow) when the document does not already declare the scheme's flows |
--device-authorization-url <URL> | URL | the document's own value | emit (--device-flow) | deviceAuthorizationUrl of the device flow |
--schema | zod, valibot, arktype | none | import | Validator used for generated resource schemas; without it resources are schema-less |
--map <file> | JSON file | none | import | { "METHOD /path": "resource.action" }: names the permission for an operation, overriding scopes, x-permdock-permissions and the inferred name |
--annotate | flag | off | import | Write the inferred keys back into the local --doc as x-permdock-permissions on every operation; JSON is rewritten with two-space indent, YAML keeps its comments and layout |
--cwd, --config and --json are shared by every command (CLI). Exit codes follow the shared contract: 0 clean, 1 drift or findings, 2 usage error (for example --target 3.0, or --profile fapi2 on a document whose scheme is not oauth2 or openIdConnect).
Emit
permdock openapi emit --doc openapi.json # 3.2 document, in place
permdock openapi emit --doc openapi.yaml --target 3.1 --out openapi.3-1.yaml
permdock openapi emit --doc openapi.json --format overlay --out permdock.overlay.json
permdock openapi emit --doc openapi.json --profile fapi2 --metadata-url https://auth.example.com/.well-known/oauth-authorization-server
permdock openapi emit --doc openapi.json --target 3.3 --profile fapi2 --out openapi.3-3.json # experimental: pinned 3.3 Security Profile draft
permdock openapi emit --doc openapi.json --check # exit 1 if the document would changeemit reads the catalog (or the definition module) and the document, then:
- Adds or updates
components.securitySchemes.<scheme>as anoauth2scheme whosescopesare every permissionscopein the catalog, withmeta.descriptionas the scope description. On 3.2 and 3.3 it setsoauth2MetadataUrland, when--device-flowis passed, adeviceAuthorizationflow. On 3.1 the device flow is written asx-oai-deviceAuthorizationwithx-oai-deviceAuthorizationUrl, both registered in the OAI Extension Registry, and the metadata URL asx-permdock-oauth2MetadataUrl; nox-oai-*extension is registered foroauth2MetadataUrl, so PermDock uses its own namespace rather than inventing one (OpenAPI registries). - For every operation that declares
x-permdock-permissions(an array of permission keys, written by the runtime hooks or by hand), adds asecurityentry requiring the matching scopes and validates that each key exists in the catalog. Unknown keys are errors. Permissions with a portable condition also receivex-permdock-conditions; permissions whose grants carryapprovalreceivex-permdock-approval. Per-step permissions for Arazzo workflows are checked bypermdock arazzo check, not bypermdock openapi. - Keeps a
deprecated(3.2, 3.3) orx-oai-deprecated(3.1) the scheme already carries. OpenAPI has no per-scope deprecation, so a permission'smeta.deprecateddoes not change the document. - With
--profile fapi2, writesx-permdock-securityProfile: "fapi2"on the scheme and on every covered operation, and refuses (exit2) a scheme that accepts tokens anywhere but the HTTP header, since FAPI 2.0 forbids query-parameter tokens for resource servers. On--target 3.3it additionally writes atype: profilescheme (profileMetadata.name: fapi-20-security-profile,supportedParametersSchema,serversfrom the metadata URLs) and onecomponents.securityProfileRequirementsentry per distinct scope set, in the shape of the pinned draft, setsopenapi: 3.3.0and validates the result against the pinned patch of the 3.2 schema; the extension stays as the twin (OpenAPI 3.3). - Keeps what the document already says about the scheme: flow URLs, flows PermDock does not write,
description. PermDock owns thescopes. - With
--arity, writesx-permdock-arityon every covered operation:instancewhen any listed permission is an instance action of its resource, elsecollection; forinstance,parameternames the last path template parameter (/posts/{id}givesid), omitted when the path has none. - Writes
x-permdock-catalogat document level with the catalog version and generator, so--checkcan detect drift. On--target 3.3it addsdraftswith the pinnedv3.3-devcommit and Security Profile design revision, and on--overlay 1.2the pinnedv1.2-devcommit;--checkfails on a document or Overlay whose pins differ from the installed CLI's.
When the source document validates against the official OpenAPI 3.1 or 3.2 JSON Schema, the output must too: emit exits 1 with the schema errors instead of writing a document it made invalid (the usual cause is an oauth2 scheme with no authorizationUrl or tokenUrl, which --authorization-url and --token-url fix). An Overlay 1.1 output is validated against the Overlay 1.1 schema the same way. --target 3.3 and --overlay 1.2 are drafts without a published schema and are not validated.
Everything not owned by PermDock is left untouched; the command is safe to run on documents produced by @hono/zod-openapi, hono-openapi, @orpc/openapi, trpc-to-openapi, @elysia/openapi, @fastify/swagger, @nestjs/swagger, next-openapi-gen or hand-written specs. With --format overlay the source is not modified at all. Operations whose source already carries security that PermDock would replace are reported as findings, so two tools writing security on one operation is caught in CI.
{
"paths": {
"/posts/{id}": {
"delete": {
"operationId": "deletePost",
"x-permdock-permissions": ["post.delete"],
"security": [{ "permdockOAuth": ["post:delete"] }]
}
}
},
"components": {
"securitySchemes": {
"permdockOAuth": {
"type": "oauth2",
"oauth2MetadataUrl": "https://auth.example.com/.well-known/oauth-authorization-server",
"x-permdock-securityProfile": "fapi2",
"flows": {
"authorizationCode": {
"authorizationUrl": "https://auth.example.com/authorize",
"tokenUrl": "https://auth.example.com/token",
"scopes": { "post:delete": "Delete a post you own" }
}
}
}
}
}
}Overlay format
--format overlay writes an Overlay 1.1.0 document instead: one update action for components.securitySchemes, one per operation (targeted by operationId) adding security and the x-permdock-* fields, one for the root x-permdock-catalog and, on --target 3.3, one for components.securityProfileRequirements. --overlay 1.2 writes the same content in the pinned Overlay 1.2 draft shape: the per-operation bodies move into components.actions, one reusable action per distinct set of granted permissions, and each operation becomes a $ref reference with its own target; the pin goes into x-permdock-catalog.drafts.overlay. In either version the Overlay never contains a remove action on security, securitySchemes or securityProfileRequirements, inline or in a reusable action. Both examples and the reasons to prefer this form are on OpenAPI Overlay.
permdock openapi emit --doc openapi.json --format overlay --out permdock.overlay.json
# apply with any Overlay 1.x applier, then serve or publish the result
permdock openapi emit --doc openapi.json --format overlay --overlay 1.2 --out permdock.overlay.json
# experimental: Overlay 1.2 draft; use only with an applier that accepts components.actions (next-openapi-gen today)Applying the Overlay
PermDock ships no applier; the project's existing tool does it. Two common pipelines:
// next-openapi-gen: openapi-gen.config.ts (Next.js, TanStack Start, React Router, SvelteKit, Nuxt, Astro, Hono, Express)
export default defineConfig({
openapi: "3.2.0",
overlay: { apply: ["./permdock.overlay.json"] }, // applied before the spec is written; Scalar and Arazzo see the result
});# Redocly CLI: lint, bundle and generate-client pipelines
redocly lint permdock.overlay.json # validates the Overlay itself
redocly join openapi.json --overlay permdock.overlay.json -o dist/openapi.json # applies it# Bump.sh: apply at deploy time (the GitHub Action takes the same file as its `overlay:` input)
bump overlay openapi.json permdock.overlay.json > dist/openapi.json
bump deploy dist/openapi.json --doc my-api
# Speakeasy: chain PermDock's Overlay with Speakeasy's own in one workflow
speakeasy overlay apply -s openapi.json -o permdock.overlay.json > dist/openapi.json
# Vendor-neutral: the reference JavaScript applier
npx openapi-overlays-js --openapi openapi.json --overlay permdock.overlay.json > dist/openapi.jsonThe next-openapi-gen option is documented in its Overlay guide; Redocly's join --overlay is marked experimental in its command reference. Bump.sh documents bump overlay and the Action input in its Overlays guide; Speakeasy's applier is part of its overlay workflow; Zuplo applies Overlays before importing a description as routes. Scalar's CLI lists Overlay support as roadmap, so a Scalar pipeline applies the Overlay with one of the above before scalar registry publish. Downstream, point @hey-api/openapi-ts, Orval, Kubb, Scalar, Mintlify, Fern or any docs host at dist/openapi.json; they read standard security and need no PermDock code (OpenAPI ecosystem).
Lint ruleset
The CLI ships a ruleset file for Spectral, Redocly and vacuum alongside the CLI. It encodes the document invariants --check enforces so teams that already lint descriptions see the same findings in their linter: an operation with x-permdock-permissions and no security, an x-oai-* name outside x-oai-deprecated, x-oai-deviceAuthorization and x-oai-deviceAuthorizationUrl, a vendor namespace PermDock does not own, and an Overlay remove targeting security. --check remains the authoritative gate; the ruleset is a convenience.
Import
permdock openapi import --doc https://api.example.com/openapi.json --out src/permissions.generated.ts
permdock openapi import --doc openapi.yaml --out src/permissions.generated.ts --schema zod
permdock openapi import --doc openapi.yaml --out src/permissions.generated.ts --map permdock.map.json --annotateimport needs no policy or config. It reads a JSON or YAML document from a file or an http(s) URL, validates it against the official OpenAPI 3.1 or 3.2 JSON Schema (anything else, including Swagger 2.0 and OpenAPI 3.0, exits 2), and writes a deterministic definePermissions() module with a // @generated by permdock openapi import header. Per operation, the permission key comes from the first of:
- The
--mapentry forMETHOD /path. x-permdock-permissionson the operation, so a document written byemitor the runtime hooks round-trips.- The scopes of its
oauth2oropenIdConnectsecurity requirements (the operation's, else the document's), with colons turned into dots (post:deletetopost.delete). - A name inferred from the operation: the resource is the first tag, else the first static path segment; the action is the
operationIdin camel case, else the method and arity (GETon an instance isread, on a collectionlist;POSTon a collectioncreate;PUTandPATCHupdate;DELETEdelete).
Every key is checked: at least two dot-separated identifiers and never __proto__, constructor or prototype. An operation that yields no valid key is a finding (exit 1) naming the operation, fixed with a --map entry.
Arity is instance when the path has a parameter after the resource segment (/posts/{id}), collection otherwise; an action seen on both becomes instance. Each action's meta.inferredFrom records the operations it came from (GET /posts/{id}), so a reviewer can see and override the heuristic; summary, description and tags become meta.title, meta.description and meta.tags, and read-only methods set meta.readOnly. With --schema, the resource schema is the first $ref JSON body among the resource's 200 or 201 responses and request bodies, translated for the portable core (strings, numbers, integers, booleans, string enums, arrays, objects with required); anything else becomes the validator's unknown.
--annotate writes the keys back onto each operation as x-permdock-permissions and changes nothing else, so vendor extensions and comments survive and emit can attach security afterwards.
The generated file merges with hand-written definitions through mergePermissions() like any other feature file (Larger apps). Re-running import rewrites the file; hand edits belong in a sibling definition. permdock rls import writes its module through the same generator.
Why
- No policy needed. Import is the first step of adopting PermDock on an existing API, when there is no definition to load yet.
- Validation first. A document the official schema rejects produces keys from whatever happened to parse; failing early keeps the generated module honest. The schemas ship with the CLI, so validation needs no network.
--mapover flags per operation. A committed mapping file is reviewable and survives regeneration; heuristics stay visible inmeta.inferredFrominstead of being hidden in naming rules.
--check in CI
- run: pnpm exec permdock openapi emit --doc openapi.json --check
- run: pnpm exec permdock openapi emit --doc openapi.json --format overlay --out permdock.overlay.json --checkFails when a permission was added, renamed or deprecated without regenerating the document or Overlay, when an operation references a permission that does not exist, when a committed Overlay differs from what the catalog would produce (including a remove on security that PermDock never emits), or when a --target 3.3 document or --overlay 1.2 Overlay carries a drafts pin the installed CLI does not emit. --check compares an Overlay against a regeneration in the same Overlay version and never writes.
A complete workflow adds two third-party checks behind it. Neither needs PermDock code and there is no first-party action: the two commands above are the whole integration.
name: openapi
on: [pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: pnpm/action-setup@v4
- run: pnpm install --frozen-lockfile
- run: pnpm exec permdock collect --check
- run: pnpm exec permdock openapi emit --doc openapi.json --format overlay --out permdock.overlay.json --check
- run: npx openapi-overlays-js --openapi openapi.json --overlay permdock.overlay.json > dist/openapi.json
# Breaking-change diff: a removed or weakened security requirement fails the job
- run: npx oasdiff breaking origin/main:dist/openapi.json dist/openapi.json --fail-on ERR
# Runtime parity: every operation with `security` must reject an anonymous call
- run: pipx run schemathesis run dist/openapi.json --url http://localhost:3000 --checks ignored_authoasdiff classifies security removals as breaking; Schemathesis ignored_auth sends each protected operation without credentials and fails on anything other than 401 or 403, which catches a route whose protect was removed after the description was published (OpenAPI ecosystem, testing and diff).
Why
--target 3.3is never chosen from the source document. A3.3.xsource is a draft its author opted into; emitting draft structures because of a version string would turn a pinned, experimental output into a silent default, and a producer upgrading its own version field would change PermDock's output without a flag in the diff. The target is always the flag, and3.2stays the default.- Overlay 1.2 becomes the default only when it is final. Until then
--overlay 1.2is the experimental form with its pin inx-permdock-catalog.drafts.overlay, and 1.1 is what every applier accepts. The default changes in the release that follows the final 1.2 text, with a changeset. x-permdock-arityis opt-in. Most consumers readsecurityandx-permdock-permissions; arity and the id parameter matter to generated MCP servers and SDKs that need to know whether a call targets one row. Emitting it everywhere would add a field to every operation of every document for the few consumers that use it. The id parameter is inferred from the path rather than configured, because the catalog knows the resource's id field but not what a route calls it.
Related
- OpenAPI 3.2 standard
- OpenAPI 3.3
- OpenAPI Overlay
- OpenAPI registries
- FAPI 2.0
- OpenAPI adapter
- catalog
- OpenAPI ecosystem: producers, appliers and consumers the command works with
- OpenAPI
- OpenAPI registries
- Adapters
- Watch list
Last updated on
usage
Report permissions that are defined but never used, used but never granted or granted by no role, conditions on undeclared fields, and client checks outside a snapshot include.
arazzo
Resolve every step of an Arazzo workflow to an operation's x-permdock-permissions and report steps that call undocumented operations.