Files
NianAIGC/lib/server/public-api-auth.ts
2026-05-29 12:32:02 +08:00

70 lines
2.3 KiB
TypeScript

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);
}