Commit 7 silently shifted /api/scenario/v1/run-scenario's response
contract in two ways that the commit message covered only partially:
1. HTTP 202 Accepted → HTTP 200 OK
2. Dropped `statusUrl` string from the response body
The `statusUrl` drop was mentioned as "unused by SupplyChainPanel" but
not framed as a contract change. The 202 → 200 shift was not mentioned
at all. This is a same-version (v1 → v1) migration, so external callers
that key off either signal — `response.status === 202` or
`response.body.statusUrl` — silently branch incorrectly.
Evaluated options:
(a) sebuf per-RPC status-code config — not available. sebuf's
HttpConfig only models `path` and `method`; no status annotation.
(b) Bump to scenario/v2 — judged heavier than the break itself for
a single status-code shift. No in-repo caller uses 202 or
statusUrl; the docs-level impact is containable.
(c) Accept the break, document explicitly, partially restore.
Took option (c):
- Restored `statusUrl` in the proto (new field `string status_url = 3`
on RunScenarioResponse). Server computes
`/api/scenario/v1/get-scenario-status?jobId=<encoded job_id>` and
populates it on every successful enqueue. External callers that
followed this URL keep working unchanged.
- 202 → 200 is not recoverable inside the sebuf generator, so it is
called out explicitly in two places:
- docs/api-scenarios.mdx now includes a prominent `<Warning>` block
documenting the v1→v1 contract shift + the suggested migration
(branch on response body shape, not HTTP status).
- RunScenarioResponse proto comment explains why 200 is the new
success status on enqueue.
OpenAPI bundle regenerated to reflect the restored statusUrl field.
- Regression test added in tests/scenario-handler.test.mjs pinning
`statusUrl` to the exact URL-encoded shape — locks the invariant so
a future proto rename or handler refactor can't silently drop it
again.
From koala73 review (#3242 second-pass, HIGH new #3).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Promote the three literal-filename scenario endpoints to a typed sebuf
service with three RPCs:
POST /api/scenario/v1/run-scenario (RunScenario)
GET /api/scenario/v1/get-scenario-status (GetScenarioStatus)
GET /api/scenario/v1/list-scenario-templates (ListScenarioTemplates)
Preserves all security invariants from the legacy handlers:
- 405 for wrong method (sebuf service-config method gate)
- scenarioId validation against SCENARIO_TEMPLATES registry
- iso2 regex ^[A-Z]{2}$
- JOB_ID_RE path-traversal guard on status
- Per-IP 10/min rate limit (moved to gateway ENDPOINT_RATE_POLICIES)
- Queue-depth backpressure (>100 → 429)
- PRO gating via isCallerPremium
- AbortSignal.timeout on every Redis pipeline (runRedisPipeline helper)
Wire-level diffs vs legacy:
- Per-user RL now enforced at the gateway (same 10/min/IP budget).
- Rate-limit response omits Retry-After header; retryAfter is in the
body per error-mapper.ts convention.
- ListScenarioTemplates emits affectedHs2: [] when the registry entry
is null (all-sectors sentinel); proto repeated cannot carry null.
- RunScenario returns { jobId, status } (no statusUrl field — unused
by SupplyChainPanel, drop from wire).
Gateway wiring:
- server/gateway.ts RPC_CACHE_TIER: list-scenario-templates → 'daily'
(matches legacy max-age=3600); get-scenario-status → 'slow-browser'
(premium short-circuit target, explicit entry required by
tests/route-cache-tier.test.mjs).
- src/shared/premium-paths.ts: swap old run/status for the new
run-scenario/get-scenario-status paths.
- api/scenario/v1/{run,status,templates}.ts deleted; 3 manifest
exceptions removed (63 → 52 → 49 migration-pending).
Client:
- src/services/scenario/index.ts — typed client wrapper using
premiumFetch (injects Clerk bearer / API key).
- src/components/SupplyChainPanel.ts — polling loop swapped from
premiumFetch strings to runScenario/getScenarioStatus. Hard 20s
timeout on run preserved via AbortSignal.any.
Tests:
- tests/scenario-handler.test.mjs — 18 new handler-level tests
covering every security invariant + the worker envelope coercion.
- tests/edge-functions.test.mjs — scenario sections removed,
replaced with a breadcrumb pointer to the new test file.
Docs: api-scenarios.mdx, scenario-engine.mdx, usage-rate-limits.mdx,
usage-errors.mdx, supply-chain.mdx refreshed with new paths.
Verified: typecheck, typecheck:api, lint:api-contract (49 entries),
lint:rate-limit-policies (6/180), lint:boundaries, route-cache-tier
(parity), full edge-functions (117) + scenario-handler (18).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>