/** Upstream proxy config persistence for upstream_proxy_config table. */ import { getDbInstance } from "./core"; interface UpstreamProxyConfig { id: number; providerId: string; mode: string; cliproxyapiModelMapping: Record | null; nativePriority: number; cliproxyapiPriority: number; enabled: boolean; family: string; createdAt: string; updatedAt: string; } interface UpstreamProxyRow { id: unknown; provider_id: unknown; mode: unknown; cliproxyapi_model_mapping: unknown; native_priority: unknown; cliproxyapi_priority: unknown; enabled: unknown; family: unknown; created_at: unknown; updated_at: unknown; } function toRecord(value: unknown): Record { return value && typeof value === "object" ? (value as Record) : {}; } const BLOCKED_HOSTNAMES = ["metadata.google.internal", "169.254.169.254", "metadata.aws.internal"]; function isPrivateHost(hostname: string): boolean { // CLIProxyAPI runs on localhost:8317 — allow loopback explicitly if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1") return false; if (BLOCKED_HOSTNAMES.includes(hostname)) return true; if ( /^10\./.test(hostname) || /^172\.(1[6-9]|2\d|3[01])\./.test(hostname) || /^192\.168\./.test(hostname) ) return true; if ( /^0\./.test(hostname) || /^127\./.test(hostname) || /^224\./.test(hostname) || /^169\.254\./.test(hostname) ) return true; return false; } export function validateProxyUrl( url: string ): { valid: true; url: string } | { valid: false; error: string } { try { const parsed = new URL(url); if (!["http:", "https:"].includes(parsed.protocol)) { return { valid: false, error: `Unsupported protocol "${parsed.protocol}" — use http or https`, }; } if (isPrivateHost(parsed.hostname)) { return { valid: false, error: `Proxy URL cannot point to private/internal address "${parsed.hostname}"`, }; } return { valid: true, url }; } catch { return { valid: false, error: `Invalid URL: "${url}"` }; } } function rowToConfig(record: Record): UpstreamProxyConfig { let mapping: Record | null = null; if (record.cliproxyapi_model_mapping && typeof record.cliproxyapi_model_mapping === "string") { try { mapping = JSON.parse(record.cliproxyapi_model_mapping); } catch { mapping = null; } } return { id: record.id as number, providerId: record.provider_id as string, mode: record.mode as string, cliproxyapiModelMapping: mapping, nativePriority: record.native_priority as number, cliproxyapiPriority: record.cliproxyapi_priority as number, enabled: record.enabled === 1 || record.enabled === true, family: typeof record.family === "string" ? record.family : "auto", createdAt: record.created_at as string, updatedAt: record.updated_at as string, }; } export async function getUpstreamProxyConfigs() { const db = getDbInstance(); const rows = db .prepare("SELECT * FROM upstream_proxy_config ORDER BY provider_id") .all() as UpstreamProxyRow[]; return rows.map((row) => rowToConfig(toRecord(row))); } export async function getUpstreamProxyConfig(providerId: string) { const db = getDbInstance(); const row = db .prepare("SELECT * FROM upstream_proxy_config WHERE provider_id = ?") .get(providerId) as UpstreamProxyRow | undefined; if (!row) return null; return rowToConfig(toRecord(row)); } export async function upsertUpstreamProxyConfig(data: { providerId: string; mode?: string; cliproxyapiModelMapping?: Record | null; nativePriority?: number; cliproxyapiPriority?: number; enabled?: boolean; family?: string; }) { const db = getDbInstance(); const mode = data.mode ?? "native"; const cliproxyapiModelMapping = data.cliproxyapiModelMapping !== undefined ? JSON.stringify(data.cliproxyapiModelMapping) : null; const nativePriority = data.nativePriority ?? 1; const cliproxyapiPriority = data.cliproxyapiPriority ?? 2; const enabled = data.enabled !== false ? 1 : 0; const family = data.family ?? "auto"; db.prepare( `INSERT INTO upstream_proxy_config (provider_id, mode, cliproxyapi_model_mapping, native_priority, cliproxyapi_priority, enabled, family, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now')) ON CONFLICT(provider_id) DO UPDATE SET mode = excluded.mode, cliproxyapi_model_mapping = excluded.cliproxyapi_model_mapping, native_priority = excluded.native_priority, cliproxyapi_priority = excluded.cliproxyapi_priority, enabled = excluded.enabled, family = excluded.family, updated_at = datetime('now')` ).run( data.providerId, mode, cliproxyapiModelMapping, nativePriority, cliproxyapiPriority, enabled, family ); return getUpstreamProxyConfig(data.providerId); } export async function updateUpstreamProxyConfig( providerId: string, updates: Record ) { const db = getDbInstance(); const current = await getUpstreamProxyConfig(providerId); if (!current) { throw new Error(`Provider ${providerId} not found`); } const sets: string[] = ["updated_at = datetime('now')"]; const params: unknown[] = []; if (updates.mode !== undefined) { sets.push("mode = ?"); params.push(updates.mode); } if (updates.cliproxyapiModelMapping !== undefined) { sets.push("cliproxyapi_model_mapping = ?"); params.push( updates.cliproxyapiModelMapping === null ? null : JSON.stringify(updates.cliproxyapiModelMapping) ); } if (updates.nativePriority !== undefined) { sets.push("native_priority = ?"); params.push(updates.nativePriority); } if (updates.cliproxyapiPriority !== undefined) { sets.push("cliproxyapi_priority = ?"); params.push(updates.cliproxyapiPriority); } if (updates.enabled !== undefined) { sets.push("enabled = ?"); params.push(updates.enabled === true ? 1 : 0); } if (updates.family !== undefined) { sets.push("family = ?"); params.push(updates.family); } params.push(providerId); db.prepare(`UPDATE upstream_proxy_config SET ${sets.join(", ")} WHERE provider_id = ?`).run( ...params ); return getUpstreamProxyConfig(providerId); } export async function deleteUpstreamProxyConfig(providerId: string) { const db = getDbInstance(); const result = db .prepare("DELETE FROM upstream_proxy_config WHERE provider_id = ?") .run(providerId); return result.changes > 0; } export async function getProvidersByMode(mode: string) { const db = getDbInstance(); const rows = db .prepare( "SELECT * FROM upstream_proxy_config WHERE mode = ? AND enabled = 1 ORDER BY provider_id" ) .all(mode) as UpstreamProxyRow[]; return rows.map((row) => rowToConfig(toRecord(row))); } export async function getFallbackChainForProvider(providerId: string) { const config = await getUpstreamProxyConfig(providerId); if (!config) return []; const chain: { executor: "native" | "cliproxyapi"; priority: number }[] = []; if (config.enabled) { chain.push({ executor: "native", priority: config.nativePriority }); if (config.mode === "cliproxyapi" || config.mode === "fallback") { chain.push({ executor: "cliproxyapi", priority: config.cliproxyapiPriority }); } } chain.sort((a, b) => a.priority - b.priority); return chain; }