mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
docs(mintlify): cover MCP, OAuth, non-RPC endpoints, and usage (#3209)
* docs(mintlify): cover MCP, OAuth, non-RPC endpoints, and usage Audit against api/ + proto/ revealed 9 OpenAPI specs missing from nav, the scenario/v1 service undocumented, and MCP (32 tools + OAuth 2.1 flow) with no user-facing docs. The stale Docs_To_Review/API_REFERENCE.md still pointed at pre-migration endpoints that no longer exist. - Wire 9 orphaned specs into docs.json: ConsumerPrices, Forecast, Health, Imagery, Radiation, Resilience, Sanctions, Thermal, Webcam - Hand-write ScenarioService.openapi.yaml (3 RPCs) until it's proto-backed (tracked in issue #3207) - New MCP page with tool catalog + client setup (Claude Desktop/web, Cursor) - New MDX for OAuth, Platform, Brief, Commerce, Notifications, Shipping v2, Proxies - New Usage group: quickstart, auth matrix, rate limits, errors - Remove docs/Docs_To_Review/API_REFERENCE.md and EXTERNAL_APIS.md (referenced dead endpoints); add README flagging dir as archival * docs(mintlify): move scenario docs out of generated docs/api/ tree The pre-push hook enforces that docs/api/ is proto-generated only. Replace the hand-written ScenarioService.openapi.yaml with a plain MDX page (docs/api-scenarios.mdx) until the proto migration lands (tracked in issue #3207). * docs(mintlify): fix factual errors flagged in PR review Reviewer caught 5 endpoints where I speculated on shape/method/limits instead of reading the code. All fixes cross-checked against the source: - api-shipping-v2: route-intelligence is GET with query params (fromIso2, toIso2, cargoType, hs2), not POST with a JSON body. Response shape is {primaryRouteId, chokepointExposures[], bypassOptions[], warRiskTier, disruptionScore, ...}. - api-commerce: /api/product-catalog returns {tiers, fetchedAt, cachedUntil, priceSource} with tier groups free|pro|api_starter| enterprise, not the invented {currency, plans}. Document the DELETE purge path too. - api-notifications: Slack/Discord /oauth/start are POST + Clerk JWT + PRO (returning {oauthUrl}), not GET redirects. Callbacks remain GET. - api-platform: /api/version returns the latest GitHub Release ({version, tag, url, prerelease}), not deployed commit/build metadata. - api-oauth + mcp: /api/oauth/register limit is 5/60s/IP (match code), not 10/hour. Also caught while double-checking: /api/register-interest and /api/contact are 5/60min and 3/60min respectively (1-hour window, not 1-minute). Both require Turnstile. Removed the fabricated limits for share-url, notification-channels, create-checkout (they fall back to the default per-IP limit). * docs(mintlify): second-round fixes — verify every claim against source Reviewer caught 7 more cases where I described API behavior I hadn't read. Each fix below cross-checked against the handler. - api-commerce (product-catalog): tiers are flat objects with monthlyPrice/annualPrice/monthlyProductId/annualProductId on paid tiers, price+period for free, price:null for enterprise. There is no nested plans[] array. - api-commerce (referral/me): returns {code, shareUrl}, not counts. Code is a deterministic 8-char HMAC of the Clerk userId; binding into Convex is fire-and-forget via ctx.waitUntil. - api-notifications (notification-channels): actual action set is create-pairing-token, set-channel, set-web-push, delete-channel, set-alert-rules, set-quiet-hours, set-digest-settings. Replaced the made-up list. - api-shipping-v2 (webhooks): alertThreshold is numeric 0-100 (default 50), not a severity string. Subscriber IDs are wh_+24hex; secret is raw 64-char hex (no whsec_ prefix). POST registration returns 201. Added the management routes: GET /{id}, POST /{id}/rotate-secret, POST /{id}/reactivate. - api-platform (cache-purge): auth is Authorization: Bearer RELAY_SHARED_SECRET, not an admin-key header. Body takes keys[] and/or patterns[] (not {key} or {tag}), with explicit per-request caps and prefix-blocklist behavior. - api-platform (download): platform+variant query params, not file=<id>. Response is a 302 to a GitHub release asset; documented the full platform/variant tables. - mcp: server also accepts direct X-WorldMonitor-Key in addition to OAuth bearer. Fixed the curl example which was incorrectly sending a wm_live_ API key as a bearer token. - api-notifications (youtube/live): handler reads channel or videoId, not channelId. - usage-auth: corrected the auth-matrix row for /api/mcp to reflect that OAuth is one of two accepted modes. * docs(mintlify): fix Greptile review findings - mcp.mdx: 'Five' slow tools → 'Six' (list contains 6 tools) - api-scenarios.mdx: replace invalid JSON numeric separator (8_400_000_000) with plain integer (8400000000) Greptile's third finding — /api/oauth/register rate-limit contradiction across api-oauth.mdx / mcp.mdx / usage-rate-limits.mdx — was already resolved in commit4f2600b2a(reviewed commit waseb5654647).
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
26
docs/Docs_To_Review/README.md
Normal file
26
docs/Docs_To_Review/README.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Docs To Review (archival)
|
||||
|
||||
This directory is **not published by Mintlify** (`.mintignore` excludes it).
|
||||
|
||||
Files here are internal / archival / pending-audit. They are **not** the source of truth.
|
||||
|
||||
## Canonical docs live at the root of `docs/`
|
||||
|
||||
| If you want... | See |
|
||||
|---|---|
|
||||
| Architecture overview | `/docs/architecture.mdx` |
|
||||
| API reference (RPC) | `/docs/api/*.openapi.yaml` + https://docs.worldmonitor.app |
|
||||
| Non-RPC API endpoints | `/docs/api-{platform,brief,commerce,notifications,shipping-v2,proxies,oauth}.mdx` |
|
||||
| MCP server | `/docs/mcp.mdx` |
|
||||
| External data sources | `/docs/data-sources.mdx` |
|
||||
| Release / desktop packaging | `/docs/release-packaging.mdx`, `/docs/desktop-app.mdx` |
|
||||
| Country instability index | `/docs/country-instability-index.mdx` |
|
||||
|
||||
## Removed (2026-04-19)
|
||||
|
||||
- `API_REFERENCE.md` — referenced legacy pre-migration endpoints (`/api/acled`, `/api/finnhub`, `/api/fred-data`, etc.) that no longer exist. Superseded by the auto-generated OpenAPI specs under `/docs/api/`.
|
||||
- `EXTERNAL_APIS.md` — duplicated content now in `/docs/data-sources.mdx`.
|
||||
|
||||
## Remaining files
|
||||
|
||||
The rest of this directory needs a case-by-case audit — contents may be stale, useful, or mergeable into canonical docs. Don't cite them externally until audited.
|
||||
81
docs/api-brief.mdx
Normal file
81
docs/api-brief.mdx
Normal file
@@ -0,0 +1,81 @@
|
||||
---
|
||||
title: "AI Brief Endpoints"
|
||||
description: "Read, share, and render the AI-composed daily intelligence brief."
|
||||
---
|
||||
|
||||
WorldMonitor composes a per-user **daily intelligence brief** on Railway, stores it in Redis at `brief:{userId}:{issueDate}`, and exposes these routes for dashboard readback, public sharing, and Telegram/Slack carousel rendering.
|
||||
|
||||
<Info>
|
||||
All read routes require a valid Clerk session and a PRO tier, except the public share route (`/api/brief/public/{hash}`).
|
||||
</Info>
|
||||
|
||||
## Latest brief (authenticated)
|
||||
|
||||
### `GET /api/latest-brief`
|
||||
|
||||
Returns a short summary of the caller's most recent composed brief, or `{ status: "composing" }` if today's run hasn't produced a brief yet.
|
||||
|
||||
| Status | Response |
|
||||
|--------|----------|
|
||||
| 200 OK | `{ issueDate, dateLong, greeting, threadCount, magazineUrl }` |
|
||||
| 200 OK | `{ status: "composing" }` — no brief for today yet |
|
||||
| 401 | Missing / invalid Clerk JWT |
|
||||
| 403 | `pro_required` |
|
||||
| 503 | `BRIEF_URL_SIGNING_SECRET` not configured |
|
||||
|
||||
The `magazineUrl` is a freshly-signed URL — the HMAC binds `{userId, issueDate}` so it only works for the authenticated owner.
|
||||
|
||||
### `GET /api/brief/{userId}/{issueDate}`
|
||||
|
||||
Full brief body for `issueDate` (YYYY-MM-DD). HMAC-signed URL required. Returns the structured brief JSON used by the magazine reader view.
|
||||
|
||||
## Sharing
|
||||
|
||||
### `POST /api/brief/share-url?date=YYYY-MM-DD`
|
||||
|
||||
Materialises a public share pointer for the caller's brief on `date`. Idempotent — hash is a pure function of `{userId, issueDate, BRIEF_SHARE_SECRET}`.
|
||||
|
||||
| Status | Response |
|
||||
|--------|----------|
|
||||
| 200 | `{ shareUrl, hash, issueDate }` |
|
||||
| 400 | `invalid_date_shape` / `invalid_payload` |
|
||||
| 401 | `UNAUTHENTICATED` |
|
||||
| 403 | `pro_required` |
|
||||
| 404 | `brief_not_found` — reader can't share what doesn't exist |
|
||||
| 503 | `service_unavailable` |
|
||||
|
||||
### `GET /api/brief/public/{hash}`
|
||||
|
||||
**Unauthenticated** public read of a previously-shared brief. The hash resolves to a `brief:public:{hash} → {userId, issueDate}` Redis pointer; if absent, the brief was never shared. Share pointers are written lazily (on share, not on compose).
|
||||
|
||||
## Carousel (images for social)
|
||||
|
||||
### `GET /api/brief/carousel/{userId}/{issueDate}/{page}.png`
|
||||
|
||||
Server-rendered PNG page (`page` = 1..N) of the brief, intended for Telegram `sendMediaGroup`, Slack `chat.postMessage`, LinkedIn, etc.
|
||||
|
||||
- Rendered via `@resvg/resvg-js` with the bundled Linux native binding.
|
||||
- `Content-Type: image/png`, 1080×1350 (4:5 portrait).
|
||||
- Not gated — uses the HMAC'd path as the capability.
|
||||
|
||||
## Ancillary
|
||||
|
||||
### `GET /api/story?date=YYYY-MM-DD`
|
||||
|
||||
Public read-only "story view" (web reader) for a shared brief. SEO-friendly HTML response.
|
||||
|
||||
### `GET /api/og-story?date=YYYY-MM-DD`
|
||||
|
||||
Open Graph preview image for `/api/story`. Returns `image/png`, cached aggressively.
|
||||
|
||||
### `POST /api/chat-analyst`
|
||||
|
||||
Streaming chat endpoint for the "Ask the analyst" in-dashboard assistant. Takes a user prompt + recent-signal context; returns SSE tokens.
|
||||
|
||||
- Auth: Clerk JWT + PRO
|
||||
- Streams: `text/event-stream`
|
||||
- Back-end: `intelligence/v1/chat-analyst-*` handlers compose context + prompt
|
||||
|
||||
### `POST /api/widget-agent`
|
||||
|
||||
Single-shot completion endpoint used by embedded widget iframes. Auth via `X-WorldMonitor-Key` (partner keys). Rate-limited per key.
|
||||
117
docs/api-commerce.mdx
Normal file
117
docs/api-commerce.mdx
Normal file
@@ -0,0 +1,117 @@
|
||||
---
|
||||
title: "Commerce Endpoints"
|
||||
description: "Checkout, customer portal, product catalog, and referrals. Thin edge proxies over Convex + Dodo Payments."
|
||||
---
|
||||
|
||||
WorldMonitor uses [Dodo Payments](https://dodopayments.com) for PRO subscriptions and [Convex](https://convex.dev) as the source-of-truth for entitlements. These edge endpoints are thin auth proxies — they validate the Clerk JWT, then forward to Convex HTTP actions via `RELAY_SHARED_SECRET`.
|
||||
|
||||
## Checkout
|
||||
|
||||
### `POST /api/create-checkout`
|
||||
|
||||
Creates a Dodo checkout session and returns the hosted-checkout URL.
|
||||
|
||||
- **Auth**: Clerk bearer (required)
|
||||
- **Body**:
|
||||
```json
|
||||
{ "productId": "pro-monthly", "returnUrl": "https://www.worldmonitor.app/pro/success" }
|
||||
```
|
||||
- **Response**: `{ "checkoutUrl": "https://checkout.dodopayments.com/..." }`
|
||||
- **returnUrl** is validated against an allowlist on the Convex side.
|
||||
|
||||
### `POST /api/customer-portal`
|
||||
|
||||
Creates a Dodo customer-portal session for an existing subscriber (update card, cancel, view invoices).
|
||||
|
||||
- **Auth**: Clerk bearer + active entitlement
|
||||
- **Response**: `{ "portalUrl": "..." }`
|
||||
|
||||
## Product catalog
|
||||
|
||||
### `GET /api/product-catalog`
|
||||
|
||||
Returns the tier view-model used by the `/pro` pricing page. Cached in Redis under `product-catalog:v2` for 1 hour; on cache miss, fetches live prices from Dodo Payments and falls back to `_product-fallback-prices.js` if Dodo is unreachable. Response carries an `X-Product-Catalog-Source` header so probes can tell cache hits from live fetches.
|
||||
|
||||
**Response** (tiers ordered `free`, `pro`, `api_starter`, `enterprise`):
|
||||
|
||||
```json
|
||||
{
|
||||
"tiers": [
|
||||
{
|
||||
"name": "Free",
|
||||
"description": "Get started with the essentials",
|
||||
"features": ["Core dashboard panels", "..."],
|
||||
"cta": "Get Started",
|
||||
"href": "https://worldmonitor.app",
|
||||
"highlighted": false,
|
||||
"price": 0,
|
||||
"period": "forever"
|
||||
},
|
||||
{
|
||||
"name": "Pro",
|
||||
"description": "Full intelligence dashboard",
|
||||
"features": ["..."],
|
||||
"highlighted": true,
|
||||
"monthlyPrice": 20,
|
||||
"monthlyProductId": "pdt_0Nbtt71uObulf7fGXhQup",
|
||||
"annualPrice": 180,
|
||||
"annualProductId": "pdt_0NbttMIfjLWC10jHQWYgJ"
|
||||
},
|
||||
{
|
||||
"name": "API",
|
||||
"description": "Programmatic access to intelligence data",
|
||||
"features": ["..."],
|
||||
"highlighted": false,
|
||||
"monthlyPrice": 99,
|
||||
"monthlyProductId": "pdt_0NbttVmG1SERrxhygbbUq",
|
||||
"annualPrice": 990,
|
||||
"annualProductId": "pdt_0Nbu2lawHYE3dv2THgSEV"
|
||||
},
|
||||
{
|
||||
"name": "Enterprise",
|
||||
"description": "Custom solutions for organizations",
|
||||
"features": ["..."],
|
||||
"cta": "Contact Sales",
|
||||
"href": "mailto:enterprise@worldmonitor.app",
|
||||
"highlighted": false,
|
||||
"price": null
|
||||
}
|
||||
],
|
||||
"fetchedAt": "2026-04-19T12:00:00Z",
|
||||
"cachedUntil": "2026-04-19T13:00:00Z",
|
||||
"priceSource": "dodo"
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
- Price fields are flat on the tier. Paid tiers expose `monthlyPrice` / `monthlyProductId` and `annualPrice` / `annualProductId`. Free uses `price: 0, period: "forever"`; Enterprise uses `price: null`.
|
||||
- Prices are dollars (Dodo returns cents; the handler divides by 100). Currency is implicit USD for the published catalog.
|
||||
|
||||
### `DELETE /api/product-catalog`
|
||||
|
||||
Purges the cached catalog. Requires `Authorization: Bearer $RELAY_SHARED_SECRET`. Internal.
|
||||
|
||||
## Referrals
|
||||
|
||||
### `GET /api/referral/me`
|
||||
|
||||
Returns the caller's deterministic referral code (an 8-char HMAC of the Clerk userId, stable for the life of the account) and a pre-built share URL. Clerk bearer required. The handler also fires a best-effort `ctx.waitUntil` Convex binding so future `/pro?ref=<code>` signups can attribute — this never blocks the response.
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "a1b2c3d4",
|
||||
"shareUrl": "https://worldmonitor.app/pro?ref=a1b2c3d4"
|
||||
}
|
||||
```
|
||||
|
||||
Errors:
|
||||
- `401 UNAUTHENTICATED` — missing or invalid Clerk JWT.
|
||||
- `503 service_unavailable` — `BRIEF_URL_SIGNING_SECRET` not configured (the referral-code HMAC reuses that secret).
|
||||
|
||||
No `referrals` count or `rewardMonths` is returned today — Dodo's `affonso_referral` attribution doesn't yet flow into Convex, and exposing only the waitlist-side count would mislead.
|
||||
|
||||
## Waitlist
|
||||
|
||||
### `POST /api/register-interest`
|
||||
|
||||
Captures an email into the Convex waitlist table. Turnstile-verified, rate-limited per IP. See [Platform endpoints](/api-platform) for the request shape.
|
||||
115
docs/api-notifications.mdx
Normal file
115
docs/api-notifications.mdx
Normal file
@@ -0,0 +1,115 @@
|
||||
---
|
||||
title: "Notifications & Integrations"
|
||||
description: "Notification channels, webhook delivery, and Telegram/Slack/Discord/YouTube integration endpoints."
|
||||
---
|
||||
|
||||
## Notification channels
|
||||
|
||||
Users can register multiple delivery channels (webhook, Telegram, Slack, Discord, email) and bind alert rules to them.
|
||||
|
||||
### `GET /api/notification-channels`
|
||||
|
||||
Lists the caller's registered channels and alert rules.
|
||||
|
||||
```json
|
||||
{
|
||||
"channels": [
|
||||
{ "id": "chn_01", "type": "webhook", "url": "https://hooks.example.com/...", "active": true },
|
||||
{ "id": "chn_02", "type": "telegram", "chatId": "@alerts_xyz", "active": true }
|
||||
],
|
||||
"alertRules": [
|
||||
{ "id": "rul_01", "channelId": "chn_01", "trigger": "brief_ready", "filter": null }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `POST /api/notification-channels`
|
||||
|
||||
Action-dispatched writer. The body's `action` field selects the mutation:
|
||||
|
||||
| action | Purpose |
|
||||
|--------|---------|
|
||||
| `create-pairing-token` | Mint a one-time pairing token (optional `variant`) for the mobile / Tauri client to bind a push channel. |
|
||||
| `set-channel` | Register or update a channel. For `webhook` channels the `webhookEnvelope` URL is validated HTTPS-only, must not resolve to a private/loopback address, and is AES-256-GCM encrypted before storage. Optional `email`, `webhookLabel` (truncated to 100 chars). |
|
||||
| `set-web-push` | Register a browser Web Push subscription for the signed-in user. |
|
||||
| `delete-channel` | Remove a channel by type (`email`, `webhook`, `telegram`, `web-push`, etc.). |
|
||||
| `set-alert-rules` | Replace the caller's alert-rules set in one shot. |
|
||||
| `set-quiet-hours` | Set do-not-disturb windows. |
|
||||
| `set-digest-settings` | Configure digest cadence and channel routing. |
|
||||
|
||||
All actions require Clerk bearer + PRO (`tier >= 1`). Invalid actions return `400 Unknown action`. Requests are forwarded to Convex via `RELAY_SHARED_SECRET`.
|
||||
|
||||
## Webhook delivery contract
|
||||
|
||||
When an alert fires, registered webhook URLs receive:
|
||||
|
||||
- **Method**: `POST`
|
||||
- **Headers**:
|
||||
- `Content-Type: application/json`
|
||||
- `X-WM-Signature: sha256=<HMAC-SHA256(body, channelSecret)>`
|
||||
- `X-WM-Delivery-Id: <ulid>`
|
||||
- `X-WM-Event: <event-name>`
|
||||
- **Body** (envelope v1):
|
||||
```json
|
||||
{
|
||||
"envelope": 1,
|
||||
"event": "brief_ready",
|
||||
"deliveryId": "01HX...",
|
||||
"occurredAt": "2026-04-19T06:00:00Z",
|
||||
"data": { "issueDate": "2026-04-19", "magazineUrl": "..." }
|
||||
}
|
||||
```
|
||||
|
||||
Signature verification: `hmac_sha256(rawBody, channelSecret) == X-WM-Signature[7:]`.
|
||||
|
||||
<Warning>
|
||||
The envelope version is **shared across three producers** (`notification-relay`, `proactive-intelligence`, `seed-digest-notifications`). Bumping it requires coordinated updates.
|
||||
</Warning>
|
||||
|
||||
### `POST /api/notify`
|
||||
|
||||
Internal ingestion endpoint called by Railway producers to enqueue a notification. Requires `RELAY_SHARED_SECRET`. Not a public API.
|
||||
|
||||
## Telegram
|
||||
|
||||
### `GET /api/telegram-feed?userId=...`
|
||||
|
||||
Returns the pre-rendered brief feed for a given Telegram-linked user. Used by the Telegram mini-app.
|
||||
|
||||
## YouTube
|
||||
|
||||
### `GET /api/youtube/embed?videoId=...`
|
||||
|
||||
SSR'd YouTube embed iframe with CSP-compatible wrapping. Used to bypass WKWebView autoplay restrictions on the desktop app.
|
||||
|
||||
### `GET /api/youtube/live?channel=<handle>` or `?videoId=<11-char-id>`
|
||||
|
||||
Returns live-stream metadata for a YouTube channel (`channel` — handle with or without `@` prefix) or a specific video (`videoId` — 11-char YouTube id). At least one of the two params is required; returns `400 Missing channel or videoId parameter` otherwise. Response cached 10 min for channel lookups, 1 hour for videoId lookups.
|
||||
|
||||
Proxies to the Railway relay first (residential proxy for YouTube scraping). On relay failure, falls back to YouTube oEmbed (for `videoId`) or direct channel scraping — both are unreliable from datacenter IPs.
|
||||
|
||||
## Slack integration
|
||||
|
||||
### `POST /api/slack/oauth/start`
|
||||
|
||||
Authenticated (Clerk JWT + PRO). Body is empty. Server generates a one-time CSRF state token, stores the caller's userId in Upstash keyed by that state (10-min TTL), and returns the Slack authorize URL for the frontend to open in a popup.
|
||||
|
||||
```json
|
||||
{ "oauthUrl": "https://slack.com/oauth/v2/authorize?client_id=...&scope=incoming-webhook&..." }
|
||||
```
|
||||
|
||||
Errors: 401 (missing/invalid JWT), 403 `pro_required`, 503 (OAuth not configured or Upstash unavailable).
|
||||
|
||||
### `GET /api/slack/oauth/callback`
|
||||
|
||||
Unauthenticated — the popup lands here after Slack redirects. Validates the state token, exchanges `code` for an incoming-webhook URL, AES-256-GCM encrypts the webhook, and stores it in Convex. Returns a tiny HTML page that `postMessage`s the opener and closes.
|
||||
|
||||
## Discord integration
|
||||
|
||||
### `POST /api/discord/oauth/start`
|
||||
|
||||
Authenticated (Clerk JWT + PRO). Same shape as the Slack start route — returns `{ oauthUrl }` for a popup.
|
||||
|
||||
### `GET /api/discord/oauth/callback`
|
||||
|
||||
Unauthenticated. Exchanges `code`, stores the guild webhook, and `postMessage`s the opener.
|
||||
118
docs/api-oauth.mdx
Normal file
118
docs/api-oauth.mdx
Normal file
@@ -0,0 +1,118 @@
|
||||
---
|
||||
title: "OAuth 2.1 Server"
|
||||
description: "Dynamic client registration, authorization, and token exchange endpoints that back the WorldMonitor MCP server."
|
||||
---
|
||||
|
||||
WorldMonitor runs a minimal OAuth 2.1 authorization server whose only client-facing purpose today is **granting access to the MCP server** at `/api/mcp`. It implements:
|
||||
|
||||
- [RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591) — Dynamic Client Registration
|
||||
- [RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636) — PKCE (required, S256 only)
|
||||
- [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) — Authorization Server Metadata
|
||||
- [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) — Protected Resource Metadata
|
||||
|
||||
## Discovery
|
||||
|
||||
| URL | Purpose |
|
||||
|-----|---------|
|
||||
| `/.well-known/oauth-authorization-server` | AS metadata (endpoints, supported grants, PKCE methods) |
|
||||
| `/.well-known/oauth-protected-resource` | Resource metadata (authorization servers, scopes) |
|
||||
|
||||
## Endpoints
|
||||
|
||||
### `POST /api/oauth/register`
|
||||
|
||||
Dynamic Client Registration. Returns a `client_id` (public clients, no secret).
|
||||
|
||||
**Request**:
|
||||
```json
|
||||
{
|
||||
"redirect_uris": ["https://claude.ai/api/mcp/auth_callback"],
|
||||
"client_name": "Claude Desktop",
|
||||
"token_endpoint_auth_method": "none"
|
||||
}
|
||||
```
|
||||
|
||||
**Response**: `{ "client_id": "c_01HX...", "client_id_issued_at": 1713456789, ... }`
|
||||
|
||||
**Redirect URI allowlist**: only these prefixes are accepted:
|
||||
- `https://claude.ai/api/mcp/auth_callback`
|
||||
- `https://claude.com/api/mcp/auth_callback`
|
||||
- `http://localhost:<port>` / `http://127.0.0.1:<port>` — any port
|
||||
|
||||
**Rate limit**: 5 registrations / 60 s / IP.
|
||||
|
||||
**Client TTL**: 90 days sliding (every successful token exchange refreshes).
|
||||
|
||||
### `GET /api/oauth/authorize`
|
||||
|
||||
Starts the OAuth flow. Renders a consent page that redirects to Clerk for sign-in, then issues an authorization code bound to the caller's PRO entitlement.
|
||||
|
||||
**Required query params**:
|
||||
- `response_type=code`
|
||||
- `client_id` — from DCR
|
||||
- `redirect_uri` — must match the one registered
|
||||
- `code_challenge` — PKCE S256
|
||||
- `code_challenge_method=S256`
|
||||
- `state` — opaque
|
||||
- `scope` (optional)
|
||||
|
||||
**Code TTL**: 10 minutes. Single-use (atomic `GETDEL` on exchange).
|
||||
|
||||
### `POST /api/oauth/token`
|
||||
|
||||
Exchanges an authorization code for an access token, or refreshes an existing token.
|
||||
|
||||
**Grant type: `authorization_code`**:
|
||||
```
|
||||
grant_type=authorization_code
|
||||
code=<from /authorize>
|
||||
code_verifier=<PKCE>
|
||||
client_id=<from DCR>
|
||||
redirect_uri=<same as /authorize>
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"access_token": "wm_oat_...",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 3600,
|
||||
"refresh_token": "wm_ort_...",
|
||||
"scope": "mcp:read"
|
||||
}
|
||||
```
|
||||
|
||||
**Grant type: `refresh_token`**:
|
||||
```
|
||||
grant_type=refresh_token
|
||||
refresh_token=<from previous exchange>
|
||||
client_id=<from DCR>
|
||||
```
|
||||
|
||||
**Rate limit**: 10 token requests / minute / IP.
|
||||
|
||||
**Token TTLs**:
|
||||
- Access token: 1 hour
|
||||
- Refresh token: 7 days
|
||||
|
||||
All token-endpoint responses include `Cache-Control: no-store, Pragma: no-cache`.
|
||||
|
||||
## Using tokens
|
||||
|
||||
Pass the access token on every MCP request:
|
||||
|
||||
```
|
||||
Authorization: Bearer wm_oat_...
|
||||
```
|
||||
|
||||
Tokens are bound to the user's account and re-check entitlement on every call — a downgrade revokes access on the next request.
|
||||
|
||||
## Error responses
|
||||
|
||||
Per [RFC 6749 §5.2](https://datatracker.ietf.org/doc/html/rfc6749#section-5.2):
|
||||
|
||||
```json
|
||||
{ "error": "invalid_grant", "error_description": "..." }
|
||||
```
|
||||
|
||||
Common errors: `invalid_request`, `invalid_client`, `invalid_grant`, `unsupported_grant_type`, `invalid_scope`.
|
||||
155
docs/api-platform.mdx
Normal file
155
docs/api-platform.mdx
Normal file
@@ -0,0 +1,155 @@
|
||||
---
|
||||
title: "Platform Endpoints"
|
||||
description: "Bootstrap, health, versioning, cache-purge, and user preferences — the plumbing endpoints every WorldMonitor client talks to."
|
||||
---
|
||||
|
||||
These endpoints are not part of a domain RPC service — they sit at the root of the API surface and handle platform concerns.
|
||||
|
||||
## Bootstrap
|
||||
|
||||
### `GET /api/bootstrap`
|
||||
|
||||
Single round-trip hydration for the dashboard. Returns **all bootstrap-registered Redis cache keys** unwrapped from their seed envelopes in one response.
|
||||
|
||||
- **Auth**: browser origin OR `X-WorldMonitor-Key`
|
||||
- **Cache**: `Cache-Control: public, max-age=30, s-maxage=30`
|
||||
- **Shape**: `{ earthquakes, outages, marketQuotes, commodityQuotes, imfMacro, bisPolicy, ... }` — ~40+ top-level fields, each the unwrapped payload of one seeded domain.
|
||||
|
||||
Use this on initial page load to avoid 40 parallel RPC calls.
|
||||
|
||||
## Version
|
||||
|
||||
### `GET /api/version`
|
||||
|
||||
Returns the latest **GitHub Release** of `koala73/worldmonitor`. Used by the desktop app to detect a newer published release and prompt the user to update. It is **not** the currently-deployed Vercel commit.
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "2.6.7",
|
||||
"tag": "v2.6.7",
|
||||
"url": "https://github.com/koala73/worldmonitor/releases/tag/v2.6.7",
|
||||
"prerelease": false
|
||||
}
|
||||
```
|
||||
|
||||
Cached `public, s-maxage=300, stale-while-revalidate=60, stale-if-error=3600`. Returns `502 { "error": "upstream" }` or `502 { "error": "fetch_failed" }` when the GitHub API is unreachable.
|
||||
|
||||
## Cache purge
|
||||
|
||||
### `POST /api/cache-purge`
|
||||
|
||||
Internal. Invalidates Redis cache keys by explicit list or glob patterns.
|
||||
|
||||
- **Auth**: `Authorization: Bearer $RELAY_SHARED_SECRET` (timing-safe compared). Anything else returns `401`.
|
||||
- **Body** (at least one of `keys` / `patterns` required):
|
||||
```json
|
||||
{
|
||||
"keys": ["market:stocks-bootstrap:v1", "infra:outages:v1"],
|
||||
"patterns": ["market:sectors:*"],
|
||||
"dryRun": false
|
||||
}
|
||||
```
|
||||
- **Limits**: up to 20 explicit keys, up to 3 patterns (each must end in `*`, bare `*` rejected), up to 200 deletions total, up to 5 SCAN iterations per pattern.
|
||||
- **Safety**: keys with prefixes `rl:` / `__` are always skipped; patterns that would match `military:bases:*`, `conflict:iran-events:*`, `conflict:ucdp-events:*` (durable seeds) are skipped.
|
||||
- **Non-production**: on preview / development deploys, keys are auto-prefixed with `{env}:{git-sha}:` so purges can't affect production data.
|
||||
- **Response**:
|
||||
```json
|
||||
{ "matched": 4, "deleted": 4, "keys": ["..."], "dryRun": false, "truncated": false }
|
||||
```
|
||||
|
||||
## Health
|
||||
|
||||
### `GET /api/health`
|
||||
|
||||
Aggregated freshness report for **all registered seed keys**. Returns `HEALTHY`, `WARNING` (stale), or `CRIT` (missing / empty with `emptyDataIsFailure`).
|
||||
|
||||
Monitor via UptimeRobot / Better Stack — alert on any status other than `HEALTHY`.
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "HEALTHY",
|
||||
"checkedAt": "2026-04-19T12:00:00Z",
|
||||
"keys": {
|
||||
"market:stocks-bootstrap:v1": { "status": "HEALTHY", "fetchedAt": "...", "recordCount": 78 },
|
||||
"seismology:earthquakes:v1": { "status": "WARNING", "staleMin": 12 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /api/seed-health`
|
||||
|
||||
Parallel registry for Railway-cron-driven seeders with their own cadence thresholds. Distinct from `/api/health` — both must be updated when cadence changes. See [health endpoints](/health-endpoints).
|
||||
|
||||
### `POST /api/seed-contract-probe`
|
||||
|
||||
Internal probe that validates each seed producer's envelope shape matches its consumers. Returns violations if any consumer reads a field the producer no longer emits.
|
||||
|
||||
## User preferences
|
||||
|
||||
### `GET /api/user-prefs`
|
||||
### `POST /api/user-prefs`
|
||||
|
||||
Per-user dashboard preferences (layout, toggles, filters). Clerk bearer required. Backed by Convex.
|
||||
|
||||
```json
|
||||
{
|
||||
"layout": "classic",
|
||||
"enabledLayers": ["conflict", "aviation", "maritime"],
|
||||
"defaultCountry": "US"
|
||||
}
|
||||
```
|
||||
|
||||
## API key cache invalidation
|
||||
|
||||
### `POST /api/invalidate-user-api-key-cache`
|
||||
|
||||
Invalidates a user's entitlement cache after a subscription change (Dodo webhook → Convex → this endpoint). Internal — requires `RELAY_SHARED_SECRET`.
|
||||
|
||||
## Geo utilities
|
||||
|
||||
### `GET /api/geo?iso2=US`
|
||||
|
||||
Returns country metadata: centroid, bbox, capital, ISO codes.
|
||||
|
||||
### `GET /api/reverse-geocode?lat=40.7&lon=-74.0`
|
||||
|
||||
Reverse geocodes a lat/lon to the nearest country + city using the bundled coordinate dataset.
|
||||
|
||||
### `GET /api/data/city-coords?q=Tokyo`
|
||||
|
||||
City name → coordinates lookup.
|
||||
|
||||
## Utilities
|
||||
|
||||
### `GET /api/download?platform=<id>&variant=<id>`
|
||||
|
||||
Redirects to the matching asset on the latest GitHub release of `koala73/worldmonitor`. Returns `302` to the asset URL on success, or `302` to [releases/latest](https://github.com/koala73/worldmonitor/releases/latest) on any failure (unknown platform, no match, GitHub error).
|
||||
|
||||
**`platform`** (required, exact string):
|
||||
|
||||
| value | matches |
|
||||
|-------|---------|
|
||||
| `windows-exe` | `*_x64-setup.exe` |
|
||||
| `windows-msi` | `*_x64_en-US.msi` |
|
||||
| `macos-arm64` | `*_aarch64.dmg` |
|
||||
| `macos-x64` | `*_x64.dmg` (excluding `*setup*`) |
|
||||
| `linux-appimage` | `*_amd64.AppImage` |
|
||||
| `linux-appimage-arm64` | `*_aarch64.AppImage` |
|
||||
|
||||
**`variant`** (optional):
|
||||
|
||||
| value | filters asset name to |
|
||||
|-------|-----------------------|
|
||||
| `full` / `world` | `worldmonitor` |
|
||||
| `tech` | `techmonitor` |
|
||||
| `finance` | `financemonitor` |
|
||||
|
||||
Caches the 302 for 5 minutes (`s-maxage=300`, `stale-while-revalidate=60`, `stale-if-error=600`).
|
||||
|
||||
### `POST /api/contact`
|
||||
|
||||
Public contact form. Turnstile-verified, rate-limited per IP.
|
||||
|
||||
### `POST /api/register-interest`
|
||||
|
||||
Captures email for desktop-app early-access waitlist. Writes to Convex.
|
||||
65
docs/api-proxies.mdx
Normal file
65
docs/api-proxies.mdx
Normal file
@@ -0,0 +1,65 @@
|
||||
---
|
||||
title: "Proxies & Raw-Data Passthroughs"
|
||||
description: "Thin server-side proxies for upstream APIs — used by the dashboard to avoid CORS, hide API keys, and add rate-limiting/caching."
|
||||
---
|
||||
|
||||
These endpoints pass caller requests through to an upstream data source. They exist to:
|
||||
|
||||
1. Hide upstream API keys server-side
|
||||
2. Work around CORS
|
||||
3. Add caching + per-IP rate limiting
|
||||
4. Normalize response shapes
|
||||
|
||||
<Warning>
|
||||
Most proxies are intended for **our own dashboard** and are lightly gated. They are not part of the public API contract and may change or be removed without notice. Prefer the domain RPC services (`/api/<domain>/v1/*`) for stable integrations.
|
||||
</Warning>
|
||||
|
||||
## Raw-data passthroughs
|
||||
|
||||
| Endpoint | Upstream | Purpose |
|
||||
|----------|----------|---------|
|
||||
| `GET /api/eia/[...path]` | EIA v2 (energy.gov) | Catch-all proxy for US Energy Information Administration series. Path segments are appended to `https://api.eia.gov/v2/...`. |
|
||||
| `GET /api/opensky` | OpenSky Network | Live ADS-B state vectors. Used by the flights layer. |
|
||||
| `GET /api/polymarket` | Polymarket gamma-api | Active event contracts. |
|
||||
| `GET /api/satellites` | CelesTrak | LEO/GEO satellite orbital elements. |
|
||||
| `GET /api/military-flights` | ADS-B Exchange / adsb.lol | Identified military aircraft. |
|
||||
| `GET /api/ais-snapshot` | Internal AIS seed | Snapshot of latest vessel positions for the currently-loaded bbox. |
|
||||
| `GET /api/gpsjam` | gpsjam.org | GPS interference hotspot reports. |
|
||||
| `GET /api/sanctions-entity-search?q=...` | OFAC SDN | Fuzzy-match sanctions entity search. |
|
||||
| `GET /api/oref-alerts` | OREF (Israel Home Front Command) | Tzeva Adom rocket alert mirror. |
|
||||
| `GET /api/supply-chain/hormuz-tracker` | Internal AIS + registry | Real-time Hormuz transit dashboard data. |
|
||||
|
||||
All proxies:
|
||||
- Respect the origin CORS allowlist.
|
||||
- Apply per-IP rate limits via `_ip-rate-limit.js` (~ 60 req/min/IP default).
|
||||
- Cache aggressively (`s-maxage` varies by upstream).
|
||||
|
||||
## Content proxies
|
||||
|
||||
### `GET /api/rss-proxy?url=<allowed-feed>`
|
||||
|
||||
Fetches an RSS/Atom feed and returns the parsed JSON. The URL must match one of the patterns in `_rss-allowed-domains.js` — arbitrary URLs are refused to prevent SSRF.
|
||||
|
||||
### `GET /api/enrichment/company?domain=<host>`
|
||||
|
||||
Returns company metadata (name, logo, industry, HQ country) for a website domain. Composite of public sources.
|
||||
|
||||
### `GET /api/enrichment/signals?domain=<host>`
|
||||
|
||||
Returns trust and risk signals (TLS grade, DNS age, WHOIS country, threat-list membership) for a domain.
|
||||
|
||||
## Skills registry
|
||||
|
||||
### `GET /api/skills/fetch-agentskills`
|
||||
|
||||
Returns the catalog of "skills" (agent capabilities) the WorldMonitor chat analyst can invoke. Used internally by `chat-analyst.ts` and the widget agent.
|
||||
|
||||
## Legacy / internal
|
||||
|
||||
### `POST /api/fwdstart`
|
||||
|
||||
Forward-starting scenario helper used by the desktop app during first-run. Internal.
|
||||
|
||||
### `GET /api/mcp-proxy`
|
||||
|
||||
Legacy MCP shim — forwards to the current MCP route. Deprecated; use [`/api/mcp`](/mcp) directly.
|
||||
130
docs/api-scenarios.mdx
Normal file
130
docs/api-scenarios.mdx
Normal file
@@ -0,0 +1,130 @@
|
||||
---
|
||||
title: "Scenarios API"
|
||||
description: "Run pre-defined supply-chain disruption scenarios against any country and poll for worker-computed results."
|
||||
---
|
||||
|
||||
The **scenarios** API is a PRO-only, job-queued surface on top of the WorldMonitor chokepoint + trade dataset. Callers enqueue a named scenario template against an optional country, then poll a job-id until the worker completes.
|
||||
|
||||
<Info>
|
||||
This service is documented inline (not yet proto-backed). Proto migration is tracked in [issue #3207](https://github.com/koala73/worldmonitor/issues/3207) and will replace this page with auto-generated reference.
|
||||
</Info>
|
||||
|
||||
## List templates
|
||||
|
||||
### `GET /api/scenario/v1/templates`
|
||||
|
||||
Returns the catalog of pre-defined scenario templates. Cached `public, max-age=3600`.
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"templates": [
|
||||
{
|
||||
"id": "hormuz-closure-30d",
|
||||
"name": "Strait of Hormuz closure (30 days)",
|
||||
"affectedChokepointIds": ["hormuz"],
|
||||
"disruptionPct": 100,
|
||||
"durationDays": 30,
|
||||
"affectedHs2": ["27"],
|
||||
"costShockMultiplier": 1.8
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Run a scenario
|
||||
|
||||
### `POST /api/scenario/v1/run`
|
||||
|
||||
Enqueues a job. Returns `202 Accepted` with a `jobId` the caller must poll.
|
||||
|
||||
- **Auth**: `X-WorldMonitor-Key` (or trusted browser origin) + PRO
|
||||
- **Rate limits**:
|
||||
- 10 jobs / minute / user
|
||||
- Global queue capped at 100 in-flight jobs; excess rejected with `429` + `Retry-After: 30`
|
||||
|
||||
**Request**:
|
||||
```json
|
||||
{
|
||||
"scenarioId": "hormuz-closure-30d",
|
||||
"iso2": "SG"
|
||||
}
|
||||
```
|
||||
|
||||
- `scenarioId` — id from `/templates`. Required.
|
||||
- `iso2` — optional ISO-3166-1 alpha-2 (uppercase). Scopes the scenario to one country.
|
||||
|
||||
**Response (`202`)**:
|
||||
```json
|
||||
{
|
||||
"jobId": "scenario:1713456789012:a1b2c3d4",
|
||||
"status": "pending",
|
||||
"statusUrl": "/api/scenario/v1/status?jobId=scenario:1713456789012:a1b2c3d4"
|
||||
}
|
||||
```
|
||||
|
||||
**Errors**:
|
||||
|
||||
| Status | `error` | Cause |
|
||||
|--------|---------|-------|
|
||||
| 400 | `Invalid JSON body` | Body is not valid JSON |
|
||||
| 400 | `scenarioId is required` | Missing field |
|
||||
| 400 | `Unknown scenario: ...` | `scenarioId` not in the template catalog |
|
||||
| 400 | `iso2 must be a 2-letter uppercase country code` | Malformed `iso2` |
|
||||
| 403 | `PRO subscription required` | Not PRO |
|
||||
| 405 | — | Method other than `POST` |
|
||||
| 429 | `Rate limit exceeded: 10 scenario jobs per minute` | Per-user rate limit |
|
||||
| 429 | `Scenario queue is at capacity, please try again later` | Global queue > 100 |
|
||||
| 502 | `Failed to enqueue scenario job` | Redis enqueue failure |
|
||||
| 503 | `Service temporarily unavailable` | Missing env |
|
||||
|
||||
## Poll job status
|
||||
|
||||
### `GET /api/scenario/v1/status?jobId=<jobId>`
|
||||
|
||||
Returns the job result or `{ status: "pending" }` while the worker is still running.
|
||||
|
||||
- **Auth**: same as `/run`
|
||||
- **jobId format**: `scenario:{unix-ms}:{8-char-suffix}` — strictly validated to guard against path traversal
|
||||
|
||||
**Pending response (`200`)**:
|
||||
```json
|
||||
{
|
||||
"jobId": "scenario:1713456789012:a1b2c3d4",
|
||||
"status": "pending"
|
||||
}
|
||||
```
|
||||
|
||||
**Completed response (`200`)** — shape is scenario-specific; always includes:
|
||||
```json
|
||||
{
|
||||
"jobId": "scenario:1713456789012:a1b2c3d4",
|
||||
"scenarioId": "hormuz-closure-30d",
|
||||
"iso2": "SG",
|
||||
"status": "completed",
|
||||
"completedAt": 1713456890123,
|
||||
"result": {
|
||||
"costShockPct": 14.2,
|
||||
"affectedImportValueUsd": 8400000000,
|
||||
"topExposedSectors": ["refined_petroleum", "chemicals"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Errors**:
|
||||
|
||||
| Status | `error` | Cause |
|
||||
|--------|---------|-------|
|
||||
| 400 | `Invalid or missing jobId` | Missing or malformed `jobId` |
|
||||
| 403 | `PRO subscription required` | Not PRO |
|
||||
| 405 | — | Method other than `GET` |
|
||||
| 500 | `Corrupted job result` | Worker wrote invalid JSON |
|
||||
| 502 | `Failed to fetch job status` | Redis read failure |
|
||||
| 503 | `Service temporarily unavailable` | Missing env |
|
||||
|
||||
## Polling strategy
|
||||
|
||||
- First poll: ~1s after enqueue.
|
||||
- Subsequent polls: exponential backoff (1s → 2s → 4s, cap 10s).
|
||||
- Workers typically complete in 5-30 seconds depending on scenario complexity.
|
||||
- If still pending after 2 minutes, the job is probably dead — re-enqueue.
|
||||
170
docs/api-shipping-v2.mdx
Normal file
170
docs/api-shipping-v2.mdx
Normal file
@@ -0,0 +1,170 @@
|
||||
---
|
||||
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", "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", "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 the record and the owner-index set (re-register or rotate to extend).
|
||||
- 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", "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",
|
||||
"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`.
|
||||
@@ -63,6 +63,15 @@
|
||||
"architecture"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Usage",
|
||||
"pages": [
|
||||
"usage-quickstart",
|
||||
"usage-auth",
|
||||
"usage-rate-limits",
|
||||
"usage-errors"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Platform & Features",
|
||||
"pages": [
|
||||
@@ -109,6 +118,25 @@
|
||||
"desktop-app"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "MCP & Integrations",
|
||||
"pages": [
|
||||
"mcp",
|
||||
"api-oauth",
|
||||
"api-notifications"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "HTTP API",
|
||||
"pages": [
|
||||
"api-platform",
|
||||
"api-brief",
|
||||
"api-commerce",
|
||||
"api-scenarios",
|
||||
"api-shipping-v2",
|
||||
"api-proxies"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Developer Guide",
|
||||
"pages": [
|
||||
@@ -160,6 +188,10 @@
|
||||
{
|
||||
"group": "Cyber",
|
||||
"openapi": "api/CyberService.openapi.yaml"
|
||||
},
|
||||
{
|
||||
"group": "Sanctions",
|
||||
"openapi": "api/SanctionsService.openapi.yaml"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -181,6 +213,14 @@
|
||||
{
|
||||
"group": "Wildfires",
|
||||
"openapi": "api/WildfireService.openapi.yaml"
|
||||
},
|
||||
{
|
||||
"group": "Radiation",
|
||||
"openapi": "api/RadiationService.openapi.yaml"
|
||||
},
|
||||
{
|
||||
"group": "Thermal",
|
||||
"openapi": "api/ThermalService.openapi.yaml"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -203,9 +243,17 @@
|
||||
"group": "Supply Chain",
|
||||
"openapi": "api/SupplyChainService.openapi.yaml"
|
||||
},
|
||||
{
|
||||
"group": "Consumer Prices",
|
||||
"openapi": "api/ConsumerPricesService.openapi.yaml"
|
||||
},
|
||||
{
|
||||
"group": "Predictions",
|
||||
"openapi": "api/PredictionService.openapi.yaml"
|
||||
},
|
||||
{
|
||||
"group": "Forecasts",
|
||||
"openapi": "api/ForecastService.openapi.yaml"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -223,6 +271,27 @@
|
||||
{
|
||||
"group": "Infrastructure",
|
||||
"openapi": "api/InfrastructureService.openapi.yaml"
|
||||
},
|
||||
{
|
||||
"group": "Resilience",
|
||||
"openapi": "api/ResilienceService.openapi.yaml"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Health & Environment",
|
||||
"pages": [
|
||||
{
|
||||
"group": "Public Health",
|
||||
"openapi": "api/HealthService.openapi.yaml"
|
||||
},
|
||||
{
|
||||
"group": "Imagery",
|
||||
"openapi": "api/ImageryService.openapi.yaml"
|
||||
},
|
||||
{
|
||||
"group": "Webcams",
|
||||
"openapi": "api/WebcamService.openapi.yaml"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
244
docs/mcp.mdx
Normal file
244
docs/mcp.mdx
Normal file
@@ -0,0 +1,244 @@
|
||||
---
|
||||
title: "MCP Server"
|
||||
description: "Connect Claude, Cursor, and other MCP-compatible clients to WorldMonitor's live global-intelligence data via the Model Context Protocol."
|
||||
---
|
||||
|
||||
WorldMonitor exposes its intelligence stack as a [Model Context Protocol](https://modelcontextprotocol.io) server so any MCP-compatible client (Claude Desktop, Claude web, Cursor, MCP Inspector, custom agents) can pull live conflict, market, aviation, maritime, economic, and forecasting data directly into a model's context.
|
||||
|
||||
<Info>
|
||||
The MCP server is gated behind **PRO**. Free-tier users see a 401 at the OAuth step.
|
||||
</Info>
|
||||
|
||||
## Endpoints
|
||||
|
||||
| Endpoint | Purpose |
|
||||
|----------|---------|
|
||||
| `https://api.worldmonitor.app/api/mcp` | JSON-RPC server (Streamable HTTP transport, protocol `2025-03-26`) |
|
||||
| `https://api.worldmonitor.app/api/oauth/register` | Dynamic Client Registration (RFC 7591) |
|
||||
| `https://api.worldmonitor.app/api/oauth/authorize` | OAuth 2.1 authorization endpoint (PKCE required) |
|
||||
| `https://api.worldmonitor.app/api/oauth/token` | Token endpoint (authorization_code + refresh_token) |
|
||||
| `https://api.worldmonitor.app/.well-known/oauth-authorization-server` | AS metadata (RFC 8414) |
|
||||
| `https://api.worldmonitor.app/.well-known/oauth-protected-resource` | Resource server metadata (RFC 9728) |
|
||||
|
||||
Protocol version: `2025-03-26`. Server identifier: `worldmonitor` v1.0.
|
||||
|
||||
## Authentication
|
||||
|
||||
The MCP handler accepts two auth modes, in priority order:
|
||||
|
||||
1. **OAuth 2.1 bearer** — `Authorization: Bearer <token>` where `<token>` was issued by `/api/oauth/token`. This is what Claude Desktop, claude.ai, Cursor, and MCP Inspector use automatically. Required for any client that hits MCP from a browser origin.
|
||||
2. **Direct API key** — `X-WorldMonitor-Key: wm_live_...`. Intended for server-side scripts, `curl`, and custom integrations. Do **not** send a `wm_live_...` key as a `Bearer` token — it will fail OAuth resolution and return `401 invalid_token`.
|
||||
|
||||
Both modes check the same PRO entitlement before every tool call, so a subscription downgrade revokes access on the next request.
|
||||
|
||||
### Redirect URI allowlist
|
||||
|
||||
Dynamic Client Registration is **not** open to arbitrary HTTPS redirects. Only these prefixes are accepted:
|
||||
|
||||
- `https://claude.ai/api/mcp/auth_callback`
|
||||
- `https://claude.com/api/mcp/auth_callback`
|
||||
- `http://localhost:<port>` / `http://127.0.0.1:<port>` (any port) — for Claude Code, MCP Inspector, local development
|
||||
|
||||
Other clients must proxy via one of these redirects or run locally.
|
||||
|
||||
### Token lifetimes
|
||||
|
||||
| Artifact | TTL |
|
||||
|----------|-----|
|
||||
| Authorization code | 10 min |
|
||||
| Access token | 1 hour |
|
||||
| Refresh token | 7 days |
|
||||
| Registered client record | 90 days (sliding) |
|
||||
|
||||
## Rate limits
|
||||
|
||||
- **MCP calls**: 60 requests / minute / API key (sliding window)
|
||||
- **OAuth authorize**: 10 requests / minute / IP
|
||||
- **OAuth token**: 10 requests / minute / IP
|
||||
- **Dynamic registration**: 5 registrations / minute / IP
|
||||
|
||||
Exceeding returns `429` with a `Retry-After` header.
|
||||
|
||||
## Client setup
|
||||
|
||||
### Claude Desktop
|
||||
|
||||
`~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) — use the remote MCP entry:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"worldmonitor": {
|
||||
"url": "https://api.worldmonitor.app/api/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Claude Desktop handles the OAuth flow automatically on first connection.
|
||||
|
||||
### Claude web (claude.ai)
|
||||
|
||||
Add via **Settings → Connectors → Add custom connector**:
|
||||
|
||||
- Name: `WorldMonitor`
|
||||
- URL: `https://api.worldmonitor.app/api/mcp`
|
||||
|
||||
### Cursor
|
||||
|
||||
`~/.cursor/mcp.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"worldmonitor": {
|
||||
"url": "https://api.worldmonitor.app/api/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### MCP Inspector (debugging)
|
||||
|
||||
```bash
|
||||
npx @modelcontextprotocol/inspector https://api.worldmonitor.app/api/mcp
|
||||
```
|
||||
|
||||
## Tool catalog
|
||||
|
||||
The server exposes 32 tools. Most are cache-reads over pre-seeded Redis keys (sub-second). Six (`get_world_brief`, `get_country_brief`, `analyze_situation`, `generate_forecasts`, `search_flights`, `search_flight_prices_by_date`) call live LLMs or external APIs and are slower.
|
||||
|
||||
### Markets & economy
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `get_market_data` | Equity quotes, commodity prices (incl. gold GC=F), crypto, FX, sector performance, ETF flows, Gulf markets. |
|
||||
| `get_economic_data` | Fed Funds, economic calendar, fuel prices, ECB FX, EU yield curve, earnings, COT, energy storage, BIS DSR + property prices. |
|
||||
| `get_country_macro` | IMF WEO bundle per country: inflation, current account, GDP/capita, unemployment, savings-investment gap (~210 countries). |
|
||||
| `get_eu_housing_cycle` | Eurostat annual house price index (prc_hpi_a), 10-yr sparkline per EU member + EA20/EU27. |
|
||||
| `get_eu_quarterly_gov_debt` | Eurostat quarterly gross government debt (%GDP), 8-quarter sparkline. |
|
||||
| `get_eu_industrial_production` | Eurostat monthly industrial production index, 12-month sparkline. |
|
||||
| `get_prediction_markets` | Active Polymarket event contracts with live probabilities. |
|
||||
| `get_supply_chain_data` | Dry bulk shipping stress index, customs flows, COMTRADE bilateral trade. |
|
||||
| `get_commodity_geo` | 71 major mining sites worldwide (gold, silver, copper, lithium, uranium, coal). |
|
||||
|
||||
### Geopolitical & security
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `get_conflict_events` | Active UCDP/Iran conflicts, unrest w/ geo-coords, country risk scores. |
|
||||
| `get_country_risk` | CII score 0-100, component breakdown, travel advisory, OFAC exposure per country. Fast, no LLM. |
|
||||
| `get_military_posture` | Theater posture + military risk scores. |
|
||||
| `get_cyber_threats` | URLhaus/Feodotracker malware IOCs, CISA KEV catalog, active C2 infra. |
|
||||
| `get_sanctions_data` | OFAC SDN entities + sanctions pressure scores by country. |
|
||||
| `get_news_intelligence` | AI-classified threat news, GDELT signals, cross-source intel. |
|
||||
| `get_positive_events` | Diplomatic agreements, humanitarian aid, peace initiatives. |
|
||||
| `get_social_velocity` | Reddit r/worldnews + r/geopolitics top posts, engagement scores. |
|
||||
|
||||
### Movement & infrastructure
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `get_airspace` | Live ADS-B over a country. Params: `iso2` (2-letter code), `filter` (`all`/`civilian`/`military`). |
|
||||
| `get_maritime_activity` | AIS density zones, dark-ship events, chokepoint congestion per country. Params: `iso2`. |
|
||||
| `get_aviation_status` | FAA airport delays, NOTAM closures, tracked military aircraft. |
|
||||
| `get_infrastructure_status` | Cloudflare Radar outages, major cloud/internet service status. |
|
||||
| `search_flights` | Google Flights real-time search between IATA airport codes on a specific date. |
|
||||
| `search_flight_prices_by_date` | Date-grid cheapest-day pricing across a range. |
|
||||
|
||||
### Environment & science
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `get_natural_disasters` | USGS earthquakes, NASA FIRMS wildfires, hazard events. |
|
||||
| `get_climate_data` | Temp/precip anomalies vs WMO normals, GDACS/FIRMS alerts, Mauna Loa CO2, OpenAQ PM2.5, sea ice, ocean heat. |
|
||||
| `get_radiation_data` | Global radiation monitoring station readings + anomaly flags. |
|
||||
| `get_research_signals` | Emerging technology events from curated research feeds. |
|
||||
|
||||
### AI intelligence (live LLM)
|
||||
|
||||
| Tool | Description | Cost |
|
||||
|------|-------------|------|
|
||||
| `get_world_brief` | AI-summarized world intel brief. Optional `geo_context` param. | LLM |
|
||||
| `get_country_brief` | Per-country geopolitical + economic assessment. Supports analytical frameworks. | LLM |
|
||||
| `analyze_situation` | Ad-hoc geopolitical deduction from a query + context. Returns confidence + supporting signals. | LLM |
|
||||
| `generate_forecasts` | Fresh probability estimates (bypasses cache). | LLM |
|
||||
| `get_forecast_predictions` | Pre-computed cached forecasts. Fast. | Cache |
|
||||
|
||||
## JSON-RPC example
|
||||
|
||||
Server-side with a direct API key — send it as `X-WorldMonitor-Key`, **not** as a bearer token.
|
||||
|
||||
```bash
|
||||
WM_KEY="wm_live_..."
|
||||
|
||||
# 1. List tools
|
||||
curl -s https://api.worldmonitor.app/api/mcp \
|
||||
-H "X-WorldMonitor-Key: $WM_KEY" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
|
||||
|
||||
# 2. Call a cache tool
|
||||
curl -s https://api.worldmonitor.app/api/mcp \
|
||||
-H "X-WorldMonitor-Key: $WM_KEY" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"jsonrpc":"2.0","id":2,
|
||||
"method":"tools/call",
|
||||
"params":{"name":"get_country_risk","arguments":{"iso2":"IR"}}
|
||||
}'
|
||||
```
|
||||
|
||||
If instead you've completed the OAuth flow and hold an access token from `/api/oauth/token`, pass it as `Authorization: Bearer $TOKEN`.
|
||||
|
||||
## Response shape
|
||||
|
||||
Tool responses use the standard MCP content block format:
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"result": {
|
||||
"content": [
|
||||
{ "type": "text", "text": "{...json payload...}" }
|
||||
],
|
||||
"isError": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For cache tools, the JSON payload includes `_meta` with `fetchedAt` and `staleness` so the model can reason about freshness.
|
||||
|
||||
## Data freshness
|
||||
|
||||
All cache tools read from Redis keys written by Railway cron seeders. Typical freshness:
|
||||
|
||||
| Domain | Typical freshness |
|
||||
|--------|-------------------|
|
||||
| Markets (intraday) | 1–5 min |
|
||||
| Flights (ADS-B) | 1–3 min |
|
||||
| Maritime (AIS) | 5–15 min |
|
||||
| Conflicts / unrest | 15–60 min |
|
||||
| Macro / BIS / Eurostat | daily–weekly |
|
||||
| IMF WEO | monthly |
|
||||
|
||||
Seed-level health per key: [status.worldmonitor.app](https://status.worldmonitor.app/).
|
||||
|
||||
## Errors
|
||||
|
||||
| Code | Meaning |
|
||||
|------|---------|
|
||||
| 401 | Invalid / missing token |
|
||||
| 403 | Token valid but account not PRO |
|
||||
| 404 | Unknown tool name |
|
||||
| 429 | Rate limited (60 req/min/key) |
|
||||
| 503 | Upstream cache unavailable |
|
||||
|
||||
Tool-level errors return `isError: true` with a text explanation in `content`.
|
||||
|
||||
## Related
|
||||
|
||||
- [Authentication overview](/authentication) — browser vs bearer vs OAuth
|
||||
- [API Reference](/api/ConflictService.openapi.yaml) — the same data via REST
|
||||
- [Pro features](https://www.worldmonitor.app/pro)
|
||||
88
docs/usage-auth.mdx
Normal file
88
docs/usage-auth.mdx
Normal file
@@ -0,0 +1,88 @@
|
||||
---
|
||||
title: "Authentication"
|
||||
description: "Three auth modes — browser origin, API key, and OAuth bearer — plus how server-side enforcement works."
|
||||
---
|
||||
|
||||
WorldMonitor has **three** authentication modes. Which one applies depends on how you're calling.
|
||||
|
||||
## Auth matrix
|
||||
|
||||
| Mode | Header | Used by | Trusted on which endpoints? |
|
||||
|------|--------|---------|------------------------------|
|
||||
| **Browser origin** | `Origin: https://www.worldmonitor.app` (browser-set) | Dashboard, desktop app | Most public endpoints — but **not** `forceKey: true` routes. |
|
||||
| **API key** | `X-WorldMonitor-Key: wm_live_...` | Server-to-server, scripts, SDKs | All endpoints, including `forceKey: true`. |
|
||||
| **OAuth bearer** | `Authorization: Bearer <oauth-token>` | MCP clients (Claude, Cursor, Inspector) | `/api/mcp`. The handler also accepts a direct `X-WorldMonitor-Key` in lieu of an OAuth token — see [MCP](/mcp#authentication). |
|
||||
| **Clerk session JWT** | `Authorization: Bearer <clerk-jwt>` | Authenticated browser users | User-specific routes: `/api/latest-brief`, `/api/user-prefs`, `/api/notification-channels`, `/api/brief/share-url`, etc. |
|
||||
|
||||
## `forceKey: true` — which endpoints ignore browser origin?
|
||||
|
||||
Some endpoints explicitly reject the "trusted browser origin" shortcut and require a real API key even from inside the dashboard:
|
||||
|
||||
- `/api/v2/shipping/route-intelligence`
|
||||
- `/api/v2/shipping/webhooks`
|
||||
- `/api/widget-agent`
|
||||
- Vendor / partner endpoints
|
||||
|
||||
For these, you **must** send `X-WorldMonitor-Key`.
|
||||
|
||||
## Browser origin mode
|
||||
|
||||
CORS and `validateApiKey` together decide whether a given `Origin` is trusted. The allowlist is centralized in `api/_cors.js`.
|
||||
|
||||
- Allowed origins get `Access-Control-Allow-Origin: <echoed>` and pass the key check.
|
||||
- Disallowed origins get no CORS header (browser rejects) and fail the key check.
|
||||
|
||||
See [CORS](/cors) for the origin patterns.
|
||||
|
||||
<Warning>
|
||||
**A Cloudflare Worker** (`api-cors-preflight`) is the authoritative CORS handler for `api.worldmonitor.app` — it overrides `_cors.js` and `vercel.json`. If you're changing origin rules, change them in the Cloudflare dashboard.
|
||||
</Warning>
|
||||
|
||||
## API key mode
|
||||
|
||||
### Generate a key
|
||||
|
||||
PRO subscribers get a key automatically on subscription. To rotate, contact support.
|
||||
|
||||
### Use it
|
||||
|
||||
```
|
||||
X-WorldMonitor-Key: wm_live_abcdef0123456789...
|
||||
```
|
||||
|
||||
Minimum 16 characters. Keep keys out of client-side code — use a server-side proxy if you need to call from the browser to a `forceKey` endpoint.
|
||||
|
||||
### Server-side validation
|
||||
|
||||
The edge function calls `validateApiKey(req, { forceKey?: boolean })`:
|
||||
|
||||
1. If `forceKey` is false AND the origin is trusted → pass.
|
||||
2. Else, check `X-WorldMonitor-Key` against `WORLDMONITOR_VALID_KEYS` (env).
|
||||
3. Also check the caller's entitlement cache (`invalidate-user-api-key-cache` flushes this).
|
||||
4. If neither passes → 401.
|
||||
|
||||
## OAuth bearer (MCP only)
|
||||
|
||||
Full flow documented at [OAuth 2.1 Server](/api-oauth). For client setup, see [MCP](/mcp).
|
||||
|
||||
## Clerk session (authenticated dashboard)
|
||||
|
||||
The dashboard exchanges Clerk's `__session` cookie for a JWT and sends it on user-specific API calls:
|
||||
|
||||
```
|
||||
Authorization: Bearer eyJhbGc...
|
||||
```
|
||||
|
||||
Server-side verification uses `jose` with a cached JWKS — no round-trip to Clerk per request. Implemented in `server/auth-session.ts`. See [Authentication overview](/authentication) for full details.
|
||||
|
||||
## Entitlement / tier gating
|
||||
|
||||
Valid key ≠ access. Every PRO-gated endpoint also checks the caller's entitlement via `isCallerPremium(req)` before returning data.
|
||||
|
||||
| Tier | Access |
|
||||
|------|--------|
|
||||
| Anonymous | Public reads only (conflicts, natural disasters, markets basics) |
|
||||
| Signed-in free | Same as anonymous + user preferences |
|
||||
| PRO | All endpoints, MCP, AI Brief, Shipping v2, Scenarios |
|
||||
|
||||
Tier is resolved from Convex on each call, so a subscription change takes effect on the next request (after cache invalidation).
|
||||
72
docs/usage-errors.mdx
Normal file
72
docs/usage-errors.mdx
Normal file
@@ -0,0 +1,72 @@
|
||||
---
|
||||
title: "Errors"
|
||||
description: "Standard error shape, common status codes, and retry strategies."
|
||||
---
|
||||
|
||||
## Error shape
|
||||
|
||||
All error responses are JSON with `Content-Type: application/json`:
|
||||
|
||||
```json
|
||||
{ "error": "brief_not_found" }
|
||||
```
|
||||
|
||||
Some endpoints include additional fields:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "invalid_request",
|
||||
"error_description": "iso2 must be a 2-letter uppercase country code",
|
||||
"field": "iso2"
|
||||
}
|
||||
```
|
||||
|
||||
OAuth endpoints follow [RFC 6749 §5.2](https://datatracker.ietf.org/doc/html/rfc6749#section-5.2) error codes: `invalid_request`, `invalid_client`, `invalid_grant`, `unsupported_grant_type`, `invalid_scope`.
|
||||
|
||||
## Status codes
|
||||
|
||||
| Code | Meaning | Retry? |
|
||||
|------|---------|--------|
|
||||
| `200` | OK | — |
|
||||
| `202` | Accepted — job enqueued (e.g. `scenario/v1/run`). Poll `statusUrl`. | — |
|
||||
| `304` | Not Modified (conditional cache hit) | — |
|
||||
| `400` | Bad request — validation error | No — fix input |
|
||||
| `401` | Missing / invalid auth | No — fix auth |
|
||||
| `403` | Authenticated but not entitled (usually `pro_required`) | No — upgrade |
|
||||
| `404` | Resource / tool / brief / entity not found | No |
|
||||
| `405` | Method not allowed | No |
|
||||
| `409` | Conflict (duplicate webhook registration, etc.) | No |
|
||||
| `413` | Payload too large | No |
|
||||
| `429` | Rate limited | Yes — honor `Retry-After` |
|
||||
| `500` | Server bug | Yes — with backoff, then report |
|
||||
| `502` | Upstream / Convex / Dodo failure | Yes — exponential backoff |
|
||||
| `503` | Service unavailable — missing env or dependency down | Yes — exponential backoff |
|
||||
| `504` | Upstream timeout | Yes — with backoff |
|
||||
|
||||
## Common error strings
|
||||
|
||||
| `error` value | Meaning |
|
||||
|---------------|---------|
|
||||
| `UNAUTHENTICATED` | No valid Clerk JWT or API key. |
|
||||
| `pro_required` | Authenticated, but account is not PRO. |
|
||||
| `invalid_payload` | Body failed schema validation. |
|
||||
| `invalid_date_shape` | Date param not `YYYY-MM-DD`. |
|
||||
| `brief_not_found` | No composed brief for the requested `{userId, issueDate}`. |
|
||||
| `Rate limit exceeded` | 429 fired; honor `Retry-After`. |
|
||||
| `Service temporarily unavailable` | Upstash or another hard dependency missing at request time. |
|
||||
| `service_unavailable` | Signing secret / required env not configured. |
|
||||
| `Failed to enqueue scenario job` | Redis pipeline failure on `/api/scenario/v1/run`. |
|
||||
|
||||
## Retry strategy
|
||||
|
||||
**Idempotent reads** (`GET`): retry 429/5xx with exponential backoff (1s, 2s, 4s, cap 30s, 5 attempts). Most GET responses are cached at the edge, so the retry usually goes faster.
|
||||
|
||||
**Writes**: never auto-retry 4xx. For 5xx on writes, inspect: `POST /api/brief/share-url` and `POST /api/v2/shipping/webhooks` are idempotent; `POST /api/scenario/v1/run` is **not** — it enqueues a new job on each call. Retrying a 5xx on `run` may double-charge the rate-limit counter.
|
||||
|
||||
**MCP**: the server returns tool errors in the JSON-RPC result with `isError: true` and a text explanation — those are not HTTP errors. Handle them at the tool-call layer.
|
||||
|
||||
## Debugging
|
||||
|
||||
- Every edge response includes `x-vercel-id` and `x-worldmonitor-deploy` headers — include those when reporting issues.
|
||||
- Sentry alerts forward to [status.worldmonitor.app](https://status.worldmonitor.app/).
|
||||
- `GET /api/health` and `GET /api/seed-health` show per-seed freshness; a stale seed is the most common root cause of unexpected empty payloads.
|
||||
74
docs/usage-quickstart.mdx
Normal file
74
docs/usage-quickstart.mdx
Normal file
@@ -0,0 +1,74 @@
|
||||
---
|
||||
title: "Quickstart"
|
||||
description: "Make your first WorldMonitor API call in under a minute — curl, JavaScript, and Python."
|
||||
---
|
||||
|
||||
## 1. Get an API key
|
||||
|
||||
PRO subscribers get an API key from [worldmonitor.app/pro](https://www.worldmonitor.app/pro). Keys look like `wm_live_...` and are at least 16 characters.
|
||||
|
||||
<Tip>
|
||||
Browser origins on `worldmonitor.app` / `localhost` are trusted without a key. You only need a key for server-to-server use or from untrusted origins.
|
||||
</Tip>
|
||||
|
||||
## 2. Call an endpoint
|
||||
|
||||
### curl
|
||||
|
||||
```bash
|
||||
curl -s 'https://api.worldmonitor.app/api/conflict/v1/list-conflict-events?limit=5' \
|
||||
-H 'X-WorldMonitor-Key: wm_live_...'
|
||||
```
|
||||
|
||||
### JavaScript (Node or browser)
|
||||
|
||||
```js
|
||||
const res = await fetch(
|
||||
'https://api.worldmonitor.app/api/market/v1/get-quotes?symbols=AAPL,TSLA',
|
||||
{ headers: { 'X-WorldMonitor-Key': process.env.WM_KEY } }
|
||||
);
|
||||
const data = await res.json();
|
||||
console.log(data);
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import os, httpx
|
||||
|
||||
r = httpx.get(
|
||||
"https://api.worldmonitor.app/api/economic/v1/get-fred-series",
|
||||
params={"series_id": "FEDFUNDS"},
|
||||
headers={"X-WorldMonitor-Key": os.environ["WM_KEY"]},
|
||||
)
|
||||
r.raise_for_status()
|
||||
print(r.json())
|
||||
```
|
||||
|
||||
## 3. Hydrate everything at once
|
||||
|
||||
For dashboards, use `/api/bootstrap` to pull all seeded caches in one request:
|
||||
|
||||
```bash
|
||||
curl -s 'https://api.worldmonitor.app/api/bootstrap' \
|
||||
-H 'X-WorldMonitor-Key: wm_live_...'
|
||||
```
|
||||
|
||||
See [Platform endpoints](/api-platform#bootstrap) for the response shape.
|
||||
|
||||
## 4. Plug into an MCP client
|
||||
|
||||
Point any MCP-compatible client at the server URL — the client handles OAuth automatically:
|
||||
|
||||
```
|
||||
https://api.worldmonitor.app/api/mcp
|
||||
```
|
||||
|
||||
See [MCP](/mcp) for client-specific setup.
|
||||
|
||||
## Next
|
||||
|
||||
- [Authentication](/usage-auth) — all three auth modes
|
||||
- [Rate limits](/usage-rate-limits) — per-endpoint caps
|
||||
- [Errors](/usage-errors) — response shapes and retry guidance
|
||||
- [API Reference](/api/ConflictService.openapi.yaml) — every RPC with request/response schemas
|
||||
78
docs/usage-rate-limits.mdx
Normal file
78
docs/usage-rate-limits.mdx
Normal file
@@ -0,0 +1,78 @@
|
||||
---
|
||||
title: "Rate Limits"
|
||||
description: "Per-endpoint, per-key, and per-IP rate limits across the WorldMonitor API surface."
|
||||
---
|
||||
|
||||
Rate limits are enforced at the Vercel Edge runtime using Upstash Redis sliding-window counters. All limits are **sliding 60-second windows** unless noted.
|
||||
|
||||
## Default public API rate limit
|
||||
|
||||
| Scope | Limit | Window |
|
||||
|-------|-------|--------|
|
||||
| Per IP (default) | **600 requests** | 60 s |
|
||||
|
||||
Applies to all `/api/*` routes that don't have a stricter override. Implemented by `api/_rate-limit.js` / `api/_ip-rate-limit.js`.
|
||||
|
||||
## MCP server
|
||||
|
||||
| Scope | Limit | Window |
|
||||
|-------|-------|--------|
|
||||
| Per API key (MCP tools) | **60 requests** | 60 s |
|
||||
|
||||
See [MCP](/mcp) for details.
|
||||
|
||||
## OAuth endpoints
|
||||
|
||||
| Endpoint | Limit | Window | Scope |
|
||||
|----------|-------|--------|-------|
|
||||
| `POST /api/oauth/register` | 5 | 60 s | Per IP |
|
||||
| `GET /api/oauth/authorize` | 10 | 60 s | Per IP |
|
||||
| `POST /api/oauth/token` | 10 | 60 s | Per IP |
|
||||
|
||||
Matches the implementations in `api/oauth/{register,authorize,token}.js`.
|
||||
|
||||
Exceeding any of these during the OAuth flow will cause the MCP client to fail the connection handshake — wait 60 s and retry.
|
||||
|
||||
## Write endpoints
|
||||
|
||||
| Endpoint | Limit | Window | Scope |
|
||||
|----------|-------|--------|-------|
|
||||
| `POST /api/scenario/v1/run` | 10 | 60 s | Per user |
|
||||
| `POST /api/scenario/v1/run` (queue depth) | 100 in-flight | — | Global |
|
||||
| `POST /api/register-interest` | 5 | 60 min | Per IP + Turnstile |
|
||||
| `POST /api/contact` | 3 | 60 min | Per IP + Turnstile |
|
||||
|
||||
Other write endpoints (`/api/brief/share-url`, `/api/notification-channels`, `/api/create-checkout`, `/api/customer-portal`, etc.) fall back to the default per-IP limit above.
|
||||
|
||||
## Bootstrap / health
|
||||
|
||||
These are cached aggressively and have no custom limit beyond the default:
|
||||
|
||||
- `GET /api/bootstrap` — `s-maxage=30`
|
||||
- `GET /api/health` — `s-maxage=15`
|
||||
- `GET /api/version` — `s-maxage=60`
|
||||
|
||||
## Response when limited
|
||||
|
||||
HTTP 429 with:
|
||||
|
||||
```
|
||||
Retry-After: <seconds>
|
||||
Content-Type: application/json
|
||||
|
||||
{ "error": "Rate limit exceeded" }
|
||||
```
|
||||
|
||||
## Retry guidance
|
||||
|
||||
- Respect `Retry-After`. Don't pound on a 429.
|
||||
- For batch work, pace yourself: at 600 req/min/IP the default gives you ~10 req/s headroom.
|
||||
- For MCP, 60/min is generous for conversational use but tight for scripted batch fetches — prefer the REST API for batch.
|
||||
- Spurious 429s often mean you're sharing an egress IP (corporate proxy, CI runner). Contact support for a per-key limit bump if needed.
|
||||
|
||||
## Hard caps (not soft limits)
|
||||
|
||||
- Webhook callback URLs must be HTTPS (except localhost).
|
||||
- `api/download` file sizes capped at ~50 MB per request.
|
||||
- `POST /api/scenario/v1/run` globally pauses new jobs when the pending queue exceeds **100** — returns 429 with `Retry-After: 30`.
|
||||
- `api/v2/shipping/webhooks` TTL is **30 days** — re-register to extend.
|
||||
Reference in New Issue
Block a user