Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 17 additions & 6 deletions packages/extension/src/api-client.ts
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -32,6 +33,7 @@ export interface ThreatResponse {
export interface ApiClientOptions {
baseUrl?: string;
timeout?: number;
apiKey?: string;
}

export type ApiErrorCategory =
Expand All @@ -55,22 +57,29 @@ export interface ApiClientResult {
async function fetchWithTimeout(
url: string,
timeout: number,
apiKey?: string,
signal?: AbortSignal,
): Promise<Response> {
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<string, string> = {
'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;
Expand Down Expand Up @@ -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';
Expand All @@ -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) {
Expand Down Expand Up @@ -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';
Expand All @@ -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');
Expand Down
Loading