settings: verify API keys via provider probes

This commit is contained in:
Elie Habib
2026-02-15 21:31:54 +04:00
parent 723279eedc
commit 0738e38baa
6 changed files with 588 additions and 24 deletions

View File

@@ -314,6 +314,253 @@ function makeCorsHeaders(req) {
};
}
async function fetchWithTimeout(url, options = {}, timeoutMs = 12000) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
return await fetch(url, { ...options, signal: controller.signal });
} finally {
clearTimeout(timer);
}
}
function relayToHttpUrl(rawUrl) {
try {
const parsed = new URL(rawUrl);
if (parsed.protocol === 'ws:') parsed.protocol = 'http:';
if (parsed.protocol === 'wss:') parsed.protocol = 'https:';
return parsed.toString().replace(/\/$/, '');
} catch {
return null;
}
}
function isAuthFailure(status, text = '') {
if (status === 401 || status === 403) return true;
return /unauthori[sz]ed|forbidden|invalid api key|invalid token|bad credentials/i.test(text);
}
async function validateSecretAgainstProvider(key, rawValue, context = {}) {
const value = String(rawValue || '').trim();
if (!value) return { valid: false, message: 'Value is required' };
const fail = (message) => ({ valid: false, message });
const ok = (message) => ({ valid: true, message });
try {
switch (key) {
case 'GROQ_API_KEY': {
const response = await fetchWithTimeout('https://api.groq.com/openai/v1/models', {
headers: { Authorization: `Bearer ${value}` },
});
const text = await response.text();
if (isAuthFailure(response.status, text)) return fail('Groq rejected this key');
if (!response.ok) return fail(`Groq probe failed (${response.status})`);
return ok('Groq key verified');
}
case 'OPENROUTER_API_KEY': {
const response = await fetchWithTimeout('https://openrouter.ai/api/v1/models', {
headers: { Authorization: `Bearer ${value}` },
});
const text = await response.text();
if (isAuthFailure(response.status, text)) return fail('OpenRouter rejected this key');
if (!response.ok) return fail(`OpenRouter probe failed (${response.status})`);
return ok('OpenRouter key verified');
}
case 'FRED_API_KEY': {
const response = await fetchWithTimeout(
`https://api.stlouisfed.org/fred/series?series_id=GDP&api_key=${encodeURIComponent(value)}&file_type=json`,
{ headers: { Accept: 'application/json' } }
);
const text = await response.text();
if (!response.ok) return fail(`FRED probe failed (${response.status})`);
let payload = null;
try { payload = JSON.parse(text); } catch { /* ignore */ }
if (payload?.error_code || payload?.error_message) return fail('FRED rejected this key');
if (!Array.isArray(payload?.seriess)) return fail('Unexpected FRED response');
return ok('FRED key verified');
}
case 'EIA_API_KEY': {
const response = await fetchWithTimeout(
`https://api.eia.gov/v2/seriesid/PET.RWTC.W?api_key=${encodeURIComponent(value)}&num=1`,
{ headers: { Accept: 'application/json' } }
);
const text = await response.text();
if (isAuthFailure(response.status, text)) return fail('EIA rejected this key');
if (!response.ok) return fail(`EIA probe failed (${response.status})`);
let payload = null;
try { payload = JSON.parse(text); } catch { /* ignore */ }
if (!Array.isArray(payload?.response?.data)) return fail('Unexpected EIA response');
return ok('EIA key verified');
}
case 'CLOUDFLARE_API_TOKEN': {
const response = await fetchWithTimeout('https://api.cloudflare.com/client/v4/user/tokens/verify', {
headers: { Authorization: `Bearer ${value}` },
});
const text = await response.text();
if (isAuthFailure(response.status, text)) return fail('Cloudflare rejected this token');
if (!response.ok) return fail(`Cloudflare probe failed (${response.status})`);
let payload = null;
try { payload = JSON.parse(text); } catch { /* ignore */ }
if (payload?.success !== true) return fail('Cloudflare token verification failed');
return ok('Cloudflare token verified');
}
case 'ACLED_ACCESS_TOKEN': {
const response = await fetchWithTimeout('https://acleddata.com/api/acled/read?_format=json&limit=1', {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${value}`,
},
});
const text = await response.text();
if (isAuthFailure(response.status, text)) return fail('ACLED rejected this token');
if (!response.ok) return fail(`ACLED probe failed (${response.status})`);
return ok('ACLED token verified');
}
case 'URLHAUS_AUTH_KEY': {
const response = await fetchWithTimeout('https://urlhaus-api.abuse.ch/v1/urls/recent/limit/1/', {
headers: {
Accept: 'application/json',
'Auth-Key': value,
},
});
const text = await response.text();
if (isAuthFailure(response.status, text)) return fail('URLhaus rejected this key');
if (!response.ok) return fail(`URLhaus probe failed (${response.status})`);
return ok('URLhaus key verified');
}
case 'OTX_API_KEY': {
const response = await fetchWithTimeout('https://otx.alienvault.com/api/v1/user/me', {
headers: {
Accept: 'application/json',
'X-OTX-API-KEY': value,
},
});
const text = await response.text();
if (isAuthFailure(response.status, text)) return fail('OTX rejected this key');
if (!response.ok) return fail(`OTX probe failed (${response.status})`);
return ok('OTX key verified');
}
case 'ABUSEIPDB_API_KEY': {
const response = await fetchWithTimeout('https://api.abuseipdb.com/api/v2/check?ipAddress=8.8.8.8&maxAgeInDays=90', {
headers: {
Accept: 'application/json',
Key: value,
},
});
const text = await response.text();
if (isAuthFailure(response.status, text)) return fail('AbuseIPDB rejected this key');
if (!response.ok) return fail(`AbuseIPDB probe failed (${response.status})`);
return ok('AbuseIPDB key verified');
}
case 'WINGBITS_API_KEY': {
const response = await fetchWithTimeout('https://customer-api.wingbits.com/v1/flights/details/3c6444', {
headers: {
Accept: 'application/json',
'x-api-key': value,
},
});
const text = await response.text();
if (isAuthFailure(response.status, text)) return fail('Wingbits rejected this key');
if (response.status >= 500) return fail(`Wingbits probe failed (${response.status})`);
return ok('Wingbits key accepted');
}
case 'FINNHUB_API_KEY': {
const response = await fetchWithTimeout(`https://finnhub.io/api/v1/quote?symbol=AAPL&token=${encodeURIComponent(value)}`, {
headers: { Accept: 'application/json' },
});
const text = await response.text();
if (isAuthFailure(response.status, text)) return fail('Finnhub rejected this key');
if (response.status === 429) return ok('Finnhub key accepted (rate limited)');
if (!response.ok) return fail(`Finnhub probe failed (${response.status})`);
let payload = null;
try { payload = JSON.parse(text); } catch { /* ignore */ }
if (typeof payload?.error === 'string' && payload.error.toLowerCase().includes('invalid')) {
return fail('Finnhub rejected this key');
}
if (typeof payload?.c !== 'number') return fail('Unexpected Finnhub response');
return ok('Finnhub key verified');
}
case 'NASA_FIRMS_API_KEY': {
const response = await fetchWithTimeout(
`https://firms.modaps.eosdis.nasa.gov/api/area/csv/${encodeURIComponent(value)}/VIIRS_SNPP_NRT/22,44,40,53/1`,
{ headers: { Accept: 'text/csv' } }
);
const text = await response.text();
if (isAuthFailure(response.status, text)) return fail('NASA FIRMS rejected this key');
if (!response.ok) return fail(`NASA FIRMS probe failed (${response.status})`);
if (/invalid api key|not authorized|forbidden/i.test(text)) return fail('NASA FIRMS rejected this key');
return ok('NASA FIRMS key verified');
}
case 'WS_RELAY_URL':
case 'VITE_WS_RELAY_URL':
case 'VITE_OPENSKY_RELAY_URL': {
const probeUrl = relayToHttpUrl(value);
if (!probeUrl) return fail('Relay URL is invalid');
const response = await fetchWithTimeout(probeUrl, { method: 'GET' });
if (response.status >= 500) return fail(`Relay probe failed (${response.status})`);
return ok('Relay URL is reachable');
}
case 'OPENSKY_CLIENT_ID':
case 'OPENSKY_CLIENT_SECRET': {
const contextClientId = typeof context.OPENSKY_CLIENT_ID === 'string' ? context.OPENSKY_CLIENT_ID.trim() : '';
const contextClientSecret = typeof context.OPENSKY_CLIENT_SECRET === 'string' ? context.OPENSKY_CLIENT_SECRET.trim() : '';
const clientId = key === 'OPENSKY_CLIENT_ID'
? value
: (contextClientId || String(process.env.OPENSKY_CLIENT_ID || '').trim());
const clientSecret = key === 'OPENSKY_CLIENT_SECRET'
? value
: (contextClientSecret || String(process.env.OPENSKY_CLIENT_SECRET || '').trim());
if (!clientId || !clientSecret) {
return fail('Set both OPENSKY_CLIENT_ID and OPENSKY_CLIENT_SECRET before verification');
}
const body = new URLSearchParams({
grant_type: 'client_credentials',
client_id: clientId,
client_secret: clientSecret,
});
const response = await fetchWithTimeout(
'https://auth.opensky-network.org/auth/realms/opensky-network/protocol/openid-connect/token',
{
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body,
}
);
const text = await response.text();
if (isAuthFailure(response.status, text)) return fail('OpenSky rejected these credentials');
if (!response.ok) return fail(`OpenSky auth probe failed (${response.status})`);
let payload = null;
try { payload = JSON.parse(text); } catch { /* ignore */ }
if (!payload?.access_token) return fail('OpenSky auth response did not include an access token');
return ok('OpenSky credentials verified');
}
case 'AISSTREAM_API_KEY':
return ok('AISSTREAM key stored (live verification not available in sidecar)');
default:
return ok('Key stored');
}
} catch (error) {
const message = error instanceof Error ? error.message : 'provider probe failed';
return fail(`Verification request failed: ${message}`);
}
}
async function dispatch(requestUrl, req, routes, context) {
if (req.method === 'OPTIONS') {
return new Response(null, { status: 204, headers: makeCorsHeaders(req) });
@@ -387,6 +634,25 @@ async function dispatch(requestUrl, req, routes, context) {
return json({ error: 'POST required' }, 405);
}
if (requestUrl.pathname === '/api/local-validate-secret') {
if (req.method !== 'POST') {
return json({ error: 'POST required' }, 405);
}
const body = await readBody(req);
if (!body) return json({ error: 'expected { key, value }' }, 400);
try {
const { key, value, context } = JSON.parse(body.toString());
if (typeof key !== 'string' || !ALLOWED_ENV_KEYS.has(key)) {
return json({ error: 'key not in allowlist' }, 403);
}
const safeContext = (context && typeof context === 'object') ? context : {};
const result = await validateSecretAgainstProvider(key, value, safeContext);
return json(result, result.valid ? 200 : 422);
} catch {
return json({ error: 'expected { key, value }' }, 400);
}
}
if (context.cloudFallback && cloudPreferred.has(requestUrl.pathname)) {
const cloudResponse = await tryCloudFallback(requestUrl, req, context);
if (cloudResponse) return cloudResponse;
@@ -462,7 +728,10 @@ export async function createLocalApiServer(options = {}) {
}
const start = Date.now();
const skipRecord = requestUrl.pathname === '/api/local-traffic-log' || requestUrl.pathname === '/api/local-debug-toggle' || requestUrl.pathname === '/api/local-env-update';
const skipRecord = requestUrl.pathname === '/api/local-traffic-log'
|| requestUrl.pathname === '/api/local-debug-toggle'
|| requestUrl.pathname === '/api/local-env-update'
|| requestUrl.pathname === '/api/local-validate-secret';
try {
const response = await dispatch(requestUrl, req, routes, context);