Skip to content

fix(api): correct OpenAPI sample values, missing 403s, servers URL & doc-truth (adversarial audit)#4657

Merged
koala73 merged 6 commits into
mainfrom
fix/openapi-docs-adversarial-fixes
Jul 3, 2026
Merged

fix(api): correct OpenAPI sample values, missing 403s, servers URL & doc-truth (adversarial audit)#4657
koala73 merged 6 commits into
mainfrom
fix/openapi-docs-adversarial-fixes

Conversation

@koala73

@koala73 koala73 commented Jul 3, 2026

Copy link
Copy Markdown
Owner

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:

Param Was Now Source
chokepointId (×4) / chokepointIds suez-canal suez / ["suez"] chokepoint registry
scenarioId oil-price-shock hormuz-tanker-blockade SCENARIO_TEMPLATES
FRED series_id example-id GDP
icao24 (×4) example a835af hex format

BLS series_id stays USPRIV (enum-resolved, untouched).

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, matching the {error} body). PREMIUM_ONLY = premium − entitlement so 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 the api.example.com fallback. Now injects servers: [{url: https://api.worldmonitor.app}] into per-service JSON+YAML (bundle already had it). Wired into make generate + npm run gen:openapi:servers.

U4 — behavioral doc-truth (9 proto comments)
Descriptions aligned to handler reality: runId (drives the note warning, not ignored), webcam lastUpdated (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-events clampInt ranges).

Verification

  • Every defect confirmed fixed in the regenerated docs (chokepointId→suez, scenarioId→hormuz-tanker-blockade, icao24→a835af, FRED→GDP, BLS still USPRIV, servers→api.worldmonitor.app, register-interest + premium 403s, runId/webcam/clamp descriptions).
  • make generate idempotent (re-run = 0 diff); pre-push freshness gate passed.
  • src/generated unchanged; only proto/scripts/tests/docs.api/Makefile/package.json.
  • 500 contract tests pass — new guards added: curated-example validity (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)

Review follow-ups (post-review commits on this PR)

Accepted breaking change

proto/worldmonitor/webcam/v1/get_webcam_image.proto field 5 last_updated was changed int64string (U4 / Greptile fix). make breaking (a manual target — not run by any CI workflow or the pre-push hook) flags this as a WIRE_JSON change — this is intentional and accepted: the handler has always emitted new Date(...).toISOString(), so the old int64 declaration 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, when main becomes the string baseline.

https://claude.ai/code/session_01EsjtheWqt8GxEZfd3DyTMu

…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
@koala73 koala73 added enhancement New feature or request area: API Backend API, sidecar, keys labels Jul 3, 2026
@vercel

vercel Bot commented Jul 3, 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 3, 2026 1:06pm

Request Review

@koala73

koala73 commented Jul 3, 2026

Copy link
Copy Markdown
Owner Author

Follow-up handler/contract bugs surfaced by this audit are tracked in #4658 (webcam int64, aircraft-batch truncation, gdelt/seismology/tech-events clamps, #4604 gaps).

@mintlify

mintlify Bot commented Jul 3, 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 3, 2026, 10:11 AM

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

@greptile-apps

greptile-apps Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This 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 (docs/api/) are regenerated via make generate and are not hand-edited.

  • U1 (examples): Replaces heuristic-generated placeholder values that handlers reject with real accepted values sourced from server-side allowlists — chokepointIdsuez, scenarioIdhormuz-tanker-blockade, icao24a835af, FRED series_idGDP.
  • U2 (403s): Documents the gateway's 403 \"Pro subscription required\" for 11 legacy-Pro paths not covered by ENDPOINT_ENTITLEMENTS, and adds the register-interest Turnstile/HMAC 403 gate to PUBLIC_FORBIDDEN_GATES.
  • U3 (servers): New openapi-inject-servers.mjs stamps servers: [{url: https://api.worldmonitor.app}] into per-service specs that previously fell back to the placeholder api.example.com on the Mintlify docs site.
  • U4 (doc-truth): Nine proto comment updates aligning descriptions with handler reality — disabled intelligence stubs, runId note behavior, aircraft-batch truncation at 10, seismology page_size default of 500 with no clamp, and clampInt ranges for tech-events.

Confidence Score: 3/5

Safe 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

Filename Overview
scripts/openapi-inject-security.mjs Adds PREMIUM_ONLY_PATHS 403 injection and register-interest public gate; YAML always uses hardcoded YAML_BOT_FORBIDDEN_RESPONSE template for all public gates, producing a wrong 403 description for register-interest compared to the JSON artifact.
scripts/openapi-inject-examples.mjs Introduces curated per-parameter example overrides for chokepointId, scenarioId, icao24, and FRED series_id with runtime fallbacks; scenario ID regex only matches single-quoted literals, a minor fragility the contract test would catch post-generation.
scripts/openapi-inject-servers.mjs New script that injects the production servers URL into per-service JSON and YAML specs; idempotent, correctly skips the bundle, and uses surgical text insertion consistent with sibling injectors.
tests/openapi-security-contract.test.mjs Adds assertPremiumForbiddenGateContract and register-interest gate assertions; the public gate contract check does not verify the 403 response description text, so the JSON/YAML inconsistency for register-interest is not caught by these tests.
tests/openapi-examples-contract.test.mjs New curated-example validity suite checks chokepoint, scenario, icao24, and FRED/BLS series values against the same runtime constants as the injector; well-structured and drift-resistant.
tests/openapi-servers-contract.test.mjs New test guards the servers block injection for all service JSON and YAML specs plus the bundle; correctly asserts the production URL and a minimum spec count.
proto/worldmonitor/webcam/v1/get_webcam_image.proto Adds an ISO-8601 description comment to an int64 field; the description correctly documents handler reality but the generated OpenAPI will still carry format: int64 alongside the ISO-8601 description, which is contradictory for OpenAPI consumers.

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]
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[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]
Loading

Comments Outside Diff (1)

  1. scripts/openapi-inject-security.mjs, line 856-862 (link)

    P1 JSON/YAML 403 description divergence for register-interest

    All public forbidden gates share the hardcoded YAML_BOT_FORBIDDEN_RESPONSE template (description: "Bot verification failed."), but the new register-interest entry in PUBLIC_FORBIDDEN_GATES has a custom response.description: "Bot verification or desktop authentication failed.". The JSON injector uses publicForbiddenGate.response directly (line 360), so the JSON spec is correct. The YAML injector always passes YAML_BOT_FORBIDDEN_RESPONSE here and ignores the custom description, so LeadsService.openapi.yaml ends up with description: Bot verification failed. — omitting the desktop-HMAC failure path that the register-interest handler actually returns.

    A developer reading the YAML-backed Mintlify page for register-interest would not learn that desktop-source requests without a valid HMAC also receive a 403, making the documentation incomplete for that audience.

Reviews (1): Last reviewed commit: "fix(api): correct OpenAPI sample values,..." | Re-trigger Greptile

Comment thread docs/api/WebcamService.openapi.yaml
Comment thread scripts/openapi-inject-examples.mjs Outdated
…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
@koala73

koala73 commented Jul 3, 2026

Copy link
Copy Markdown
Owner Author

Both Greptile P2s addressed in 5ffb602:

  1. webcam last_updated int64→string — you're right, format: int64 contradicted the ISO-8601 description. Since the handler already emits new Date(...).toISOString() and the generated TS type was already string, changing the proto field to string is zero-impact and removes the conflict. (This resolves item 1 of chore(api): handler/contract follow-ups surfaced by the OpenAPI adversarial audit (#4657) #4658, so it's no longer deferred.)
  2. scenario-id regex — broadened the quote class to ['"]` in both the injector and the test so a future quote-style change in scenario-templates.ts can't silently fall back to the literal.

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
koala73 added 2 commits July 3, 2026 15:51
…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
@koala73
koala73 merged commit 1b72226 into main Jul 3, 2026
25 checks passed
@koala73
koala73 deleted the fix/openapi-docs-adversarial-fixes branch July 3, 2026 13:20
koala73 added a commit that referenced this pull request Jul 3, 2026
…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
koala73 added a commit that referenced this pull request Jul 3, 2026
…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
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