import { timingSafeEqual } from "node:crypto"; export type PublicApiClient = { id: string; key: string; }; export class PublicApiAuthError extends Error { status: number; constructor(message: string, status = 401) { super(message); this.name = "PublicApiAuthError"; this.status = status; } } export function getPublicApiClients(): PublicApiClient[] { const configured = process.env.ZHINIAN_API_KEYS?.trim(); if (!configured) return []; return configured .split(/[\n,]+/) .map((entry) => entry.trim()) .filter(Boolean) .map((entry) => { const separator = entry.indexOf(":"); if (separator === -1) return { id: "default", key: entry }; return { id: entry.slice(0, separator).trim(), key: entry.slice(separator + 1).trim() }; }) .filter((client) => client.id && client.key); } export function authenticatePublicApiRequest(request: Request): PublicApiClient { const presented = getPresentedApiKey(request); if (!presented) throw new PublicApiAuthError("Missing API key."); const client = getPublicApiClients().find((candidate) => safeEqual(candidate.key, presented)); if (!client) throw new PublicApiAuthError("Invalid API key."); return client; } export function assertInternalWorkerToken(request: Request) { const expected = process.env.ZHINIAN_INTERNAL_WORKER_TOKEN?.trim(); if (!expected && process.env.NODE_ENV !== "production") return; if (!expected) throw new PublicApiAuthError("Worker token is not configured.", 500); const presented = request.headers.get("x-zhinian-worker-token") || bearerToken(request); if (!presented || !safeEqual(expected, presented)) { throw new PublicApiAuthError("Invalid worker token.", 401); } } function getPresentedApiKey(request: Request): string | undefined { return bearerToken(request) || request.headers.get("x-zhinian-api-key") || undefined; } function bearerToken(request: Request): string | undefined { const authorization = request.headers.get("authorization") || ""; const match = authorization.match(/^Bearer\s+(.+)$/i); return match?.[1]?.trim() || undefined; } function safeEqual(expected: string, presented: string): boolean { const left = Buffer.from(expected); const right = Buffer.from(presented); if (left.length !== right.length) return false; return timingSafeEqual(left, right); }