From 8c3ae3d598dfa611a2cf2c738ee597bd1858e2de Mon Sep 17 00:00:00 2001 From: Anton Lykhoyda Date: Mon, 6 Apr 2026 15:22:58 +0200 Subject: [PATCH] fix: add X-API-Key header to API client requests The testudo-api now requires X-API-Key authentication on all /api/v1/* routes. Read TESTUDO_API_KEY from env at build time and send as header. Co-Authored-By: Claude Opus 4.6 (1M context) --- .env.example | 3 +++ packages/extension/src/api-client.ts | 23 +++++++++++++++++------ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/.env.example b/.env.example index 9d4c1ab..34c12e1 100644 --- a/.env.example +++ b/.env.example @@ -14,6 +14,9 @@ # - Production: https://api.testudo.security TESTUDO_API_URL=http://localhost:3001 +# API key for authenticating with the Testudo API +TESTUDO_API_KEY= + # Safe Filter CDN URL (for bloom filter sync) # - Local development: Leave empty to use hardcoded fallback # - Production: https://cdn.testudo.security diff --git a/packages/extension/src/api-client.ts b/packages/extension/src/api-client.ts index 86cdff8..ef3cefc 100644 --- a/packages/extension/src/api-client.ts +++ b/packages/extension/src/api-client.ts @@ -1,4 +1,5 @@ const DEFAULT_API_URL = process.env.TESTUDO_API_URL; +const DEFAULT_API_KEY = process.env.TESTUDO_API_KEY; const DEFAULT_TIMEOUT = 800; // ms const MAX_RETRIES = 1; @@ -32,6 +33,7 @@ export interface ThreatResponse { export interface ApiClientOptions { baseUrl?: string; timeout?: number; + apiKey?: string; } export type ApiErrorCategory = @@ -55,22 +57,29 @@ export interface ApiClientResult { async function fetchWithTimeout( url: string, timeout: number, + apiKey?: string, signal?: AbortSignal, ): Promise { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); - // Combine with external signal if provided if (signal) { signal.addEventListener('abort', () => controller.abort(), { once: true }); } + const headers: Record = { + 'Content-Type': 'application/json', + }; + + const key = apiKey || DEFAULT_API_KEY; + if (key) { + headers['X-API-Key'] = key; + } + try { const response = await fetch(url, { signal: controller.signal, - headers: { - 'Content-Type': 'application/json', - }, + headers, }); clearTimeout(timeoutId); return response; @@ -98,6 +107,7 @@ export async function checkAddressThreat( }; } + const apiKey = options?.apiKey; const url = `${baseUrl}/api/v1/threats/address/${address.toLowerCase()}`; let lastError: string = ''; let lastCategory: ApiErrorCategory = 'unknown'; @@ -108,7 +118,7 @@ export async function checkAddressThreat( console.log(`[API Client] Retry attempt ${attempt} for ${address}`); } - const response = await fetchWithTimeout(url, timeout); + const response = await fetchWithTimeout(url, timeout, apiKey); // Handle rate limiting if (response.status === 429) { @@ -186,6 +196,7 @@ export async function checkDomainThreat( }; } + const apiKey = options?.apiKey; const url = `${baseUrl}/api/v1/threats/domain/${encodeURIComponent(domain)}`; let lastError: string = ''; let lastCategory: ApiErrorCategory = 'unknown'; @@ -196,7 +207,7 @@ export async function checkDomainThreat( console.log(`[API Client] Retry attempt ${attempt} for ${domain}`); } - const response = await fetchWithTimeout(url, timeout); + const response = await fetchWithTimeout(url, timeout, apiKey); if (response.status === 429) { console.warn('[API Client] Rate limited');