c30d11d60b
* fix(security): harden BYO API header against SSRF and input abuse - Add lib/validateUrl.ts with HTTPS-only + public-IP enforcement, provider allowlist, IPv6 rejection, and userinfo-in-URL blocking. - Add lib/byoHeaders.ts — single source of truth for client-side BYO header construction (deduplicates app/page.tsx & app/play/page.tsx). - config.ts: validate BYO endpoints via isPublicUrl(), cap header at 2 KB, truncate apiKey/model strings, sanitize log output. - fetchWithRetry: default redirect to "manual" to block 302-to-intranet. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(security): address Copilot review — trim endpoint, strip control chars, drop unused import - safeEndpoint: trim whitespace before URL validation - safeString: strip ASCII control characters to prevent header injection - play/page.tsx: remove unused BYO_STORAGE_KEY import Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
41 lines
1.2 KiB
TypeScript
41 lines
1.2 KiB
TypeScript
type RetryInit = RequestInit & { retries?: number; retryDelayMs?: number };
|
|
|
|
export async function fetchWithRetry(
|
|
url: string,
|
|
init: RetryInit,
|
|
): Promise<Response> {
|
|
const { retries = 2, retryDelayMs = 1500, ...fetchInit } = init;
|
|
if (!fetchInit.redirect) fetchInit.redirect = "manual";
|
|
|
|
let lastError: unknown;
|
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
try {
|
|
const res = await fetch(url, fetchInit);
|
|
if (res.ok) return res;
|
|
// Don't retry 4xx (client errors won't fix themselves)
|
|
if (res.status >= 400 && res.status < 500) return res;
|
|
// 5xx: retry if we have budget left
|
|
if (attempt < retries) {
|
|
await sleep(retryDelayMs * (attempt + 1));
|
|
continue;
|
|
}
|
|
return res;
|
|
} catch (err) {
|
|
lastError = err;
|
|
const isAbort =
|
|
err instanceof DOMException && err.name === "AbortError";
|
|
if (isAbort) throw err;
|
|
if (attempt < retries) {
|
|
await sleep(retryDelayMs * (attempt + 1));
|
|
continue;
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
throw lastError;
|
|
}
|
|
|
|
function sleep(ms: number): Promise<void> {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|