Something went wrong. Try again.
Something went wrong. Try again.
32 kB · 794 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795// Admin API + UI, mounted at /admin.//// A configured bridge is gated on the "admin" capability rather than mere// possession of a valid key: inference clients must not be able to re-auth the// provider accounts they use. With no configured keys, startup restricts the// bridge to loopback and the admin API follows the bridge's open mode.
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"import type { BridgeConfig } from "./config"import { authenticate, generateApiKey, hasCap, parseApiKeyEntries, writeApiKeysFile, type ApiKeyEntry } from "./auth"import fastifyStatic from "@fastify/static"import { randomUUID } from "node:crypto"import { cancelLogin, getSession, logout, providerStatus, publicSession, reapSessions, registerProvider, resolveAuthPath, startLogin, startSessionReaper, stateDir, type LoginProviderId,} from "./oauth-login"import { apiKeyIdForKey, configureKeyRouting, createAccount, deleteAccount, defaultAccount, getAccount, isAccountProvider, keyForApiKeyId, listAccounts, removeKeyRouting, renameAccount, routingForKey, type AccountProvider,} from "./accounts"import { getAccountUsage } from "./usage"import { getAccountTokenUsageHistory, getModelObservedUsage, listRequestMetrics, listUpstreamErrors, removeRoutingStateForKey,} from "./token-usage"import { FAILURE_STATISTICS_MODEL_MAX_LENGTH, getFailureStatistics, isFailureStatisticsWindowId,} from "./failure-statistics"import type { FailureStatisticsTransport, FailureStatisticsWindowId } from "./admin/domain"import { ADMIN_DIST_DIR, adminPage, hasAdminBuild } from "./admin-shell"import { createGenericProvider, deleteGenericProvider, GenericProviderError, listGenericProviders, type GenericProviderSummary,} from "./generic-providers"
const ADMIN_CAP = "admin"const ADMIN_SESSION_COOKIE = "llm_bridge_admin_session"const LOGIN_PROVIDERS: LoginProviderId[] = ["anthropic", "codex", "antigravity", "opencode-go"]
function isLoginProvider(value: string): value is LoginProviderId { return (LOGIN_PROVIDERS as string[]).includes(value)}
export function registerAdminRoutes( app: FastifyInstance, config: BridgeConfig, apiKeys: Map<string, ApiKeyEntry>, refreshModels: () => Promise<void>,): void { const adminSessions = new Map<string, string>()
function cookieValue(req: any, name: string): string | undefined { const header = req.headers?.cookie if (typeof header !== "string") return undefined for (const item of header.split(";")) { const separator = item.indexOf("=") if (separator < 0 || item.slice(0, separator).trim() !== name) continue try { return decodeURIComponent(item.slice(separator + 1).trim()) } catch { return undefined } } return undefined }
function secureRequest(req: any): boolean { if (req.protocol === "https") return true const forwarded = req.headers?.["x-forwarded-proto"] const value = Array.isArray(forwarded) ? forwarded[0] : forwarded return typeof value === "string" && value.split(",")[0]?.trim().toLowerCase() === "https" }
function sessionCookie(value: string, req: any, clear = false): string { return `${ADMIN_SESSION_COOKIE}=${clear ? "" : encodeURIComponent(value)}; Path=/admin; HttpOnly; SameSite=Strict${secureRequest(req) ? "; Secure" : ""}${clear ? "; Max-Age=0" : ""}` }
function requestApiKey(req: any): string | undefined { const authorization = req.headers?.authorization if (typeof authorization === "string" && authorization.trim()) return authorization const header = req.headers?.["x-api-key"] if (typeof header === "string" && header.trim()) return header if (Array.isArray(header) && typeof header[0] === "string") return header[0] return undefined }
/** Resolve the admin caller, or send the error and return null. */ function requireAdmin(req: FastifyRequest, reply: FastifyReply): ApiKeyEntry | true | null { if (apiKeys.size === 0) return true const sessionId = cookieValue(req, ADMIN_SESSION_COOKIE) const sessionKey = sessionId ? adminSessions.get(sessionId) : undefined const key = sessionKey ? (apiKeys.get(sessionKey) ?? null) : authenticate(requestApiKey(req), apiKeys) if (!key) { reply.code(401).send({ error: { message: "Invalid or missing API key", type: "authentication_error" } }) return null } if (!hasCap(key, ADMIN_CAP)) { reply.code(403).send({ error: { message: "Key lacks the 'admin' cap", type: "permission_error" } }) return null } return key }
const reaper = startSessionReaper() app.addHook("onClose", async () => clearInterval(reaper))
if (hasAdminBuild()) { app.register(fastifyStatic, { root: `${ADMIN_DIST_DIR}/assets`, prefix: "/admin/assets/", decorateReply: false, cacheControl: false, }) // Provider marks are plain files so they stay out of the JS bundle and can be // replaced without a rebuild of the app code. Their contents never change for // a given path, so let browsers keep them. app.register(fastifyStatic, { root: `${ADMIN_DIST_DIR}/logos`, prefix: "/admin/logos/", decorateReply: false, maxAge: "2w", immutable: true, }) }
// The shell is not secret — it holds no data and prompts for a key on load. // Everything under /admin/api is what actually needs guarding. const adminShell = async (_req: FastifyRequest, reply: FastifyReply) => { if (!hasAdminBuild()) { return reply.code(503).send({ error: "admin UI is not built yet; run `npm run build:admin` first" }) } return reply.type("text/html; charset=utf-8").send(adminPage()) } // Browsers append a trailing slash to a bare directory path, so both spellings // serve the shell rather than 404ing on whichever one a user typed. app.get("/admin", adminShell) app.get("/admin/", adminShell)
app.post("/admin/api/auth/session", async (req: any, reply: any) => { const body = req.body && typeof req.body === "object" && !Array.isArray(req.body) ? (req.body as Record<string, unknown>) : {} const supplied = typeof body.key === "string" ? body.key.trim() : "" const entry = supplied ? authenticate(supplied, apiKeys) : null if (!entry) return reply.code(401).send({ error: { message: "Invalid or missing API key", type: "authentication_error" } }) if (!hasCap(entry, ADMIN_CAP)) return reply.code(403).send({ error: { message: "Key lacks the 'admin' cap", type: "permission_error" } }) const sessionId = randomUUID() adminSessions.set(sessionId, entry.key) return reply .header("cache-control", "no-store") .header("set-cookie", sessionCookie(sessionId, req)) .send({ authenticated: true }) })
app.get("/admin/api/auth/session", async (req: any, reply: any) => { if (!requireAdmin(req, reply)) return return reply.header("cache-control", "no-store").send({ authenticated: true }) })
app.delete("/admin/api/auth/session", async (req: any, reply: any) => { const sessionId = cookieValue(req, ADMIN_SESSION_COOKIE) if (sessionId) adminSessions.delete(sessionId) return reply .header("cache-control", "no-store") .header("set-cookie", sessionCookie("", req, true)) .code(204) .send() })
const publicProviderStatus = (provider: LoginProviderId, accountId?: string) => { const { authPath: _authPath, ...status } = providerStatus(config, provider, accountId) return status }
app.get("/admin/api/providers", async (req: any, reply: any) => { const caller = requireAdmin(req, reply) if (!caller) return reapSessions() return reply.send({ providers: LOGIN_PROVIDERS.map((provider) => publicProviderStatus(provider)) }) })
const genericProviderError = (reply: FastifyReply, error: unknown) => { const status = error instanceof GenericProviderError ? error.statusCode : 500 const message = error instanceof GenericProviderError ? error.message : "Could not access saved generic providers" const type = status === 400 ? "invalid_request_error" : status === 404 ? "not_found" : status === 409 ? "conflict_error" : "server_error" return reply.code(status).send({ error: { message, type } }) }
app.get("/admin/api/generic-providers", async (req, reply) => { if (!requireAdmin(req, reply)) return try { return reply.header("cache-control", "no-store").send({ providers: listGenericProviders(config.providers) }) } catch (error) { return genericProviderError(reply, error) } })
app.post("/admin/api/generic-providers", async (req, reply) => { if (!requireAdmin(req, reply)) return let provider: GenericProviderSummary try { provider = createGenericProvider(config.providers, stateDir(), req.body) } catch (error) { return genericProviderError(reply, error) } try { await refreshModels() } catch { return reply .code(500) .send({ error: { message: "Generic provider was saved, but model refresh failed", type: "server_error" } }) } return reply.code(201).send({ provider }) })
app.delete<{ Params: { id: string } }>("/admin/api/generic-providers/:id", async (req, reply) => { if (!requireAdmin(req, reply)) return try { deleteGenericProvider(config.providers, stateDir(), req.params.id) } catch (error) { return genericProviderError(reply, error) } try { await refreshModels() } catch { return reply .code(500) .send({ error: { message: "Generic provider was removed, but model refresh failed", type: "server_error" } }) } return reply.send({ ok: true }) })
app.get("/admin/api/models", async (req: any, reply: any) => { if (!requireAdmin(req, reply)) return const observedByModel = getModelObservedUsage(stateDir()) const models = Object.values(config.models) .map((model) => ({ id: model.id, name: model.name, provider: model.provider, ...(model.contextWindow !== undefined ? { contextWindow: model.contextWindow } : {}), ...(model.maxTokens !== undefined ? { maxTokens: model.maxTokens } : {}), ...(model.reasoning !== undefined ? { reasoning: model.reasoning } : {}), ...(model.description ? { description: model.description } : {}), ...(model.defaultReasoningEffort ? { defaultReasoningEffort: model.defaultReasoningEffort } : {}), ...(model.supportedReasoningEfforts ? { supportedReasoningEfforts: model.supportedReasoningEfforts } : {}), ...(model.speedTiers ? { speedTiers: model.speedTiers } : {}), ...(model.cost ? { cost: model.cost } : {}), ...(observedByModel.has(model.id) ? { observedUsage: observedByModel.get(model.id) } : {}), })) .sort((left, right) => left.id.localeCompare(right.id)) return reply.send({ models }) })
app.get("/admin/api/settings", async (req: any, reply: any) => { if (!requireAdmin(req, reply)) return return reply.send({ codexFastMaxOutputCostPerMTok: config.codexFastMaxOutputCostPerMTok ?? 20, }) })
app.put("/admin/api/settings", async (req: any, reply: any) => { if (!requireAdmin(req, reply)) return const body = req.body && typeof req.body === "object" && !Array.isArray(req.body) ? (req.body as Record<string, unknown>) : {} const hasCap = Object.hasOwn(body, "codexFastMaxOutputCostPerMTok") if (!hasCap) return reply.code(400).send({ error: { message: "Supply a settings field", type: "invalid_request_error" } }) const cap = typeof body.codexFastMaxOutputCostPerMTok === "number" ? body.codexFastMaxOutputCostPerMTok : Number.NaN if (!Number.isFinite(cap) || cap < 0) { return reply.code(400).send({ error: { message: "codexFastMaxOutputCostPerMTok must be a non-negative number", type: "invalid_request_error", }, }) } config.codexFastMaxOutputCostPerMTok = cap await refreshModels() return reply.send({ codexFastMaxOutputCostPerMTok: cap }) })
app.get("/admin/api/upstream-errors", async (req: any, reply: any) => { if (!requireAdmin(req, reply)) return const rawLimit = Number(req.query?.limit) const rawOffset = Number(req.query?.offset) const limit = Number.isFinite(rawLimit) ? Math.min(500, Math.max(1, Math.trunc(rawLimit))) : 100 const offset = Number.isFinite(rawOffset) ? Math.max(0, Math.trunc(rawOffset)) : 0 return reply.send(listUpstreamErrors(stateDir(), limit, offset)) })
app.get("/admin/api/request-metrics", async (req: any, reply: any) => { if (!requireAdmin(req, reply)) return return reply.send( listRequestMetrics( stateDir(), Number(req.query?.limit), Number(req.query?.offset), typeof req.query?.conversation_hash === "string" ? req.query.conversation_hash : undefined, ), ) })
// Aggregated Codex attempt failures. The heavy work runs in a worker inside // getFailureStatistics, so this route only validates the window and forwards. const failureStatisticsAborts = new WeakMap<object, AbortController>() app.get<{ Querystring: { window?: unknown; model?: unknown; transport?: unknown } }>( "/admin/api/failure-stats", { onRequestAbort: async (request) => { failureStatisticsAborts.get(request)?.abort() }, }, async (request, reply) => { if (!requireAdmin(request, reply)) return reply.header("Cache-Control", "no-store") const { window: windowParam, model: modelParam, transport: transportParam } = request.query
if (windowParam !== undefined && typeof windowParam !== "string") return reply .code(400) .send({ error: { message: "window must be 24h, 7d or 30d", type: "invalid_request_error" } }) if (typeof windowParam === "string" && windowParam !== "" && !isFailureStatisticsWindowId(windowParam)) return reply.code(400).send({ error: { message: `Unknown window '${windowParam}'; expected 24h, 7d or 30d`, type: "invalid_request_error" }, }) const windowId: FailureStatisticsWindowId = isFailureStatisticsWindowId(windowParam) ? windowParam : "24h"
if (modelParam !== undefined && typeof modelParam !== "string") return reply .code(400) .send({ error: { message: "model must be a single string", type: "invalid_request_error" } }) if (typeof modelParam === "string" && modelParam.length > FAILURE_STATISTICS_MODEL_MAX_LENGTH) return reply.code(400).send({ error: { message: `model must be at most ${FAILURE_STATISTICS_MODEL_MAX_LENGTH} characters`, type: "invalid_request_error", }, }) const model = typeof modelParam === "string" && modelParam.length > 0 ? modelParam : null
if (transportParam !== undefined && typeof transportParam !== "string") return reply .code(400) .send({ error: { message: "transport must be http or ws", type: "invalid_request_error" } }) if ( typeof transportParam === "string" && transportParam !== "" && transportParam !== "http" && transportParam !== "ws" ) return reply.code(400).send({ error: { message: `Unknown transport '${transportParam}'; expected http or ws`, type: "invalid_request_error", }, }) const transport: FailureStatisticsTransport | null = transportParam === "http" || transportParam === "ws" ? transportParam : null
const abort = new AbortController() failureStatisticsAborts.set(request, abort) if (request.raw.aborted) abort.abort() try { const statistics = await getFailureStatistics(stateDir(), { window: windowId, model, transport, signal: abort.signal, }) if (abort.signal.aborted) return return reply.send(statistics) } catch (error) { if (abort.signal.aborted || reply.sent) return return reply .code(500) .send({ error: { message: error instanceof Error ? error.message : String(error), type: "server_error" } }) } }, )
const maskKey = (key: string): string => `${key.slice(0, 8)}…${key.slice(-4)}` const publicKey = (entry: ApiKeyEntry) => { const routing = routingForKey(stateDir(), entry.key) return { id: apiKeyIdForKey(entry.key), maskedKey: maskKey(entry.key), ...(entry.name ? { name: entry.name } : {}), allowedModels: entry.allowedModels, caps: entry.caps, usageScheme: routing.usageScheme, accounts: Object.fromEntries( Object.entries(routing.accounts) .filter(([provider]) => isLoginProvider(provider)) .map(([provider, ids]) => [provider, ids]), ), preferredAccounts: Object.fromEntries( Object.entries(routing.preferredAccounts).filter(([provider]) => isLoginProvider(provider)), ), } } app.get("/admin/api/keys", async (req: any, reply: any) => { if (!requireAdmin(req, reply)) return const editable = config.keysFile?.editable === true return reply.send({ enabled: editable, reason: !config.keysFile ? "API keys were not supplied with --keys" : (config.keysFile.error ?? undefined), keys: [...apiKeys.values()].map(publicKey), }) })
app.post("/admin/api/keys", async (req: any, reply: any) => { const caller = requireAdmin(req, reply) if (!caller) return if (config.keysFile?.editable !== true) { return reply.code(403).send({ error: { message: "API key management is disabled because the --keys file is not editable", type: "permission_error", }, }) } const body = req.body && typeof req.body === "object" ? (req.body as Record<string, unknown>) : {} const entry = { key: typeof body.key === "string" && body.key.trim() ? body.key.trim() : generateApiKey(), name: body.name, allowedModels: body.allowedModels ?? ["*"], caps: body.caps ?? [], } let parsed: ApiKeyEntry try { parsed = parseApiKeyEntries(entry)[0]! if (apiKeys.has(parsed.key)) throw new Error("that API key already exists") const next = [...apiKeys.values(), parsed] writeApiKeysFile(config.keysFile, next) apiKeys.set(parsed.key, parsed) } catch (err) { return reply .code(400) .send({ error: { message: err instanceof Error ? err.message : String(err), type: "invalid_request_error" } }) } return reply.code(201).send({ key: parsed.key, entry: publicKey(parsed) }) })
app.delete("/admin/api/keys/:id", async (req: any, reply: any) => { const caller = requireAdmin(req, reply) if (!caller) return if (config.keysFile?.editable !== true) { return reply.code(403).send({ error: { message: "API key management is disabled because the --keys file is not editable", type: "permission_error", }, }) } const key = keyForApiKeyId(req.params?.id, apiKeys.keys()) if (!key) return reply.code(404).send({ error: { message: "unknown API key", type: "invalid_request_error" } }) if (caller !== true && key === caller.key) return reply .code(409) .send({ error: { message: "cannot delete the key used for this admin request", type: "conflict_error" } }) const next = [...apiKeys.values()].filter((entry) => entry.key !== key) try { writeApiKeysFile(config.keysFile, next) } catch (err) { return reply .code(500) .send({ error: { message: err instanceof Error ? err.message : String(err), type: "server_error" } }) } apiKeys.delete(key) removeKeyRouting(stateDir(), key) removeRoutingStateForKey(stateDir(), apiKeyIdForKey(key)) return reply.code(204).send() })
app.get("/admin/api/accounts", async (req: any, reply: any) => { if (!requireAdmin(req, reply)) return const force = String(req.query?.refresh ?? "") === "true" const newestFirst = listAccounts(stateDir()).sort((left, right) => right.createdAt - left.createdAt) const accounts = await Promise.all( newestFirst.map(async (account) => { try { const usage = await getAccountUsage(config, stateDir(), account, { force }) return { ...account, history: getAccountTokenUsageHistory(stateDir(), account.id), status: { ...publicProviderStatus(account.provider, account.id), usage }, } } catch { return { ...account, history: getAccountTokenUsageHistory(stateDir(), account.id), status: { ...publicProviderStatus(account.provider, account.id), usage: { available: false as const, reason: "fetch-failed" as const }, }, } } }), ) return reply.send({ accounts }) })
app.post("/admin/api/accounts", async (req: any, reply: any) => { if (!requireAdmin(req, reply)) return const body = req.body && typeof req.body === "object" ? (req.body as Record<string, unknown>) : {} if (!isAccountProvider(body.provider)) { return reply.code(400).send({ error: { message: "provider must be 'anthropic', 'codex', 'antigravity', or 'opencode-go'", type: "invalid_request_error", }, }) } try { return reply.code(201).send({ account: createAccount(stateDir(), body.provider, body.label) }) } catch (err) { return reply .code(400) .send({ error: { message: err instanceof Error ? err.message : String(err), type: "invalid_request_error" } }) } })
app.patch<{ Params: { id: string }; Body: unknown }>("/admin/api/accounts/:id", async (req, reply) => { if (!requireAdmin(req, reply)) return const body = req.body const label = body && typeof body === "object" && !Array.isArray(body) && "label" in body ? body.label : undefined try { const account = renameAccount(stateDir(), req.params.id, label) return reply.send({ account }) } catch (err) { const message = err instanceof Error ? err.message : String(err) const status = message === "unknown provider account" ? 404 : 400 return reply.code(status).send({ error: { message, type: "invalid_request_error" } }) } })
app.delete("/admin/api/accounts/:id", async (req: any, reply: any) => { if (!requireAdmin(req, reply)) return try { const account = getAccount(stateDir(), req.params?.id) if (!account || !deleteAccount(stateDir(), req.params?.id)) { return reply.code(404).send({ error: { message: "account not found", type: "invalid_request_error" } }) } return reply.code(204).send() } catch (err) { return reply .code(400) .send({ error: { message: err instanceof Error ? err.message : String(err), type: "invalid_request_error" } }) } }) app.patch("/admin/api/keys/:id", async (req: any, reply: any) => { if (!requireAdmin(req, reply)) return if (config.keysFile?.editable !== true) { return reply.code(403).send({ error: { message: "API key management is disabled because the --keys file is not editable", type: "permission_error", }, }) } const key = keyForApiKeyId(req.params?.id, apiKeys.keys()) if (!key) return reply.code(404).send({ error: { message: "unknown API key", type: "invalid_request_error" } }) const existing = apiKeys.get(key)! const body = req.body && typeof req.body === "object" ? (req.body as Record<string, unknown>) : {} try { const updated = parseApiKeyEntries({ key, name: body.name === undefined ? existing.name : body.name, allowedModels: body.allowedModels === undefined ? existing.allowedModels : body.allowedModels, caps: body.caps === undefined ? existing.caps : body.caps, })[0]! const next = [...apiKeys.values()].map((entry) => (entry.key === key ? updated : entry)) writeApiKeysFile(config.keysFile, next) apiKeys.set(key, updated) return reply.send({ entry: publicKey(updated) }) } catch (err) { return reply .code(400) .send({ error: { message: err instanceof Error ? err.message : String(err), type: "invalid_request_error" } }) } })
app.put("/admin/api/keys/:id/routing", async (req: any, reply: any) => { if (!requireAdmin(req, reply)) return const key = keyForApiKeyId(req.params?.id, apiKeys.keys()) if (!key) return reply.code(404).send({ error: { message: "unknown API key", type: "invalid_request_error" } }) const body = req.body && typeof req.body === "object" && !Array.isArray(req.body) ? (req.body as Record<string, unknown>) : {} const allowed: Record<string, true> = { usageScheme: true, accounts: true, preferredAccounts: true, } if (Object.keys(body).some((field) => !Object.hasOwn(allowed, field))) { return reply .code(400) .send({ error: { message: "routing body contains unknown fields", type: "invalid_request_error" } }) } const usageScheme = body.usageScheme const accounts = body.accounts const preferredAccounts = body.preferredAccounts if ( usageScheme !== "fill-first" && usageScheme !== "lowest-usage" && usageScheme !== "round-robin" && usageScheme !== "specific-plan-first" ) { return reply.code(400).send({ error: { message: "unknown usage scheme", type: "invalid_request_error" } }) } if ( !accounts || typeof accounts !== "object" || Array.isArray(accounts) || !preferredAccounts || typeof preferredAccounts !== "object" || Array.isArray(preferredAccounts) ) { return reply.code(400).send({ error: { message: "routing accounts and preferredAccounts must be objects", type: "invalid_request_error" }, }) } if ( Object.keys(accounts).some((provider) => !isLoginProvider(provider)) || Object.keys(preferredAccounts).some((provider) => !isLoginProvider(provider)) ) { return reply .code(400) .send({ error: { message: "routing supports only known login providers", type: "invalid_request_error" } }) } for (const provider of LOGIN_PROVIDERS) { const pool = (accounts as Record<string, unknown>)[provider] const preferred = (preferredAccounts as Record<string, unknown>)[provider] if (pool !== undefined && (!Array.isArray(pool) || pool.some((id) => typeof id !== "string"))) { return reply.code(400).send({ error: { message: `${provider} account pool must be an array of IDs`, type: "invalid_request_error" }, }) } if (preferred !== undefined && typeof preferred !== "string") { return reply .code(400) .send({ error: { message: `${provider} preferred account must be an ID`, type: "invalid_request_error" } }) } } try { configureKeyRouting(stateDir(), key, { usageScheme, accounts: accounts as Partial<Record<AccountProvider, string[]>>, preferredAccounts: preferredAccounts as Partial<Record<AccountProvider, string>>, }) return reply.send({ key: publicKey(apiKeys.get(key)!) }) } catch (err) { return reply .code(400) .send({ error: { message: err instanceof Error ? err.message : String(err), type: "invalid_request_error" } }) } })
app.post("/admin/api/login/:provider", async (req: any, reply: any) => { if (!requireAdmin(req, reply)) return const provider = String(req.params?.provider ?? "") if (!isLoginProvider(provider)) { return reply .code(404) .send({ error: { message: `Unknown login provider '${provider}'`, type: "invalid_request_error" } }) } const body = req.body && typeof req.body === "object" ? (req.body as Record<string, unknown>) : {} const requestedAccountId = typeof body.accountId === "string" ? body.accountId : undefined const account = requestedAccountId ? getAccount(stateDir(), requestedAccountId, provider) : (defaultAccount(stateDir(), provider) ?? createAccount(stateDir(), provider)) if (!account) { return reply.code(404).send({ error: { message: "unknown provider account", type: "invalid_request_error" } }) } const accountId = account.id try { const session = await startLogin( config, provider, async () => { registerProvider(config, provider, resolveAuthPath(config, provider, accountId)) await refreshModels() }, accountId, ) return reply.send(session) } catch (err) { const message = err instanceof Error ? err.message : String(err) console.error(`[bridge] failed to start ${provider} login: ${message}`) return reply.code(502).send({ error: { message, type: "server_error" } }) } })
app.get("/admin/api/session/:id", async (req: any, reply: any) => { if (!requireAdmin(req, reply)) return reapSessions() const session = getSession(String(req.params?.id ?? "")) if (!session) return reply.code(404).send({ error: { message: "Unknown login session", type: "invalid_request_error" } }) return reply.send(publicSession(session)) })
app.post("/admin/api/session/:id/code", async (req: any, reply: any) => { if (!requireAdmin(req, reply)) return const session = getSession(String(req.params?.id ?? "")) if (!session) return reply.code(404).send({ error: { message: "Unknown login session", type: "invalid_request_error" } }) if (session.kind !== "paste") { return reply.code(400).send({ error: { message: `The ${session.provider} flow does not take a pasted code`, type: "invalid_request_error" }, }) } if (session.status !== "awaiting_code") { return reply.code(409).send({ error: { message: `Session is ${session.status}`, type: "invalid_request_error" } }) }
const code = typeof req.body?.code === "string" ? req.body.code.trim() : "" if (!code) return reply .code(400) .send({ error: { message: "Body must include a non-empty 'code'", type: "invalid_request_error" } })
session.status = "polling" session.submitCode?.(code) return reply.send(publicSession(session)) })
app.delete("/admin/api/session/:id", async (req: any, reply: any) => { if (!requireAdmin(req, reply)) return const cancelled = await cancelLogin(String(req.params?.id ?? "")) if (!cancelled) return reply.code(404).send({ error: { message: "Unknown login session", type: "invalid_request_error" } }) return reply.code(204).send() })
app.post("/admin/api/logout/:provider", async (req: any, reply: any) => { if (!requireAdmin(req, reply)) return const provider = String(req.params?.provider ?? "") if (!isLoginProvider(provider)) { return reply .code(404) .send({ error: { message: `Unknown login provider '${provider}'`, type: "invalid_request_error" } }) } const body = req.body && typeof req.body === "object" ? (req.body as Record<string, unknown>) : {} const accountId = typeof body.accountId === "string" ? body.accountId : "" if (!getAccount(stateDir(), accountId, provider)) { return reply.code(404).send({ error: { message: "unknown provider account", type: "invalid_request_error" } }) } try { logout(config, provider, accountId) return reply.send(publicProviderStatus(provider, accountId)) } catch (err) { const message = err instanceof Error ? err.message : String(err) return reply.code(500).send({ error: { message, type: "server_error" } }) } })}