File size: 10,291 Bytes
d705bb5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
---
title: "Platform Endpoints"
description: "Bootstrap, health, version, cache-purge, and user-preference endpoints β€” the plumbing every World Monitor client talks to on startup and cleanup."
---

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 `wm-session` cookie, `X-WorldMonitor-Key`, or the `X-Api-Key` alias. User-issued keys are validated for current API access.
- **Anonymous weather**: `?keys=weatherAlerts` is public **only when no API key header is sent**. If you attach `X-WorldMonitor-Key` / `X-Api-Key`, the request is fully validated even for weather β€” a malformed key returns `401`, a key without current API access returns `403`. Callers that always send a key must send a valid, entitled key (or omit the header to use the anonymous weather path). This URL is `no-store`, which is what makes that contract hold at the edge as well as the origin: nothing caches it, so an invalid key can never be answered by a warm anonymous entry.
- **Public weather**: `?keys=weatherAlerts&public=1` is the CDN-cached weather read. Like `?tier=fast&public=1`, it returns the same shared seed payload to every caller and ignores any credentials you attach β€” a CDN hit precedes auth, so the marker is what lets the response be cached safely. Prefer it for high-volume anonymous reads; use the bare URL when you need your key validated.
- **Server-to-server**: call `https://api.worldmonitor.app/api/bootstrap` directly with `X-WorldMonitor-Key: wm_...`. There is no separate gateway host, token exchange, activation step, or IP allow-list requirement for this endpoint.
- **Cache**: only the explicitly-marked `?...&public=1` URLs are shared-cacheable, because only they answer every caller identically. `?tier=fast&public=1` / `?tier=slow&public=1` use browser `max-age=60` / `max-age=300` and CDN `s-maxage=600` / `s-maxage=7200`. Single-key public URLs: on-demand keys (`?keys=<onDemandName>&public=1`) inherit the slow profile β€” browser `max-age=300`, CDN `s-maxage=7200` β€” unless the key declares its own, which today only `chinaDecisionSignals` does (browser `max-age=60`, CDN `s-maxage=900`); `?keys=weatherAlerts&public=1` uses `Cache-Control: public, s-maxage=600, stale-while-revalidate=120, stale-if-error=900` with the fast-tier CDN shield. Everything else β€” key-authenticated, session-authenticated, the unmarked `?tier=...` URLs, and the anonymous `?keys=weatherAlerts` path β€” uses `Cache-Control: no-store` and emits no CDN cache headers. That split is deliberate: a CDN hit precedes auth, so a URL whose answer depends on credentials must never be cacheable.
- **Rate limit**: user API key validation on this endpoint has a fail-closed fixed 60 s per-IP pre-validation limit of 600 attempts, separate from the default API sliding-window limiter.
- **Shape**: `{ "data": { "earthquakes": ..., "outages": ..., "marketQuotes": ... }, "missing": [] }` β€” ~40+ unwrapped seeded-domain payloads nested under `data`, plus a `missing` list for cache keys not present in Redis.

Use this on initial page load to avoid 40 parallel RPC calls.

The on-demand tier includes `chinaDecisionSignals`, the bounded six-domain China
country-summary contract. Its anonymous country/RPC representation, Pro MCP
representation, and operator health registration share stable group IDs and
provenance; see [China Decision Signals](/china-decision-signals).

## 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`, `DEGRADED`, `UNHEALTHY`, or `REDIS_DOWN` in the JSON `status` field.

All states except `REDIS_DOWN` return HTTP 200; `REDIS_DOWN` returns HTTP 503 because Redis was unreachable and the endpoint could not assess seed state. Responses are not cached (`private, no-store, max-age=0` plus `CDN-Cache-Control: no-store`).

Monitor via UptimeRobot / Better Stack with `?compact=1` β€” alert on any status other than `HEALTHY`. The full detailed view requires an operator/enterprise API key because it includes canonical cache key names and freshness thresholds.

```json
{
  "status": "HEALTHY",
  "checkedAt": "2026-04-19T12:00:00Z",
  "summary": {
    "total": 194,
    "ok": 180,
    "warn": 5,
    "onDemandWarn": 9,
    "staleContent": 0,
    "crit": 0
  },
  "checks": {
    "marketQuotes": { "status": "OK", "records": 78, "seedAgeMin": 12 },
    "earthquakes": { "status": "OK", "records": 142, "seedAgeMin": 8 }
  }
}
```

### `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).

`chinaDecisionSignals` is refreshed by the derived-signals bundle every 15
minutes. `/api/health` allows 60 minutes before `STALE_SEED`;
`/api/seed-health` uses a 30-minute interval (60-minute alarm) so both operator
surfaces agree.

### `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"
}
```

- **Idempotency**: optional `Idempotency-Key` supported on `POST /api/user-prefs`. Retrying the same key with an identical body replays the original preferences response instead of applying the update again.

## 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/leads/v1/submit-contact`

Public enterprise contact form. Turnstile-verified, rate-limited per IP. Part of `LeadsService`.

### `POST /api/leads/v1/register-interest`

Captures email for Pro-waitlist signup. Writes to Convex and sends a confirmation email. Part of `LeadsService`.

Browser callers must pass Turnstile. Desktop callers using `source: "desktop-settings"` bypass Turnstile only when the request is authenticated with the shared desktop secret:

- `X-WorldMonitor-Desktop-Timestamp`: Unix epoch milliseconds, within 5 minutes of server time.
- `X-WorldMonitor-Desktop-Signature`: `sha256=<hex HMAC-SHA256>`.

The HMAC input is `<timestamp>\n<canonical JSON>`, where canonical JSON contains `email`, `source`, `appVersion`, `referredBy`, `website`, and `turnstileToken` in that order. Configure `WM_DESKTOP_SHARED_SECRET` on both the desktop sidecar and the cloud API. During rollout, `WM_DESKTOP_AUTH_ALLOW_LEGACY=true` only accepts unsigned legacy desktop requests while the cloud API has no `WM_DESKTOP_SHARED_SECRET` configured. Once the cloud secret is set, desktop requests fail closed unless they include a valid signature, still subject to the tighter desktop rate limit.