fix(mitm): harden packaged runtime and privileged command execution

Compile MITM utilities as NodeNext ESM for prepublish builds, copy the
CommonJS MITM server into standalone artifacts, and resolve MITM data
paths without relying on Next.js aliases at runtime.

Replace shell-interpolated setup and elevated command flows with
argument-based spawn and execFile helpers for database setup, DNS edits,
certificate install flows, and Tailscale sudo execution. Also tighten
the Electron production CSP, update provider icon loading to use direct
@lobehub/icons imports with local fallbacks, and pin dependency
configuration to avoid unused peer installs and known audit findings.
This commit is contained in:
diegosouzapw 2026-04-25 00:31:43 -03:00
parent 0e15988233
commit 1f962a2167
18 changed files with 816 additions and 7516 deletions

4
.npmrc Normal file
View file

@ -0,0 +1,4 @@
# @lobehub/icons declares UI peers that are not needed by our deep icon imports.
# Keeping peer auto-install disabled prevents npm from pulling @lobehub/ui/mermaid
# back into the tree and reopening npm audit findings for unused packages.
legacy-peer-deps=true

View file

@ -2,7 +2,16 @@
## [Unreleased]
- _No unreleased changes._
### 🐛 Bug Fixes
- **fix(mitm):** Compile MITM utilities as NodeNext ESM during prepublish, copy the CommonJS MITM server into the standalone artifact, and resolve MITM data paths without relying on Next.js aliases in packaged runtime.
- **fix(electron):** Harden the production desktop CSP by removing `unsafe-eval` outside development and adding object, base URI, form action, frame ancestor, and worker restrictions.
- **fix(cli):** Replace shell-interpolated setup and privileged command execution paths with argument-based `spawn`/`execFile` helpers for database setup, Tailscale sudo commands, MITM DNS edits, and certificate install/uninstall flows.
- **fix(ui):** Keep provider icons resilient by using direct `@lobehub/icons` components first, then local PNG/SVG fallbacks, avoiding the `@lobehub/ui` peer runtime in the dashboard.
### 📦 Dependencies
- **deps:** Update `@lobehub/icons` to `5.5.4`, add explicit `react-is@19.2.5` for Recharts, pin npm installs to skip unused peer auto-installs, and override Electron's transitive `@xmldom/xmldom` to `0.9.10` so audit findings stay closed.
---

View file

