mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-26 01:24:59 +02:00
* docs(mintlify): add Route Explorer + Scenario Engine workflow pages Checkpoint for review on the IA refresh (per plan docs/plans/2026-04-19-001-feat-docs-user-facing-ia-refresh-plan.md). - docs/docs.json: link Country Resilience Index methodology under Intelligence & Analysis so the flagship 222-country feature is reachable from the main nav (previously orphaned). Add a new Workflows group containing route-explorer and scenario-engine. - docs/route-explorer.mdx: standalone workflow page. Who it is for, Cmd+K entry, four tabs (Current / Alternatives / Land / Impact), inputs, keyboard bindings, map-state integration, PRO gating with free-tier blur + public-route highlight, data sources. - docs/scenario-engine.mdx: standalone workflow page. Template categories (conflict / weather / sanctions / tariff_shock / infrastructure / pandemic), how a scenario activates on the map, PRO gating, pointers to the async job API. Deferred to follow-up commits in the same PR: - documentation.mdx landing rewrite - features.mdx refresh - maritime-intelligence.mdx link-out to Route Explorer - Panels nav group (waits for PR 2 content) All content grounded in live source files cited inline. * docs(mintlify): fix Route Explorer + Scenario Engine review findings Reviewer caught 4 cases where I described behavior I hadn't read carefully. All fixes cross-checked against source. - route-explorer (free-tier): the workflow does NOT blur a numeric payload behind a public demo route. On free tier, fetchLane() short-circuits to renderFreeGate() which blurs the left rail, replaces the tab area with an Upgrade-to-PRO card, and applies a generic public-route highlight on the map. No lane data is rendered in any tab. See src/components/RouteExplorer/ RouteExplorer.ts:212 + :342. - route-explorer (keyboard): Tab / Shift+Tab moves focus between the panel and the map. Direct field jumps are F (From), T (To), P (Product/HS2), not Tab-cycling. Also added the full KeyboardHelp binding list (S swap, ↑/↓ list nav, Enter commit, Cmd+, copy URL, Esc close, ? help, 1-4 tabs). See src/components/RouteExplorer/ KeyboardHelp.ts:9 and RouteExplorer.ts:623. - scenario-engine: the SCENARIO_TEMPLATES array only ships templates of 4 types today (conflict, weather, sanctions, tariff_shock). The ScenarioType union includes infrastructure and pandemic but no templates of those types ship. Dropped them from the shipped table and noted the type union leaves room for future additions. - scenario-engine + api-scenarios: the worker writes status: 'done' (not 'completed') on success, 'failed' on error; pending is synthesised by the status endpoint when no worker record exists. Fixed both the new workflow page and the merged api-scenarios.mdx completed-response example + polling language. See scripts/scenario-worker.mjs:421 and src/components/SupplyChainPanel.ts:870. * docs(mintlify): fix third-round review findings (real IDs + 4-state lifecycle) - api-scenarios (template example): replaced invented hormuz-closure-30d / ["hormuz"] with the actually-shipped hormuz-tanker-blockade / ["hormuz_strait"] from scenario- templates.ts:80. Listed the other 5 shipped template IDs so scripted users aren't dependent on a single example. - api-scenarios (status lifecycle): worker writes FOUR states, not three. Added the intermediate "processing" state with startedAt, written by the worker at job pickup (scenario- worker.mjs:411). Lifecycle now: pending → processing → done|failed. Both pending and processing are non-terminal. - scenario-engine (scripted use blurb): mirror the 4-state language and link into the lifecycle table. - scenario-engine (UI dismiss): replaced "Click Deactivate" with the actual × dismiss control on the scenario banner (aria-label: "Dismiss scenario") per src/components/SupplyChainPanel.ts:790. Also described the banner contents (name, chokepoints, countries, tagline). - api-shipping-v2: while fixing chokepoint IDs, also corrected "hormuz" → "hormuz_strait" and "bab-el-mandeb" → "bab_el_mandeb" across all four occurrences in the shipping v2 page (from PR #3209). Real IDs come from server/_shared/chokepoint- registry.ts (snake_case, not kebab-case, not bare "hormuz"). * docs(mintlify): fix fourth-round findings (banner DOM, webhook TTL refresh) - scenario-engine: accurate description of the rendered scenario banner. Always-present elements are the ⚠ icon, scenario name, top-5 impacted countries with impact %, and dismiss ×. Params chip (e.g. '14d · +110% cost') and 'Simulating …' tagline are conditional on the worker result carrying template parameters (durationDays, disruptionPct, costShockMultiplier). The banner never lists affected chokepoints by name — the map and the chokepoint cards surface those. Per renderScenarioBanner at src/components/SupplyChainPanel.ts:750. - api-shipping-v2 (webhook TTL): register extends both the record and the owner-index set's 30-day TTL via atomic pipeline (SET + SADD + EXPIRE). rotate-secret and reactivate only extend the record's TTL — neither touches the owner-index set, so the owner index can expire independently if a caller only rotates/reactivates within a 30-day window. Re-register to keep both alive. Per api/v2/shipping/webhooks.ts:230 (register pipeline) and :325 (rotate setCachedJson on record only). * docs(mintlify): fix PRO auth contract (trusted origin ≠ PRO) - api-scenarios: 'X-WorldMonitor-Key (or trusted browser origin) + PRO' was wrong — isCallerPremium() explicitly skips trusted-origin short-circuits (keyCheck.required === false) and only counts (a) an env-valid or user-owned wm_-prefixed API key with apiAccess entitlement, or (b) a Clerk bearer with role=pro or Dodo tier ≥ 1. Browser calls work because premiumFetch() injects one of those credentials per request, not because Origin alone authenticates. Per server/_shared/premium-check.ts:34 and src/services/premium-fetch.ts:66. - usage-auth: strengthened the 'Entitlement / tier gating' section to state outright that authentication and PRO entitlement are orthogonal, and that trusted Origin is NOT accepted as PRO even though it is accepted for public endpoints. Listed the two real credential forms that pass the gate. * docs(mintlify): fix stale line cite (MapContainer.activateScenario at :1010) Greptile review P2: prose cited MapContainer.ts:1004 but activateScenario is declared at :1010. Line 1004 landed inside the JSDoc block. * docs(mintlify): finish PR 1 — landing rewrite, features refresh, maritime link-out Completes the PR 1 items from docs/plans/2026-04-19-001-feat-docs-user- facing-ia-refresh-plan.md that were deferred after the checkpoint on Route Explorer + Scenario Engine + CRI nav. No new pages — only edits to existing pages to point at and cohere with the new workflow pages. - documentation.mdx: landing rewrite. Dropped brittle counts (344 news sources, 49 layers, 24 CII countries, 31+ sources, 24 typed services) in favor of durable product framing. Surfaced the shipped differentiators that were invisible on the landing previously: Country Resilience Index (222 countries, linked to its methodology page), AI daily brief, Route Explorer, Scenario Engine, MCP server. Kept CII and CRI as two distinct country-risk surfaces — do not conflate. - features.mdx: replaced the 'all 55 panels' Cmd+K claim and the stale inventory list with family-grouped descriptions that include the panels this audit surfaced as missing (disease- outbreaks, radiation-watch, thermal-escalation, consumer-prices, latest-brief, forecast, country-resilience). Added a Workflows section linking to Route Explorer and Scenario Engine, and a Country-level risk section linking CII + CRI. Untouched sections (map, marker clustering, data layers, export, monitors, activity tracking) left as-is. - maritime-intelligence.mdx: collapsed the embedded Route Explorer subsection to a one-paragraph pointer at /route-explorer so the standalone page is the canonical home. Panels nav group remains intentionally unadded; it waits on PR 2 content to avoid rendering an empty group in Mintlify.
171 lines
5.9 KiB
Plaintext
171 lines
5.9 KiB
Plaintext
---
|
|
title: "Shipping v2 API"
|
|
description: "Chokepoint route-intelligence queries and webhook subscriptions for supply-chain disruption alerts."
|
|
---
|
|
|
|
The v2 shipping API is a **PRO-gated** read + webhook-subscription surface on top of WorldMonitor's chokepoint registry and AIS tracking data.
|
|
|
|
<Info>
|
|
All v2 shipping endpoints require `X-WorldMonitor-Key` (server-to-server). Browser origins are **not** trusted here — `validateApiKey` runs with `forceKey: true`.
|
|
</Info>
|
|
|
|
## Route intelligence
|
|
|
|
### `GET /api/v2/shipping/route-intelligence`
|
|
|
|
Scores a country-pair trade route for chokepoint exposure and current disruption risk.
|
|
|
|
**Query parameters**:
|
|
|
|
| Param | Required | Description |
|
|
|-------|----------|-------------|
|
|
| `fromIso2` | yes | Origin country, ISO-3166-1 alpha-2 (uppercase). |
|
|
| `toIso2` | yes | Destination country, ISO-3166-1 alpha-2 (uppercase). |
|
|
| `cargoType` | no | One of `container` (default), `tanker`, `bulk`, `roro`. |
|
|
| `hs2` | no | 2-digit HS commodity code (default `27` — mineral fuels). |
|
|
|
|
**Example**:
|
|
```
|
|
GET /api/v2/shipping/route-intelligence?fromIso2=AE&toIso2=NL&cargoType=tanker&hs2=27
|
|
```
|
|
|
|
**Response (`200`)**:
|
|
```json
|
|
{
|
|
"fromIso2": "AE",
|
|
"toIso2": "NL",
|
|
"cargoType": "tanker",
|
|
"hs2": "27",
|
|
"primaryRouteId": "ae-to-eu-via-hormuz-suez",
|
|
"chokepointExposures": [
|
|
{ "chokepointId": "hormuz_strait", "chokepointName": "Strait of Hormuz", "exposurePct": 100 },
|
|
{ "chokepointId": "suez", "chokepointName": "Suez Canal", "exposurePct": 100 }
|
|
],
|
|
"bypassOptions": [
|
|
{
|
|
"id": "cape-of-good-hope",
|
|
"name": "Cape of Good Hope",
|
|
"type": "maritime_detour",
|
|
"addedTransitDays": 12,
|
|
"addedCostMultiplier": 1.35,
|
|
"activationThreshold": "DISRUPTION_SCORE_60"
|
|
}
|
|
],
|
|
"warRiskTier": "WAR_RISK_TIER_ELEVATED",
|
|
"disruptionScore": 68,
|
|
"fetchedAt": "2026-04-19T12:00:00Z"
|
|
}
|
|
```
|
|
|
|
- `disruptionScore` is 0-100 on the **primary** chokepoint for the route (higher = more disruption).
|
|
- `warRiskTier` is one of the `WAR_RISK_TIER_*` enum values from the chokepoint status feed.
|
|
- `bypassOptions` are filtered to those whose `suitableCargoTypes` includes `cargoType` (or is unset).
|
|
|
|
**Caching**: `Cache-Control: public, max-age=60, stale-while-revalidate=120`.
|
|
|
|
**Errors**:
|
|
|
|
| Status | Cause |
|
|
|--------|-------|
|
|
| 400 | `fromIso2` or `toIso2` missing/malformed |
|
|
| 401 | API key required or invalid |
|
|
| 403 | `PRO subscription required` |
|
|
| 405 | Method other than `GET` |
|
|
|
|
## Webhook subscriptions
|
|
|
|
### `POST /api/v2/shipping/webhooks`
|
|
|
|
Registers a webhook for chokepoint disruption alerts. Returns `201 Created`.
|
|
|
|
**Request**:
|
|
```json
|
|
{
|
|
"callbackUrl": "https://hooks.example.com/shipping-alerts",
|
|
"chokepointIds": ["hormuz_strait", "suez", "bab_el_mandeb"],
|
|
"alertThreshold": 60
|
|
}
|
|
```
|
|
|
|
- `callbackUrl` — required, HTTPS only, must not resolve to a private/loopback address (SSRF guard at registration).
|
|
- `chokepointIds` — optional. Omitting or passing an empty array subscribes to **all** registered chokepoints. Unknown IDs return `400`.
|
|
- `alertThreshold` — numeric 0-100 (default `50`). Values outside that range return `400 "alertThreshold must be a number between 0 and 100"`.
|
|
|
|
**Response (`201`)**:
|
|
```json
|
|
{
|
|
"subscriberId": "wh_a1b2c3d4e5f6a7b8c9d0e1f2",
|
|
"secret": "64-char-lowercase-hex-string"
|
|
}
|
|
```
|
|
|
|
- `subscriberId` — `wh_` prefix + 24 hex chars (12 random bytes).
|
|
- `secret` — raw 64-char lowercase hex (32 random bytes). There is no `whsec_` prefix. Persist it — the server never returns it again except on rotation.
|
|
- **TTL**: 30 days on both the subscriber record and the per-owner index set. Only **re-registration** refreshes both, via an atomic pipeline (`SET` record with `EX`, `SADD` + `EXPIRE` on the owner index). `rotate-secret` and `reactivate` refresh the record's TTL only — they do not touch the owner-index set's expiry, so the owner index can expire independently if a caller only ever rotates or reactivates within a 30-day window. Re-register to keep both alive.
|
|
- Ownership is tracked via SHA-256 of the caller's API key (never secret — stored as `ownerTag`).
|
|
|
|
Auth: `X-WorldMonitor-Key` (forceKey: true) + PRO. Returns `401` / `403` otherwise.
|
|
|
|
### `GET /api/v2/shipping/webhooks`
|
|
|
|
Lists the caller's registered webhooks (filtered by the SHA-256 owner tag of the calling API key).
|
|
|
|
```json
|
|
{
|
|
"webhooks": [
|
|
{
|
|
"subscriberId": "wh_...",
|
|
"callbackUrl": "https://hooks.example.com/...",
|
|
"chokepointIds": ["hormuz_strait", "suez"],
|
|
"alertThreshold": 60,
|
|
"createdAt": "2026-04-19T12:00:00Z",
|
|
"active": true
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
The `secret` is intentionally omitted from list and status responses.
|
|
|
|
### `GET /api/v2/shipping/webhooks/{subscriberId}`
|
|
|
|
Status read for a single webhook. Returns the same record shape as in `GET /webhooks` (no `secret`). `404` if unknown, `403` if owned by a different API key.
|
|
|
|
### `POST /api/v2/shipping/webhooks/{subscriberId}/rotate-secret`
|
|
|
|
Generates and returns a **new** secret. The record's `secret` is replaced in place; the old secret stops validating immediately.
|
|
|
|
```json
|
|
{ "subscriberId": "wh_...", "secret": "new-64-char-hex", "rotatedAt": "2026-04-19T12:05:00Z" }
|
|
```
|
|
|
|
### `POST /api/v2/shipping/webhooks/{subscriberId}/reactivate`
|
|
|
|
Flips `active: true` on the record (use after investigating and fixing a delivery failure that caused deactivation).
|
|
|
|
```json
|
|
{ "subscriberId": "wh_...", "active": true }
|
|
```
|
|
|
|
### Delivery format
|
|
|
|
```
|
|
POST <callbackUrl>
|
|
Content-Type: application/json
|
|
X-WM-Signature: sha256=<HMAC-SHA256(body, secret)>
|
|
X-WM-Delivery-Id: <ulid>
|
|
X-WM-Event: chokepoint.disruption
|
|
|
|
{
|
|
"subscriberId": "wh_...",
|
|
"chokepointId": "hormuz_strait",
|
|
"score": 74,
|
|
"alertThreshold": 60,
|
|
"triggeredAt": "2026-04-19T12:03:00Z",
|
|
"reason": "ais_congestion_spike",
|
|
"details": { ... }
|
|
}
|
|
```
|
|
|
|
The delivery worker re-resolves `callbackUrl` before each send and re-checks against `PRIVATE_HOSTNAME_PATTERNS` to mitigate DNS rebinding. Delivery is at-least-once — consumers must handle duplicates via `X-WM-Delivery-Id`.
|