feat(conflict): wire UCDP (#760)

* feat(conflict): wire UCDP API access token across full stack

UCDP API now requires an `x-ucdp-access-token` header. Renames the
stub `UC_DP_KEY` to `UCDP_ACCESS_TOKEN` (matching ACLED convention)
and wires it through Rust keychain, sidecar allowlist + verification,
handler fetch headers, feature toggles, and desktop settings UI.

- Rename UC_DP_KEY → UCDP_ACCESS_TOKEN in type system and labels
- Add ucdpConflicts feature toggle with required secret
- Add UCDP_ACCESS_TOKEN to Rust SUPPORTED_SECRET_KEYS (24→25)
- Add sidecar ALLOWED_ENV_KEYS entry + validation with dynamic GED version probing
- Handler sends x-ucdp-access-token header when token is present
- UC_DP_KEY fallback in handler for one-release migration window
- Update .env.example, desktop-readiness, and docs

* feat(conflict): pre-fetch UCDP events via Railway cron + Redis cache

Replace the 228-line edge handler that fetched UCDP GED API on every
request with a thin Redis reader. The heavy fetch logic (version
discovery, paginated backward fetch, 1-year trailing window filter)
now runs as a setInterval loop in the Railway relay (ais-relay.cjs)
every 6 hours, writing to Redis key conflict:ucdp-events:v1.

Changes:
- Add UCDP seed loop to ais-relay.cjs (6h interval, 6 pages, 2K cap)
- Rewrite list-ucdp-events.ts as thin Redis reader (35 lines)
- Add conflict:ucdp-events:v1 to bootstrap batch keys
- Protect key from cache-purge via durable data prefix
- Add manual-only seed-ucdp-events workflow + standalone script
- Rename panel "UCDP Events" → "Armed Conflict Events" in locale
- Add 24h TTL + 25h staleness check as safety nets
This commit is contained in:
Elie Habib
2026-03-02 16:17:17 +04:00
committed by GitHub
parent 4d6e031392
commit b423995363
14 changed files with 433 additions and 215 deletions

View File

@@ -105,7 +105,7 @@ const ALLOWED_ENV_KEYS = new Set([
'VITE_OPENSKY_RELAY_URL', 'OPENSKY_CLIENT_ID', 'OPENSKY_CLIENT_SECRET',
'AISSTREAM_API_KEY', 'VITE_WS_RELAY_URL', 'FINNHUB_API_KEY', 'NASA_FIRMS_API_KEY',
'OLLAMA_API_URL', 'OLLAMA_MODEL', 'WORLDMONITOR_API_KEY', 'WTO_API_KEY',
'AVIATIONSTACK_API', 'ICAO_API_KEY',
'AVIATIONSTACK_API', 'ICAO_API_KEY', 'UCDP_ACCESS_TOKEN',
]);
const CHROME_UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
@@ -821,6 +821,26 @@ async function validateSecretAgainstProvider(key, rawValue, context = {}) {
return ok('NASA FIRMS key verified');
}
case 'UCDP_ACCESS_TOKEN': {
const year = new Date().getFullYear() - 2000;
const candidates = [...new Set([`${year}.1`, `${year - 1}.1`, '25.1', '24.1'])];
for (const version of candidates) {
try {
const response = await fetchWithTimeout(
`https://ucdpapi.pcr.uu.se/api/gedevents/${version}?pagesize=1`,
{ headers: { Accept: 'application/json', 'x-ucdp-access-token': value, 'User-Agent': CHROME_UA } }
);
if (isAuthFailure(response.status)) return fail('UCDP rejected this token');
if (!response.ok) continue;
const text = await response.text();
let payload = null;
try { payload = JSON.parse(text); } catch { /* ignore */ }
if (Array.isArray(payload?.Result)) return ok(`UCDP token verified (GED v${version})`);
} catch { continue; }
}
return fail('Could not verify UCDP token (all GED versions failed)');
}
case 'OLLAMA_API_URL': {
let probeUrl;
try {

View File

@@ -27,7 +27,7 @@ const MENU_HELP_GITHUB_ID: &str = "help.github";
#[cfg(feature = "devtools")]
const MENU_HELP_DEVTOOLS_ID: &str = "help.devtools";
const TRUSTED_WINDOWS: [&str; 3] = ["main", "settings", "live-channels"];
const SUPPORTED_SECRET_KEYS: [&str; 24] = [
const SUPPORTED_SECRET_KEYS: [&str; 25] = [
"GROQ_API_KEY",
"OPENROUTER_API_KEY",
"FRED_API_KEY",
@@ -46,6 +46,7 @@ const SUPPORTED_SECRET_KEYS: [&str; 24] = [
"VITE_WS_RELAY_URL",
"FINNHUB_API_KEY",
"NASA_FIRMS_API_KEY",
"UCDP_ACCESS_TOKEN",
"OLLAMA_API_URL",
"OLLAMA_MODEL",
"WORLDMONITOR_API_KEY",