@ -280,14 +280,22 @@ function installUpdate() {
// ── Content Security Policy (#15) ──────────────────────────
function setupContentSecurityPolicy() {
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
const scriptSrc = isDev
? "script-src 'self' 'unsafe-inline' 'unsafe-eval'"
: "script-src 'self' 'unsafe-inline'";
const csp = [
"default-src 'self'",
`connect-src 'self' http://localhost:* ws://localhost:* https://*.omniroute.online https://*.omniroute.dev`,
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
scriptSrc,
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
"font-src 'self' https://fonts.gstatic.com data:",
"img-src 'self' data: blob: https:",
"media-src 'self'",
"worker-src 'self' blob:",
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
"frame-ancestors 'none'",
].join("; ");
callback({

View file

@ -29,6 +29,9 @@
"electron": "^41.2.0",
"electron-builder": "^26.8.1"
},
"overrides": {
"@xmldom/xmldom": "^0.9.10"
},
"build": {
"appId": "online.omniroute.desktop",
"productName": "OmniRoute",

View file

@ -59,7 +59,7 @@ const nextConfig = {
"util",
"process",
],
transpilePackages: ["@omniroute/open-sse"],
transpilePackages: ["@omniroute/open-sse", "@lobehub/icons"],
allowedDevOrigins: ["localhost", "127.0.0.1", "192.168.*"],
typescript: {
// TODO: Re-enable after fixing all sub-component useTranslations scope issues

7265
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -106,7 +106,7 @@
"system-info": "node scripts/system-info.mjs"
},
"dependencies": {
"@lobehub/icons": "^5.0.1",
"@lobehub/icons": "^5.5.4",
"@modelcontextprotocol/sdk": "^1.27.1",
"@monaco-editor/react": "^4.7.0",
"@swc/helpers": "0.5.21",
@ -133,6 +133,7 @@
"pino-pretty": "^13.1.3",
"react": "19.2.5",
"react-dom": "19.2.5",
"react-is": "^19.2.5",
"recharts": "^3.7.0",
"selfsigned": "^5.5.0",
"tsx": "^4.21.0",
@ -199,6 +200,7 @@
"lodash-es": "^4.18.1",
"dompurify": "^3.4.0",
"path-to-regexp": "^8.4.0",
"postcss": "^8.5.10",
"hono": "^4.12.14",
"@hono/node-server": "^1.19.13",
"react": "$react",

View file

@ -4,19 +4,23 @@ import { spawn } from "node:child_process";
const env = { ...process.env };
await exec("npx next build --experimental-build-mode generate");
await exec("npx", ["next", "build", "--experimental-build-mode", "generate"]);
// launch application
await exec(process.argv.slice(2).join(" "));
const [command, ...args] = process.argv.slice(2);
if (!command) {
throw new Error("Missing command to launch after database setup");
}
await exec(command, args);
function exec(command) {
const child = spawn(command, { shell: true, stdio: "inherit", env });
function exec(command, args = []) {
const child = spawn(command, args, { stdio: "inherit", env });
return new Promise((resolve, reject) => {
child.on("exit", (code) => {
if (code === 0) {
resolve();
} else {
reject(new Error(`${command} failed rc=${code}`));
reject(new Error(`${[command, ...args].join(" ")} failed rc=${code}`));
}
});
});

View file

@ -331,10 +331,17 @@ if (existsSync(mitmSrc)) {
// Write a temporary tsconfig.json targeting the mitm directory
const mitmTsconfig = {
compilerOptions: {
target: "ES2020",
module: "CommonJS",
target: "ES2022",
module: "NodeNext",
moduleResolution: "NodeNext",
outDir: mitmDest,
rootDir: mitmSrc,
strict: false,
noImplicitAny: false,
strictNullChecks: false,
noEmitOnError: true,
allowImportingTsExtensions: true,
rewriteRelativeImportExtensions: true,
ignoreDeprecations: "6.0",
resolveJsonModule: true,
esModuleInterop: true,
@ -352,6 +359,10 @@ if (existsSync(mitmSrc)) {
try {
execSync("npx tsc -p tsconfig.mitm.tmp.json", { cwd: ROOT, stdio: "inherit" });
const mitmServerSrc = join(mitmSrc, "server.cjs");
if (existsSync(mitmServerSrc)) {
cpSync(mitmServerSrc, join(mitmDest, "server.cjs"));
}
console.log(" ✅ MITM utilities compiled to app/src/mitm/");
} catch (err: any) {
console.warn(" ⚠️ MITM compile warning (non-fatal):", err.message);

View file

@ -7,8 +7,8 @@ import { promisify } from "util";
import { getSettings, updateSettings } from "@/lib/db/settings";
import { resolveDataDir } from "@/lib/dataPaths";
import { getRuntimePorts } from "@/lib/runtime/ports";
import { execWithPassword } from "@/mitm/dns/dnsConfig";
import { getCachedPassword, setCachedPassword } from "@/mitm/manager";
import { execFileWithPassword } from "@/mitm/systemCommands";
import { getConsistentMachineId } from "@/shared/utils/machineId";
const execFileAsync = promisify(execFile);
@ -442,7 +442,7 @@ async function runSudoShell(command: string, password: string) {
if (!normalizedPassword) {
throw new Error("Sudo password required");
}
await execWithPassword(`sudo -S sh -c ${shellEscape(command)}`, normalizedPassword);
await execFileWithPassword("sudo", ["-S", "sh", "-c", command], normalizedPassword);
}
export async function startTailscaleDaemon({

View file

@ -1,14 +1,14 @@
import path from "path";
import fs from "fs";
import { resolveDataDir } from "@/lib/dataPaths";
import { resolveMitmDataDir } from "../dataDir.ts";
const TARGET_HOST = "daily-cloudcode-pa.googleapis.com";
/**
* Generate self-signed SSL certificate using selfsigned (pure JS, no openssl needed)
*/
export async function generateCert() {
const certDir = path.join(resolveDataDir(), "mitm");
export async function generateCert(): Promise<{ key: string; cert: string }> {
const certDir = path.join(resolveMitmDataDir(), "mitm");
const keyPath = path.join(certDir, "server.key");
const certPath = path.join(certDir, "server.crt");

View file

@ -1,56 +1,64 @@
import fs from "fs";
import crypto from "crypto";
import { exec } from "child_process";
import { execWithPassword } from "../dns/dnsConfig";
import {
execFileText,
execFileWithPassword,
getErrorMessage,
quotePowerShell,
runElevatedPowerShell,
} from "../systemCommands.ts";
const IS_WIN = process.platform === "win32";
// Get SHA1 fingerprint from cert file using Node.js crypto
function getCertFingerprint(certPath) {
function getCertFingerprint(certPath: string): string {
const pem = fs.readFileSync(certPath, "utf-8");
const der = Buffer.from(pem.replace(/-----[^-]+-----/g, "").replace(/\s/g, ""), "base64");
return crypto.createHash("sha1").update(der).digest("hex").toUpperCase().match(/.{2}/g).join(":");
const pairs = crypto.createHash("sha1").update(der).digest("hex").toUpperCase().match(/.{2}/g);
if (!pairs) {
throw new Error(`Unable to compute certificate fingerprint for ${certPath}`);
}
return pairs.join(":");
}
/**
* Check if certificate is already installed in system store
*/
export async function checkCertInstalled(certPath) {
export async function checkCertInstalled(certPath: string): Promise<boolean> {
if (IS_WIN) {
return checkCertInstalledWindows(certPath);
}
return checkCertInstalledMac(certPath);
}
function checkCertInstalledMac(certPath) {
return new Promise((resolve) => {
try {
const fingerprint = getCertFingerprint(certPath);
exec(
`security find-certificate -a -Z /Library/Keychains/System.keychain | grep -i "${fingerprint}"`,
(error) => {
resolve(!error);
}
);
} catch {
resolve(false);
}
});
async function checkCertInstalledMac(certPath: string): Promise<boolean> {
try {
const fingerprint = getCertFingerprint(certPath);
const output = await execFileText("security", [
"find-certificate",
"-a",
"-Z",
"/Library/Keychains/System.keychain",
]);
return output.toUpperCase().includes(fingerprint);
} catch {
return false;
}
}
function checkCertInstalledWindows(certPath) {
return new Promise((resolve) => {
// Check Root store for our cert by subject name
exec("certutil -store Root daily-cloudcode-pa.googleapis.com", (error) => {
resolve(!error);
});
});
async function checkCertInstalledWindows(_certPath: string): Promise<boolean> {
try {
await execFileText("certutil", ["-store", "Root", "daily-cloudcode-pa.googleapis.com"]);
return true;
} catch {
return false;
}
}
/**
* Install SSL certificate to system trust store
*/
export async function installCert(sudoPassword, certPath) {
export async function installCert(sudoPassword: string, certPath: string): Promise<void> {
if (!fs.existsSync(certPath)) {
throw new Error(`Certificate file not found: ${certPath}`);
}
@ -68,41 +76,46 @@ export async function installCert(sudoPassword, certPath) {
}
}
async function installCertMac(sudoPassword, certPath) {
const command = `sudo -S security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "${certPath}"`;
async function installCertMac(sudoPassword: string, certPath: string): Promise<void> {
try {
await execWithPassword(command, sudoPassword);
await execFileWithPassword(
"sudo",
[
"-S",
"security",
"add-trusted-cert",
"-d",
"-r",
"trustRoot",
"-k",
"/Library/Keychains/System.keychain",
certPath,
],
sudoPassword
);
console.log(`✅ Installed certificate to system keychain: ${certPath}`);
} catch (error) {
const msg = error.message?.includes("canceled")
const message = getErrorMessage(error);
const msg = message.includes("canceled")
? "User canceled authorization"
: "Certificate install failed";
throw new Error(msg);
}
}
async function installCertWindows(certPath) {
// Use PowerShell elevated to add cert to Root store and capture exit code
const psScript = `
$proc = Start-Process certutil -ArgumentList '-addstore','Root','${certPath.replace(/'/g, "''")}' -Verb RunAs -Wait -PassThru;
async function installCertWindows(certPath: string): Promise<void> {
await runElevatedPowerShell(`
$certPath = ${quotePowerShell(certPath)};
$proc = Start-Process certutil -ArgumentList @('-addstore','Root',$certPath) -Verb RunAs -Wait -PassThru;
if ($proc.ExitCode -ne 0) { throw "certutil exited with code $($proc.ExitCode)" }
`;
return new Promise((resolve, reject) => {
exec(`powershell -Command "${psScript.replace(/\n/g, " ")}"`, (error) => {
if (error) {
reject(new Error(`Failed to install certificate: ${error.message}`));
} else {
console.log(`✅ Installed certificate to Windows Root store`);
resolve(void 0);
}
});
});
`);
console.log(`✅ Installed certificate to Windows Root store`);
}
/**
* Uninstall SSL certificate from system store
*/
export async function uninstallCert(sudoPassword, certPath) {
export async function uninstallCert(sudoPassword: string, certPath: string): Promise<void> {
const isInstalled = await checkCertInstalled(certPath);
if (!isInstalled) {
console.log("Certificate not found in system store");
@ -116,27 +129,31 @@ export async function uninstallCert(sudoPassword, certPath) {
}
}
async function uninstallCertMac(sudoPassword, certPath) {
async function uninstallCertMac(sudoPassword: string, certPath: string): Promise<void> {
const fingerprint = getCertFingerprint(certPath).replace(/:/g, "");
const command = `sudo -S security delete-certificate -Z "${fingerprint}" /Library/Keychains/System.keychain`;
try {
await execWithPassword(command, sudoPassword);
await execFileWithPassword(
"sudo",
[
"-S",
"security",
"delete-certificate",
"-Z",
fingerprint,
"/Library/Keychains/System.keychain",
],
sudoPassword
);
console.log("✅ Uninstalled certificate from system keychain");
} catch (err) {
throw new Error("Failed to uninstall certificate");
}
}
async function uninstallCertWindows() {
const psCommand = `Start-Process certutil -ArgumentList '-delstore','Root','daily-cloudcode-pa.googleapis.com' -Verb RunAs -Wait`;
return new Promise((resolve, reject) => {
exec(`powershell -Command "${psCommand}"`, (error) => {
if (error) {
reject(new Error(`Failed to uninstall certificate: ${error.message}`));
} else {
console.log("✅ Uninstalled certificate from Windows Root store");
resolve(void 0);
}
});
});
async function uninstallCertWindows(): Promise<void> {
await runElevatedPowerShell(`
$proc = Start-Process certutil -ArgumentList @('-delstore','Root','daily-cloudcode-pa.googleapis.com') -Verb RunAs -Wait -PassThru;
if ($proc.ExitCode -ne 0) { throw "certutil exited with code $($proc.ExitCode)" }
`);
console.log("✅ Uninstalled certificate from Windows Root store");
}

41
src/mitm/dataDir.ts Normal file
View file

@ -0,0 +1,41 @@
import os from "os";
import path from "path";
const APP_NAME = "omniroute";
function fallbackHomeDir(): string {
const envHome = process.env.HOME || process.env.USERPROFILE;
return typeof envHome === "string" && envHome.trim() ? path.resolve(envHome) : os.tmpdir();
}
function safeHomeDir(): string {
try {
return os.homedir();
} catch {
return fallbackHomeDir();
}
}
function normalizeConfiguredPath(dir: unknown): string | null {
if (typeof dir !== "string") return null;
const trimmed = dir.trim();
return trimmed ? path.resolve(trimmed) : null;
}
export function resolveMitmDataDir(): string {
const configured = normalizeConfiguredPath(process.env.DATA_DIR);
if (configured) return configured;
const homeDir = safeHomeDir();
if (process.platform === "win32") {
const appData = process.env.APPDATA || path.join(homeDir, "AppData", "Roaming");
return path.join(appData, APP_NAME);
}
const xdgConfigHome = normalizeConfiguredPath(process.env.XDG_CONFIG_HOME);
if (xdgConfigHome) {
return path.join(xdgConfigHome, APP_NAME);
}
return path.join(homeDir, `.${APP_NAME}`);
}

View file

@ -1,6 +1,11 @@
import { exec } from "child_process";
import fs from "fs";
import path from "path";
import {
execFileWithPassword,
getErrorMessage,
quotePowerShell,
runElevatedPowerShell,
} from "../systemCommands.ts";
const TARGET_HOST = "daily-cloudcode-pa.googleapis.com";
const IS_WIN = process.platform === "win32";
@ -8,46 +13,22 @@ const HOSTS_FILE = IS_WIN
? path.join(process.env.SystemRoot || "C:\\Windows", "System32", "drivers", "etc", "hosts")
: "/etc/hosts";
/**
* Execute command with sudo password via stdin (macOS/Linux only)
*/
export function execWithPassword(command, password) {
return new Promise((resolve, reject) => {
const child = exec(command, (error, stdout, stderr) => {
if (error) {
reject(new Error(`Command failed: ${error.message}\n${stderr}`));
} else {
resolve(stdout);
}
});
child.stdin.write(`${password}\n`);
child.stdin.end();
});
}
/**
* Execute elevated command on Windows via PowerShell RunAs
*/
function execElevatedWindows(command) {
return new Promise((resolve, reject) => {
const psScript = `
$proc = Start-Process cmd -ArgumentList '/c','${command.replace(/'/g, "''")}' -Verb RunAs -Wait -PassThru;
if ($proc.ExitCode -ne 0) { throw "Elevated command exited with code $($proc.ExitCode)" }
`;
exec(`powershell -Command "${psScript.replace(/\n/g, " ")}"`, (error, stdout, stderr) => {
if (error) {
reject(new Error(`Elevated command failed: ${error.message}\n${stderr}`));
} else {
resolve(stdout);
}
});
});
}
const REMOVE_HOSTS_ENTRY_SCRIPT = `
const fs = require("fs");
const filePath = process.argv[1];
const targetHost = process.argv[2];
const content = fs.readFileSync(filePath, "utf8");
const filtered = content.split(/\\r?\\n/).filter((line) => {
const parts = line.trim().split(/\\s+/).filter(Boolean);
return !(parts.length >= 2 && parts.includes(targetHost));
});
fs.writeFileSync(filePath, filtered.join("\\n").replace(/\\n*$/, "\\n"));
`;
/**
* Check if DNS entry already exists
*/
export function checkDNSEntry() {
export function checkDNSEntry(): boolean {
try {
const hostsContent = fs.readFileSync(HOSTS_FILE, "utf8");
const lines = hostsContent.split(/\r?\n/);
@ -63,7 +44,7 @@ export function checkDNSEntry() {
/**
* Add DNS entry to hosts file
*/
export async function addDNSEntry(sudoPassword) {
export async function addDNSEntry(sudoPassword: string): Promise<void> {
if (checkDNSEntry()) {
console.log(`DNS entry for ${TARGET_HOST} already exists`);
return;
@ -73,22 +54,27 @@ export async function addDNSEntry(sudoPassword) {
try {
if (IS_WIN) {
// Windows: use elevated echo >> hosts
await execElevatedWindows(`echo ${entry} >> "${HOSTS_FILE}"`);
await runElevatedPowerShell(
`Add-Content -LiteralPath ${quotePowerShell(HOSTS_FILE)} -Value ${quotePowerShell(entry)}`
);
} else {
const command = `echo "${entry}" | sudo -S tee -a ${HOSTS_FILE} > /dev/null`;
await execWithPassword(command, sudoPassword);
await execFileWithPassword(
"sudo",
["-S", "tee", "-a", HOSTS_FILE],
sudoPassword,
`${entry}\n`
);
}
console.log(`✅ Added DNS entry: ${entry}`);
} catch (error) {
throw new Error(`Failed to add DNS entry: ${error.message}`);
throw new Error(`Failed to add DNS entry: ${getErrorMessage(error)}`);
}
}
/**
* Remove DNS entry from hosts file
*/
export async function removeDNSEntry(sudoPassword) {
export async function removeDNSEntry(sudoPassword: string): Promise<void> {
if (!checkDNSEntry()) {
console.log(`DNS entry for ${TARGET_HOST} does not exist`);
return;
@ -96,21 +82,25 @@ export async function removeDNSEntry(sudoPassword) {
try {
if (IS_WIN) {
// Windows: read, filter, write back via elevated PowerShell
const psScript = `(Get-Content '${HOSTS_FILE}') | Where-Object { $_ -notmatch '${TARGET_HOST}' } | Set-Content '${HOSTS_FILE}'`;
const psCommand = `Start-Process powershell -ArgumentList '-Command','${psScript.replace(/'/g, "''")}' -Verb RunAs -Wait`;
await new Promise((resolve, reject) => {
exec(`powershell -Command "${psCommand}"`, (error) => {
if (error) reject(new Error(`Failed to remove DNS entry: ${error.message}`));
else resolve(void 0);
});
});
await runElevatedPowerShell(`
$hostsFile = ${quotePowerShell(HOSTS_FILE)};
$targetHost = ${quotePowerShell(TARGET_HOST)};
$lines = Get-Content -LiteralPath $hostsFile;
$filtered = $lines | Where-Object {
$parts = ($_ -split '\\s+') | Where-Object { $_ };
-not (($parts.Length -ge 2) -and ($parts -contains $targetHost))
};
Set-Content -LiteralPath $hostsFile -Value $filtered;
`);
} else {
const command = `sudo -S sed -i '' '/${TARGET_HOST}/d' ${HOSTS_FILE}`;
await execWithPassword(command, sudoPassword);
await execFileWithPassword(
"sudo",
["-S", process.execPath, "-e", REMOVE_HOSTS_ENTRY_SCRIPT, HOSTS_FILE, TARGET_HOST],
sudoPassword
);
}
console.log(`✅ Removed DNS entry for ${TARGET_HOST}`);
} catch (error) {
throw new Error(`Failed to remove DNS entry: ${error.message}`);
throw new Error(`Failed to remove DNS entry: ${getErrorMessage(error)}`);
}
}

View file

@ -1,29 +1,29 @@
import { spawn } from "child_process";
import { spawn, type ChildProcess } from "child_process";
import path from "path";
import fs from "fs";
import { resolveDataDir } from "@/lib/dataPaths";
import { addDNSEntry, removeDNSEntry } from "./dns/dnsConfig";
import { generateCert } from "./cert/generate";
import { installCert } from "./cert/install";
import { resolveMitmDataDir } from "./dataDir.ts";
import { addDNSEntry, removeDNSEntry } from "./dns/dnsConfig.ts";
import { generateCert } from "./cert/generate.ts";
import { installCert } from "./cert/install.ts";
// Store server process
let serverProcess = null;
let serverPid = null;
let serverProcess: ChildProcess | null = null;
let serverPid: number | null = null;
// Module-scoped password cache (not exposed on globalThis).
// Cleared automatically when the MITM proxy is stopped.
let _cachedPassword = null;
export function getCachedPassword() {
let _cachedPassword: string | null = null;
export function getCachedPassword(): string | null {
return _cachedPassword;
}
export function setCachedPassword(pwd) {
export function setCachedPassword(pwd: string | null | undefined): void {
_cachedPassword = pwd || null;
}
export function clearCachedPassword() {
export function clearCachedPassword(): void {
_cachedPassword = null;
}
const PID_FILE = path.join(resolveDataDir(), "mitm", ".mitm.pid");
const PID_FILE = path.join(resolveMitmDataDir(), "mitm", ".mitm.pid");
const MITM_SERVER_URL = new URL("./server.cjs", import.meta.url);
const urlPath =
process.platform === "win32" && MITM_SERVER_URL.pathname.startsWith("/")
@ -34,7 +34,7 @@ const cwdPath = path.join(process.cwd(), "src", "mitm", "server.cjs");
const MITM_SERVER_PATH = fs.existsSync(cwdPath) ? cwdPath : urlPath;
// Check if a PID is alive
function isProcessAlive(pid) {
function isProcessAlive(pid: number): boolean {
try {
process.kill(pid, 0);
return true;
@ -46,7 +46,12 @@ function isProcessAlive(pid) {
/**
* Get MITM status
*/
export async function getMitmStatus() {
export async function getMitmStatus(): Promise<{
running: boolean;
pid: number | null;
dnsConfigured: boolean;
certExists: boolean;
}> {
// Check in-memory process first, then fallback to PID file
let running = serverProcess !== null && !serverProcess.killed;
let pid = serverPid;
@ -78,7 +83,7 @@ export async function getMitmStatus() {
}
// Check cert
const certDir = path.join(resolveDataDir(), "mitm");
const certDir = path.join(resolveMitmDataDir(), "mitm");
const certExists = fs.existsSync(path.join(certDir, "server.crt"));
return { running, pid, dnsConfigured, certExists };
@ -89,14 +94,17 @@ export async function getMitmStatus() {
* @param {string} apiKey - OmniRoute API key
* @param {string} sudoPassword - Sudo password for DNS/cert operations
*/
export async function startMitm(apiKey, sudoPassword) {
export async function startMitm(
apiKey: string,
sudoPassword: string
): Promise<{ running: true; pid: number | null }> {
// Check if already running
if (serverProcess && !serverProcess.killed) {
throw new Error("MITM proxy is already running");
}
// 1. Generate SSL certificate if not exists
const certPath = path.join(resolveDataDir(), "mitm", "server.crt");
const certPath = path.join(resolveMitmDataDir(), "mitm", "server.crt");
if (!fs.existsSync(certPath)) {
console.log("Generating SSL certificate...");
await generateCert();
@ -121,21 +129,24 @@ export async function startMitm(apiKey, sudoPassword) {
stdio: ["ignore", "pipe", "pipe"],
});
serverPid = serverProcess.pid;
const proc = serverProcess;
serverPid = proc.pid ?? null;
// Save PID to file
fs.writeFileSync(PID_FILE, String(serverPid));
if (serverPid !== null) {
fs.writeFileSync(PID_FILE, String(serverPid));
}
// Log server output
serverProcess.stdout.on("data", (data) => {
proc.stdout?.on("data", (data) => {
console.log(`[MITM Server] ${data.toString().trim()}`);
});
serverProcess.stderr.on("data", (data) => {
proc.stderr?.on("data", (data) => {
console.error(`[MITM Server Error] ${data.toString().trim()}`);
});
serverProcess.on("exit", (code) => {
proc.on("exit", (code) => {
console.log(`MITM server exited with code ${code}`);
serverProcess = null;
serverPid = null;
@ -149,7 +160,7 @@ export async function startMitm(apiKey, sudoPassword) {
});
// Wait and verify server actually started
const started = await new Promise((resolve) => {
const started = await new Promise<boolean>((resolve) => {
let resolved = false;
const timeout = setTimeout(() => {
if (!resolved) {
@ -158,7 +169,7 @@ export async function startMitm(apiKey, sudoPassword) {
}
}, 2000);
serverProcess.on("exit", (code) => {
proc.on("exit", () => {
clearTimeout(timeout);
if (!resolved) {
resolved = true;
@ -167,7 +178,7 @@ export async function startMitm(apiKey, sudoPassword) {
});
// Check stderr for error messages
serverProcess.stderr.on("data", (data) => {
proc.stderr?.on("data", (data) => {
const msg = data.toString().trim();
if (msg.includes("Port") && msg.includes("already in use")) {
clearTimeout(timeout);
@ -193,7 +204,7 @@ export async function startMitm(apiKey, sudoPassword) {
* Stop MITM proxy
* @param {string} sudoPassword - Sudo password for DNS cleanup
*/
export async function stopMitm(sudoPassword) {
export async function stopMitm(sudoPassword: string): Promise<{ running: false; pid: null }> {
// 1. Kill server process (in-memory or from PID file)
const proc = serverProcess;
if (proc && !proc.killed) {

View file

@ -0,0 +1,96 @@
import { execFile, spawn } from "child_process";
export function getErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
export function execFileText(command: string, args: string[]): Promise<string> {
return new Promise((resolve, reject) => {
execFile(command, args, { encoding: "utf8" }, (error, stdout, stderr) => {
if (error) {
reject(new Error(`Command failed: ${getErrorMessage(error)}\n${stderr}`));
return;
}
resolve(stdout);
});
});
}
export function execFileWithPassword(
command: string,
args: string[],
password: string,
stdinAfterPassword = ""
): Promise<string> {
return new Promise((resolve, reject) => {
const child = spawn(command, args, {
stdio: ["pipe", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
let settled = false;
const settle = (error: Error | null) => {
if (settled) return;
settled = true;
if (error) {
reject(error);
return;
}
resolve(stdout);
};
child.stdout?.on("data", (chunk) => {
stdout += chunk.toString();
});
child.stderr?.on("data", (chunk) => {
stderr += chunk.toString();
});
child.on("error", (error) => {
settle(new Error(`Command failed: ${getErrorMessage(error)}\n${stderr}`));
});
child.on("close", (code) => {
if (code === 0) {
settle(null);
return;
}
settle(new Error(`Command failed with code ${code}\n${stderr}`));
});
child.stdin?.write(`${password}\n${stdinAfterPassword}`);
child.stdin?.end();
});
}
export function quotePowerShell(value: string): string {
return `'${value.replace(/'/g, "''")}'`;
}
export function runPowerShell(script: string): Promise<string> {
return execFileText("powershell", [
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-Command",
script,
]);
}
export function runElevatedPowerShell(script: string): Promise<string> {
const encoded = Buffer.from(script, "utf16le").toString("base64");
const wrapper = `
$proc = Start-Process powershell -ArgumentList @(
'-NoProfile',
'-NonInteractive',
'-ExecutionPolicy',
'Bypass',
'-EncodedCommand',
'${encoded}'
) -Verb RunAs -Wait -PassThru;
if ($proc.ExitCode -ne 0) {
throw "Elevated command exited with code $($proc.ExitCode)"
}
`;
return runPowerShell(wrapper);
}

View file

@ -1,10 +1,10 @@
"use client";
/**
* ProviderIcon Renders a provider logo using @lobehub/icons with PNG fallback.
* ProviderIcon Renders a provider logo using @lobehub/icons with static asset fallbacks.
*
* Strategy (#529):
* 1. Try @lobehub/icons ProviderIcon (130+ providers, React components)
* 1. Try @lobehub/icons direct icon components (no @lobehub/ui peer runtime)
* 2. Fall back to /providers/{id}.png (existing static assets)
* 3. Fall back to /providers/{id}.svg (SVG assets)
* 4. Fall back to a generic AI icon
@ -14,100 +14,10 @@
* <ProviderIcon providerId="anthropic" size={28} type="color" />
*/
import { memo, useState, Component, type ReactNode } from "react";
import { createElement, memo, useState } from "react";
import Image from "next/image";
import { ProviderIcon as LobehubProviderIcon } from "@lobehub/icons";
const LOBEHUB_PROVIDER_MAP: Record<string, string> = {
openai: "openai",
anthropic: "anthropic",
claude: "anthropic",
gemini: "google",
"gemini-cli": "gemini",
google: "google",
"google-pse-search": "google",
deepseek: "deepseek",
groq: "groq",
mistral: "mistral",
codestral: "mistral",
cohere: "cohere",
perplexity: "perplexity",
"perplexity-search": "perplexity",
"perplexity-web": "perplexity",
xai: "xai",
grok: "xai",
"grok-web": "xai",
together: "together",
fireworks: "fireworks",
"fireworks-ai": "fireworks",
cerebras: "cerebras",
huggingface: "huggingface",
"hugging-face": "huggingface",
openrouter: "openrouter",
"open-router": "openrouter",
ollama: "ollama",
"ollama-cloud": "ollama",
minimax: "minimax",
"minimax-cn": "minimax",
qwen: "qwen",
alibaba: "qwen",
moonshot: "moonshot",
kimi: "moonshot",
"kimi-coding": "kimi",
"kimi-coding-apikey": "kimi",
baidu: "baidu",
ernie: "baidu",
spark: "iflytek",
"zhipu-ai": "zhipu",
zhipu: "zhipu",
glm: "zhipu",
glmt: "zhipu",
lmsys: "lmsys",
"stability-ai": "stability",
stability: "stability",
replicate: "replicate",
ai21: "ai21",
nvidia: "nvidia",
cloudflare: "cloudflare",
"cloudflare-ai": "cloudflare",
"amazon-q": "bedrock",
"aws-bedrock": "bedrock",
bedrock: "bedrock",
azure: "azure",
"azure-openai": "azure",
copilot: "githubcopilot",
"github-copilot": "githubcopilot",
github: "github",
mistralai: "mistral",
"azure-ai": "azureai",
baseten: "baseten",
"black-forest-labs": "bfl",
deepinfra: "deepinfra",
"fal-ai": "fal",
"featherless-ai": "featherless",
friendliai: "friendli",
"glm-cn": "zhipu",
"inference-net": "inference",
"jina-ai": "jina",
"lambda-ai": "lambda",
"lm-studio": "lmstudio",
"meta-llama": "meta",
"muse-spark-web": "meta",
"nous-research": "nousresearch",
novita: "novita",
sambanova: "sambanova",
"searchapi-search": "searchapi",
snowflake: "snowflake",
upstage: "upstage",
"v0-vercel": "v0",
"vercel-ai-gateway": "vercelaigateway",
"vertex-partner": "vertexai",
vllm: "vllm",
volcengine: "volcengine",
watsonx: "ibm",
"xiaomi-mimo": "xiaomimimo",
xinference: "xinference",
};
import { getLobeProviderIcon } from "./lobeProviderIcons";
interface ProviderIconProps {
providerId: string;
@ -117,30 +27,6 @@ interface ProviderIconProps {
style?: React.CSSProperties;
}
/** Error boundary to catch Lobehub component render errors gracefully. */
class LobehubErrorBoundary extends Component<
{ children: ReactNode; onError: () => void },
{ hasError: boolean }
> {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch() {
this.props.onError();
}
render() {
if (this.state.hasError) return null;
return this.props.children;
}
}
function GenericProviderIcon({ size }: { size: number }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" style={{ flex: "none" }}>
@ -280,23 +166,27 @@ const ProviderIcon = memo(function ProviderIcon({
style,
}: ProviderIconProps) {
const normalizedId = providerId.toLowerCase();
const lobehubId = LOBEHUB_PROVIDER_MAP[normalizedId] ?? null;
const lobeIcon = getLobeProviderIcon(normalizedId, type);
const hasPng = KNOWN_PNGS.has(normalizedId);
const hasSvg = KNOWN_SVGS.has(normalizedId);
const [useLobehub, setUseLobehub] = useState(lobehubId !== null);
const [usePng, setUsePng] = useState(hasPng);
const [useSvg, setUseSvg] = useState(!hasPng && hasSvg);
const [failedAssets, setFailedAssets] = useState<Record<string, true>>({});
const pngKey = `${normalizedId}:png`;
const svgKey = `${normalizedId}:svg`;
const usePng = !lobeIcon && hasPng && !failedAssets[pngKey];
const useSvg = !lobeIcon && hasSvg && !failedAssets[svgKey] && (!hasPng || failedAssets[pngKey]);
if (useLobehub && lobehubId) {
if (lobeIcon) {
return (
<span
className={className}
style={{ display: "inline-flex", alignItems: "center", ...style }}
>
<LobehubErrorBoundary onError={() => setUseLobehub(false)}>
<LobehubProviderIcon provider={lobehubId} size={size} type={type} />
</LobehubErrorBoundary>
{createElement(lobeIcon, {
"aria-label": providerId,
size,
style: { flex: "none" },
})}
</span>
);
}
@ -314,8 +204,7 @@ const ProviderIcon = memo(function ProviderIcon({
height={size}
style={{ objectFit: "contain" }}
onError={() => {
setUsePng(false);
setUseSvg(hasSvg);
setFailedAssets((current) => ({ ...current, [pngKey]: true }));
}}
unoptimized
/>
@ -335,7 +224,7 @@ const ProviderIcon = memo(function ProviderIcon({
width={size}
height={size}
style={{ objectFit: "contain" }}
onError={() => setUseSvg(false)}
onError={() => setFailedAssets((current) => ({ ...current, [svgKey]: true }))}
unoptimized
/>
</span>

View file

@ -0,0 +1,404 @@
import type { CSSProperties, ElementType } from "react";
import Ai21MonoIcon from "@lobehub/icons/es/Ai21/components/Mono";
import AlibabaColorIcon from "@lobehub/icons/es/Alibaba/components/Color";
import AlibabaMonoIcon from "@lobehub/icons/es/Alibaba/components/Mono";
import AnthropicMonoIcon from "@lobehub/icons/es/Anthropic/components/Mono";
import AntigravityColorIcon from "@lobehub/icons/es/Antigravity/components/Color";
import AntigravityMonoIcon from "@lobehub/icons/es/Antigravity/components/Mono";
import AssemblyAIColorIcon from "@lobehub/icons/es/AssemblyAI/components/Color";
import AssemblyAIMonoIcon from "@lobehub/icons/es/AssemblyAI/components/Mono";
import AutomaticColorIcon from "@lobehub/icons/es/Automatic/components/Color";
import AutomaticMonoIcon from "@lobehub/icons/es/Automatic/components/Mono";
import AwsColorIcon from "@lobehub/icons/es/Aws/components/Color";
import AwsMonoIcon from "@lobehub/icons/es/Aws/components/Mono";
import AzureColorIcon from "@lobehub/icons/es/Azure/components/Color";
import AzureMonoIcon from "@lobehub/icons/es/Azure/components/Mono";
import AzureAIColorIcon from "@lobehub/icons/es/AzureAI/components/Color";
import AzureAIMonoIcon from "@lobehub/icons/es/AzureAI/components/Mono";
import BaiduColorIcon from "@lobehub/icons/es/Baidu/components/Color";
import BaiduMonoIcon from "@lobehub/icons/es/Baidu/components/Mono";
import BailianColorIcon from "@lobehub/icons/es/Bailian/components/Color";
import BailianMonoIcon from "@lobehub/icons/es/Bailian/components/Mono";
import BasetenMonoIcon from "@lobehub/icons/es/Baseten/components/Mono";
import BedrockColorIcon from "@lobehub/icons/es/Bedrock/components/Color";
import BedrockMonoIcon from "@lobehub/icons/es/Bedrock/components/Mono";
import BflMonoIcon from "@lobehub/icons/es/Bfl/components/Mono";
import CerebrasColorIcon from "@lobehub/icons/es/Cerebras/components/Color";
import CerebrasMonoIcon from "@lobehub/icons/es/Cerebras/components/Mono";
import ClaudeCodeColorIcon from "@lobehub/icons/es/ClaudeCode/components/Color";
import ClaudeCodeMonoIcon from "@lobehub/icons/es/ClaudeCode/components/Mono";
import ClineMonoIcon from "@lobehub/icons/es/Cline/components/Mono";
import CloudflareColorIcon from "@lobehub/icons/es/Cloudflare/components/Color";
import CloudflareMonoIcon from "@lobehub/icons/es/Cloudflare/components/Mono";
import CodexColorIcon from "@lobehub/icons/es/Codex/components/Color";
import CodexMonoIcon from "@lobehub/icons/es/Codex/components/Mono";
import CohereColorIcon from "@lobehub/icons/es/Cohere/components/Color";
import CohereMonoIcon from "@lobehub/icons/es/Cohere/components/Mono";
import ComfyUIColorIcon from "@lobehub/icons/es/ComfyUI/components/Color";
import ComfyUIMonoIcon from "@lobehub/icons/es/ComfyUI/components/Mono";
import CursorMonoIcon from "@lobehub/icons/es/Cursor/components/Mono";
import DbrxColorIcon from "@lobehub/icons/es/Dbrx/components/Color";
import DbrxMonoIcon from "@lobehub/icons/es/Dbrx/components/Mono";
import DeepInfraColorIcon from "@lobehub/icons/es/DeepInfra/components/Color";
import DeepInfraMonoIcon from "@lobehub/icons/es/DeepInfra/components/Mono";
import DeepSeekColorIcon from "@lobehub/icons/es/DeepSeek/components/Color";
import DeepSeekMonoIcon from "@lobehub/icons/es/DeepSeek/components/Mono";
import ElevenLabsMonoIcon from "@lobehub/icons/es/ElevenLabs/components/Mono";
import ExaColorIcon from "@lobehub/icons/es/Exa/components/Color";
import ExaMonoIcon from "@lobehub/icons/es/Exa/components/Mono";
import FalColorIcon from "@lobehub/icons/es/Fal/components/Color";
import FalMonoIcon from "@lobehub/icons/es/Fal/components/Mono";
import FeatherlessColorIcon from "@lobehub/icons/es/Featherless/components/Color";
import FeatherlessMonoIcon from "@lobehub/icons/es/Featherless/components/Mono";
import FireworksColorIcon from "@lobehub/icons/es/Fireworks/components/Color";
import FireworksMonoIcon from "@lobehub/icons/es/Fireworks/components/Mono";
import FriendliMonoIcon from "@lobehub/icons/es/Friendli/components/Mono";
import GeminiColorIcon from "@lobehub/icons/es/Gemini/components/Color";
import GeminiMonoIcon from "@lobehub/icons/es/Gemini/components/Mono";
import GeminiCLIColorIcon from "@lobehub/icons/es/GeminiCLI/components/Color";
import GeminiCLIMonoIcon from "@lobehub/icons/es/GeminiCLI/components/Mono";
import GithubCopilotMonoIcon from "@lobehub/icons/es/GithubCopilot/components/Mono";
import GoogleColorIcon from "@lobehub/icons/es/Google/components/Color";
import GoogleMonoIcon from "@lobehub/icons/es/Google/components/Mono";
import GrokMonoIcon from "@lobehub/icons/es/Grok/components/Mono";
import GroqMonoIcon from "@lobehub/icons/es/Groq/components/Mono";
import HuggingFaceColorIcon from "@lobehub/icons/es/HuggingFace/components/Color";
import HuggingFaceMonoIcon from "@lobehub/icons/es/HuggingFace/components/Mono";
import HyperbolicColorIcon from "@lobehub/icons/es/Hyperbolic/components/Color";
import HyperbolicMonoIcon from "@lobehub/icons/es/Hyperbolic/components/Mono";
import IBMMonoIcon from "@lobehub/icons/es/IBM/components/Mono";
import InferenceMonoIcon from "@lobehub/icons/es/Inference/components/Mono";
import JinaMonoIcon from "@lobehub/icons/es/Jina/components/Mono";
import KiloCodeMonoIcon from "@lobehub/icons/es/KiloCode/components/Mono";
import KimiColorIcon from "@lobehub/icons/es/Kimi/components/Color";
import KimiMonoIcon from "@lobehub/icons/es/Kimi/components/Mono";
import LambdaMonoIcon from "@lobehub/icons/es/Lambda/components/Mono";
import LmStudioMonoIcon from "@lobehub/icons/es/LmStudio/components/Mono";
import MetaColorIcon from "@lobehub/icons/es/Meta/components/Color";
import MetaMonoIcon from "@lobehub/icons/es/Meta/components/Mono";
import MetaAIColorIcon from "@lobehub/icons/es/MetaAI/components/Color";
import MetaAIMonoIcon from "@lobehub/icons/es/MetaAI/components/Mono";
import MinimaxColorIcon from "@lobehub/icons/es/Minimax/components/Color";
import MinimaxMonoIcon from "@lobehub/icons/es/Minimax/components/Mono";
import MistralColorIcon from "@lobehub/icons/es/Mistral/components/Color";
import MistralMonoIcon from "@lobehub/icons/es/Mistral/components/Mono";
import MoonshotMonoIcon from "@lobehub/icons/es/Moonshot/components/Mono";
import MorphColorIcon from "@lobehub/icons/es/Morph/components/Color";
import MorphMonoIcon from "@lobehub/icons/es/Morph/components/Mono";
import NanoBananaColorIcon from "@lobehub/icons/es/NanoBanana/components/Color";
import NanoBananaMonoIcon from "@lobehub/icons/es/NanoBanana/components/Mono";
import NebiusMonoIcon from "@lobehub/icons/es/Nebius/components/Mono";
import NousResearchMonoIcon from "@lobehub/icons/es/NousResearch/components/Mono";
import NovitaColorIcon from "@lobehub/icons/es/Novita/components/Color";
import NovitaMonoIcon from "@lobehub/icons/es/Novita/components/Mono";
import NvidiaColorIcon from "@lobehub/icons/es/Nvidia/components/Color";
import NvidiaMonoIcon from "@lobehub/icons/es/Nvidia/components/Mono";
import OllamaMonoIcon from "@lobehub/icons/es/Ollama/components/Mono";
import OpenAIMonoIcon from "@lobehub/icons/es/OpenAI/components/Mono";
import OpenClawColorIcon from "@lobehub/icons/es/OpenClaw/components/Color";
import OpenClawMonoIcon from "@lobehub/icons/es/OpenClaw/components/Mono";
import OpenCodeMonoIcon from "@lobehub/icons/es/OpenCode/components/Mono";
import OpenRouterMonoIcon from "@lobehub/icons/es/OpenRouter/components/Mono";
import PerplexityColorIcon from "@lobehub/icons/es/Perplexity/components/Color";
import PerplexityMonoIcon from "@lobehub/icons/es/Perplexity/components/Mono";
import PoeColorIcon from "@lobehub/icons/es/Poe/components/Color";
import PoeMonoIcon from "@lobehub/icons/es/Poe/components/Mono";
import QoderColorIcon from "@lobehub/icons/es/Qoder/components/Color";
import QoderMonoIcon from "@lobehub/icons/es/Qoder/components/Mono";
import QwenColorIcon from "@lobehub/icons/es/Qwen/components/Color";
import QwenMonoIcon from "@lobehub/icons/es/Qwen/components/Mono";
import RecraftMonoIcon from "@lobehub/icons/es/Recraft/components/Mono";
import ReplicateMonoIcon from "@lobehub/icons/es/Replicate/components/Mono";
import RunwayMonoIcon from "@lobehub/icons/es/Runway/components/Mono";
import SambaNovaColorIcon from "@lobehub/icons/es/SambaNova/components/Color";
import SambaNovaMonoIcon from "@lobehub/icons/es/SambaNova/components/Mono";
import SearchApiMonoIcon from "@lobehub/icons/es/SearchApi/components/Mono";
import SiliconCloudColorIcon from "@lobehub/icons/es/SiliconCloud/components/Color";
import SiliconCloudMonoIcon from "@lobehub/icons/es/SiliconCloud/components/Mono";
import SnowflakeColorIcon from "@lobehub/icons/es/Snowflake/components/Color";
import SnowflakeMonoIcon from "@lobehub/icons/es/Snowflake/components/Mono";
import StabilityColorIcon from "@lobehub/icons/es/Stability/components/Color";
import StabilityMonoIcon from "@lobehub/icons/es/Stability/components/Mono";
import TavilyColorIcon from "@lobehub/icons/es/Tavily/components/Color";
import TavilyMonoIcon from "@lobehub/icons/es/Tavily/components/Mono";
import TogetherColorIcon from "@lobehub/icons/es/Together/components/Color";
import TogetherMonoIcon from "@lobehub/icons/es/Together/components/Mono";
import TopazLabsMonoIcon from "@lobehub/icons/es/TopazLabs/components/Mono";
import UpstageColorIcon from "@lobehub/icons/es/Upstage/components/Color";
import UpstageMonoIcon from "@lobehub/icons/es/Upstage/components/Mono";
import V0MonoIcon from "@lobehub/icons/es/V0/components/Mono";
import VeniceColorIcon from "@lobehub/icons/es/Venice/components/Color";
import VeniceMonoIcon from "@lobehub/icons/es/Venice/components/Mono";
import VercelMonoIcon from "@lobehub/icons/es/Vercel/components/Mono";
import VertexAIColorIcon from "@lobehub/icons/es/VertexAI/components/Color";
import VertexAIMonoIcon from "@lobehub/icons/es/VertexAI/components/Mono";
import VllmColorIcon from "@lobehub/icons/es/Vllm/components/Color";
import VllmMonoIcon from "@lobehub/icons/es/Vllm/components/Mono";
import VolcengineColorIcon from "@lobehub/icons/es/Volcengine/components/Color";
import VolcengineMonoIcon from "@lobehub/icons/es/Volcengine/components/Mono";
import VoyageColorIcon from "@lobehub/icons/es/Voyage/components/Color";
import VoyageMonoIcon from "@lobehub/icons/es/Voyage/components/Mono";
import WorkersAIColorIcon from "@lobehub/icons/es/WorkersAI/components/Color";
import WorkersAIMonoIcon from "@lobehub/icons/es/WorkersAI/components/Mono";
import XAIMonoIcon from "@lobehub/icons/es/XAI/components/Mono";
import XiaomiMiMoMonoIcon from "@lobehub/icons/es/XiaomiMiMo/components/Mono";
import XinferenceColorIcon from "@lobehub/icons/es/Xinference/components/Color";
import XinferenceMonoIcon from "@lobehub/icons/es/Xinference/components/Mono";
import ZAIMonoIcon from "@lobehub/icons/es/ZAI/components/Mono";
import ZhipuColorIcon from "@lobehub/icons/es/Zhipu/components/Color";
import ZhipuMonoIcon from "@lobehub/icons/es/Zhipu/components/Mono";
type LobeIconComponent = ElementType<{
"aria-label"?: string;
className?: string;
size?: number | string;
style?: CSSProperties;
}>;
type LobeIconEntry = {
color?: LobeIconComponent;
mono: LobeIconComponent;
};
const LOBE_ICON_COMPONENTS = {
Ai21: { mono: Ai21MonoIcon },
Alibaba: { mono: AlibabaMonoIcon, color: AlibabaColorIcon },
Anthropic: { mono: AnthropicMonoIcon },
Antigravity: { mono: AntigravityMonoIcon, color: AntigravityColorIcon },
AssemblyAI: { mono: AssemblyAIMonoIcon, color: AssemblyAIColorIcon },
Automatic: { mono: AutomaticMonoIcon, color: AutomaticColorIcon },
Aws: { mono: AwsMonoIcon, color: AwsColorIcon },
Azure: { mono: AzureMonoIcon, color: AzureColorIcon },
AzureAI: { mono: AzureAIMonoIcon, color: AzureAIColorIcon },
Baidu: { mono: BaiduMonoIcon, color: BaiduColorIcon },
Bailian: { mono: BailianMonoIcon, color: BailianColorIcon },
Baseten: { mono: BasetenMonoIcon },
Bedrock: { mono: BedrockMonoIcon, color: BedrockColorIcon },
Bfl: { mono: BflMonoIcon },
Cerebras: { mono: CerebrasMonoIcon, color: CerebrasColorIcon },
ClaudeCode: { mono: ClaudeCodeMonoIcon, color: ClaudeCodeColorIcon },
Cline: { mono: ClineMonoIcon },
Cloudflare: { mono: CloudflareMonoIcon, color: CloudflareColorIcon },
Codex: { mono: CodexMonoIcon, color: CodexColorIcon },
Cohere: { mono: CohereMonoIcon, color: CohereColorIcon },
ComfyUI: { mono: ComfyUIMonoIcon, color: ComfyUIColorIcon },
Cursor: { mono: CursorMonoIcon },
Dbrx: { mono: DbrxMonoIcon, color: DbrxColorIcon },
DeepInfra: { mono: DeepInfraMonoIcon, color: DeepInfraColorIcon },
DeepSeek: { mono: DeepSeekMonoIcon, color: DeepSeekColorIcon },
ElevenLabs: { mono: ElevenLabsMonoIcon },
Exa: { mono: ExaMonoIcon, color: ExaColorIcon },
Fal: { mono: FalMonoIcon, color: FalColorIcon },
Featherless: { mono: FeatherlessMonoIcon, color: FeatherlessColorIcon },
Fireworks: { mono: FireworksMonoIcon, color: FireworksColorIcon },
Friendli: { mono: FriendliMonoIcon },
Gemini: { mono: GeminiMonoIcon, color: GeminiColorIcon },
GeminiCLI: { mono: GeminiCLIMonoIcon, color: GeminiCLIColorIcon },
GithubCopilot: { mono: GithubCopilotMonoIcon },
Google: { mono: GoogleMonoIcon, color: GoogleColorIcon },
Grok: { mono: GrokMonoIcon },
Groq: { mono: GroqMonoIcon },
HuggingFace: { mono: HuggingFaceMonoIcon, color: HuggingFaceColorIcon },
Hyperbolic: { mono: HyperbolicMonoIcon, color: HyperbolicColorIcon },
IBM: { mono: IBMMonoIcon },
Inference: { mono: InferenceMonoIcon },
Jina: { mono: JinaMonoIcon },
KiloCode: { mono: KiloCodeMonoIcon },
Kimi: { mono: KimiMonoIcon, color: KimiColorIcon },
Lambda: { mono: LambdaMonoIcon },
LmStudio: { mono: LmStudioMonoIcon },
Meta: { mono: MetaMonoIcon, color: MetaColorIcon },
MetaAI: { mono: MetaAIMonoIcon, color: MetaAIColorIcon },
Minimax: { mono: MinimaxMonoIcon, color: MinimaxColorIcon },
Mistral: { mono: MistralMonoIcon, color: MistralColorIcon },
Moonshot: { mono: MoonshotMonoIcon },
Morph: { mono: MorphMonoIcon, color: MorphColorIcon },
NanoBanana: { mono: NanoBananaMonoIcon, color: NanoBananaColorIcon },
Nebius: { mono: NebiusMonoIcon },
NousResearch: { mono: NousResearchMonoIcon },
Novita: { mono: NovitaMonoIcon, color: NovitaColorIcon },
Nvidia: { mono: NvidiaMonoIcon, color: NvidiaColorIcon },
Ollama: { mono: OllamaMonoIcon },
OpenAI: { mono: OpenAIMonoIcon },
OpenClaw: { mono: OpenClawMonoIcon, color: OpenClawColorIcon },
OpenCode: { mono: OpenCodeMonoIcon },
OpenRouter: { mono: OpenRouterMonoIcon },
Perplexity: { mono: PerplexityMonoIcon, color: PerplexityColorIcon },
Poe: { mono: PoeMonoIcon, color: PoeColorIcon },
Qoder: { mono: QoderMonoIcon, color: QoderColorIcon },
Qwen: { mono: QwenMonoIcon, color: QwenColorIcon },
Recraft: { mono: RecraftMonoIcon },
Replicate: { mono: ReplicateMonoIcon },
Runway: { mono: RunwayMonoIcon },
SambaNova: { mono: SambaNovaMonoIcon, color: SambaNovaColorIcon },
SearchApi: { mono: SearchApiMonoIcon },
SiliconCloud: { mono: SiliconCloudMonoIcon, color: SiliconCloudColorIcon },
Snowflake: { mono: SnowflakeMonoIcon, color: SnowflakeColorIcon },
Stability: { mono: StabilityMonoIcon, color: StabilityColorIcon },
Tavily: { mono: TavilyMonoIcon, color: TavilyColorIcon },
Together: { mono: TogetherMonoIcon, color: TogetherColorIcon },
TopazLabs: { mono: TopazLabsMonoIcon },
Upstage: { mono: UpstageMonoIcon, color: UpstageColorIcon },
V0: { mono: V0MonoIcon },
Venice: { mono: VeniceMonoIcon, color: VeniceColorIcon },
Vercel: { mono: VercelMonoIcon },
VertexAI: { mono: VertexAIMonoIcon, color: VertexAIColorIcon },
Vllm: { mono: VllmMonoIcon, color: VllmColorIcon },
Volcengine: { mono: VolcengineMonoIcon, color: VolcengineColorIcon },
Voyage: { mono: VoyageMonoIcon, color: VoyageColorIcon },
WorkersAI: { mono: WorkersAIMonoIcon, color: WorkersAIColorIcon },
XAI: { mono: XAIMonoIcon },
XiaomiMiMo: { mono: XiaomiMiMoMonoIcon },
Xinference: { mono: XinferenceMonoIcon, color: XinferenceColorIcon },
ZAI: { mono: ZAIMonoIcon },
Zhipu: { mono: ZhipuMonoIcon, color: ZhipuColorIcon },
} satisfies Record<string, LobeIconEntry>;
const LOBE_PROVIDER_ALIASES = {
ai21: "Ai21",
alibaba: "Alibaba",
alicode: "Alibaba",
"alicode-intl": "Alibaba",
"amazon-q": "Aws",
anthropic: "Anthropic",
antigravity: "Antigravity",
assemblyai: "AssemblyAI",
"aws-polly": "Aws",
azure: "Azure",
"azure-ai": "AzureAI",
"azure-openai": "AzureAI",
baidu: "Baidu",
"bailian-coding-plan": "Bailian",
baseten: "Baseten",
bedrock: "Bedrock",
bfl: "Bfl",
"black-forest-labs": "Bfl",
cerebras: "Cerebras",
claude: "ClaudeCode",
cline: "Cline",
cloudflare: "Cloudflare",
"cloudflare-ai": "WorkersAI",
codestral: "Mistral",
codex: "Codex",
cohere: "Cohere",
comfyui: "ComfyUI",
copilot: "GithubCopilot",
cursor: "Cursor",
databricks: "Dbrx",
deepinfra: "DeepInfra",
deepseek: "DeepSeek",
elevenlabs: "ElevenLabs",
exa: "Exa",
"exa-search": "Exa",
fal: "Fal",
"fal-ai": "Fal",
featherless: "Featherless",
"featherless-ai": "Featherless",
fireworks: "Fireworks",
"fireworks-ai": "Fireworks",
friendli: "Friendli",
friendliai: "Friendli",
gemini: "Gemini",
"gemini-cli": "GeminiCLI",
github: "GithubCopilot",
"github-copilot": "GithubCopilot",
glm: "Zhipu",
"glm-cn": "Zhipu",
glmt: "Zhipu",
"google-pse-search": "Google",
grok: "Grok",
"grok-web": "Grok",
groq: "Groq",
"hugging-face": "HuggingFace",
huggingface: "HuggingFace",
hyperbolic: "Hyperbolic",
ibm: "IBM",
"inference-net": "Inference",
jina: "Jina",
"jina-ai": "Jina",
kilocode: "KiloCode",
kimi: "Kimi",
"kimi-coding": "Kimi",
"kimi-coding-apikey": "Kimi",
lambda: "Lambda",
"lambda-ai": "Lambda",
"lm-studio": "LmStudio",
lmstudio: "LmStudio",
"meta-llama": "Meta",
minimax: "Minimax",
"minimax-cn": "Minimax",
mistral: "Mistral",
mistralai: "Mistral",
moonshot: "Moonshot",
morph: "Morph",
"muse-spark-web": "MetaAI",
nanobanana: "NanoBanana",
nebius: "Nebius",
"nous-research": "NousResearch",
nousresearch: "NousResearch",
novita: "Novita",
nvidia: "Nvidia",
ollama: "Ollama",
"ollama-cloud": "Ollama",
openai: "OpenAI",
openclaw: "OpenClaw",
opencode: "OpenCode",
"opencode-go": "OpenCode",
"opencode-zen": "OpenCode",
"open-router": "OpenRouter",
openrouter: "OpenRouter",
perplexity: "Perplexity",
"perplexity-search": "Perplexity",
"perplexity-web": "Perplexity",
poe: "Poe",
qoder: "Qoder",
qwen: "Qwen",
recraft: "Recraft",
replicate: "Replicate",
runwayml: "Runway",
sambanova: "SambaNova",
sdwebui: "Automatic",
searchapi: "SearchApi",
"searchapi-search": "SearchApi",
siliconflow: "SiliconCloud",
snowflake: "Snowflake",
stability: "Stability",
"stability-ai": "Stability",
tavily: "Tavily",
"tavily-search": "Tavily",
together: "Together",
topaz: "TopazLabs",
upstage: "Upstage",
v0: "V0",
"v0-vercel": "V0",
venice: "Venice",
"vercel-ai-gateway": "Vercel",
vertex: "VertexAI",
"vertex-partner": "VertexAI",
vertexai: "VertexAI",
vllm: "Vllm",
volcengine: "Volcengine",
voyage: "Voyage",
"voyage-ai": "Voyage",
watsonx: "IBM",
"workers-ai": "WorkersAI",
workersai: "WorkersAI",
xai: "XAI",
"xiaomi-mimo": "XiaomiMiMo",
xiaomimimo: "XiaomiMiMo",
xinference: "Xinference",
zai: "ZAI",
zhipu: "Zhipu",
} satisfies Record<string, keyof typeof LOBE_ICON_COMPONENTS>;
export function getLobeProviderIcon(
providerId: string,
type: "mono" | "color" = "color"
): LobeIconComponent | null {
const iconKey = LOBE_PROVIDER_ALIASES[providerId.toLowerCase()];
if (!iconKey) return null;
const entry = LOBE_ICON_COMPONENTS[iconKey];
return type === "color" && entry.color ? entry.color : entry.mono;
}