Something went wrong. Try again.
Something went wrong. Try again.
5.3 kB · 151 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152import { readFileSync, existsSync, lstatSync, openSync, closeSync, writeFileSync, fsyncSync, mkdirSync, renameSync, chmodSync,} from "node:fs"import path from "node:path"import { randomBytes } from "node:crypto"import { normalizeApiKey } from "./api-key"
export type ApiKeyEntry = { key: string name?: string allowedModels: string[] caps: string[]}
export type ApiKeysFileStatus = { path: string editable: boolean shape?: "array" | "object" error?: string}
export function generateApiKey(): string { // Keep 37 complete base64url characters: 222 uniformly random bits, 48 total characters. return `sk-bridget-${randomBytes(28).toString("base64url").slice(0, 37)}`}
function parseApiKeyEntry(entry: unknown): ApiKeyEntry { if (!entry || typeof entry !== "object") throw new Error("[bridge] API key entry must be an object") const value = entry as Record<string, unknown> const key = typeof value.key === "string" ? normalizeApiKey(value.key) : undefined if (!key) { throw new Error( "[bridge] invalid API key: expected sk-bridget- with 37 base64url characters, a UUID, or sk-bridget-uuid-<uuid>", ) } const allowedModels = value.allowedModels if (!Array.isArray(allowedModels) || allowedModels.some((model) => typeof model !== "string")) { throw new Error(`[bridge] API key "${key}" has a malformed allowedModels field: expected an array of strings`) } const caps = value.caps ?? [] if (!Array.isArray(caps) || caps.some((cap) => typeof cap !== "string")) { throw new Error(`[bridge] API key "${key}" has a malformed caps field: expected an array of strings`) } return { key, allowedModels: [...allowedModels], caps: [...caps], ...(typeof value.name === "string" && value.name.trim() ? { name: value.name.trim() } : {}), }}
export function parseApiKeyEntries(parsed: unknown): ApiKeyEntry[] { const entries = Array.isArray(parsed) ? parsed : [parsed] return entries.map(parseApiKeyEntry)}
/** Convert a glob pattern to a RegExp. Supports * and ?. */function globToRegex(pattern: string): RegExp { const escaped = pattern .replace(/[.+^${}()|[\]\\]/g, "\\$&") .replace(/\*/g, ".*") .replace(/\?/g, ".") return new RegExp(`^${escaped}$`, "i")}
export function probeApiKeysFile(keysPath: string): ApiKeysFileStatus { try { const stat = lstatSync(keysPath) if (stat.isSymbolicLink() || !stat.isFile()) { return { path: keysPath, editable: false, error: "keys path must be a regular file, not a symlink" } } const fd = openSync(keysPath, "r+") closeSync(fd) const parsed = JSON.parse(readFileSync(keysPath, "utf8")) as unknown if (!Array.isArray(parsed) && (!parsed || typeof parsed !== "object")) { return { path: keysPath, editable: false, error: "keys file must contain an object or array" } } return { path: keysPath, editable: true, shape: Array.isArray(parsed) ? "array" : "object" } } catch (err) { return { path: keysPath, editable: false, error: err instanceof Error ? err.message : String(err) } }}
export function writeApiKeysFile(status: ApiKeysFileStatus, entries: ApiKeyEntry[]): void { if (!status.editable) throw new Error(status.error ?? "keys file is not editable") const useObject = status.shape === "object" && entries.length === 1 status.shape = useObject ? "object" : "array" const payload = useObject ? entries[0] : entries const text = `${JSON.stringify(payload, null, 2)}\n` const directory = path.dirname(status.path) mkdirSync(directory, { recursive: true }) const tempPath = path.join(directory, `.${path.basename(status.path)}.${process.pid}.${Date.now()}.tmp`) const fd = openSync(tempPath, "wx", 0o600) try { writeFileSync(fd, text, "utf8") fsyncSync(fd) } finally { closeSync(fd) } renameSync(tempPath, status.path) try { chmodSync(status.path, 0o600) } catch { /* best effort */ }}
export function isModelAllowed(keyEntry: ApiKeyEntry, modelId: string): boolean { return keyEntry.allowedModels.some((pattern) => globToRegex(pattern).test(modelId))}
export function hasCap(keyEntry: ApiKeyEntry, cap: string): boolean { return keyEntry.caps.includes(cap)}
export function loadApiKeys(keysPath?: string, configuredEntries?: unknown): Map<string, ApiKeyEntry> { const keys = new Map<string, ApiKeyEntry>() let entries: ApiKeyEntry[] = [] if (configuredEntries !== undefined) { entries = parseApiKeyEntries(configuredEntries) } else if (keysPath && existsSync(keysPath)) { let parsed: unknown try { parsed = JSON.parse(readFileSync(keysPath, "utf-8")) } catch { throw new Error(`[bridge] invalid JSON in keys file: ${keysPath}`) } entries = parseApiKeyEntries(parsed) } for (const entry of entries) { if (keys.has(entry.key)) throw new Error(`[bridge] duplicate API key "${entry.key}"`) keys.set(entry.key, entry) } return keys}
export function authenticate(authHeader: string | undefined, apiKeys: Map<string, ApiKeyEntry>): ApiKeyEntry | null { if (!authHeader) return null const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7).trim() : authHeader.trim() const key = normalizeApiKey(token) return key ? (apiKeys.get(key) ?? null) : null}