feat: add task workflow and asset downloads
This commit is contained in:
69
lib/server/public-api-auth.ts
Normal file
69
lib/server/public-api-auth.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user