docs(api): document API-key auth contract across all OpenAPI specs (#4599 root cause #1)#4602
Conversation
…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
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
Greptile SummaryThis PR adds a post-generation injection step (
Confidence Score: 4/5Safe 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
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
%%{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
Reviews (1): Last reviewed commit: "docs(api): document API-key auth contrac..." | Re-trigger Greptile |
- Validate existing bundle security blocks by content, not just key presence - Strengthen bundle contract assertions for security scheme names and fields
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
…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]>
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
401was described.The sebuf
protoc-gen-openapiv3plugin has no option or annotation for auth (only the annotation.protois 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 intomake generate(afterbuf generate) and exposed asnpm 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-Keyalias),BearerAuth(Authorization: Bearer) — mirrors the headers the gateway actually accepts (server/gateway.ts).security: OR of the three schemes.401→UnauthorizedError({ error }) on every authenticated operation.7 genuinely public RPCs opt out correctly (operation-level
security: [], no401) — the list is parsed at runtime fromserver/gateway.tsPUBLIC_NO_AUTH_RPC_PATHSso it can't drift: conflict/acled, natural events, resilience manifest, seismology quakes, unrest events, and the twoleadsforms.Bundle (
docs/api/worldmonitor.openapi.yaml→public/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 exactorigin/mainbytes (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
--checkmode passes).401(185 + 7 = 192).mcp-api-parity,generated-api-description-guard,deploy-config, andleads-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