mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
fix(acled): add OAuth token manager with automatic refresh (#1437)
* fix(acled): add OAuth token manager with automatic refresh ACLED access tokens expire every 24 hours, but WorldMonitor stores a static ACLED_ACCESS_TOKEN with no refresh logic — causing all ACLED API calls to fail after the first day. This commit adds `acled-auth.ts`, an OAuth token manager that: - Exchanges ACLED_EMAIL + ACLED_PASSWORD for an access token (24h) and refresh token (14d) via the official ACLED OAuth endpoint - Caches tokens in memory and auto-refreshes before expiry - Falls back to static ACLED_ACCESS_TOKEN for backward compatibility - Deduplicates concurrent refresh attempts - Degrades gracefully when no credentials are configured The only change to the existing `acled.ts` is replacing the synchronous `process.env.ACLED_ACCESS_TOKEN` read with an async call to the new `getAcledAccessToken()` helper. Fixes #1283 Relates to #290 * fix: address review feedback on ACLED OAuth PR - Use Redis (Upstash) as L2 token cache to survive Vercel Edge cold starts (in-memory cache retained as fast-path L1) - Add CHROME_UA User-Agent header on OAuth token exchange and refresh - Update seed script to use OAuth flow via getAcledToken() helper instead of raw process.env.ACLED_ACCESS_TOKEN - Add security comment to .env.example about plaintext password trade-offs - Sidecar ACLED_ACCESS_TOKEN case is a validation probe (tests user-provided value, not process.env) — data fetching delegates to handler modules * feat(sidecar): add ACLED_EMAIL/ACLED_PASSWORD to env allowlist and validation - Add ACLED_EMAIL and ACLED_PASSWORD to ALLOWED_ENV_KEYS set - Add ACLED_EMAIL validation case (store-only, verified with password) - Add ACLED_PASSWORD validation case with OAuth token exchange via acleddata.com/api/acled/user/login - On successful login, store obtained OAuth token in ACLED_ACCESS_TOKEN - Follows existing validation patterns (Cloudflare challenge handling, auth failure detection, User-Agent header) * fix: address remaining review feedback (duplicate OAuth, em dashes, emoji) - Extract shared ACLED OAuth helper into scripts/shared/acled-oauth.mjs - Remove ~55 lines of duplicate OAuth logic from seed-unrest-events.mjs, now imports getAcledToken from the shared helper - Replace em dashes with ASCII dashes in acled-auth.ts section comments - Replace em dash with parentheses in sidecar validation message - Remove emoji from .env.example security note Addresses koala73's second review: MEDIUM (duplicate OAuth), LOW (em dashes), LOW (emoji). * fix: align sidecar OAuth endpoint, fix L1/L2 cache, cleanup artifacts - Sidecar: switch from /api/acled/user/login (JSON) to /oauth/token (URL-encoded) to match server/_shared/acled-auth.ts exactly - acled-auth.ts: check L2 Redis when L1 is expired, not only when L1 is null (fixes stale L1 skipping fresher L2 from another isolate) - acled-oauth.mjs: remove stray backslash on line 9 - seed-unrest-events.mjs: remove extra blank line at line 13 --------- Co-authored-by: Elie Habib <elie.habib@gmail.com> Co-authored-by: RepairYourTech <30200484+RepairYourTech@users.noreply.github.com>
This commit is contained in:
@@ -137,7 +137,7 @@ globalThis.fetch = async function ipv4Fetch(input, init) {
|
||||
|
||||
const ALLOWED_ENV_KEYS = new Set([
|
||||
'GROQ_API_KEY', 'OPENROUTER_API_KEY', 'TAVILY_API_KEYS', 'BRAVE_API_KEYS', 'SERPAPI_API_KEYS', 'FRED_API_KEY', 'EIA_API_KEY',
|
||||
'CLOUDFLARE_API_TOKEN', 'ACLED_ACCESS_TOKEN', 'URLHAUS_AUTH_KEY',
|
||||
'CLOUDFLARE_API_TOKEN', 'ACLED_ACCESS_TOKEN', 'ACLED_EMAIL', 'ACLED_PASSWORD', 'URLHAUS_AUTH_KEY',
|
||||
'OTX_API_KEY', 'ABUSEIPDB_API_KEY', 'WINGBITS_API_KEY', 'WS_RELAY_URL',
|
||||
'VITE_OPENSKY_RELAY_URL', 'OPENSKY_CLIENT_ID', 'OPENSKY_CLIENT_SECRET',
|
||||
'AISSTREAM_API_KEY', 'VITE_WS_RELAY_URL', 'FINNHUB_API_KEY', 'NASA_FIRMS_API_KEY',
|
||||
@@ -806,6 +806,51 @@ async function validateSecretAgainstProvider(key, rawValue, context = {}) {
|
||||
return ok('ACLED token verified');
|
||||
}
|
||||
|
||||
case 'ACLED_EMAIL':
|
||||
// Email is validated together with ACLED_PASSWORD; store it for now.
|
||||
return ok('ACLED email stored');
|
||||
|
||||
case 'ACLED_PASSWORD': {
|
||||
// Validate ACLED credentials via OAuth token exchange.
|
||||
// Uses the same /oauth/token endpoint as server/_shared/acled-auth.ts.
|
||||
// Requires ACLED_EMAIL to be set first (via local-env-update).
|
||||
const email = String(context.ACLED_EMAIL || process.env.ACLED_EMAIL || '').trim();
|
||||
if (!email) {
|
||||
return fail('Set ACLED_EMAIL before verifying the password');
|
||||
}
|
||||
const oauthBody = new URLSearchParams({
|
||||
username: email,
|
||||
password: value,
|
||||
grant_type: 'password',
|
||||
client_id: 'acled',
|
||||
});
|
||||
const loginResponse = await fetchWithTimeout('https://acleddata.com/oauth/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Accept: 'application/json',
|
||||
'User-Agent': CHROME_UA,
|
||||
},
|
||||
body: oauthBody.toString(),
|
||||
});
|
||||
const loginText = await loginResponse.text();
|
||||
if (isCloudflareChallenge403(loginResponse, loginText)) {
|
||||
return ok('ACLED credentials stored (Cloudflare blocked verification)');
|
||||
}
|
||||
if (isAuthFailure(loginResponse.status, loginText)) {
|
||||
return fail('ACLED rejected these credentials');
|
||||
}
|
||||
if (!loginResponse.ok) return fail(`ACLED OAuth probe failed (${loginResponse.status})`);
|
||||
let loginPayload = null;
|
||||
try { loginPayload = JSON.parse(loginText); } catch { /* ignore */ }
|
||||
if (loginPayload?.access_token) {
|
||||
// Store the obtained OAuth token so API handlers can use it.
|
||||
process.env.ACLED_ACCESS_TOKEN = loginPayload.access_token;
|
||||
return ok('ACLED credentials verified (OAuth token obtained)');
|
||||
}
|
||||
return ok('ACLED credentials accepted');
|
||||
}
|
||||
|
||||
case 'URLHAUS_AUTH_KEY': {
|
||||
const response = await fetchWithTimeout('https://urlhaus-api.abuse.ch/v1/urls/recent/limit/1/', {
|
||||
headers: {
|
||||
|
||||
Reference in New Issue
Block a user