Spaces:
Runtime error
Runtime error
| import { printHeading } from "../io.mjs"; | |
| import { | |
| ensureProviderSchema, | |
| getProviderApiKey, | |
| listProviderConnections, | |
| removeProviderConnectionByProvider, | |
| upsertApiKeyProviderConnection, | |
| } from "../provider-store.mjs"; | |
| import { openOmniRouteDb } from "../sqlite.mjs"; | |
| import { loadAvailableProviders } from "../provider-catalog.mjs"; | |
| import { apiFetch, isServerUp } from "../api.mjs"; | |
| import { t } from "../i18n.mjs"; | |
| function getValidProviderIds() { | |
| try { | |
| return new Set(loadAvailableProviders().map((p) => p.id)); | |
| } catch { | |
| return null; | |
| } | |
| } | |
| function maskKey(raw) { | |
| if (!raw || raw.length <= 8) return "***"; | |
| return raw.slice(0, 6) + "***" + raw.slice(-4); | |
| } | |
| export function registerKeys(program) { | |
| const keys = program.command("keys").description(t("keys.title")); | |
| keys | |
| .command("add <provider> [apiKey]") | |
| .description(t("keys.addDescription")) | |
| .option("--stdin", t("keys.stdinOpt")) | |
| .action(async (provider, apiKey, opts, cmd) => { | |
| const globalOpts = cmd.parent.optsWithGlobals(); | |
| const exitCode = await runKeysAddCommand(provider, apiKey, { ...opts, ...globalOpts }); | |
| if (exitCode !== 0) process.exit(exitCode); | |
| }); | |
| keys | |
| .command("list") | |
| .description(t("keys.listDescription")) | |
| .option("--json", t("common.jsonOpt")) | |
| .action(async (opts, cmd) => { | |
| const globalOpts = cmd.parent.optsWithGlobals(); | |
| const exitCode = await runKeysListCommand({ ...opts, ...globalOpts }); | |
| if (exitCode !== 0) process.exit(exitCode); | |
| }); | |
| keys | |
| .command("remove <provider>") | |
| .description(t("keys.removeDescription")) | |
| .option("--yes", t("common.yesOpt")) | |
| .action(async (provider, opts, cmd) => { | |
| const globalOpts = cmd.parent.optsWithGlobals(); | |
| const exitCode = await runKeysRemoveCommand(provider, { ...opts, ...globalOpts }); | |
| if (exitCode !== 0) process.exit(exitCode); | |
| }); | |
| keys | |
| .command("regenerate <id>") | |
| .description(t("keys.regenerateDescription")) | |
| .option("--yes", t("common.yesOpt")) | |
| .action(async (id, opts, cmd) => { | |
| const globalOpts = cmd.parent.optsWithGlobals(); | |
| const exitCode = await runKeysRegenerateCommand(id, { ...opts, ...globalOpts }); | |
| if (exitCode !== 0) process.exit(exitCode); | |
| }); | |
| keys | |
| .command("revoke <id>") | |
| .description(t("keys.revokeDescription")) | |
| .option("--yes", t("common.yesOpt")) | |
| .action(async (id, opts, cmd) => { | |
| const globalOpts = cmd.parent.optsWithGlobals(); | |
| const exitCode = await runKeysRevokeCommand(id, { ...opts, ...globalOpts }); | |
| if (exitCode !== 0) process.exit(exitCode); | |
| }); | |
| keys | |
| .command("reveal <id>") | |
| .description(t("keys.revealDescription")) | |
| .action(async (id, opts, cmd) => { | |
| const globalOpts = cmd.parent.optsWithGlobals(); | |
| const exitCode = await runKeysRevealCommand(id, { ...opts, ...globalOpts }); | |
| if (exitCode !== 0) process.exit(exitCode); | |
| }); | |
| keys | |
| .command("usage <id>") | |
| .description(t("keys.usageDescription")) | |
| .option("--limit <n>", t("keys.usageLimitOpt"), "20") | |
| .action(async (id, opts, cmd) => { | |
| const globalOpts = cmd.parent.optsWithGlobals(); | |
| const exitCode = await runKeysUsageCommand(id, { ...opts, ...globalOpts }); | |
| if (exitCode !== 0) process.exit(exitCode); | |
| }); | |
| const policy = keys.command("policy").description(t("keys.policy.title")); | |
| policy | |
| .command("show <id>") | |
| .description(t("keys.policy.showDescription")) | |
| .action(async (id, opts, cmd) => { | |
| const globalOpts = cmd.parent.parent.optsWithGlobals(); | |
| const exitCode = await runKeysPolicyShowCommand(id, { ...opts, ...globalOpts }); | |
| if (exitCode !== 0) process.exit(exitCode); | |
| }); | |
| policy | |
| .command("set <id>") | |
| .description(t("keys.policy.setDescription")) | |
| .option("--rate-limit <n>", t("keys.policy.rateLimitOpt"), parseInt) | |
| .option("--max-cost <n>", t("keys.policy.maxCostOpt"), parseFloat) | |
| .option("--allowed-models <list>", t("keys.policy.allowedModelsOpt")) | |
| .action(async (id, opts, cmd) => { | |
| const globalOpts = cmd.parent.parent.optsWithGlobals(); | |
| const exitCode = await runKeysPolicySetCommand(id, { ...opts, ...globalOpts }); | |
| if (exitCode !== 0) process.exit(exitCode); | |
| }); | |
| const expiration = keys.command("expiration").description(t("keys.expiration.title")); | |
| expiration | |
| .command("list") | |
| .description(t("keys.expiration.listDescription")) | |
| .option("--days <n>", t("keys.expiration.daysOpt"), "30") | |
| .option("--json", t("common.jsonOpt")) | |
| .action(async (opts, cmd) => { | |
| const globalOpts = cmd.parent.parent.optsWithGlobals(); | |
| const exitCode = await runKeysExpirationListCommand({ ...opts, ...globalOpts }); | |
| if (exitCode !== 0) process.exit(exitCode); | |
| }); | |
| keys | |
| .command("rotate <id>") | |
| .description(t("keys.rotateDescription")) | |
| .option("--grace-period <ms>", t("keys.graceOpt"), "60000") | |
| .option("--yes", t("common.yesOpt")) | |
| .action(async (id, opts, cmd) => { | |
| const globalOpts = cmd.parent.optsWithGlobals(); | |
| const exitCode = await runKeysRotateCommand(id, { ...opts, ...globalOpts }); | |
| if (exitCode !== 0) process.exit(exitCode); | |
| }); | |
| } | |
| export async function runKeysAddCommand(provider, apiKey, opts = {}) { | |
| if (!provider) { | |
| console.error(t("keys.providerRequired")); | |
| return 1; | |
| } | |
| let key = apiKey; | |
| if (opts.stdin) { | |
| key = await readStdin(); | |
| if (!key) { | |
| console.error(t("keys.stdinEmpty")); | |
| return 1; | |
| } | |
| } | |
| if (!key) { | |
| console.error(t("keys.keyRequired")); | |
| return 1; | |
| } | |
| const providerLower = provider.toLowerCase(); | |
| const validIds = getValidProviderIds(); | |
| if (validIds && !validIds.has(providerLower)) { | |
| console.error(t("keys.unknownProvider", { provider: providerLower })); | |
| return 1; | |
| } | |
| const serverUp = await isServerUp(); | |
| if (serverUp) { | |
| try { | |
| const res = await apiFetch("/api/v1/providers/keys", { | |
| method: "POST", | |
| body: { provider: providerLower, apiKey: key }, | |
| retry: false, | |
| acceptNotOk: true, | |
| }); | |
| if (res.ok) { | |
| console.log(t("keys.added", { provider: providerLower })); | |
| return 0; | |
| } | |
| if (res.status >= 400 && res.status < 500) { | |
| console.error(t("common.error", { message: `HTTP ${res.status}` })); | |
| return 1; | |
| } | |
| } catch {} | |
| } | |
| const { db } = await openOmniRouteDb(); | |
| try { | |
| const existing = listProviderConnections(db).find( | |
| (c) => c.provider === providerLower && c.authType === "apikey" | |
| ); | |
| upsertApiKeyProviderConnection(db, { | |
| provider: providerLower, | |
| name: existing?.name || providerLower, | |
| apiKey: key, | |
| }); | |
| console.log(t("keys.added", { provider: providerLower })); | |
| return 0; | |
| } finally { | |
| db.close(); | |
| } | |
| } | |
| export async function runKeysListCommand(opts = {}) { | |
| const serverUp = await isServerUp(); | |
| if (serverUp) { | |
| try { | |
| const res = await apiFetch("/api/v1/providers/keys", { retry: false, acceptNotOk: true }); | |
| if (res.ok) { | |
| const data = await res.json(); | |
| const connections = data.keys || data.connections || data.items || data; | |
| if (Array.isArray(connections)) { | |
| return _printKeysList(connections, opts); | |
| } | |
| } | |
| } catch {} | |
| } | |
| const { db } = await openOmniRouteDb(); | |
| try { | |
| ensureProviderSchema(db); | |
| const connections = listProviderConnections(db).filter( | |
| (c) => c.authType === "apikey" && c.apiKey | |
| ); | |
| return _printKeysList(connections, opts); | |
| } finally { | |
| db.close(); | |
| } | |
| } | |
| function _printKeysList(connections, opts) { | |
| if (opts.json || opts.output === "json") { | |
| const rows = connections.map((c) => ({ | |
| id: c.id, | |
| provider: c.provider, | |
| name: c.name, | |
| isActive: c.isActive !== false, | |
| maskedKey: maskKey(c.apiKey || c.maskedKey || ""), | |
| })); | |
| console.log(JSON.stringify({ keys: rows }, null, 2)); | |
| return 0; | |
| } | |
| printHeading(t("keys.title")); | |
| if (connections.length === 0) { | |
| console.log(t("keys.noKeys")); | |
| return 0; | |
| } | |
| for (const c of connections) { | |
| let masked = c.maskedKey || ""; | |
| if (!masked && c.apiKey) { | |
| try { | |
| masked = maskKey(getProviderApiKey(c)); | |
| } catch { | |
| masked = maskKey(c.apiKey); | |
| } | |
| } | |
| const status = c.isActive !== false ? "\x1b[32m● enabled\x1b[0m" : "\x1b[33m○ disabled\x1b[0m"; | |
| console.log(` ${(c.provider || "").padEnd(20)} ${masked.padEnd(22)} ${status}`); | |
| } | |
| console.log(`\n${t("keys.listed", { count: connections.length })}`); | |
| return 0; | |
| } | |
| export async function runKeysRemoveCommand(provider, opts = {}) { | |
| if (!provider) { | |
| console.error(t("keys.providerRequired")); | |
| return 1; | |
| } | |
| const providerLower = provider.toLowerCase(); | |
| if (!opts.yes) { | |
| const readline = await import("node:readline"); | |
| const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); | |
| const answer = await new Promise((resolve) => | |
| rl.question(t("keys.confirmRemove", { id: providerLower }) + " [y/N] ", resolve) | |
| ); | |
| rl.close(); | |
| if (!/^y(es)?$/i.test(answer)) { | |
| console.log(t("common.cancelled")); | |
| return 0; | |
| } | |
| } | |
| const serverUp = await isServerUp(); | |
| if (serverUp) { | |
| try { | |
| const res = await apiFetch(`/api/v1/providers/keys/${encodeURIComponent(providerLower)}`, { | |
| method: "DELETE", | |
| retry: false, | |
| acceptNotOk: true, | |
| }); | |
| if (res.ok) { | |
| console.log(t("keys.removed")); | |
| return 0; | |
| } | |
| } catch {} | |
| } | |
| const { db } = await openOmniRouteDb(); | |
| try { | |
| const changes = removeProviderConnectionByProvider(db, providerLower); | |
| if (changes > 0) { | |
| console.log(t("keys.removed")); | |
| return 0; | |
| } | |
| console.log(t("keys.noKeys")); | |
| return 0; | |
| } finally { | |
| db.close(); | |
| } | |
| } | |
| async function readStdin() { | |
| return new Promise((resolve) => { | |
| let data = ""; | |
| process.stdin.setEncoding("utf8"); | |
| process.stdin.on("data", (chunk) => (data += chunk)); | |
| process.stdin.on("end", () => resolve(data.trim())); | |
| }); | |
| } | |
| export async function runKeysRegenerateCommand(id, opts = {}) { | |
| if (!opts.yes) { | |
| const readline = await import("node:readline"); | |
| const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); | |
| const answer = await new Promise((r) => | |
| rl.question(t("keys.confirmRegenerate", { id }) + " [y/N] ", r) | |
| ); | |
| rl.close(); | |
| if (!/^y(es)?$/i.test(answer)) { | |
| console.log(t("common.cancelled")); | |
| return 0; | |
| } | |
| } | |
| if (!(await isServerUp())) { | |
| console.error(t("common.serverOffline")); | |
| return 1; | |
| } | |
| try { | |
| const res = await apiFetch(`/api/v1/registered-keys/${encodeURIComponent(id)}/regenerate`, { | |
| method: "POST", | |
| retry: false, | |
| }); | |
| if (!res.ok) { | |
| console.error(t("common.error", { message: `HTTP ${res.status}` })); | |
| return 1; | |
| } | |
| const data = await res.json(); | |
| console.log(t("keys.regenerated", { key: data.key || data.apiKey || "(see dashboard)" })); | |
| return 0; | |
| } catch (err) { | |
| console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) })); | |
| return 1; | |
| } | |
| } | |
| export async function runKeysRevokeCommand(id, opts = {}) { | |
| if (!opts.yes) { | |
| const readline = await import("node:readline"); | |
| const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); | |
| const answer = await new Promise((r) => | |
| rl.question(t("keys.confirmRevoke", { id }) + " [y/N] ", r) | |
| ); | |
| rl.close(); | |
| if (!/^y(es)?$/i.test(answer)) { | |
| console.log(t("common.cancelled")); | |
| return 0; | |
| } | |
| } | |
| if (!(await isServerUp())) { | |
| console.error(t("common.serverOffline")); | |
| return 1; | |
| } | |
| try { | |
| const res = await apiFetch(`/api/v1/registered-keys/${encodeURIComponent(id)}/revoke`, { | |
| method: "POST", | |
| retry: false, | |
| }); | |
| if (!res.ok) { | |
| console.error(t("common.error", { message: `HTTP ${res.status}` })); | |
| return 1; | |
| } | |
| console.log(t("keys.revoked", { id })); | |
| return 0; | |
| } catch (err) { | |
| console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) })); | |
| return 1; | |
| } | |
| } | |
| export async function runKeysRevealCommand(id, opts = {}) { | |
| process.stderr.write(t("keys.revealWarning") + "\n"); | |
| if (!(await isServerUp())) { | |
| console.error(t("common.serverOffline")); | |
| return 1; | |
| } | |
| try { | |
| const res = await apiFetch(`/api/v1/registered-keys/${encodeURIComponent(id)}/reveal`, { | |
| retry: false, | |
| }); | |
| if (!res.ok) { | |
| console.error(t("common.error", { message: `HTTP ${res.status}` })); | |
| return 1; | |
| } | |
| const data = await res.json(); | |
| console.log(data.key || data.apiKey || "(not available)"); | |
| return 0; | |
| } catch (err) { | |
| console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) })); | |
| return 1; | |
| } | |
| } | |
| export async function runKeysUsageCommand(id, opts = {}) { | |
| if (!(await isServerUp())) { | |
| console.error(t("common.serverOffline")); | |
| return 1; | |
| } | |
| const limit = opts.limit || "20"; | |
| try { | |
| const res = await apiFetch( | |
| `/api/v1/registered-keys/${encodeURIComponent(id)}/usage?limit=${limit}`, | |
| { retry: false } | |
| ); | |
| if (!res.ok) { | |
| console.error(t("common.error", { message: `HTTP ${res.status}` })); | |
| return 1; | |
| } | |
| const data = await res.json(); | |
| const rows = data.usage || data.requests || data.items || []; | |
| if (rows.length === 0) { | |
| console.log(t("keys.noUsage")); | |
| return 0; | |
| } | |
| for (const r of rows) { | |
| const ts = r.timestamp || r.createdAt || ""; | |
| const path = r.path || r.endpoint || ""; | |
| const status = r.status || r.statusCode || ""; | |
| console.log(` ${ts} ${String(status).padEnd(4)} ${path}`); | |
| } | |
| return 0; | |
| } catch (err) { | |
| console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) })); | |
| return 1; | |
| } | |
| } | |
| export async function runKeysPolicyShowCommand(id, opts = {}) { | |
| if (!(await isServerUp())) { | |
| console.error(t("common.serverOffline")); | |
| return 1; | |
| } | |
| try { | |
| const res = await apiFetch(`/api/v1/registered-keys/${encodeURIComponent(id)}/policy`, { | |
| acceptNotOk: true, | |
| retry: false, | |
| }); | |
| if (!res.ok) { | |
| console.error(t("common.error", { message: `HTTP ${res.status}` })); | |
| return 1; | |
| } | |
| const data = await res.json(); | |
| if (opts.output === "json" || opts.json) { | |
| console.log(JSON.stringify(data, null, 2)); | |
| return 0; | |
| } | |
| console.log(t("keys.policy.title") + ` (${id}):`); | |
| console.log(` rate_limit: ${data.rateLimit ?? data.rate_limit ?? "(unset)"}`); | |
| console.log(` max_cost: ${data.maxCost ?? data.max_cost ?? "(unset)"}`); | |
| console.log( | |
| ` allowed_models: ${(data.allowedModels ?? data.allowed_models ?? []).join(", ") || "(all)"}` | |
| ); | |
| return 0; | |
| } catch (err) { | |
| console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) })); | |
| return 1; | |
| } | |
| } | |
| export async function runKeysPolicySetCommand(id, opts = {}) { | |
| const body = {}; | |
| if (opts.rateLimit != null) body.rateLimit = Number(opts.rateLimit); | |
| if (opts.maxCost != null) body.maxCost = Number(opts.maxCost); | |
| if (opts.allowedModels) body.allowedModels = opts.allowedModels.split(",").map((s) => s.trim()); | |
| if (Object.keys(body).length === 0) { | |
| console.error(t("keys.policy.nothingToSet")); | |
| return 1; | |
| } | |
| if (!(await isServerUp())) { | |
| console.error(t("common.serverOffline")); | |
| return 1; | |
| } | |
| try { | |
| const res = await apiFetch(`/api/v1/registered-keys/${encodeURIComponent(id)}/policy`, { | |
| method: "PATCH", | |
| body, | |
| acceptNotOk: true, | |
| retry: false, | |
| }); | |
| if (!res.ok) { | |
| console.error(t("common.error", { message: `HTTP ${res.status}` })); | |
| return 1; | |
| } | |
| console.log(t("keys.policy.updated")); | |
| return 0; | |
| } catch (err) { | |
| console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) })); | |
| return 1; | |
| } | |
| } | |
| export async function runKeysExpirationListCommand(opts = {}) { | |
| if (!(await isServerUp())) { | |
| console.error(t("common.serverOffline")); | |
| return 1; | |
| } | |
| const days = Number(opts.days || 30); | |
| try { | |
| const res = await apiFetch(`/api/v1/registered-keys?expiring=true&days=${days}`, { | |
| acceptNotOk: true, | |
| retry: false, | |
| }); | |
| if (!res.ok) { | |
| console.error(t("common.error", { message: `HTTP ${res.status}` })); | |
| return 1; | |
| } | |
| const data = await res.json(); | |
| const rows = data.keys || data.items || data; | |
| if (!Array.isArray(rows) || rows.length === 0) { | |
| console.log(t("keys.expiration.none", { days })); | |
| return 0; | |
| } | |
| if (opts.json || opts.output === "json") { | |
| console.log(JSON.stringify(rows, null, 2)); | |
| return 0; | |
| } | |
| console.log(t("keys.expiration.listTitle", { days })); | |
| for (const k of rows) { | |
| const exp = k.expiresAt || k.expires_at || "(unknown)"; | |
| console.log(` ${(k.id || "").padEnd(24)} ${(k.name || "").padEnd(20)} expires: ${exp}`); | |
| } | |
| return 0; | |
| } catch (err) { | |
| console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) })); | |
| return 1; | |
| } | |
| } | |
| export async function runKeysRotateCommand(id, opts = {}) { | |
| if (!opts.yes) { | |
| const readline = await import("node:readline"); | |
| const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); | |
| const answer = await new Promise((r) => | |
| rl.question(t("keys.confirmRotate", { id }) + " [y/N] ", r) | |
| ); | |
| rl.close(); | |
| if (!/^y(es)?$/i.test(answer)) { | |
| console.log(t("common.cancelled")); | |
| return 0; | |
| } | |
| } | |
| if (!(await isServerUp())) { | |
| console.error(t("common.serverOffline")); | |
| return 1; | |
| } | |
| const gracePeriod = Number(opts.gracePeriod || 60000); | |
| try { | |
| const res = await apiFetch(`/api/v1/registered-keys/${encodeURIComponent(id)}/rotate`, { | |
| method: "POST", | |
| body: { gracePeriodMs: gracePeriod }, | |
| acceptNotOk: true, | |
| retry: false, | |
| }); | |
| if (!res.ok) { | |
| console.error(t("common.error", { message: `HTTP ${res.status}` })); | |
| return 1; | |
| } | |
| const data = await res.json(); | |
| const newId = data.newKeyId || data.id || "(see dashboard)"; | |
| console.log(t("keys.rotated", { id, newId })); | |
| return 0; | |
| } catch (err) { | |
| console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) })); | |
| return 1; | |
| } | |
| } | |