mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-04 14:22:09 +03:00
Merge PR #2280: feat(cli): CLI v4 — Commander.js, 50+ commands, TUI, i18n, plugins (Phases 0-9)
Complete rewrite of the OmniRoute CLI: - Commander.js-based modular architecture (50+ command files) - Full i18n support (en + pt-BR, 1222 keys each) - TUI interactive interface (OAuthFlow, EvalWatch, ProvidersTestAll) - Plugin system (omniroute-cmd-*) - OpenAPI codegen (omniroute api <tag> <op>) - Commands: serve, combo, compression, keys, tunnel, backup, test-provider, health, memory, MCP, A2A, oauth, skills, webhooks, usage, cost, eval, context-eng, dashboard, doctor, env, files, logs, models, nodes, oneproxy, open, openapi, plugin, policy, pricing, providers, quota, registry, repl, reset-encrypted-columns, resilience, restart, runtime, sessions, setup, simulate, status, stop, stream, sync, tags, telemetry, translator, tray, update - Code review fixes: C1-C3, I1-I5, M1-M4 applied # Conflicts: # bin/cli/commands/config.mjs # bin/omniroute.mjs # package-lock.json # package.json
This commit is contained in:
@@ -2,6 +2,7 @@ import { isAuthRequired, isDashboardSessionAuthenticated } from "@/shared/utils/
|
||||
import { createErrorResponse } from "@/lib/api/errorResponse";
|
||||
import { extractApiKey, isValidApiKey } from "@/sse/services/auth";
|
||||
import { getApiKeyMetadata } from "@/lib/db/apiKeys";
|
||||
import { isCliTokenAuthValid } from "@/lib/middleware/cliTokenAuth";
|
||||
|
||||
export const MANAGE_SCOPE = "manage";
|
||||
|
||||
@@ -18,6 +19,11 @@ export async function requireManagementAuth(request: Request): Promise<Response
|
||||
return null;
|
||||
}
|
||||
|
||||
// CLI machine-id token allows localhost CLI access without an explicit API key.
|
||||
if (await isCliTokenAuthValid(request)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const apiKey = extractApiKey(request);
|
||||
if (apiKey) {
|
||||
let meta: Awaited<ReturnType<typeof getApiKeyMetadata>>;
|
||||
|
||||
@@ -260,3 +260,15 @@ export async function deleteCombo(id: string) {
|
||||
backupDbFile("pre-write");
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function deleteComboByName(name: string) {
|
||||
const combo = await getComboByName(name);
|
||||
if (!combo || typeof combo.id !== "string") return false;
|
||||
return deleteCombo(combo.id);
|
||||
}
|
||||
|
||||
export function setActiveCombo(name: string, db = getDbInstance()) {
|
||||
db.prepare(
|
||||
"INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES ('settings', 'activeCombo', ?)"
|
||||
).run(JSON.stringify(name));
|
||||
}
|
||||
|
||||
33
src/lib/db/recovery.ts
Normal file
33
src/lib/db/recovery.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { getDbInstance } from "./core";
|
||||
|
||||
type DbInstance = ReturnType<typeof getDbInstance>;
|
||||
|
||||
const ENCRYPTED_COLUMNS = ["api_key", "access_token", "refresh_token", "id_token"] as const;
|
||||
|
||||
const ENCRYPTED_PATTERN = "enc:v1:%";
|
||||
|
||||
function buildWhereClause(): string {
|
||||
return ENCRYPTED_COLUMNS.map((col) => `${col} LIKE '${ENCRYPTED_PATTERN}'`).join(" OR ");
|
||||
}
|
||||
|
||||
export function countEncryptedCredentials(db: DbInstance = getDbInstance()): number {
|
||||
const where = buildWhereClause();
|
||||
const row = db
|
||||
.prepare(`SELECT COUNT(*) AS cnt FROM provider_connections WHERE ${where}`)
|
||||
.get() as { cnt: number } | undefined;
|
||||
return row?.cnt ?? 0;
|
||||
}
|
||||
|
||||
export function resetEncryptedColumns(
|
||||
{ dryRun }: { dryRun: boolean },
|
||||
db: DbInstance = getDbInstance()
|
||||
): { affected: number } {
|
||||
const affected = countEncryptedCredentials(db);
|
||||
if (dryRun || affected === 0) return { affected };
|
||||
|
||||
const nullCols = ENCRYPTED_COLUMNS.map((col) => `${col} = NULL`).join(", ");
|
||||
const where = buildWhereClause();
|
||||
db.prepare(`UPDATE provider_connections SET ${nullCols} WHERE ${where}`).run();
|
||||
|
||||
return { affected };
|
||||
}
|
||||
46
src/lib/middleware/cliTokenAuth.ts
Normal file
46
src/lib/middleware/cliTokenAuth.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import crypto from "node:crypto";
|
||||
import { headers } from "next/headers";
|
||||
|
||||
const SALT = "omniroute-cli-auth-v1";
|
||||
const HEADER_NAME = "x-omniroute-cli-token";
|
||||
|
||||
export function isLoopback(ip: string): boolean {
|
||||
const normalized = ip.replace(/^::ffff:/, "");
|
||||
return normalized === "127.0.0.1" || normalized === "::1" || normalized === "localhost";
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the CLI machine-id token sent by the local omniroute CLI.
|
||||
* Only accepted from loopback IPs. Disabled via OMNIROUTE_DISABLE_CLI_TOKEN=true.
|
||||
*/
|
||||
export async function isCliTokenAuthValid(request: Request): Promise<boolean> {
|
||||
if (process.env.OMNIROUTE_DISABLE_CLI_TOKEN === "true") return false;
|
||||
|
||||
const hdrs = await headers();
|
||||
const token = hdrs.get(HEADER_NAME);
|
||||
if (!token || token.length !== 32) return false;
|
||||
|
||||
// Only allow loopback origin — check forwarded-for, real-ip, then host header.
|
||||
const ip =
|
||||
(hdrs.get("x-forwarded-for") ?? "").split(",")[0].trim() || hdrs.get("x-real-ip") || "";
|
||||
if (ip && !isLoopback(ip)) return false;
|
||||
|
||||
let expected: string;
|
||||
try {
|
||||
const { machineIdSync } = await import("node-machine-id");
|
||||
const mid = machineIdSync();
|
||||
expected = crypto
|
||||
.createHash("sha256")
|
||||
.update(mid + SALT)
|
||||
.digest("hex")
|
||||
.substring(0, 32);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return crypto.timingSafeEqual(Buffer.from(token, "utf8"), Buffer.from(expected, "utf8"));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -526,6 +526,22 @@ amp --model "{{model}}"
|
||||
},
|
||||
};
|
||||
|
||||
// ─── Registry helpers ────────────────────────────────────────────────────────
|
||||
|
||||
export type CliToolEntry = (typeof CLI_TOOLS)[keyof typeof CLI_TOOLS];
|
||||
|
||||
/** Returns an ordered list of all registered CLI tools. */
|
||||
export function listCliTools(): CliToolEntry[] {
|
||||
return Object.values(CLI_TOOLS) as CliToolEntry[];
|
||||
}
|
||||
|
||||
/** Returns a single tool by id, or undefined if not found. */
|
||||
export function getCliTool(id: string): CliToolEntry | undefined {
|
||||
return (CLI_TOOLS as Record<string, CliToolEntry>)[id];
|
||||
}
|
||||
|
||||
// ─── Provider model mapping helper ───────────────────────────────────────────
|
||||
|
||||
// Get all provider models for mapping dropdown
|
||||
export const getProviderModelsForMapping = (providers) => {
|
||||
const result = [];
|
||||
|
||||
Reference in New Issue
Block a user