PermDock
CLI

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.

Status: planned Phase: 2

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 (0014, 0019), and can write experimental 3.3 documents that carry the pinned OpenAPI 3.3 Security Profile draft (0025). 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

FlagValuesDefaultApplies toPurpose
--doc <path or URL>file path, http(s) URLrequiredemit, importThe OpenAPI document to read
--out <path>file path--doc for emit; required for importemit, importWhere to write the result
--target3.1, 3.2, 3.33.2emitOpenAPI 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)
--formatdocument, overlaydocumentemitWrite the mutated document, or an Overlay that describes the changes without touching the source
--overlay1.1, 1.21.1emit (--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)
--checkflagoffemitDo 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
--profilefapi2noneemitDeclare 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 namepermdock<Profile> (permdockFapi2)emit (--target 3.3 only)Name of the type: profile scheme under components.securitySchemes
--scheme <name>scheme namepermdockOAuthemitName of the oauth2 scheme under components.securitySchemes
--metadata-url <URL> or <name>=<URL> (repeatable)URLnoneemitValue for oauth2MetadataUrl (3.2, 3.3) or x-permdock-oauth2MetadataUrl (3.1); on 3.3 each entry also becomes a profileMetadata.servers item
--device-flowflagoffemitAdd a deviceAuthorization flow (3.2, 3.3) or x-oai-deviceAuthorization with x-oai-deviceAuthorizationUrl (3.1)
--schemazod, valibot, arktypenoneimportValidator used for generated resource schemas

--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 change

emit reads the catalog (or the definition module) and the document, then:

  1. Adds or updates components.securitySchemes.<scheme> as an oauth2 scheme whose scopes are every permission scope in the catalog, with meta.description as the scope description. On 3.2 and 3.3 it sets oauth2MetadataUrl and, when --device-flow is passed, a deviceAuthorization flow. On 3.1 the device flow is written as x-oai-deviceAuthorization with x-oai-deviceAuthorizationUrl, both registered in the OAI Extension Registry, and the metadata URL as x-permdock-oauth2MetadataUrl; no x-oai-* extension is registered for oauth2MetadataUrl, so PermDock uses its own namespace rather than inventing one (OpenAPI registries).
  2. For every operation that declares x-permdock-permissions (an array of permission keys, written by the runtime hooks or by hand), adds a security entry requiring the matching scopes and validates that each key exists in the catalog. Unknown keys are errors. Permissions with a portable condition also receive x-permdock-conditions; permissions whose grants carry approval receive x-permdock-approval.
  3. Marks scopes whose permission carries meta.deprecated as deprecated on the scheme (3.2, 3.3) or x-oai-deprecated (3.1).
  4. With --profile fapi2, writes x-permdock-securityProfile: "fapi2" on the scheme and on every covered operation, and refuses (exit 2) a scheme that accepts tokens anywhere but the HTTP header, since FAPI 2.0 forbids query-parameter tokens for resource servers. On --target 3.3 it additionally writes a type: profile scheme (profileMetadata.name: fapi-20-security-profile, supportedParametersSchema, servers from the metadata URLs) and one components.securityProfileRequirements entry per distinct scope set, in the shape of the pinned draft; the extension stays as the twin (OpenAPI 3.3).
  5. Writes x-permdock-catalog at document level with the catalog version and generator, so --check can detect drift. On --target 3.3 it adds drafts with the pinned v3.3-dev commit and Security Profile design revision, and on --overlay 1.2 the pinned v1.2-dev commit; --check fails on a document or Overlay whose pins differ from the installed CLI's.

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 (ADR 0023).

{
  "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.json

The 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

Phase 2 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

import produces a deterministic definePermissions() module with a // @generated header from a document:

  • Each x-permdock-permissions key becomes a leaf; when absent, scopes on oauth2 and openIdConnect security requirements are converted from the colon form (post:delete) to keys (post.delete).
  • Resources are inferred from the key prefix; when an operation's request or response schema is referenced, it is emitted as the resource schema in the validator selected by --schema zod|valibot|arktype, or as a schema-less resource otherwise.
  • Arity is instance when the operation path contains a parameter after the resource segment (/posts/{id}), collection otherwise; this heuristic is recorded in meta.inferredFrom so a reviewer can override it.
  • 3.1 documents are read with their fallbacks: x-oai-deprecated, x-oai-deviceAuthorization, x-oai-deviceAuthorizationUrl and x-permdock-oauth2MetadataUrl are understood exactly like the 3.2 fields.

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.

--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 --check

Fails when a permission was added, renamed or deprecated without regenerating the document or Overlay, when an operation references a permission that no longer exists, 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 no longer emits. --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 (ADR 0023).

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_auth

oasdiff 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).

Open questions

  • Whether emit should also write per-operation x-permdock-arity and the resource id parameter name so an agent can build the data loader from the document alone.
  • Decided by ADR 0025: --target 3.3 emits the pinned Security Profile draft now and switches to the released construct at 3.3.0. Open: whether a document whose openapi field already says 3.3.x should select the target automatically (OpenAPI 3.3).
  • Decided by ADR 0025: --overlay 1.2 emits the pinned Overlay 1.2 draft with 1.1 as the default. Open: the release after which 1.2 becomes the default, which waits on the appliers in the table above accepting components.actions.
  • Per-step permissions from Arazzo workflows are a Phase 4 item (permdock arazzo check, Arazzo workflows), not part of permdock openapi.

On this page