OpenAPI Overlay
How permdock openapi --format overlay emits an Overlay 1.1.0 document (or, behind --overlay 1.2, the pinned Overlay 1.2 draft with reusable actions) that adds security, securitySchemes and x-permdock-* fields to an OpenAPI description without mutating it, how to apply and check it in CI, and why the Overlay never removes security.
Status: planned
Phase: 2
Draft posture: build (Overlay 1.2 pinned to v1.2-dev at commit edd4adea, 8 August 2026, recorded in x-permdock-catalog.drafts.overlay; 1.1.0 stays the default output)
What it is
The Overlay Specification 1.1.0 (released 14 January 2026; 1.0.0 was 17 October 2024) describes a document that repeatably applies transformations to one or many OpenAPI descriptions: adding metadata, removing elements before sharing a description, updating descriptions. An Overlay document has:
| Field | Meaning |
|---|---|
overlay | The Overlay specification version, 1.1.0 |
info | title and version of the Overlay itself |
extends | URI of the OpenAPI description the Overlay applies to (optional; an applier may be given the target explicitly) |
actions | Ordered list of actions. Each has a target JSONPath expression selecting nodes in the description, plus one of update (merge a value into every selected node), remove (delete every selected node) or, new in 1.1, copy (copy a value from another location) |
An applier resolves each target against the description and applies the actions in order; the output is a new OpenAPI description. The source is never edited.
Overlay 1.2 (in development on the v1.2-dev branch, release date "TBD" in its revision history) adds one construct: reusable actions. A root components object holds components.actions, a map of Reusable Action Objects, each with a description and a fields object that is an Action Object without target. An entry in actions may then be a Reusable Action Reference Object instead of an Action Object: $ref (a same-document JSON Pointer restricted to #/components/actions/), a required target, and an optional description that overrides the one in fields. Nothing else on the object may be overridden; the update, remove and copy come from the referenced action. Earlier notes on this page and elsewhere described a targetFormat field for AsyncAPI targets; that idea is not in the v1.2-dev text at the pinned commit and PermDock does not emit it.
Why PermDock prefers an Overlay
permdock openapi emit can write the mutated document, and that is still the default (--format document) because it is the simplest thing to serve. The Overlay form is preferred whenever the OpenAPI description is owned by someone other than the team running PermDock, or whenever the security metadata should be reviewable on its own:
- The source stays owned by the API team. A generator (
@hono/zod-openapi,hono-openapi,@orpc/openapi,trpc-to-openapi, next-openapi-gen) or a hand-written file remains the single source of truth for paths, schemas and descriptions. PermDock adds authorization metadata beside it instead of rewriting it. - It is how PermDock reaches producers it has no hook in. Next.js route handlers, and every framework next-openapi-gen scans, have no in-process place to call
describe(). The Overlay is the hand-off: the producer generates, PermDock overlays, the producer's applier merges (ADR 0023). For generated descriptions the Overlay is therefore the documented default even though--format documentstays the CLI default. - PermDock's output is reviewable. The Overlay contains exactly what PermDock decided: which operations require which scopes, which keys they map to, which scheme they use. A reviewer reads a few dozen lines instead of diffing a whole regenerated description.
- Repeatable and diffable in CI. The Overlay is a deterministic function of the catalog and the source's
operationIds.permdock openapi emit --format overlay --checkfails when the committed Overlay no longer matches, which catches a permission renamed without the document following, and a document edited to change what PermDock emitted. - One Overlay, many descriptions. A monorepo with several APIs that share one catalog can apply one
securitySchemesOverlay to each, which answers the "shared scheme document" question on the adapter page without URI-referenced schemes (a 3.2-only feature).
The Overlay is the delivery mechanism; the semantics of every field it writes are on OpenAPI 3.2 and OpenAPI registries.
How PermDock uses it
Output
permdock openapi emit --doc openapi.json --format overlay --out permdock.overlay.json{
"overlay": "1.1.0",
"info": { "title": "PermDock authorization metadata", "version": "2026-09-06T10:15:00Z" },
"extends": "./openapi.json",
"actions": [
{
"target": "$.components.securitySchemes",
"description": "OAuth 2.0 scheme with every catalog scope",
"update": {
"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:read": "Read a post",
"post:update": "Edit a post you own",
"post:delete": "Delete a post you own"
}
}
}
}
}
},
{
"target": "$.paths.*[?@.operationId == 'deletePost']",
"description": "post.delete",
"update": {
"security": [{ "permdockOAuth": ["post:delete"] }],
"x-permdock-permissions": ["post.delete"],
"x-permdock-conditions": {
"post.delete": { "op": "eq", "field": "authorId", "value": { "ref": "subject.id" } }
},
"x-permdock-approval": { "post.delete": { "reason": "human" } }
}
},
{
"target": "$.paths.*[?@.operationId == 'updatePost']",
"description": "post.update",
"update": {
"security": [{ "permdockOAuth": ["post:update"] }],
"x-permdock-permissions": ["post.update"]
}
},
{
"target": "$",
"description": "Catalog fingerprint for permdock openapi --check",
"update": { "x-permdock-catalog": { "v": 1, "generator": "@permdock/cli", "catalog": "sha256:..." } }
}
]
}Rules the emitter follows:
- Operations are targeted by
operationId, never by path and method, so the Overlay survives a route being moved. Operations without anoperationIdare reported as findings (exit1) rather than targeted by position. - Which operations appear comes from the same source as the document form:
x-permdock-permissionsalready present in the source (written by the framework hooks), or the route registry when the CLI runs inside the project. - Actions are sorted (
securitySchemes, then operations byoperationId, then root) and theinfo.versionis derived from the catalog, so two runs over the same inputs produce byte-identical output. - The Overlay never contains a
removeaction onsecurityorsecuritySchemes, and never anupdatethat setssecurityto[]unless the permission is explicitly public in the catalog. Loosening a document's security is not something PermDock does on anyone's behalf. --target 3.1swaps the 3.2 fields for the registered fallbacks (x-oai-deviceAuthorization,x-oai-deviceAuthorizationUrl,x-oai-deprecated) andx-permdock-oauth2MetadataUrl, exactly as in document mode.
Overlay 1.2 output
--overlay 1.2 (adapter: overlay({ version: '1.2' })) emits the Overlay 1.2 draft at the pinned v1.2-dev commit instead of 1.1.0. It is the build posture of ADR 0025 applied to a document format rather than to a field: PermDock needs it in Phase 2 because the 1.1 form repeats the same update body once per operation, the draft's shape is small enough to write fixtures against, and the stable twin is the 1.1 form itself, one flag away and carrying exactly the same information. 1.1 stays the default until 1.2.0 is released and the appliers in the table below accept it.
The 1.2 form differs from the 1.1 form in exactly one way: every operation update body becomes a reusable action, keyed by the permission keys it grants, and the operation entries become references to it. Operations that grant the same permissions share one reusable action; the securitySchemes, securityProfileRequirements and root catalog actions are unchanged because each has one target.
{
"overlay": "1.2.0",
"info": { "title": "PermDock authorization metadata", "version": "2026-09-06T10:15:00Z" },
"extends": "./openapi.json",
"components": {
"actions": {
"post.delete": {
"description": "post.delete",
"fields": {
"update": {
"security": [{ "permdockOAuth": ["post:delete"] }],
"x-permdock-permissions": ["post.delete"],
"x-permdock-conditions": {
"post.delete": { "op": "eq", "field": "authorId", "value": { "ref": "subject.id" } }
},
"x-permdock-approval": { "post.delete": { "reason": "human" } }
}
}
},
"post.update": {
"description": "post.update",
"fields": {
"update": {
"security": [{ "permdockOAuth": ["post:update"] }],
"x-permdock-permissions": ["post.update"]
}
}
}
}
},
"actions": [
{
"target": "$.components.securitySchemes",
"description": "OAuth 2.0 scheme with every catalog scope",
"update": { "permdockOAuth": { "type": "oauth2", "flows": { "...": "as in the 1.1 example" } } }
},
{ "$ref": "#/components/actions/post.delete", "target": "$.paths.*[?@.operationId == 'deletePost']", "description": "deletePost" },
{ "$ref": "#/components/actions/post.update", "target": "$.paths.*[?@.operationId == 'updatePost']", "description": "updatePost" },
{ "$ref": "#/components/actions/post.update", "target": "$.paths.*[?@.operationId == 'patchPost']", "description": "patchPost" },
{
"target": "$",
"description": "Catalog fingerprint for permdock openapi --check",
"update": {
"x-permdock-catalog": {
"v": 1,
"generator": "@permdock/cli",
"catalog": "sha256:...",
"drafts": { "overlay": "1.2-dev@<commit>" }
}
}
}
]
}Rules specific to 1.2:
- The reusable-action key is the sorted, comma-joined list of permission keys the operation grants (
post.delete;post.read,post.updatefor an operation with two). Keys are JSON Pointer tokens, so~and/are escaped as~0and~1in the$ref, per RFC 6901; permission keys are resource and action identifiers joined by dots (naming), so in practice the key appears verbatim. components.actionsis sorted by key, references keep the sameoperationIdorder as 1.1 output, and each reference'sdescriptionis theoperationIdso a reviewer can still read the list of covered operations without resolving the pointers. Output is byte-identical across runs, and--checkcompares a committed 1.2 Overlay against a regenerated 1.2 Overlay, never across versions.- Only
targetanddescriptionappear on a reference;update,removeandcopyare never written on one, matching the draft's rule. A committed Overlay with aremoveanywhere onsecurity,securitySchemesorsecurityProfileRequirements, inline or insidecomponents.actions, still fails--check. - The pin
drafts.overlaynames thev1.2-devcommit the shape was checked against, in the rootx-permdock-catalogaction.--checkfails andpermdock doctor(PD012) warns when a committed 1.2 Overlay carries a different pin; the fix is to regenerate. 1.1 output carries nodraftsentry for the Overlay. - The 1.2 form validates against the
schemas/v1.2-devJSON Schema at the pinned commit;redocly lintand the shipped ruleset accept bothoverlay: 1.1.0andoverlay: 1.2.0. - Release switch. When 1.2.0 is published,
--overlay 1.2emits the released shape in the next PermDock minor,drafts.overlaydisappears, and any difference between draft and release is absorbed by the emitter (the importer reads nothing back from an Overlay, so there is no dual-read window). The default moves from1.1to1.2no earlier than one further minor and only once next-openapi-gen, Redocly CLI, Bump.sh, Speakeasy andoverlays-jsall acceptcomponents.actions. If reusable actions are dropped before release,--overlay 1.2emits the 1.1 shape withoverlay: 1.2.0and this page records that.
Applying
Any Overlay 1.x applier can consume the file: it takes the OpenAPI description (from extends or a command-line argument) and the Overlay and writes the combined description. Run it as a build step and serve or publish the result; the source description in the repository stays unchanged. PermDock does not ship its own applier in Phase 2; the document form covers projects that do not want one.
| Applier | Where it runs | Notes |
|---|---|---|
next-openapi-gen overlay.apply | Inside openapi-gen generate, for Next.js, TanStack Start, React Router, Remix, SvelteKit, Nuxt, Astro, Hono and Express projects | Applied before the spec is written, so the Scalar UI it scaffolds and the Arazzo files it compiles see the applied description. Accepts Overlay 1.0, 1.1 and 1.2. The Next.js recipe |
Redocly CLI join --overlay | Lint, bundle and generate-client pipelines | Experimental flag; redocly lint permdock.overlay.json validates the Overlay itself |
| Scalar CLI | Docs and registry pipelines | Overlay support is on Scalar's roadmap; apply with one of the above before scalar registry publish |
Whichever applier runs, the producer must not also write security on the operations PermDock covers (next-openapi-gen @auth and authPresets, for example). One owner per field: PermDock owns security, the producer owns paths and schemas. --check reports operations whose source already carries security the Overlay would replace.
operationId is the join key. Producers generate it, the Overlay targets it, Arazzo steps reference it. A producer configured to omit operationIds cannot be overlaid; --check fails on the first operation without one rather than falling back to path-and-method targeting.
Which Overlay version to emit. An applier that does not know components.actions rejects a 1.2 Overlay at overlay: 1.2.0 or, worse, ignores the references. Keep the default 1.1 for Redocly CLI, Bump.sh, Speakeasy and overlays-js pipelines until their release notes name Overlay 1.2; next-openapi-gen documents 1.0 to 1.2 and is the first applier the 1.2 form is tested against. The version is a per-pipeline choice; a monorepo can commit both files from one catalog.
CI recipe
# .github/workflows/permissions.yml
- run: pnpm exec permdock collect --check
- run: pnpm exec permdock openapi emit --doc openapi.json --format overlay --out permdock.overlay.json --check
# apply permdock.overlay.json to openapi.json with your Overlay applier, then publish the result--check regenerates the Overlay in memory and compares it byte-for-byte with the committed file. It fails on a renamed or removed permission, a new protected route with no entry, a changed operationId, and any hand edit to the committed Overlay, including one that adds a remove action.
Threat: an Overlay that removes security
An Overlay is code that rewrites an API description, and remove is a legitimate action in the specification. An attacker who can modify the Overlay in a pipeline, or substitute their own, can strip security from every operation in the published description:
{ "target": "$.paths.*.*.security", "remove": true }The published description would then claim the API is public. Enforcement is unaffected (PermDock's protect does not read the document), but clients, gateways and agents that trust the description would attempt unauthenticated calls, and a gateway that derives its policy from the description would stop enforcing.
Mitigations:
- PermDock's Overlay never contains
removeonsecurityorsecuritySchemes, so the presence of one is a signal, not a configuration choice. permdock openapi emit --format overlay --checkfails on any difference from the regenerated Overlay, including an addedremove. Run it in the same job that applies the Overlay.- Treat the Overlay like any other build input: commit it, review it, and do not fetch it from a mutable URL at build time.
- Keep enforcement independent of the document. PermDock's
protectguards routes from the policy, not from the OpenAPI description, so a tampered description cannot open a route (threat model).
Mapping table
| Overlay concept | PermDock concept |
|---|---|
extends | --doc |
actions[].target on $.components.securitySchemes | securitySchemes() from the adapter, every catalog scope |
actions[].target by operationId | One protected route; describe(permission) output |
update.security | security(permission) |
update.x-permdock-* | Permission keys, portable conditions, approval metadata, security profile |
remove | Never emitted for security fields |
copy (1.1) | Not used |
components.actions and $ref references (1.2 draft, --overlay 1.2) | One reusable action per distinct set of granted permission keys; one reference per operation |
| Deterministic output | --check in CI |
Sources
- Overlay Specification 1.1.0.
- Overlay Specification
v1.2-devand itsschemas/v1.2-devschema, at the commit named in theDraft postureline. - OpenAPI Initiative registries, for the extension names the Overlay writes.
Related
- OpenAPI 3.2: the fields the Overlay adds.
- OpenAPI registries: the
x-permdock-*namespace. - OpenAPI 3.3: what changes when a native security-profile construct exists.
- Arazzo workflows: reading the applied description back to pre-flight a workflow.
- OpenAPI adapter, Next.js adapter and CLI: openapi.
- OpenAPI ecosystem: producers, appliers and consumers.
- ADR 0019, ADR 0023 and ADR 0025 (the 1.2 draft posture and pin).
- Watch list: the Overlay 1.2 row.
Open questions
- Whether to target the operation object (as above) or the
securityarray itself, given Overlay's merge semantics for arrays; the operation object keeps one action per route but relies on the applier replacingsecurityrather than appending to it. - Whether PermDock should ship a minimal applier in
@permdock/cli(permdock openapi apply) so--checkcan also validate the applied result, not only the Overlay. - Signing the Overlay (a detached signature checked before applying) versus relying on repository review and
--check.
OpenAPI 3.3
What OpenAPI 3.3 contains today (v3.3-dev is still the 3.2 text; Security Profiles exist as OpenAPI Initiative Discussion #5304), the exact shape --target 3.3 emits from that pinned draft next to x-permdock-securityProfile, how the pin is recorded and checked, what happens at 3.3.0, and why GNAP is a reserved name rather than output.
Arazzo workflows
How an Arazzo 1.1.0 workflow is an agent plan, and how permdock simulate pre-flights every step by resolving its operationId to the operation's x-permdock-permissions and returning one Decision per step before anything executes.