mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
fix(cli): R1+R2 — keys.mjs HTTP-first + eliminar SQL cru + policy/expiration/rotate
- provider-store.mjs: adiciona removeProviderConnectionByProvider() para abstrair o DELETE que estava inline em keys.mjs (elimina SQL cru de bin/) - keys.mjs: runKeysListCommand e runKeysRemoveCommand agora tentam HTTP primeiro; fallback DB usa funções do provider-store sem SQL inline - keys.mjs: novos subcomandos keys policy show/set, keys expiration list, keys rotate (completa spec 8.7 para keys) - locales: chaves i18n completas incluindo common.jsonOpt e common.yesOpt - testes: cobre novos exports e comportamento offline de runKeysListCommand
This commit is contained in:
@@ -3,6 +3,7 @@ import {
|
||||
ensureProviderSchema,
|
||||
getProviderApiKey,
|
||||
listProviderConnections,
|
||||
removeProviderConnectionByProvider,
|
||||
upsertApiKeyProviderConnection,
|
||||
} from "../provider-store.mjs";
|
||||
import { openOmniRouteDb } from "../sqlite.mjs";
|
||||
@@ -28,8 +29,8 @@ export function registerKeys(program) {
|
||||
|
||||
keys
|
||||
.command("add <provider> [apiKey]")
|
||||
.description("Add or update an API key for a provider")
|
||||
.option("--stdin", "Read API key from stdin instead of argument")
|
||||
.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 });
|
||||
@@ -38,8 +39,8 @@ export function registerKeys(program) {
|
||||
|
||||
keys
|
||||
.command("list")
|
||||
.description("List all configured API keys")
|
||||
.option("--json", "Output as JSON")
|
||||
.description(t("keys.listDescription"))
|
||||
.option("--json", t("common.jsonOpt"))
|
||||
.action(async (opts, cmd) => {
|
||||
const globalOpts = cmd.parent.optsWithGlobals();
|
||||
const exitCode = await runKeysListCommand({ ...opts, ...globalOpts });
|
||||
@@ -48,8 +49,8 @@ export function registerKeys(program) {
|
||||
|
||||
keys
|
||||
.command("remove <provider>")
|
||||
.description("Remove an API key for a provider")
|
||||
.option("--yes", "Skip confirmation prompt")
|
||||
.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 });
|
||||
@@ -58,8 +59,8 @@ export function registerKeys(program) {
|
||||
|
||||
keys
|
||||
.command("regenerate <id>")
|
||||
.description("Regenerate an OmniRoute API key (management keys)")
|
||||
.option("--yes", "Skip confirmation prompt")
|
||||
.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 });
|
||||
@@ -68,8 +69,8 @@ export function registerKeys(program) {
|
||||
|
||||
keys
|
||||
.command("revoke <id>")
|
||||
.description("Revoke an OmniRoute API key")
|
||||
.option("--yes", "Skip confirmation prompt")
|
||||
.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 });
|
||||
@@ -78,7 +79,7 @@ export function registerKeys(program) {
|
||||
|
||||
keys
|
||||
.command("reveal <id>")
|
||||
.description("Reveal the unmasked API key value")
|
||||
.description(t("keys.revealDescription"))
|
||||
.action(async (id, opts, cmd) => {
|
||||
const globalOpts = cmd.parent.optsWithGlobals();
|
||||
const exitCode = await runKeysRevealCommand(id, { ...opts, ...globalOpts });
|
||||
@@ -87,18 +88,65 @@ export function registerKeys(program) {
|
||||
|
||||
keys
|
||||
.command("usage <id>")
|
||||
.description("Show recent usage for an API key")
|
||||
.option("--limit <n>", "Number of recent requests", "20")
|
||||
.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("Provider is required. Usage: omniroute keys add <provider> [apiKey]");
|
||||
console.error(t("keys.providerRequired"));
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -106,24 +154,23 @@ export async function runKeysAddCommand(provider, apiKey, opts = {}) {
|
||||
if (opts.stdin) {
|
||||
key = await readStdin();
|
||||
if (!key) {
|
||||
console.error("No API key provided via stdin.");
|
||||
console.error(t("keys.stdinEmpty"));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!key) {
|
||||
console.error("API key is required. Usage: omniroute keys add <provider> <apiKey>");
|
||||
console.error(t("keys.keyRequired"));
|
||||
return 1;
|
||||
}
|
||||
|
||||
const providerLower = provider.toLowerCase();
|
||||
const validIds = getValidProviderIds();
|
||||
if (validIds && !validIds.has(providerLower)) {
|
||||
console.error(`Unknown provider: ${providerLower}`);
|
||||
console.error(t("keys.unknownProvider", { provider: providerLower }));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Server-first
|
||||
const serverUp = await isServerUp();
|
||||
if (serverUp) {
|
||||
try {
|
||||
@@ -139,7 +186,6 @@ export async function runKeysAddCommand(provider, apiKey, opts = {}) {
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// DB fallback
|
||||
const { db } = await openOmniRouteDb();
|
||||
try {
|
||||
const existing = listProviderConnections(db).find(
|
||||
@@ -158,53 +204,72 @@ export async function runKeysAddCommand(provider, apiKey, opts = {}) {
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
if (opts.json) {
|
||||
const rows = connections.map((c) => ({
|
||||
id: c.id,
|
||||
provider: c.provider,
|
||||
name: c.name,
|
||||
isActive: c.isActive,
|
||||
maskedKey: maskKey(c.apiKey),
|
||||
}));
|
||||
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 raw = "";
|
||||
try {
|
||||
raw = getProviderApiKey(c);
|
||||
} catch {
|
||||
raw = c.apiKey || "";
|
||||
}
|
||||
const masked = maskKey(raw);
|
||||
const status = c.isActive ? "\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;
|
||||
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("Provider is required. Usage: omniroute keys remove <provider>");
|
||||
console.error(t("keys.providerRequired"));
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -223,16 +288,25 @@ export async function runKeysRemoveCommand(provider, opts = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
ensureProviderSchema(db);
|
||||
const result = db
|
||||
.prepare(
|
||||
"DELETE FROM provider_connections WHERE provider = ? AND (auth_type = 'apikey' OR (api_key IS NOT NULL AND api_key != ''))"
|
||||
)
|
||||
.run(providerLower);
|
||||
|
||||
if (result.changes > 0) {
|
||||
const changes = removeProviderConnectionByProvider(db, providerLower);
|
||||
if (changes > 0) {
|
||||
console.log(t("keys.removed"));
|
||||
return 0;
|
||||
}
|
||||
@@ -256,10 +330,12 @@ 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(`Regenerate key ${id}? [y/N] `, r));
|
||||
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("Aborted.");
|
||||
console.log(t("common.cancelled"));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -268,11 +344,11 @@ export async function runKeysRegenerateCommand(id, opts = {}) {
|
||||
...opts,
|
||||
});
|
||||
if (!res.ok) {
|
||||
console.error(`Error: HTTP ${res.status}`);
|
||||
console.error(t("common.error", { message: `HTTP ${res.status}` }));
|
||||
return 1;
|
||||
}
|
||||
const data = await res.json();
|
||||
console.log(`Regenerated. New key: ${data.key || data.apiKey || "(see dashboard)"}`);
|
||||
console.log(t("keys.regenerated", { key: data.key || data.apiKey || "(see dashboard)" }));
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -280,10 +356,12 @@ 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(`Revoke key ${id}? [y/N] `, r));
|
||||
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("Aborted.");
|
||||
console.log(t("common.cancelled"));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -292,22 +370,20 @@ export async function runKeysRevokeCommand(id, opts = {}) {
|
||||
...opts,
|
||||
});
|
||||
if (!res.ok) {
|
||||
console.error(`Error: HTTP ${res.status}`);
|
||||
console.error(t("common.error", { message: `HTTP ${res.status}` }));
|
||||
return 1;
|
||||
}
|
||||
console.log(`Key ${id} revoked.`);
|
||||
console.log(t("keys.revoked", { id }));
|
||||
return 0;
|
||||
}
|
||||
|
||||
export async function runKeysRevealCommand(id, opts = {}) {
|
||||
process.stderr.write(
|
||||
"⚠ This will display the full unmasked key. Ensure your screen is private.\n"
|
||||
);
|
||||
process.stderr.write(t("keys.revealWarning") + "\n");
|
||||
const res = await apiFetch(`/api/v1/registered-keys/${encodeURIComponent(id)}/reveal`, {
|
||||
...opts,
|
||||
});
|
||||
if (!res.ok) {
|
||||
console.error(`Error: HTTP ${res.status}`);
|
||||
console.error(t("common.error", { message: `HTTP ${res.status}` }));
|
||||
return 1;
|
||||
}
|
||||
const data = await res.json();
|
||||
@@ -322,13 +398,13 @@ export async function runKeysUsageCommand(id, opts = {}) {
|
||||
{ ...opts }
|
||||
);
|
||||
if (!res.ok) {
|
||||
console.error(`Error: HTTP ${res.status}`);
|
||||
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("No usage data found.");
|
||||
console.log(t("keys.noUsage"));
|
||||
return 0;
|
||||
}
|
||||
for (const r of rows) {
|
||||
@@ -339,3 +415,110 @@ export async function runKeysUsageCommand(id, opts = {}) {
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
export async function runKeysPolicyShowCommand(id, opts = {}) {
|
||||
const res = await apiFetch(`/api/v1/registered-keys/${encodeURIComponent(id)}/policy`, {
|
||||
...opts,
|
||||
acceptNotOk: true,
|
||||
});
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const res = await apiFetch(`/api/v1/registered-keys/${encodeURIComponent(id)}/policy`, {
|
||||
method: "PATCH",
|
||||
body,
|
||||
...opts,
|
||||
acceptNotOk: true,
|
||||
});
|
||||
if (!res.ok) {
|
||||
console.error(t("common.error", { message: `HTTP ${res.status}` }));
|
||||
return 1;
|
||||
}
|
||||
console.log(t("keys.policy.updated"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
export async function runKeysExpirationListCommand(opts = {}) {
|
||||
const days = Number(opts.days || 30);
|
||||
const res = await apiFetch(`/api/v1/registered-keys?expiring=true&days=${days}`, {
|
||||
...opts,
|
||||
acceptNotOk: true,
|
||||
});
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
const gracePeriod = Number(opts.gracePeriod || 60000);
|
||||
const res = await apiFetch(`/api/v1/registered-keys/${encodeURIComponent(id)}/rotate`, {
|
||||
method: "POST",
|
||||
body: { gracePeriodMs: gracePeriod },
|
||||
...opts,
|
||||
acceptNotOk: true,
|
||||
});
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
"no": "no",
|
||||
"confirm": "Are you sure? (yes/no)",
|
||||
"dryRun": "[dry-run] would {action}",
|
||||
"cancelled": "Cancelled."
|
||||
"cancelled": "Cancelled.",
|
||||
"jsonOpt": "Output as JSON",
|
||||
"yesOpt": "Skip confirmation prompt"
|
||||
},
|
||||
"setup": {
|
||||
"title": "OmniRoute Setup",
|
||||
@@ -62,11 +64,51 @@
|
||||
},
|
||||
"keys": {
|
||||
"title": "API Keys",
|
||||
"addDescription": "Add or update an API key for a provider",
|
||||
"listDescription": "List all configured API keys",
|
||||
"removeDescription": "Remove an API key for a provider",
|
||||
"regenerateDescription": "Regenerate an OmniRoute API key",
|
||||
"revokeDescription": "Revoke an OmniRoute API key",
|
||||
"revealDescription": "Reveal the unmasked API key value",
|
||||
"usageDescription": "Show recent usage for an API key",
|
||||
"usageLimitOpt": "Number of recent requests",
|
||||
"rotateDescription": "Generate a new key and invalidate the old one",
|
||||
"graceOpt": "Grace period (ms) before the old key is invalidated",
|
||||
"stdinOpt": "Read API key from stdin instead of argument",
|
||||
"added": "Key added for {provider}.",
|
||||
"removed": "Key removed.",
|
||||
"listed": "{count} key(s).",
|
||||
"noKeys": "No keys configured.",
|
||||
"confirmRemove": "Remove key {id}?"
|
||||
"noUsage": "No usage data found.",
|
||||
"confirmRemove": "Remove key {id}?",
|
||||
"confirmRegenerate": "Regenerate key {id}?",
|
||||
"confirmRevoke": "Revoke key {id}?",
|
||||
"confirmRotate": "Rotate key {id}? (old key will be invalidated after grace period)",
|
||||
"regenerated": "Regenerated. New key: {key}",
|
||||
"revoked": "Key {id} revoked.",
|
||||
"rotated": "Key {id} rotated. New key ID: {newId}",
|
||||
"revealWarning": "⚠ This will display the full unmasked key. Ensure your screen is private.",
|
||||
"providerRequired": "Provider is required.",
|
||||
"keyRequired": "API key is required.",
|
||||
"stdinEmpty": "No API key provided via stdin.",
|
||||
"unknownProvider": "Unknown provider: {provider}",
|
||||
"policy": {
|
||||
"title": "Key Policy",
|
||||
"showDescription": "Show rate/cost policy for a key",
|
||||
"setDescription": "Set rate/cost policy for a key",
|
||||
"rateLimitOpt": "Max requests per minute",
|
||||
"maxCostOpt": "Max cost per day (USD)",
|
||||
"allowedModelsOpt": "Comma-separated list of allowed models",
|
||||
"nothingToSet": "No policy fields provided. Use --rate-limit, --max-cost, or --allowed-models.",
|
||||
"updated": "Policy updated."
|
||||
},
|
||||
"expiration": {
|
||||
"title": "Key Expiration",
|
||||
"listDescription": "List keys expiring soon",
|
||||
"daysOpt": "Show keys expiring within N days",
|
||||
"none": "No keys expiring within {days} days.",
|
||||
"listTitle": "Keys expiring within {days} days:"
|
||||
}
|
||||
},
|
||||
"stream": {
|
||||
"description": "Stream a chat response with SSE inspection modes",
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
"no": "não",
|
||||
"confirm": "Tem certeza? (sim/não)",
|
||||
"dryRun": "[simulação] faria: {action}",
|
||||
"cancelled": "Cancelado."
|
||||
"cancelled": "Cancelado.",
|
||||
"jsonOpt": "Saída em JSON",
|
||||
"yesOpt": "Pular confirmação"
|
||||
},
|
||||
"setup": {
|
||||
"title": "Configuração do OmniRoute",
|
||||
@@ -62,11 +64,51 @@
|
||||
},
|
||||
"keys": {
|
||||
"title": "Chaves de API",
|
||||
"addDescription": "Adicionar ou atualizar uma chave de API para um provedor",
|
||||
"listDescription": "Listar todas as chaves de API configuradas",
|
||||
"removeDescription": "Remover uma chave de API de um provedor",
|
||||
"regenerateDescription": "Regenerar uma chave de API do OmniRoute",
|
||||
"revokeDescription": "Revogar uma chave de API do OmniRoute",
|
||||
"revealDescription": "Revelar o valor completo da chave (sem máscara)",
|
||||
"usageDescription": "Exibir uso recente de uma chave de API",
|
||||
"usageLimitOpt": "Número de requisições recentes",
|
||||
"rotateDescription": "Gerar nova chave e invalidar a antiga",
|
||||
"graceOpt": "Período de carência (ms) antes de invalidar a chave antiga",
|
||||
"stdinOpt": "Ler chave de API via stdin",
|
||||
"added": "Chave adicionada para {provider}.",
|
||||
"removed": "Chave removida.",
|
||||
"listed": "{count} chave(s).",
|
||||
"noKeys": "Nenhuma chave configurada.",
|
||||
"confirmRemove": "Remover chave {id}?"
|
||||
"noUsage": "Nenhum dado de uso encontrado.",
|
||||
"confirmRemove": "Remover chave {id}?",
|
||||
"confirmRegenerate": "Regenerar chave {id}?",
|
||||
"confirmRevoke": "Revogar chave {id}?",
|
||||
"confirmRotate": "Rotacionar chave {id}? (a chave antiga será invalidada após o período de carência)",
|
||||
"regenerated": "Chave regenerada. Nova chave: {key}",
|
||||
"revoked": "Chave {id} revogada.",
|
||||
"rotated": "Chave {id} rotacionada. Novo ID: {newId}",
|
||||
"revealWarning": "⚠ Isso exibirá a chave completa sem máscara. Certifique-se de que sua tela está privada.",
|
||||
"providerRequired": "Provedor é obrigatório.",
|
||||
"keyRequired": "Chave de API é obrigatória.",
|
||||
"stdinEmpty": "Nenhuma chave fornecida via stdin.",
|
||||
"unknownProvider": "Provedor desconhecido: {provider}",
|
||||
"policy": {
|
||||
"title": "Política da Chave",
|
||||
"showDescription": "Exibir política de taxa/custo de uma chave",
|
||||
"setDescription": "Definir política de taxa/custo de uma chave",
|
||||
"rateLimitOpt": "Máximo de requisições por minuto",
|
||||
"maxCostOpt": "Custo máximo por dia (USD)",
|
||||
"allowedModelsOpt": "Lista de modelos permitidos (separados por vírgula)",
|
||||
"nothingToSet": "Nenhum campo de política fornecido. Use --rate-limit, --max-cost ou --allowed-models.",
|
||||
"updated": "Política atualizada."
|
||||
},
|
||||
"expiration": {
|
||||
"title": "Expiração de Chaves",
|
||||
"listDescription": "Listar chaves prestes a expirar",
|
||||
"daysOpt": "Exibir chaves que expiram em N dias",
|
||||
"none": "Nenhuma chave expirando nos próximos {days} dias.",
|
||||
"listTitle": "Chaves que expiram nos próximos {days} dias:"
|
||||
}
|
||||
},
|
||||
"stream": {
|
||||
"description": "Transmitir resposta de chat com modos de inspeção SSE",
|
||||
|
||||
@@ -241,6 +241,16 @@ export function upsertApiKeyProviderConnection(db, input) {
|
||||
return connection;
|
||||
}
|
||||
|
||||
export function removeProviderConnectionByProvider(db, provider) {
|
||||
ensureProviderSchema(db);
|
||||
const result = db
|
||||
.prepare(
|
||||
"DELETE FROM provider_connections WHERE provider = ? AND (auth_type = 'apikey' OR (api_key IS NOT NULL AND api_key != ''))"
|
||||
)
|
||||
.run(provider);
|
||||
return result.changes;
|
||||
}
|
||||
|
||||
export function updateProviderTestResult(db, connectionId, result) {
|
||||
ensureProviderSchema(db);
|
||||
const now = new Date().toISOString();
|
||||
|
||||
@@ -27,6 +27,28 @@ test("keys.mjs exporta novos comandos", async () => {
|
||||
assert.equal(typeof mod.runKeysRevokeCommand, "function");
|
||||
assert.equal(typeof mod.runKeysRevealCommand, "function");
|
||||
assert.equal(typeof mod.runKeysUsageCommand, "function");
|
||||
assert.equal(typeof mod.runKeysPolicyShowCommand, "function");
|
||||
assert.equal(typeof mod.runKeysPolicySetCommand, "function");
|
||||
assert.equal(typeof mod.runKeysExpirationListCommand, "function");
|
||||
assert.equal(typeof mod.runKeysRotateCommand, "function");
|
||||
assert.equal(typeof mod.runKeysListCommand, "function");
|
||||
assert.equal(typeof mod.runKeysRemoveCommand, "function");
|
||||
});
|
||||
|
||||
test("keys.mjs — runKeysListCommand sem server retorna 0 ou 1", async () => {
|
||||
const { runKeysListCommand } = await import("../../bin/cli/commands/keys.mjs");
|
||||
let code = 0;
|
||||
try {
|
||||
code = await runKeysListCommand({ json: true });
|
||||
} catch {
|
||||
code = 1;
|
||||
}
|
||||
assert.ok(code === 0 || code === 1);
|
||||
});
|
||||
|
||||
test("provider-store.mjs exporta removeProviderConnectionByProvider", async () => {
|
||||
const mod = await import("../../bin/cli/provider-store.mjs");
|
||||
assert.equal(typeof mod.removeProviderConnectionByProvider, "function");
|
||||
});
|
||||
|
||||
test("health components com alertsOnly=true não lança", async () => {
|
||||
|
||||
Reference in New Issue
Block a user