mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-24 08:02:14 +03:00
feat(services): expose sanitized CLIProxyAPI account health (#11314)
Validated on a 17-PR combined board: cliproxy-accounts + cliproxy-tab + cliproxy-account-health + cliproxy-resolve-spawn-args-6877 (16/16) within the board's 287/287, typecheck:core clean, env-doc-sync clean. Exposes a sanitized read-only CLIProxyAPI account health view (5s-bounded client, explicit allowlist excluding names/paths/emails/tokens/status messages) through a management-authenticated API + dashboard card. Closes #6342. Thank you @RaviTharuma!
This commit is contained in:
@@ -2021,6 +2021,9 @@ APP_LOG_TO_FILE=true
|
||||
# CLIPROXYAPI_HOST=127.0.0.1
|
||||
# CLIPROXYAPI_PORT=5544
|
||||
# CLIPROXYAPI_CONFIG_DIR=~/.cli-proxy-api
|
||||
# Management key for an externally managed instance. Embedded instances use
|
||||
# OmniRoute's encrypted service key.
|
||||
# CLIPROXYAPI_MANAGEMENT_KEY=
|
||||
|
||||
# ── Mux embedded service ──
|
||||
# Override the port where the embedded Mux (coder/mux) agent-orchestration
|
||||
|
||||
1
changelog.d/features/6342-cliproxy-account-health.md
Normal file
1
changelog.d/features/6342-cliproxy-account-health.md
Normal file
@@ -0,0 +1 @@
|
||||
- feat(services): show sanitized CLIProxyAPI account health from its authenticated management API without exposing credentials, file paths, or raw account metadata (#6342)
|
||||
@@ -1044,6 +1044,7 @@ desktop install.
|
||||
| `EMBED_WS_PROXY_PORT` | `20131` | `src/lib/services/embedWsProxy.ts` | Port for the embedded-service WebSocket proxy server. |
|
||||
| `CLIPROXYAPI_HOST` | `127.0.0.1` | `open-sse/executors/cliproxyapi.ts` | CLIProxyAPI bridge host (legacy integration). |
|
||||
| `CLIPROXYAPI_PORT` | `5544` | `open-sse/executors/cliproxyapi.ts` | CLIProxyAPI bridge port. |
|
||||
| `CLIPROXYAPI_MANAGEMENT_KEY` | _(empty)_ | `src/lib/services/cliproxyAccountHealth.ts` | Management key for account-health reads from an externally managed CLIProxyAPI instance. |
|
||||
| `CLIPROXYAPI_CONFIG_DIR` | `~/.cli-proxy-api` | `src/lib/versionManager/processManager.ts` | CLIProxyAPI config directory. |
|
||||
| `MUX_SERVICE_PORT` | `8322` | `src/lib/services/bootstrap.ts` | Override the port where the embedded Mux (coder/mux) agent-orchestration daemon listens (always 127.0.0.1). |
|
||||
| `DARIO_HOST` | `127.0.0.1` | `open-sse/executors/dario.ts` | Dario embedded-service bind/connect host (loopback only by default). |
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Badge, Button, Card } from "@/shared/components";
|
||||
import type {
|
||||
CliproxyAccountHealth,
|
||||
CliproxyAccountHealthResult,
|
||||
} from "@/lib/services/cliproxyAccountHealth";
|
||||
|
||||
const STATE_LABELS: Record<CliproxyAccountHealthResult["state"], string> = {
|
||||
ready: "Account health",
|
||||
disabled: "CLIProxyAPI is not installed",
|
||||
missing_key: "Management key is not configured",
|
||||
unreachable: "Management API is unreachable",
|
||||
unauthorized: "Management key was rejected",
|
||||
unsupported: "This CLIProxyAPI version does not expose account health",
|
||||
invalid_response: "Management API returned an unsupported response",
|
||||
};
|
||||
|
||||
function AccountRow({ account }: { account: CliproxyAccountHealth }) {
|
||||
const state = account.disabled ? "Disabled" : account.unavailable ? "Unavailable" : account.status;
|
||||
return (
|
||||
<li className="flex flex-wrap items-center justify-between gap-3 border-t border-border py-3 first:border-t-0">
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="truncate font-medium text-text-main">
|
||||
{account.label || account.authIndex}
|
||||
</span>
|
||||
<Badge variant={account.disabled || account.unavailable ? "warning" : "success"}>
|
||||
{state || "Unknown"}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-text-muted">
|
||||
{[account.provider || account.type, account.label ? account.authIndex : ""]
|
||||
.filter(Boolean)
|
||||
.join(" · ")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right text-xs text-text-muted">
|
||||
<div>{account.success.toLocaleString()} succeeded</div>
|
||||
<div>{account.failed.toLocaleString()} failed</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
export function CliproxyAccountHealthCard() {
|
||||
const [result, setResult] = useState<CliproxyAccountHealthResult | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch("/api/services/cliproxy/accounts", { cache: "no-store" });
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
setResult(await response.json());
|
||||
} catch {
|
||||
setResult({ state: "unreachable", accounts: [], version: null });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, [load]);
|
||||
|
||||
return (
|
||||
<Card
|
||||
title="CLIProxyAPI accounts"
|
||||
subtitle="Read-only status from the authenticated management API"
|
||||
action={
|
||||
<Button variant="secondary" size="sm" onClick={() => void load()} loading={loading}>
|
||||
Refresh
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{result?.state === "ready" ? (
|
||||
result.accounts.length > 0 ? (
|
||||
<ul aria-label="CLIProxyAPI account health">
|
||||
{result.accounts.map((account) => (
|
||||
<AccountRow key={account.authIndex} account={account} />
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="text-sm text-text-muted">No CLIProxyAPI accounts found.</p>
|
||||
)
|
||||
) : (
|
||||
<p className="text-sm text-text-muted">
|
||||
{loading && !result ? "Loading account health…" : STATE_LABELS[result?.state ?? "unreachable"]}
|
||||
</p>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { AutoStartToggle } from "../components/AutoStartToggle";
|
||||
import { AutoRestartAdoptedToggle } from "../components/AutoRestartAdoptedToggle";
|
||||
import { CliproxyConnectionPanel } from "../components/CliproxyConnectionPanel";
|
||||
import { CliproxyProviderExposureCard } from "../components/CliproxyProviderExposureCard";
|
||||
import { CliproxyAccountHealthCard } from "../components/CliproxyAccountHealthCard";
|
||||
|
||||
const NAME = "cliproxy";
|
||||
|
||||
@@ -19,6 +20,7 @@ export function CliproxyServiceTab() {
|
||||
<AutoStartToggle name={NAME} />
|
||||
<AutoRestartAdoptedToggle name={NAME} />
|
||||
<CliproxyConnectionPanel />
|
||||
<CliproxyAccountHealthCard />
|
||||
<CliproxyProviderExposureCard />
|
||||
<CliproxyModelMappingEditor />
|
||||
<ServiceLogsPanel name={NAME} />
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { getSupervisor, registerSupervisor } from "@/lib/services/registry";
|
||||
import { ServiceSupervisor } from "@/lib/services/ServiceSupervisor";
|
||||
import { resolveSpawnArgs, CLIPROXY_DEFAULT_PORT } from "@/lib/services/installers/cliproxy";
|
||||
import { getOrCreateApiKey } from "@/lib/services/apiKey";
|
||||
|
||||
const TOOL = "cliproxy";
|
||||
const PORT = parseInt(process.env.CLIPROXYAPI_PORT ?? String(CLIPROXY_DEFAULT_PORT), 10);
|
||||
@@ -14,10 +15,11 @@ export async function getOrInitSupervisor(): Promise<ServiceSupervisor> {
|
||||
const existing = getSupervisor(TOOL);
|
||||
if (existing) return existing;
|
||||
|
||||
const managementKey = await getOrCreateApiKey(TOOL);
|
||||
const sup = new ServiceSupervisor({
|
||||
tool: TOOL,
|
||||
port: PORT,
|
||||
spawnArgs: () => resolveSpawnArgs(PORT),
|
||||
spawnArgs: () => resolveSpawnArgs(PORT, managementKey),
|
||||
healthUrl: () => `http://127.0.0.1:${PORT}/v1/models`,
|
||||
healthIntervalMs: 5_000,
|
||||
stopTimeoutMs: 15_000,
|
||||
|
||||
13
src/app/api/services/cliproxy/accounts/route.ts
Normal file
13
src/app/api/services/cliproxy/accounts/route.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { getCliproxyAccountHealth } from "@/lib/services/cliproxyAccountHealth";
|
||||
import { isAuthenticated } from "@/shared/utils/apiAuth";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(request: Request): Promise<Response> {
|
||||
if (!(await isAuthenticated(request))) {
|
||||
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
return Response.json(await getCliproxyAccountHealth(), {
|
||||
headers: { "Cache-Control": "no-store" },
|
||||
});
|
||||
}
|
||||
@@ -63,7 +63,7 @@ const SERVICES: ServiceEntry[] = [
|
||||
healthIntervalMs: 5_000,
|
||||
stopTimeoutMs: 15_000,
|
||||
logsBufferBytes: 5_242_880,
|
||||
needsApiKey: false,
|
||||
needsApiKey: true,
|
||||
},
|
||||
{
|
||||
tool: "mux",
|
||||
@@ -115,7 +115,7 @@ function buildSpawnArgsFactory(
|
||||
if (cfg.tool === "dario") {
|
||||
return () => darioSpawnArgs(apiKey, cfg.port);
|
||||
}
|
||||
return () => cliproxySpawnArgs(cfg.port);
|
||||
return () => cliproxySpawnArgs(cfg.port, apiKey);
|
||||
}
|
||||
|
||||
export async function bootstrapEmbeddedServices(): Promise<void> {
|
||||
|
||||
204
src/lib/services/cliproxyAccountHealth.ts
Normal file
204
src/lib/services/cliproxyAccountHealth.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import { getServiceRow } from "@/lib/db/versionManager";
|
||||
import { getOrCreateApiKey } from "@/lib/services/apiKey";
|
||||
import { CLIPROXY_DEFAULT_PORT } from "@/lib/services/installers/cliproxy";
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = 5_000;
|
||||
const AUTH_FILES_PATH = "/v0/management/auth-files";
|
||||
|
||||
export type CliproxyAccountHealthState =
|
||||
| "ready"
|
||||
| "disabled"
|
||||
| "missing_key"
|
||||
| "unreachable"
|
||||
| "unauthorized"
|
||||
| "unsupported"
|
||||
| "invalid_response";
|
||||
|
||||
export interface CliproxyRecentRequest {
|
||||
time: string;
|
||||
success: number;
|
||||
failed: number;
|
||||
}
|
||||
|
||||
export interface CliproxyAccountHealth {
|
||||
authIndex: string;
|
||||
provider: string;
|
||||
type: string;
|
||||
label: string;
|
||||
status: string;
|
||||
disabled: boolean;
|
||||
unavailable: boolean;
|
||||
createdAt: string | null;
|
||||
updatedAt: string | null;
|
||||
success: number;
|
||||
failed: number;
|
||||
recentRequests: CliproxyRecentRequest[];
|
||||
}
|
||||
|
||||
export interface CliproxyAccountHealthResult {
|
||||
state: CliproxyAccountHealthState;
|
||||
accounts: CliproxyAccountHealth[];
|
||||
version: string | null;
|
||||
}
|
||||
|
||||
type FetchLike = typeof fetch;
|
||||
|
||||
interface GetCliproxyAccountHealthOptions {
|
||||
fetchImpl?: FetchLike;
|
||||
timeoutMs?: number;
|
||||
host?: string;
|
||||
port?: number;
|
||||
managementKey?: string | null;
|
||||
embedded?: boolean;
|
||||
}
|
||||
|
||||
function record(value: unknown): Record<string, unknown> | null {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: null;
|
||||
}
|
||||
|
||||
function string(value: unknown): string {
|
||||
return typeof value === "string" ? value : "";
|
||||
}
|
||||
|
||||
function nullableTimestamp(value: unknown): string | null {
|
||||
const text = string(value);
|
||||
return text && !Number.isNaN(Date.parse(text)) ? text : null;
|
||||
}
|
||||
|
||||
function count(value: unknown): number {
|
||||
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
|
||||
}
|
||||
|
||||
function sanitizeRecentRequests(value: unknown): CliproxyRecentRequest[] {
|
||||
if (!Array.isArray(value)) return [];
|
||||
return value
|
||||
.slice(0, 20)
|
||||
.map(record)
|
||||
.filter((bucket): bucket is Record<string, unknown> => bucket !== null)
|
||||
.map((bucket) => ({
|
||||
time: nullableTimestamp(bucket.time) ?? "",
|
||||
success: count(bucket.success),
|
||||
failed: count(bucket.failed),
|
||||
}))
|
||||
.filter((bucket) => bucket.time !== "");
|
||||
}
|
||||
|
||||
export function sanitizeCliproxyAuthFiles(payload: unknown): CliproxyAccountHealth[] | null {
|
||||
const files = record(payload)?.files;
|
||||
if (!Array.isArray(files)) return null;
|
||||
return files
|
||||
.map(record)
|
||||
.filter((file): file is Record<string, unknown> => file !== null)
|
||||
.map((file) => ({
|
||||
authIndex: string(file.auth_index),
|
||||
provider: string(file.provider),
|
||||
type: string(file.type),
|
||||
label: string(file.label),
|
||||
status: string(file.status),
|
||||
disabled: file.disabled === true,
|
||||
unavailable: file.unavailable === true,
|
||||
createdAt: nullableTimestamp(file.created_at),
|
||||
updatedAt: nullableTimestamp(file.updated_at ?? file.modtime),
|
||||
success: count(file.success),
|
||||
failed: count(file.failed),
|
||||
recentRequests: sanitizeRecentRequests(file.recent_requests),
|
||||
}))
|
||||
.filter((file) => file.authIndex !== "");
|
||||
}
|
||||
|
||||
async function resolveConnection(
|
||||
options: GetCliproxyAccountHealthOptions
|
||||
): Promise<
|
||||
| { state: "ready"; host: string; port: number; managementKey: string }
|
||||
| { state: "disabled" | "missing_key" }
|
||||
> {
|
||||
if (options.managementKey !== undefined) {
|
||||
const key = options.managementKey?.trim();
|
||||
if (!key) return { state: "missing_key" };
|
||||
return {
|
||||
state: "ready",
|
||||
host: options.host ?? "127.0.0.1",
|
||||
port: options.port ?? CLIPROXY_DEFAULT_PORT,
|
||||
managementKey: key,
|
||||
};
|
||||
}
|
||||
|
||||
const externalHost = process.env.CLIPROXYAPI_HOST?.trim();
|
||||
const externalKey = process.env.CLIPROXYAPI_MANAGEMENT_KEY?.trim();
|
||||
const embedded = options.embedded ?? !(externalHost || externalKey);
|
||||
if (embedded) {
|
||||
const row = await getServiceRow("cliproxy");
|
||||
if (!row || row.status === "not_installed") return { state: "disabled" };
|
||||
return {
|
||||
state: "ready",
|
||||
host: options.host ?? "127.0.0.1",
|
||||
port: options.port ?? row.port ?? CLIPROXY_DEFAULT_PORT,
|
||||
managementKey: await getOrCreateApiKey("cliproxy"),
|
||||
};
|
||||
}
|
||||
|
||||
if (!externalKey) return { state: "missing_key" };
|
||||
const configuredPort = Number.parseInt(process.env.CLIPROXYAPI_PORT ?? "", 10);
|
||||
return {
|
||||
state: "ready",
|
||||
host: options.host ?? externalHost,
|
||||
port:
|
||||
options.port ??
|
||||
(Number.isInteger(configuredPort) && configuredPort > 0
|
||||
? configuredPort
|
||||
: CLIPROXY_DEFAULT_PORT),
|
||||
managementKey: externalKey,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getCliproxyAccountHealth(
|
||||
options: GetCliproxyAccountHealthOptions = {}
|
||||
): Promise<CliproxyAccountHealthResult> {
|
||||
let connection: Awaited<ReturnType<typeof resolveConnection>>;
|
||||
try {
|
||||
connection = await resolveConnection(options);
|
||||
} catch {
|
||||
return { state: "missing_key", accounts: [], version: null };
|
||||
}
|
||||
if (connection.state !== "ready") {
|
||||
return { state: connection.state, accounts: [], version: null };
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
||||
try {
|
||||
const response = await (options.fetchImpl ?? fetch)(
|
||||
`http://${connection.host}:${connection.port}${AUTH_FILES_PATH}`,
|
||||
{
|
||||
headers: { Authorization: `Bearer ${connection.managementKey}` },
|
||||
signal: controller.signal,
|
||||
}
|
||||
);
|
||||
const version = response.headers.get("x-cpa-version");
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
return { state: "unauthorized", accounts: [], version };
|
||||
}
|
||||
if (response.status === 404) {
|
||||
return { state: "unsupported", accounts: [], version };
|
||||
}
|
||||
if (!response.ok) {
|
||||
return { state: "unreachable", accounts: [], version };
|
||||
}
|
||||
let payload: unknown;
|
||||
try {
|
||||
payload = await response.json();
|
||||
} catch {
|
||||
return { state: "invalid_response", accounts: [], version };
|
||||
}
|
||||
const accounts = sanitizeCliproxyAuthFiles(payload);
|
||||
return accounts
|
||||
? { state: "ready", accounts, version }
|
||||
: { state: "invalid_response", accounts: [], version };
|
||||
} catch {
|
||||
return { state: "unreachable", accounts: [], version: null };
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
@@ -101,7 +101,7 @@ export async function update(): Promise<InstallResult> {
|
||||
* ServiceSupervisor calls spawnArgs() synchronously just before spawn(), so
|
||||
* async file I/O is not available here.
|
||||
*/
|
||||
export function resolveSpawnArgs(port: number): SpawnArgs {
|
||||
export function resolveSpawnArgs(port: number, managementKey?: string): SpawnArgs {
|
||||
// #11236 (bug 3 residual): runtime os.platform() read — a process.platform
|
||||
// literal here is constant-folded to the Linux build machine when the
|
||||
// published artifact is bundled, dropping the `.exe` suffix from the spawn
|
||||
@@ -116,10 +116,12 @@ export function resolveSpawnArgs(port: number): SpawnArgs {
|
||||
fs.writeFileSync(configPath, `port: ${port}\nhost: 127.0.0.1\nlog_level: warn\n`, "utf8");
|
||||
}
|
||||
|
||||
const env = { ...process.env };
|
||||
if (managementKey) env.MANAGEMENT_PASSWORD = managementKey;
|
||||
return {
|
||||
command: symlinkPath,
|
||||
args: ["--config", configPath],
|
||||
env: { ...process.env },
|
||||
env,
|
||||
cwd: CONFIG_DIR,
|
||||
};
|
||||
}
|
||||
|
||||
47
tests/unit/api/services/cliproxy-accounts.test.ts
Normal file
47
tests/unit/api/services/cliproxy-accounts.test.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { before, after, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-cliproxy-accounts-api-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
process.env.API_KEY_SECRET = "cliproxy-accounts-api-test-secret";
|
||||
process.env.DISABLE_SQLITE_AUTO_BACKUP = "true";
|
||||
|
||||
const core = await import("../../../../src/lib/db/core.ts");
|
||||
const settingsDb = await import("../../../../src/lib/db/settings.ts");
|
||||
const apiKeysDb = await import("../../../../src/lib/db/apiKeys.ts");
|
||||
const { GET } = await import("../../../../src/app/api/services/cliproxy/accounts/route.ts");
|
||||
|
||||
before(async () => {
|
||||
await settingsDb.updateSettings({ requireLogin: true });
|
||||
process.env.INITIAL_PASSWORD = "cliproxy-accounts-test-password";
|
||||
});
|
||||
|
||||
after(() => {
|
||||
delete process.env.INITIAL_PASSWORD;
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("requires OmniRoute management authentication", async () => {
|
||||
const response = await GET(
|
||||
new Request("http://localhost/api/services/cliproxy/accounts")
|
||||
);
|
||||
assert.equal(response.status, 401);
|
||||
});
|
||||
|
||||
it("accepts a scoped OmniRoute management API key", async () => {
|
||||
const { key } = await apiKeysDb.createApiKey("cliproxy-accounts", "test", ["manage"]);
|
||||
const response = await GET(
|
||||
new Request("http://localhost/api/services/cliproxy/accounts", {
|
||||
headers: { Authorization: `Bearer ${key}` },
|
||||
})
|
||||
);
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(response.headers.get("cache-control"), "no-store");
|
||||
const body = await response.json();
|
||||
assert.equal(body.state, "disabled");
|
||||
assert.deepEqual(body.accounts, []);
|
||||
});
|
||||
@@ -17,6 +17,14 @@ describe("CliproxyServiceTab — module shape", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("CliproxyServiceTab — account health", () => {
|
||||
it("exports the read-only account health card", async () => {
|
||||
const mod =
|
||||
await import("../../../../../src/app/(dashboard)/dashboard/providers/services/components/CliproxyAccountHealthCard.tsx");
|
||||
assert.equal(typeof mod.CliproxyAccountHealthCard, "function");
|
||||
});
|
||||
});
|
||||
|
||||
// ── URL validation (mirrors isValidUrl inside the tab) ────────────────────────
|
||||
|
||||
function isValidUrl(value: string): boolean {
|
||||
|
||||
148
tests/unit/services/cliproxy-account-health.test.ts
Normal file
148
tests/unit/services/cliproxy-account-health.test.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
getCliproxyAccountHealth,
|
||||
sanitizeCliproxyAuthFiles,
|
||||
} from "../../../src/lib/services/cliproxyAccountHealth.ts";
|
||||
|
||||
describe("CLIProxyAPI account health", () => {
|
||||
it("keeps only the documented health allowlist", () => {
|
||||
const accounts = sanitizeCliproxyAuthFiles({
|
||||
files: [
|
||||
{
|
||||
auth_index: "acct-1",
|
||||
provider: "codex",
|
||||
type: "codex",
|
||||
label: "Work",
|
||||
status: "active",
|
||||
disabled: false,
|
||||
unavailable: true,
|
||||
created_at: "2026-08-23T10:00:00Z",
|
||||
updated_at: "2026-08-23T11:00:00Z",
|
||||
success: 9,
|
||||
failed: 2,
|
||||
recent_requests: [
|
||||
{ time: "2026-08-23T11:00:00Z", success: 3, failed: 1, token: "secret" },
|
||||
],
|
||||
path: "/home/user/.cli-proxy-api/acct.json",
|
||||
access_token: "secret",
|
||||
metadata: { refresh_token: "secret" },
|
||||
email: "private@example.com",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
assert.deepEqual(accounts, [
|
||||
{
|
||||
authIndex: "acct-1",
|
||||
provider: "codex",
|
||||
type: "codex",
|
||||
label: "Work",
|
||||
status: "active",
|
||||
disabled: false,
|
||||
unavailable: true,
|
||||
createdAt: "2026-08-23T10:00:00Z",
|
||||
updatedAt: "2026-08-23T11:00:00Z",
|
||||
success: 9,
|
||||
failed: 2,
|
||||
recentRequests: [{ time: "2026-08-23T11:00:00Z", success: 3, failed: 1 }],
|
||||
},
|
||||
]);
|
||||
const serialized = JSON.stringify(accounts);
|
||||
for (const secret of ["path", "access_token", "refresh_token", "private@example.com"]) {
|
||||
assert.equal(serialized.includes(secret), false);
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects malformed payloads", () => {
|
||||
assert.equal(sanitizeCliproxyAuthFiles({ files: "not-an-array" }), null);
|
||||
assert.equal(sanitizeCliproxyAuthFiles(null), null);
|
||||
});
|
||||
|
||||
it("uses management auth and never forwards the key", async () => {
|
||||
let observed: { url: string; authorization: string | null } | undefined;
|
||||
const result = await getCliproxyAccountHealth({
|
||||
managementKey: "management-secret",
|
||||
host: "127.0.0.1",
|
||||
port: 8317,
|
||||
fetchImpl: async (input, init) => {
|
||||
const headers = new Headers(init?.headers);
|
||||
observed = { url: String(input), authorization: headers.get("authorization") };
|
||||
return Response.json(
|
||||
{ files: [{ auth_index: "acct-1", status: "active" }] },
|
||||
{ headers: { "x-cpa-version": "7.5.0" } }
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(observed, {
|
||||
url: "http://127.0.0.1:8317/v0/management/auth-files",
|
||||
authorization: "Bearer management-secret",
|
||||
});
|
||||
assert.equal(result.state, "ready");
|
||||
assert.equal(result.version, "7.5.0");
|
||||
assert.equal(JSON.stringify(result).includes("management-secret"), false);
|
||||
});
|
||||
|
||||
it("distinguishes missing, unauthorized, unsupported, invalid, and unreachable states", async () => {
|
||||
assert.equal(
|
||||
(await getCliproxyAccountHealth({ managementKey: null, embedded: false })).state,
|
||||
"missing_key"
|
||||
);
|
||||
assert.equal(
|
||||
(
|
||||
await getCliproxyAccountHealth({
|
||||
managementKey: "key",
|
||||
fetchImpl: async () => new Response(null, { status: 401 }),
|
||||
})
|
||||
).state,
|
||||
"unauthorized"
|
||||
);
|
||||
assert.equal(
|
||||
(
|
||||
await getCliproxyAccountHealth({
|
||||
managementKey: "key",
|
||||
fetchImpl: async () => new Response(null, { status: 404 }),
|
||||
})
|
||||
).state,
|
||||
"unsupported"
|
||||
);
|
||||
assert.equal(
|
||||
(
|
||||
await getCliproxyAccountHealth({
|
||||
managementKey: "key",
|
||||
fetchImpl: async () => Response.json({ unexpected: true }),
|
||||
})
|
||||
).state,
|
||||
"invalid_response"
|
||||
);
|
||||
assert.equal(
|
||||
(
|
||||
await getCliproxyAccountHealth({
|
||||
managementKey: "key",
|
||||
fetchImpl: async () => {
|
||||
throw new Error("connection refused");
|
||||
},
|
||||
})
|
||||
).state,
|
||||
"unreachable"
|
||||
);
|
||||
});
|
||||
|
||||
it("bounds a hanging request", async () => {
|
||||
const started = Date.now();
|
||||
const result = await getCliproxyAccountHealth({
|
||||
managementKey: "key",
|
||||
timeoutMs: 10,
|
||||
fetchImpl: (_input, init) =>
|
||||
new Promise((_resolve, reject) => {
|
||||
init?.signal?.addEventListener("abort", () =>
|
||||
reject(new DOMException("aborted", "AbortError"))
|
||||
);
|
||||
}),
|
||||
});
|
||||
assert.equal(result.state, "unreachable");
|
||||
assert.ok(Date.now() - started < 1_000);
|
||||
});
|
||||
});
|
||||
@@ -66,6 +66,14 @@ describe("resolveSpawnArgs (#6877 — real filesystem)", () => {
|
||||
);
|
||||
assert.ok(!result.args.includes("-c"), "args must never contain the short -c flag");
|
||||
});
|
||||
it("injects the management password without persisting it in config.yaml", async () => {
|
||||
const { resolveSpawnArgs } =
|
||||
await import("../../../../src/lib/services/installers/cliproxy.ts");
|
||||
const result = resolveSpawnArgs(8317, "management-secret");
|
||||
assert.equal(result.env.MANAGEMENT_PASSWORD, "management-secret");
|
||||
const configPath = path.join(dataDir, "services", "cliproxy", "config.yaml");
|
||||
assert.equal(fs.readFileSync(configPath, "utf8").includes("management-secret"), false);
|
||||
});
|
||||
|
||||
it("uses the .exe command name on Windows", async () => {
|
||||
// resolveSpawnArgs reads os.platform() at call time (#11236 — a
|
||||
|
||||
Reference in New Issue
Block a user