Skip to content

Commit 2e1f950

Browse files
Lykhoydaclaude
andauthored
fix: add X-API-Key header to API client requests (#2)
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) <noreply@anthropic.com>
1 parent 4d39a02 commit 2e1f950

2 files changed

Lines changed: 20 additions & 6 deletions

File tree

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
# - Production: https://api.testudo.security
1515
TESTUDO_API_URL=http://localhost:3001
1616

17+
# API key for authenticating with the Testudo API
18+
TESTUDO_API_KEY=
19+
1720
# Safe Filter CDN URL (for bloom filter sync)
1821
# - Local development: Leave empty to use hardcoded fallback
1922
# - Production: https://cdn.testudo.security

packages/extension/src/api-client.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const DEFAULT_API_URL = process.env.TESTUDO_API_URL;
2+
const DEFAULT_API_KEY = process.env.TESTUDO_API_KEY;
23
const DEFAULT_TIMEOUT = 800; // ms
34
const MAX_RETRIES = 1;
45

@@ -32,6 +33,7 @@ export interface ThreatResponse {
3233
export interface ApiClientOptions {
3334
baseUrl?: string;
3435
timeout?: number;
36+
apiKey?: string;
3537
}
3638

3739
export type ApiErrorCategory =
@@ -55,22 +57,29 @@ export interface ApiClientResult {
5557
async function fetchWithTimeout(
5658
url: string,
5759
timeout: number,
60+
apiKey?: string,
5861
signal?: AbortSignal,
5962
): Promise<Response> {
6063
const controller = new AbortController();
6164
const timeoutId = setTimeout(() => controller.abort(), timeout);
6265

63-
// Combine with external signal if provided
6466
if (signal) {
6567
signal.addEventListener('abort', () => controller.abort(), { once: true });
6668
}
6769

70+
const headers: Record<string, string> = {
71+
'Content-Type': 'application/json',
72+
};
73+
74+
const key = apiKey || DEFAULT_API_KEY;
75+
if (key) {
76+
headers['X-API-Key'] = key;
77+
}
78+
6879
try {
6980
const response = await fetch(url, {
7081
signal: controller.signal,
71-
headers: {
72-
'Content-Type': 'application/json',
73-
},
82+
headers,
7483
});
7584
clearTimeout(timeoutId);
7685
return response;
@@ -98,6 +107,7 @@ export async function checkAddressThreat(
98107
};
99108
}
100109

110+
const apiKey = options?.apiKey;
101111
const url = `${baseUrl}/api/v1/threats/address/${address.toLowerCase()}`;
102112
let lastError: string = '';
103113
let lastCategory: ApiErrorCategory = 'unknown';
@@ -108,7 +118,7 @@ export async function checkAddressThreat(
108118
console.log(`[API Client] Retry attempt ${attempt} for ${address}`);
109119
}
110120

111-
const response = await fetchWithTimeout(url, timeout);
121+
const response = await fetchWithTimeout(url, timeout, apiKey);
112122

113123
// Handle rate limiting
114124
if (response.status === 429) {
@@ -186,6 +196,7 @@ export async function checkDomainThreat(
186196
};
187197
}
188198

199+
const apiKey = options?.apiKey;
189200
const url = `${baseUrl}/api/v1/threats/domain/${encodeURIComponent(domain)}`;
190201
let lastError: string = '';
191202
let lastCategory: ApiErrorCategory = 'unknown';
@@ -196,7 +207,7 @@ export async function checkDomainThreat(
196207
console.log(`[API Client] Retry attempt ${attempt} for ${domain}`);
197208
}
198209

199-
const response = await fetchWithTimeout(url, timeout);
210+
const response = await fetchWithTimeout(url, timeout, apiKey);
200211

201212
if (response.status === 429) {
202213
console.warn('[API Client] Rate limited');

0 commit comments

Comments
 (0)