# openapi

Source: https://permdock.com/docs/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.

`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](/docs/adapters/openapi)); the CLI is for documents that already exist as files.

## Flags [#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](/docs/standards/openapi)) |
| `--format` | `document`, `overlay` | `document` | `emit` | Write the mutated document, or an [Overlay](/docs/standards/openapi-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](/docs/standards/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](/docs/standards/fapi-2)) |
| `--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](/docs/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 [#emit]

```bash
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](/docs/standards/openapi-registry)).
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`. Per-step permissions for Arazzo workflows are checked by [`permdock arazzo check`](/docs/cli/arazzo), not by `permdock openapi`.
3. Keeps a `deprecated` (3.2, 3.3) or `x-oai-deprecated` (3.1) the scheme already carries. OpenAPI has no per-scope deprecation, so a permission's `meta.deprecated` does not change the document.
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, sets `openapi: 3.3.0` and validates the result against the pinned patch of the 3.2 schema; the extension stays as the twin ([OpenAPI 3.3](/docs/standards/openapi)).
5. Keeps what the document already says about the scheme: flow URLs, flows PermDock does not write, `description`. PermDock owns the `scopes`.
6. With `--arity`, writes `x-permdock-arity` on every covered operation: `instance` when any listed permission is an instance action of its resource, else `collection`; for `instance`, `parameter` names the last path template parameter (`/posts/{id}` gives `id`), omitted when the path has none.
7. 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.

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.

```json
{
  "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 [#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](/docs/standards/openapi-overlay).

```bash
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 [#applying-the-overlay]

PermDock ships no applier; the project's existing tool does it. Two common pipelines:

```ts
// 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
});
```

```bash
# 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
```

```bash
# 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](https://github.com/tazo90/next-openapi-gen/blob/main/docs/overlay.md); Redocly's `join --overlay` is marked experimental in its [command reference](https://redocly.com/docs/cli/commands). Bump.sh documents `bump overlay` and the Action input in its [Overlays guide](https://docs.bump.sh/help/specification-support/overlays/); Speakeasy's applier is part of its [overlay workflow](https://www.speakeasy.com/docs/prep-openapi/overlays); 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](/docs/research/ecosystem-index)).

### Lint ruleset [#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 [#import]

```bash
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 --annotate
```

`import` 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:

1. The `--map` entry for `METHOD /path`.
2. `x-permdock-permissions` on the operation, so a document written by `emit` or the runtime hooks round-trips.
3. The scopes of its `oauth2` or `openIdConnect` security requirements (the operation's, else the document's), with colons turned into dots (`post:delete` to `post.delete`).
4. A name inferred from the operation: the resource is the first tag, else the first static path segment; the action is the `operationId` in camel case, else the method and arity (`GET` on an instance is `read`, on a collection `list`; `POST` on a collection `create`; `PUT` and `PATCH` `update`; `DELETE` `delete`).

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](/docs/getting-started/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 [#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.
* **`--map` over flags per operation.** A committed mapping file is reviewable and survives regeneration; heuristics stay visible in `meta.inferredFrom` instead of being hidden in naming rules.

## `--check` in CI [#--check-in-ci]

```yaml
- 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 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.

```yaml
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](https://www.oasdiff.com) classifies `security` removals as breaking; [Schemathesis](https://schemathesis.readthedocs.io/) `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](/docs/research/ecosystem-index), testing and diff).

## Why [#why-1]

* **`--target 3.3` is never chosen from the source document.** A `3.3.x` source 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, and `3.2` stays the default.
* **Overlay 1.2 becomes the default only when it is final.** Until then `--overlay 1.2` is the experimental form with its pin in `x-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-arity` is opt-in.** Most consumers read `security` and `x-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 [#related]

* [OpenAPI 3.2 standard](/docs/standards/openapi)
* [OpenAPI 3.3](/docs/standards/openapi)
* [OpenAPI Overlay](/docs/standards/openapi-overlay)
* [OpenAPI registries](/docs/standards/openapi-registry)
* [FAPI 2.0](/docs/standards/fapi-2)
* [OpenAPI adapter](/docs/adapters/openapi)
* [catalog](/docs/cli/catalog)
* [OpenAPI ecosystem](/docs/research/ecosystem-index): producers, appliers and consumers the command works with
* [OpenAPI](/docs/standards/openapi)
* [OpenAPI registries](/docs/standards/openapi-registry)
* [Adapters](/docs/adapters)
* [Watch list](/docs/standards/watch-list)
