fix(api): correct OpenAPI sample values, missing 403s, servers URL & doc-truth (adversarial audit)#4657
Conversation
…nd doc-truth (adversarial audit)
An adversarial audit of the generated OpenAPI docs (vs the actual gateway +
handlers) surfaced real defects the mechanical guards can't catch. Fixed via
four disjoint source changes, then one `make generate`:
U1 — realistic sample values (scripts/openapi-inject-examples.mjs): the
field-name heuristic emitted values handlers reject. Curated overrides sourced
from the real accepted sets: chokepointId→'suez' (was 'suez-canal', rejected),
scenarioId→'hormuz-tanker-blockade' (was 'oil-price-shock', unregistered),
FRED series_id→'GDP' (was 'example-id'), icao24→'a835af' (was 'example'),
chokepointIds→['suez']. BLS series_id stays 'USPRIV' (enum-resolved).
U2 — missing 403s (scripts/openapi-inject-security.mjs): register-interest
(public, throws 403 Turnstile/desktop-auth) now documents its 403 via
PUBLIC_FORBIDDEN_GATES; the 11 legacy-Pro PREMIUM_RPC_PATHS not covered by
ENDPOINT_ENTITLEMENTS now document the gateway's 403 'Pro subscription required'
(ForbiddenError). Entitlement wins on overlap (PREMIUM_ONLY = premium − entitlement).
U3 — servers URL (new scripts/openapi-inject-servers.mjs): per-service specs
had no `servers`, so Mintlify rendered curl with the api.example.com fallback.
Now injects servers:[{url:https://api.worldmonitor.app}] into per-service
JSON+YAML (bundle already carries it). Wired into `make generate`.
U4 — behavioral doc-truth (9 proto comments): runId (drives `note`, not
ignored), webcam lastUpdated (ISO-8601 string), disabled intelligence stubs
(company-enrichment/signals), and clamps documented to reality (gdelt cap 20,
aircraft-batch 10, seismology default 500/no-clamp, tech-events clampInt ranges).
Verified: every defect fixed in the regenerated docs; `make generate` idempotent;
src/generated unchanged; 500 contract tests pass (incl. new curated-example,
servers, and premium/register-interest 403 guards).
Follow-up issues to file: webcam int64 field vs ISO handler, gdelt/aircraft-batch/
seismology/tech-events suspected handler bugs, #4604 required-accuracy gaps (BLS
series_id / company effectively-required).
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 is an adversarial audit of the published OpenAPI docs, correcting four categories of defects identified by comparing the generated specs against the actual gateway and handlers. The generated artifacts (
Confidence Score: 3/5Safe to merge after addressing the register-interest YAML 403 description; the docs divergence between JSON and YAML is visible to Mintlify readers and under-documents the desktop HMAC gate. The register-interest 403 response description in the YAML artifact is incomplete, omitting the desktop-HMAC failure path the handler actually emits — the JSON artifact is correct but the YAML is not. The webcam lastUpdated schema also carries a contradictory format: int64 alongside an ISO-8601 description. The curated example overrides, servers injection, premium-only 403 stamping, and proto comment updates are well-structured and backed by solid contract tests. scripts/openapi-inject-security.mjs (YAML public-gate injection always uses the hardcoded YAML_BOT_FORBIDDEN_RESPONSE regardless of custom response.description) and proto/worldmonitor/webcam/v1/get_webcam_image.proto (int64 field with ISO-8601 description). Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[proto/ definitions] --> B[buf generate]
B --> C[docs/api/Service.openapi.json/yaml]
C --> D[apply-openapi-filter-param-schemas]
D --> E[openapi-inject-required]
E --> F[openapi-inject-examples + curated overrides U1]
F --> G[openapi-inject-servers NEW U3]
G --> H[openapi-inject-security U2 premium 403s]
H --> I[docs/api/ artifacts JSON + YAML]
I --> J[Mintlify docs site]
I --> K[contract tests]
%%{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[proto/ definitions] --> B[buf generate]
B --> C[docs/api/Service.openapi.json/yaml]
C --> D[apply-openapi-filter-param-schemas]
D --> E[openapi-inject-required]
E --> F[openapi-inject-examples + curated overrides U1]
F --> G[openapi-inject-servers NEW U3]
G --> H[openapi-inject-security U2 premium 403s]
H --> I[docs/api/ artifacts JSON + YAML]
I --> J[Mintlify docs site]
I --> K[contract tests]
|
…x robustness - webcam GetWebcamImageResponse.last_updated: int64 -> string. The field was int64 but the handler emits new Date(...).toISOString() (an ISO string, which is an INVALID int64), so `format: int64` contradicted the description. The generated TS type was already `string`, so this is zero TS/runtime impact and removes the format/description conflict (resolves #4658 item 1, Greptile P2). - openapi-inject-examples.mjs + test: broaden the scenario-template id regex to match single/double/backtick quotes so a future quote-style change in scenario-templates.ts can't silently fall back to the literal (Greptile P2). Regenerated; webcam lastUpdated now {type:string}; scenario example unchanged (hormuz-tanker-blockade); src/generated unchanged; make generate idempotent; contract tests pass. Claude-Session: https://claude.ai/code/session_01EsjtheWqt8GxEZfd3DyTMu
|
Both Greptile P2s addressed in 5ffb602:
Regenerated + idempotent + tests green. |
Code-review finding: UnauthorizedError was injected unconditionally, so the 4 all-public services (Leads/Natural/Seismology/Unrest) — whose ops all carry security:[] and no 401 — defined the schema but never referenced it (orphaned component, Spectral oas3-unused-component hint). Gate the injection on the spec having >=1 non-public op (the op that carries the 401 which references it), in both injectJson and injectYamlAuthContract, mirroring the existing ForbiddenError conditional. The bundle (which aggregates non-public ops) and all authenticated per-service specs keep it. Test asserts presence iff a non-public op exists AND absence otherwise (regression guard). Removes UnauthorizedError from 4 JSON + 4 YAML specs; src/generated unchanged; make generate idempotent; contract tests pass. Claude-Session: https://claude.ai/code/session_01EsjtheWqt8GxEZfd3DyTMu
…d ops (review #4) Code-review finding: the gateway returns 403 'API access requires an active subscription' on ANY non-public keyed route when a user API key resolves to an affirmatively inactive/expired entitlement (gateway.ts:1073-1083, #4611) — a global account-state gate orthogonal to the per-route entitlement/premium gates, previously undocumented on plain authed ops. Document it per-operation (parallel to how the 401 is already documented): - Every non-public op now carries a 403 -> ForbiddenError. Entitlement paths keep 'PRO entitlement access denied', premium-only keep 'Pro subscription required', and the remaining ~156 plain authed ops get the account-state description. - ForbiddenError is now gated on hasNonPublicOp (any authed op references it), same gate as UnauthorizedError; all-public specs carry neither (no orphan). - Applied to JSON + per-service YAML + bundle with full parity; test asserts every non-public op has a ForbiddenError 403 and the schema is present iff non-public. Regenerated; 185/185 non-public ops carry a 403; make generate idempotent; src/generated unchanged; 391 contract tests pass. Claude-Session: https://claude.ai/code/session_01EsjtheWqt8GxEZfd3DyTMu
…urce of truth (review #2) Code-review theme (security + testing + maintainability): the byte-faithful serializer was copy-pasted across the openapi-inject-* injectors, and the contract tests re-derived the injector's helpers / scraped its source with duplicate regexes — a tautology that catches a dropped injection but not a wrong value, plus latent drift (required.mjs's eq had already diverged). Extract scripts/lib/openapi-codegen.mjs (pure node, zero npm deps — safe in the make-generate context): the serializer (sortRec/goEscape/serialize/eq), normalizeKey, the fail-closed gateway/entitlement/premium source-of-truth parsers, and PUBLIC_FORBIDDEN_GATES. Rewire openapi-inject-{security,examples, servers}.mjs to import it, and rewire the security + examples contract tests to import the SAME parsers/gates/normalizeKey instead of re-scraping the injector — so the test can no longer drift from the injector via a divergent private regex. Pure refactor: make generate is byte-identical (0 diff across docs/api + src/generated), all 3 injectors --check idempotent, contract tests pass. Claude-Session: https://claude.ai/code/session_01EsjtheWqt8GxEZfd3DyTMu
…ng default (#4599 adversarial follow-ups A+B) (#4666) * fix(api): correct baseline-snapshot sample + tech-events omitted-paging default (adversarial follow-ups #4599) Two defects surfaced by an adversarial re-audit of the merged OpenAPI docs (#4657): A — RecordBaselineSnapshot request sample used `type: "all"`, which the handler rejects (VALID_BASELINE_TYPES has no 'all' → record-baseline-snapshot.ts:43 silently `continue`s), so a copy-paste yields `updated: 0` while the 200 example claims `updated: 1`. The examples injector's `endsWith('type')` heuristic emitted the literal 'all' with no curated override. Add a drift-anchored BASELINE_TYPE_EXAMPLE_ID (= infrastructureTemporalBaselineTypes[0], 'military_flights', matching the sibling GetTemporalBaseline enum example) + an overrideStringExample case scoped to baseline ops. Re-ran only the examples injector (version-independent). B — list-tech-events documented `limit`/`days` "defaults to 50/90 when omitted", but the REST decoder maps an omitted query param to 0, and clampInt treats a finite 0 as a value (→ min 1), not the fallback — so a no-params call returned 1 event within 1 day, not 50/90. Extract a pure, tested resolveTechEventsPaging() that guards with `|| default` (mirrors seismology `|| 500` and BLS `>0 ? : 60`); wire both handler call sites (fetchTechEvents + filterEvents) to it. The primary documented claim is now true. (The residual "explicit 0 clamps to 1" wording is a proto-comment change and rides the follow-up regen PR with #4599 items C/D.) Guard tests: baseline sample type ∈ VALID_BASELINE_TYPES; resolveTechEventsPaging omitted/0 → 50/90 (reproduced failing first, then fixed). 384 contract tests pass; tsc clean; examples injector --check idempotent. Claude-Session: https://claude.ai/code/session_01EsjtheWqt8GxEZfd3DyTMu * docs(api): correct tech-events limit/days paging comment to match the fixed default (review #B) Follow-on to the resolveTechEventsPaging fix in this PR: with the handler now treating omitted/0 as the 50/90 default, the proto field comment's residual "0 is not unlimited (it clamps up to 1)" clause was stale (0 now → 50, since an omitted query param and an explicit 0 are the same value on the wire). Reword to "defaults to 50/90 when omitted or 0" and regenerate the ResearchService specs + bundle with the pinned sebuf v0.11.1 plugin (scoped diff: 3 artifacts, param descriptions only). make generate idempotent; contract + description-guard tests pass. * fix(api): preserve explicit tech-events zero paging
…precated (#4599 C+D) (#4667) * docs(api): fill 10 empty op descriptions + mark disabled endpoints deprecated (#4599 C+D) Adversarial-audit follow-ups on the merged OpenAPI docs (#4657): C — 10 RPCs (GetForecasts, GetSimulationOutcome, GetSimulationPackage, GetOilInventories, SearchImagery, GetResilienceRuntimeManifest, GetCriticalMinerals, GetShippingRates, GetWebcamImage, ListWebcams) lacked a leading proto comment, so the generator emitted `description: null` and they rendered description-less on the docs site. Add an accurate one-line comment (sourced from each RPC's real response payload) above each; the generator now emits the operation description. D — GetCompanyEnrichment and ListCompanySignals are DISABLED empty stubs whose routes are preserved for contract stability, but they carried only prose "DISABLED" and rendered as normal, usable endpoints. Add `option deprecated = true;` (proto source of truth) and a new post-generation injector (openapi-inject-deprecated.mjs) that propagates it to OpenAPI `deprecated: true` — sebuf v0.11.1 does not. The injector is proto-sourced, so any future RPC gaining the option is covered automatically. Wired into `make generate` + `npm run gen:openapi:deprecated`. Regenerated with the pinned sebuf v0.11.1 plugin (verified reproducible: make generate on clean main is byte-identical). All 34 JSON + 34 YAML + bundle updated with full parity; make generate idempotent; new guard asserts every op has a description and deprecated ⟺ DISABLED; 383 contract tests pass; src/generated unchanged. Answers #4657 review D ("why document disabled endpoints"): exactly 2 exist; they stay documented (routes still serve 200) but are now machine-readable deprecated. Claude-Session: https://claude.ai/code/session_01EsjtheWqt8GxEZfd3DyTMu * fix(openapi): address deprecated docs review feedback
What & why
An adversarial audit of the generated OpenAPI docs against the actual gateway + handlers (8 attack lenses → per-finding verification) surfaced real defects the mechanical guards can't catch — most importantly that several example values are placeholders the handlers reject. Fixed via four disjoint source changes (implemented in parallel by subagents), integrated through a single
make generate.The four fixes
U1 — realistic sample values (
scripts/openapi-inject-examples.mjs)The examples injector picked string values by a field-name heuristic, emitting values handlers reject. Now curated from the real accepted sets:
chokepointId(×4) /chokepointIdssuez-canalsuez/["suez"]scenarioIdoil-price-shockhormuz-tanker-blockadeSCENARIO_TEMPLATESseries_idexample-idGDPicao24(×4)examplea835afBLS
series_idstaysUSPRIV(enum-resolved, untouched).U2 — missing 403s (
scripts/openapi-inject-security.mjs)register-interest(public; throws 403 Turnstile / desktop-auth) now documents its 403 viaPUBLIC_FORBIDDEN_GATES.PREMIUM_RPC_PATHSnot covered byENDPOINT_ENTITLEMENTSnow document the gateway's403 "Pro subscription required"(ForbiddenError, matching the{error}body).PREMIUM_ONLY = premium − entitlementso entitlement's 403 wins on overlap (no oscillation).U3 — server URL (new
scripts/openapi-inject-servers.mjs)Per-service specs had no
servers, so the docs site rendered curl with theapi.example.comfallback. Now injectsservers: [{url: https://api.worldmonitor.app}]into per-service JSON+YAML (bundle already had it). Wired intomake generate+npm run gen:openapi:servers.U4 — behavioral doc-truth (9 proto comments)
Descriptions aligned to handler reality:
runId(drives thenotewarning, not ignored), webcamlastUpdated(ISO-8601 string), disabled intelligence stubs (get-company-enrichment/list-company-signals), and the clamps (gdelt cap 20, aircraft-batch 10, seismology default 500/no-clamp, tech-eventsclampIntranges).Verification
suez, scenarioId→hormuz-tanker-blockade, icao24→a835af, FRED→GDP, BLS stillUSPRIV, servers→api.worldmonitor.app, register-interest + premium 403s, runId/webcam/clamp descriptions).make generateidempotent (re-run = 0 diff); pre-push freshness gate passed.src/generatedunchanged; only proto/scripts/tests/docs.api/Makefile/package.json.openapi-examples-contract), servers (openapi-servers-contract), premium + register-interest 403s (openapi-security-contract).Follow-ups (filed separately — likely handler bugs the docs now honestly describe)
buf.validateallows 250 but handler caps 20; aircraft-batchmax_items=20but handler truncates to 10 (drops 11-20); seismologypage_sizeunbounded (default 500)clampIntrequired→ OpenAPI parameterrequired:true(#4599) #4604 required-accuracy gaps: BLSseries_id/companyeffectively-required butrequired:falseReview follow-ups (post-review commits on this PR)
d53715a14— gateUnauthorizedError/ForbiddenErrorinjection on non-public ops (no orphaned, unreferenced schema in the 4 all-public specs: Leads/Natural/Seismology/Unrest).777aa62d5— document the account-state#4611403 (API access requires an active subscription,gateway.ts:1073-1083) on every authenticated op; all →ForbiddenError.571151019— extractscripts/lib/openapi-codegen.mjs(shared serializer + fail-closed source-of-truth parsers +PUBLIC_FORBIDDEN_GATES); injectors and contract tests import it (kills the test tautology). Pure refactor —make generatebyte-identical.Accepted breaking change
proto/worldmonitor/webcam/v1/get_webcam_image.protofield 5last_updatedwas changedint64→string(U4 / Greptile fix).make breaking(a manual target — not run by any CI workflow or the pre-push hook) flags this as aWIRE_JSONchange — this is intentional and accepted: the handler has always emittednew Date(...).toISOString(), so the oldint64declaration was inaccurate and no client could have parsed a valid epoch int64 from it. The change makes the schema honest. The flag is transient — it resolves on merge, whenmainbecomes thestringbaseline.https://claude.ai/code/session_01EsjtheWqt8GxEZfd3DyTMu