Skip to content

docs(api): document API-key auth contract across all OpenAPI specs (#4599 root cause #1)#4602

Merged
koala73 merged 3 commits into
mainfrom
docs/openapi-security-scheme-injection
Jul 1, 2026
Merged

docs(api): document API-key auth contract across all OpenAPI specs (#4599 root cause #1)#4602
koala73 merged 3 commits into
mainfrom
docs/openapi-security-scheme-injection

Conversation

@koala73

@koala73 koala73 commented Jul 1, 2026

Copy link
Copy Markdown
Owner

What & why

Highest-leverage fix from the full-surface OpenAPI docs audit (umbrella #4599, root cause #1): authentication was documented on 0/34 services / 0/192 endpoints, even though every WorldMonitor RPC is API-key-enforced at the gateway. Consumers had no machine-readable way to know a key was required or how to send it, and no 401 was described.

The sebuf protoc-gen-openapiv3 plugin has no option or annotation for auth (only the annotation .proto is vendored, not the generator source), and a full regenerate risks plugin-version drift (see the Makefile's pinning warnings). So this adds a byte-faithful post-generation injection step, wired into make generate (after buf generate) and exposed as npm run gen:openapi:security.

What it injects

34 per-service specs (docs/api/*Service.openapi.json):

  • components.securitySchemes: WorldMonitorKey (X-WorldMonitor-Key), ApiKeyHeader (X-Api-Key alias), BearerAuth (Authorization: Bearer) — mirrors the headers the gateway actually accepts (server/gateway.ts).
  • root security: OR of the three schemes.
  • 401UnauthorizedError ({ error }) on every authenticated operation.

7 genuinely public RPCs opt out correctly (operation-level security: [], no 401) — the list is parsed at runtime from server/gateway.ts PUBLIC_NO_AUTH_RPC_PATHS so it can't drift: conflict/acled, natural events, resilience manifest, seismology quakes, unrest events, and the two leads forms.

Bundle (docs/api/worldmonitor.openapi.yamlpublic/openapi.yaml): the generator's YAML emitter can't be reproduced by js-yaml (a re-dump reformats ~100% of 21k lines), so the bundle gets a formatting-preserving surgical insertion of the two top-level auth blocks (security + components.securitySchemes) — 19 lines added, 0 removed. Per-op detail in the bundle is left to native plugin support (tracked in #4599); the per-service specs carry the precise per-op contract.

Enforcement test (tests/openapi-security-contract.test.mjs): guards the contract on every committed spec + the bundle, so a regenerate that drops the injection fails CI.

The JSON diffs are additions-only

The specs are minified (one line), so git shows each as 1 +/1 - — but the change is strictly additive. Proven mechanically: stripping the injected keys from every modified spec recovers the exact origin/main bytes (34/34), and the injector re-serializes to the generator's exact format (recursively sorted keys, Go-style </>/&/U+2028/U+2029 escaping, no trailing newline).

Verification

  • Injector is idempotent (second run = 0 changes; --check mode passes).
  • 7/7 public ops exempted; 185/185 authenticated ops carry the 401 (185 + 7 = 192).
  • Additions-only confirmed for all 34 specs; bundle diff = +19/−0 and re-parses (191 paths unchanged).
  • 249 tests pass, including mcp-api-parity, generated-api-description-guard, deploy-config, and leads-gateway-public.

Scope

This is root cause #1 only. The other systemic causes from #4599 (required-propagation, bbox 400 / #4595, enum propagation, empty descriptions, seed-cache semantics, entitlement 403, examples) are separate follow-ups.

Closes root cause #1 of #4599.

https://claude.ai/code/session_01EsjtheWqt8GxEZfd3DyTMu

…4599 root cause #1)

The sebuf protoc-gen-openapiv3 plugin has no option or annotation for
authentication, so every generated spec omitted components.securitySchemes,
a root security requirement, and the 401 response — even though every
WorldMonitor RPC is API-key-enforced at the gateway. This is the highest-
leverage gap from the full-surface docs audit (umbrella #4599): auth was
undocumented on 0/34 services / 0/192 endpoints.

Since the generator is an external binary (only the annotation proto is
vendored) and a full regenerate risks plugin-version drift, this adds a
byte-faithful post-generation injection step wired into `make generate`
(after `buf generate`) and exposed as `npm run gen:openapi:security`.

- 34 per-service specs: securitySchemes (X-WorldMonitor-Key, its X-Api-Key
  alias, Authorization: Bearer) + root security + a 401/UnauthorizedError on
  every authenticated operation. Re-serialized to the generator's exact
  format (sorted keys, Go-style escaping) so the diff is additions-only.
- 7 genuinely public RPCs (server/gateway.ts PUBLIC_NO_AUTH_RPC_PATHS —
  sourced at runtime so it can't drift) opt out with security: [] and no 401.
- Bundle (worldmonitor.openapi.yaml → public/openapi.yaml): formatting-
  preserving surgical insertion of the two top-level auth blocks.
- New enforcement test guards the contract and catches a regen that drops it.

Idempotent; 249 tests pass (incl. mcp-api-parity, description-guard,
deploy-config, leads-gateway-public).

Claude-Session: https://claude.ai/code/session_01EsjtheWqt8GxEZfd3DyTMu
@koala73 koala73 added enhancement New feature or request area: API Backend API, sidecar, keys labels Jul 1, 2026
@vercel

vercel Bot commented Jul 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
worldmonitor Ignored Ignored Preview Jul 1, 2026 6:18pm

Request Review

@mintlify

mintlify Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
WorldMonitor 🟢 Ready View Preview Jul 1, 2026, 5:31 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@greptile-apps

greptile-apps Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a post-generation injection step (scripts/openapi-inject-security.mjs) that retrofits the API-key authentication contract — securitySchemes, root security, and per-operation 401 responses — into all 34 proto-generated per-service OpenAPI JSON specs and the unified bundle YAML. It is wired into make generate and guarded by a new enforcement test.

  • Injector: Parses PUBLIC_NO_AUTH_RPC_PATHS directly from server/gateway.ts at runtime so public-op exemptions (security: [], no 401) can never drift from the gateway. Uses deep-equal idempotency for the JSON specs and surgical text insertion for the YAML bundle (preserving the generator's exact format to keep diffs additions-only).
  • Test (tests/openapi-security-contract.test.mjs): Validates all 34 specs fully (scheme field values, 401 schema $ref, public-op overrides). The bundle assertion is shallower — it checks scheme name presence and security array length but not field values, leaving a gap vs the per-service depth.
  • Bundle idempotency: injectBundle's early-return check detects the injected blocks via regex pattern matching on raw text rather than parsed YAML content, so --check mode could pass for a bundle whose security: block has wrong content as long as the key line is present.

Confidence Score: 4/5

Safe to merge — all changes are additive documentation only; no runtime behavior is modified.

The injector logic, idempotency, and public-path exemption are sound for the 34 per-service JSON specs. The two gaps are the bundle's pattern-based idempotency check (which could silently pass for wrong content) and the bundle test's shallower assertions compared to the per-service tests. Neither gap affects the currently committed specs, which are correct.

scripts/openapi-inject-security.mjs (injectBundle idempotency logic) and tests/openapi-security-contract.test.mjs (bundle assertion depth)

Important Files Changed

Filename Overview
scripts/openapi-inject-security.mjs New post-generation injector: adds securitySchemes, root security, and per-op 401 to 34 per-service JSON specs (deep-equal idempotency) and surgically inserts global auth into the bundle YAML (pattern-based idempotency, slightly weaker). Parses PUBLIC_NO_AUTH_RPC_PATHS from gateway.ts at runtime. Logic is sound; bundle idempotency could miss a content-wrong-but-pattern-present file.
tests/openapi-security-contract.test.mjs New enforcement test guarding the injected contract on all 34 service specs and the bundle. Per-service assertions are thorough (scheme field values, 401 refs, public-op overrides). Bundle assertion only checks scheme name presence and security array length — doesn't validate header names or auth type fields, creating a gap vs the per-service depth.
Makefile Wires the injector into make generate after buf generate. Clean integration; the injector is safe to re-run (idempotent).
package.json Adds gen:openapi:security npm script as a standalone entry point for the injector. No other changes.
docs/api/worldmonitor.openapi.yaml Bundle receives +19 lines: top-level security: block before paths: and securitySchemes: under components:. Additions-only; existing content is unchanged. Format preserved by the surgical YAML insertion approach.
docs/api/LeadsService.openapi.json Both LeadsService ops (submit-contact, register-interest) correctly receive security: [] override with no 401, since both are in PUBLIC_NO_AUTH_RPC_PATHS. Root security and securitySchemes are also injected at the service level.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[make generate] --> B[buf generate\nproto → JSON specs + YAML bundle]
    B --> C[openapi-inject-security.mjs]
    C --> D[readPublicNoAuthPaths\nfrom server/gateway.ts]
    D --> E{For each\nService.openapi.json}
    E --> F[injectJson\ndeep-equal idempotency]
    F -->|changed| G[writeFileSync\nsorted-keys + goEscape serialization]
    F -->|no change| H[skip write]
    C --> I{worldmonitor.openapi.yaml\npresent?}
    I -->|yes| J[injectBundle\npattern-based idempotency]
    J -->|patterns found| K[skip write]
    J -->|pattern missing| L[surgical YAML insertion\nbefore paths:/after components:]
    L --> M[writeFileSync\nformatting preserved]
    C --> N[CI test\nopenapi-security-contract.test.mjs]
    N --> O{Per-service specs\ndeep field validation}
    N --> P{Bundle\nscheme name + array length check}
    style P fill:#ffe599
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[make generate] --> B[buf generate\nproto → JSON specs + YAML bundle]
    B --> C[openapi-inject-security.mjs]
    C --> D[readPublicNoAuthPaths\nfrom server/gateway.ts]
    D --> E{For each\nService.openapi.json}
    E --> F[injectJson\ndeep-equal idempotency]
    F -->|changed| G[writeFileSync\nsorted-keys + goEscape serialization]
    F -->|no change| H[skip write]
    C --> I{worldmonitor.openapi.yaml\npresent?}
    I -->|yes| J[injectBundle\npattern-based idempotency]
    J -->|patterns found| K[skip write]
    J -->|pattern missing| L[surgical YAML insertion\nbefore paths:/after components:]
    L --> M[writeFileSync\nformatting preserved]
    C --> N[CI test\nopenapi-security-contract.test.mjs]
    N --> O{Per-service specs\ndeep field validation}
    N --> P{Bundle\nscheme name + array length check}
    style P fill:#ffe599
Loading

Reviews (1): Last reviewed commit: "docs(api): document API-key auth contrac..." | Re-trigger Greptile

Comment thread scripts/openapi-inject-security.mjs Outdated
Comment thread tests/openapi-security-contract.test.mjs
koala73 added 2 commits July 1, 2026 17:53
- Validate existing bundle security blocks by content, not just key presence

- Strengthen bundle contract assertions for security scheme names and fields
@koala73
koala73 merged commit 7df7e54 into main Jul 1, 2026
25 checks passed
koala73 added a commit that referenced this pull request Jul 2, 2026
The per-service YAML specs (docs/api/*Service.openapi.yaml) and the served
bundle (worldmonitor.openapi.yaml → public/openapi.yaml) carried only the
entitlement 403s from the injector chain — never the auth half the per-service
JSON specs already have. PR #4602 deferred per-op YAML auth surgery, and the
sibling injectors (#4621/#4623/#4625/#4626) that later reached the YAML never
backfilled it.

This generalizes the bundle-only top-level insertion into a reusable
injectYamlAuthContract() applied to BOTH the per-service YAML loop and the
bundle, reaching full parity with injectJson():
- top-level securitySchemes (2 API-key, or 3 incl. BearerAuth when the artifact
  has a bearer-capable op — mirrors expectedSchemesForSpec)
- root API-key security requirement
- UnauthorizedError schema
- per-operation 401 on authenticated ops (185), security:[] opt-outs on the 7
  public RPCs, and BearerAuth stamping on the ~19 entitlement/premium ops

Reuses the entitlement pass's YAML range-finders; js-yaml is used read-only to
enumerate operation paths (handling the one multi-method path,
/api/v2/shipping/webhooks); all writes stay formatting-preserving and
additions-only. The narrower injectBundle() is removed.

Scope note: widened from the plan's bundle-only DoD to all 34 per-service YAML
after discovering they shared the identical gap (confirmed with the user).

Verified: 401 parity 185 (per-service YAML) / 185 (bundle); idempotent (full
Makefile injector chain re-run = 0 drift); per-service JSON byte-unchanged;
395 contract tests pass (incl. mcp-api-parity, doc-parity, deploy-config).

Claude-Session: https://claude.ai/code/session_01EsjtheWqt8GxEZfd3DyTMu
koala73 added a commit that referenced this pull request Jul 3, 2026
…e + per-service parity (#4650) (#4654)

* fix(api): extend OpenAPI auth injection to all YAML artifacts (#4650)

The per-service YAML specs (docs/api/*Service.openapi.yaml) and the served
bundle (worldmonitor.openapi.yaml → public/openapi.yaml) carried only the
entitlement 403s from the injector chain — never the auth half the per-service
JSON specs already have. PR #4602 deferred per-op YAML auth surgery, and the
sibling injectors (#4621/#4623/#4625/#4626) that later reached the YAML never
backfilled it.

This generalizes the bundle-only top-level insertion into a reusable
injectYamlAuthContract() applied to BOTH the per-service YAML loop and the
bundle, reaching full parity with injectJson():
- top-level securitySchemes (2 API-key, or 3 incl. BearerAuth when the artifact
  has a bearer-capable op — mirrors expectedSchemesForSpec)
- root API-key security requirement
- UnauthorizedError schema
- per-operation 401 on authenticated ops (185), security:[] opt-outs on the 7
  public RPCs, and BearerAuth stamping on the ~19 entitlement/premium ops

Reuses the entitlement pass's YAML range-finders; js-yaml is used read-only to
enumerate operation paths (handling the one multi-method path,
/api/v2/shipping/webhooks); all writes stay formatting-preserving and
additions-only. The narrower injectBundle() is removed.

Scope note: widened from the plan's bundle-only DoD to all 34 per-service YAML
after discovering they shared the identical gap (confirmed with the user).

Verified: 401 parity 185 (per-service YAML) / 185 (bundle); idempotent (full
Makefile injector chain re-run = 0 drift); per-service JSON byte-unchanged;
395 contract tests pass (incl. mcp-api-parity, doc-parity, deploy-config).

Claude-Session: https://claude.ai/code/session_01EsjtheWqt8GxEZfd3DyTMu

* fix(api): close JSON/YAML parity gaps in OpenAPI auth injector

Follow-up hardening on the YAML auth-contract injector so it fully
mirrors injectJson and stays convergent under source-of-truth edits:

- Per-method entitlement stamping: injectYamlEntitlementContract now
  iterates every HTTP method of a gated path (via enumerateYamlOperations)
  instead of only the first, so a multi-method entitlement path gets the
  403 + PRO-gated note on all methods, matching the JSON sibling.
- Skip public paths in the entitlement pass, mirroring injectJson's
  isPublic-first branch (a public+entitlement overlap no longer stamps a
  stray ForbiddenError 403 on the YAML).
- Auth pass now removes stale operation-level artifacts: a public op drops
  a leftover 401 (removeYamlUnauthorizedResponse), and a non-bearer op
  strips a leftover BearerAuth security block (removeYamlOperationSecurity),
  matching injectJson's delete-op.security / delete-op.responses['401'].
- Fail closed when a path appears in both ENDPOINT_ENTITLEMENTS and
  PUBLIC_FORBIDDEN_GATES, which would otherwise oscillate the 403 body on
  every run and permanently break the pre-push freshness gate.

Removed the now-unused first-method-only findYamlOperationRange. All fixes
target latent reclassification cases; the 34 committed YAML artifacts +
bundle are byte-unchanged (make-generate idempotent, --check clean) and
all 246 contract tests pass.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_014Ua87UaGSbnHspLCZwfNZK

* fix(api): close residual JSON/YAML parity gaps found in adversarial review

Follow-up to the parity fixes, closing three latent divergences surfaced
by adversarial validation (all dormant under the current config; the 34
YAML artifacts + bundle stay byte-unchanged and --check is clean):

- Quoted-scalar descriptions: appendNoteToYamlScalar now inserts the
  entitlement/gate note INSIDE a single- or double-quoted description scalar
  instead of after the closing quote, which previously produced unparseable
  YAML for any gated op whose description the generator chose to quote (e.g.
  one containing ": "). The per-method stamping had widened this pre-existing
  hazard's blast radius.
- ForbiddenError schema parity: set matchedEntitlementPath before the
  public-path opt-out so a spec whose only entitlement path is also public
  still emits the ForbiddenError schema, matching injectJson's
  public-agnostic hasEntitlementPath.
- Public-forbidden-gate parity: skip non-public gate paths in the gate loop,
  mirroring injectJson which applies the bot-verification 403 only inside its
  isPublic branch (a non-public gate path now takes the authenticated 401
  path instead of getting a stray bot 403).

Validated end-to-end against synthetic fixtures (quoted single/double
scalars, public-only entitlement spec, non-public gate path): output is
valid YAML, idempotent, and matches the JSON contract. 246 contract tests
still pass.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_014Ua87UaGSbnHspLCZwfNZK

---------

Co-authored-by: Claude <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: API Backend API, sidecar, keys enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant