70 lines
2.3 KiB
TypeScript
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);
|
|
}
|