Spaces:
Runtime error
Runtime error
| import { randomUUID } from "node:crypto"; | |
| import { decryptCredential, encryptCredential } from "./encryption.mjs"; | |
| const REQUIRED_PROVIDER_COLUMNS = [ | |
| ["auth_type", "TEXT"], | |
| ["name", "TEXT"], | |
| ["email", "TEXT"], | |
| ["priority", "INTEGER DEFAULT 0"], | |
| ["is_active", "INTEGER DEFAULT 1"], | |
| ["access_token", "TEXT"], | |
| ["refresh_token", "TEXT"], | |
| ["expires_at", "TEXT"], | |
| ["token_expires_at", "TEXT"], | |
| ["scope", "TEXT"], | |
| ["project_id", "TEXT"], | |
| ["test_status", "TEXT"], | |
| ["error_code", "TEXT"], | |
| ["last_error", "TEXT"], | |
| ["last_error_at", "TEXT"], | |
| ["last_error_type", "TEXT"], | |
| ["last_error_source", "TEXT"], | |
| ["backoff_level", "INTEGER DEFAULT 0"], | |
| ["rate_limited_until", "TEXT"], | |
| ["health_check_interval", "INTEGER"], | |
| ["last_health_check_at", "TEXT"], | |
| ["last_tested", "TEXT"], | |
| ["api_key", "TEXT"], | |
| ["id_token", "TEXT"], | |
| ["provider_specific_data", "TEXT"], | |
| ["expires_in", "INTEGER"], | |
| ["display_name", "TEXT"], | |
| ["global_priority", "INTEGER"], | |
| ["default_model", "TEXT"], | |
| ["token_type", "TEXT"], | |
| ["consecutive_use_count", "INTEGER DEFAULT 0"], | |
| ["rate_limit_protection", "INTEGER DEFAULT 0"], | |
| ["last_used_at", "TEXT"], | |
| ['"group"', "TEXT"], | |
| ["max_concurrent", "INTEGER"], | |
| ["created_at", "TEXT"], | |
| ["updated_at", "TEXT"], | |
| ]; | |
| function ensureProviderColumns(db) { | |
| const existingColumns = new Set( | |
| db | |
| .prepare("PRAGMA table_info(provider_connections)") | |
| .all() | |
| .map((column) => column.name) | |
| ); | |
| const missingColumns = REQUIRED_PROVIDER_COLUMNS.filter(([name]) => { | |
| const normalizedName = name.replaceAll('"', ""); | |
| return !existingColumns.has(normalizedName); | |
| }); | |
| if (missingColumns.length === 0) return; | |
| db.transaction(() => { | |
| for (const [name, type] of missingColumns) { | |
| db.prepare(`ALTER TABLE provider_connections ADD COLUMN ${name} ${type}`).run(); | |
| } | |
| })(); | |
| } | |
| export function ensureProviderSchema(db) { | |
| db.prepare( | |
| `CREATE TABLE IF NOT EXISTS provider_connections ( | |
| id TEXT PRIMARY KEY, | |
| provider TEXT NOT NULL, | |
| auth_type TEXT, | |
| name TEXT, | |
| email TEXT, | |
| priority INTEGER DEFAULT 0, | |
| is_active INTEGER DEFAULT 1, | |
| access_token TEXT, | |
| refresh_token TEXT, | |
| expires_at TEXT, | |
| token_expires_at TEXT, | |
| scope TEXT, | |
| project_id TEXT, | |
| test_status TEXT, | |
| error_code TEXT, | |
| last_error TEXT, | |
| last_error_at TEXT, | |
| last_error_type TEXT, | |
| last_error_source TEXT, | |
| backoff_level INTEGER DEFAULT 0, | |
| rate_limited_until TEXT, | |
| health_check_interval INTEGER, | |
| last_health_check_at TEXT, | |
| last_tested TEXT, | |
| api_key TEXT, | |
| id_token TEXT, | |
| provider_specific_data TEXT, | |
| expires_in INTEGER, | |
| display_name TEXT, | |
| global_priority INTEGER, | |
| default_model TEXT, | |
| token_type TEXT, | |
| consecutive_use_count INTEGER DEFAULT 0, | |
| rate_limit_protection INTEGER DEFAULT 0, | |
| last_used_at TEXT, | |
| "group" TEXT, | |
| max_concurrent INTEGER, | |
| created_at TEXT NOT NULL, | |
| updated_at TEXT NOT NULL | |
| )` | |
| ).run(); | |
| ensureProviderColumns(db); | |
| db.prepare("CREATE INDEX IF NOT EXISTS idx_pc_provider ON provider_connections(provider)").run(); | |
| db.prepare("CREATE INDEX IF NOT EXISTS idx_pc_active ON provider_connections(is_active)").run(); | |
| db.prepare( | |
| "CREATE INDEX IF NOT EXISTS idx_pc_priority ON provider_connections(provider, priority)" | |
| ).run(); | |
| } | |
| function nextPriority(db, provider) { | |
| const row = db | |
| .prepare("SELECT MAX(priority) as max_priority FROM provider_connections WHERE provider = ?") | |
| .get(provider); | |
| return Number(row?.max_priority || 0) + 1; | |
| } | |
| function parseJsonObject(value) { | |
| if (!value || typeof value !== "string") return undefined; | |
| try { | |
| const parsed = JSON.parse(value); | |
| return parsed && typeof parsed === "object" ? parsed : null; | |
| } catch { | |
| return null; | |
| } | |
| } | |
| function rowToConnection(row) { | |
| return { | |
| id: row.id, | |
| provider: row.provider, | |
| authType: row.auth_type || "oauth", | |
| name: row.name || row.display_name || row.email || row.provider, | |
| email: row.email || null, | |
| priority: row.priority || 0, | |
| isActive: row.is_active !== 0, | |
| apiKey: row.api_key || null, | |
| accessToken: row.access_token || null, | |
| refreshToken: row.refresh_token || null, | |
| idToken: row.id_token || null, | |
| providerSpecificData: parseJsonObject(row.provider_specific_data), | |
| testStatus: row.test_status || "unknown", | |
| defaultModel: row.default_model || null, | |
| lastTested: row.last_tested || null, | |
| lastError: row.last_error || null, | |
| updatedAt: row.updated_at || null, | |
| createdAt: row.created_at || null, | |
| }; | |
| } | |
| export function listProviderConnections(db) { | |
| ensureProviderSchema(db); | |
| return db | |
| .prepare( | |
| `SELECT * FROM provider_connections | |
| ORDER BY provider ASC, priority ASC, updated_at DESC` | |
| ) | |
| .all() | |
| .map(rowToConnection); | |
| } | |
| export function findProviderConnection(db, selector) { | |
| const normalized = String(selector || "") | |
| .trim() | |
| .toLowerCase(); | |
| if (!normalized) return null; | |
| const connections = listProviderConnections(db); | |
| return ( | |
| connections.find((connection) => connection.id.toLowerCase() === normalized) || | |
| connections.find((connection) => connection.id.toLowerCase().startsWith(normalized)) || | |
| connections.find((connection) => String(connection.name || "").toLowerCase() === normalized) || | |
| connections.find((connection) => connection.provider.toLowerCase() === normalized) || | |
| null | |
| ); | |
| } | |
| export function getProviderApiKey(connection) { | |
| if (connection.authType !== "apikey") { | |
| throw new Error(`Connection ${connection.name} is not an API-key provider.`); | |
| } | |
| if (!connection.apiKey) { | |
| throw new Error(`Connection ${connection.name} has no API key configured.`); | |
| } | |
| return decryptCredential(connection.apiKey); | |
| } | |
| export function upsertApiKeyProviderConnection(db, input) { | |
| ensureProviderSchema(db); | |
| const provider = input.provider; | |
| const name = input.name || provider; | |
| const now = new Date().toISOString(); | |
| const existing = db | |
| .prepare( | |
| "SELECT id, priority FROM provider_connections WHERE provider = ? AND auth_type = 'apikey' AND name = ?" | |
| ) | |
| .get(provider, name); | |
| const connection = { | |
| id: existing?.id || randomUUID(), | |
| provider, | |
| authType: "apikey", | |
| name, | |
| priority: existing?.priority || nextPriority(db, provider), | |
| isActive: 1, | |
| apiKey: encryptCredential(input.apiKey), | |
| testStatus: input.testStatus || "unknown", | |
| defaultModel: input.defaultModel || null, | |
| providerSpecificData: input.providerSpecificData | |
| ? JSON.stringify(input.providerSpecificData) | |
| : null, | |
| createdAt: input.createdAt || now, | |
| updatedAt: now, | |
| }; | |
| db.prepare( | |
| `INSERT INTO provider_connections ( | |
| id, provider, auth_type, name, priority, is_active, api_key, provider_specific_data, | |
| test_status, default_model, created_at, updated_at | |
| ) VALUES ( | |
| @id, @provider, @authType, @name, @priority, @isActive, @apiKey, @providerSpecificData, | |
| @testStatus, @defaultModel, @createdAt, @updatedAt | |
| ) | |
| ON CONFLICT(id) DO UPDATE SET | |
| api_key = excluded.api_key, | |
| provider_specific_data = excluded.provider_specific_data, | |
| test_status = excluded.test_status, | |
| default_model = excluded.default_model, | |
| is_active = excluded.is_active, | |
| updated_at = excluded.updated_at` | |
| ).run(connection); | |
| return connection; | |
| } | |
| export function removeProviderConnectionByProvider(db, provider) { | |
| ensureProviderSchema(db); | |
| const result = db | |
| .prepare( | |
| "DELETE FROM provider_connections WHERE provider = ? AND (auth_type = 'apikey' OR (api_key IS NOT NULL AND api_key != ''))" | |
| ) | |
| .run(provider); | |
| return result.changes; | |
| } | |
| /** | |
| * Replace the encrypted API key for a connection and clear any cooldown state. | |
| * `encryptedKey` must already be passed through `encryptCredential()`. | |
| */ | |
| export function updateProviderApiKey(db, connectionId, encryptedKey) { | |
| ensureProviderSchema(db); | |
| const now = new Date().toISOString(); | |
| const result = db | |
| .prepare( | |
| `UPDATE provider_connections | |
| SET api_key = @apiKey, | |
| test_status = 'unknown', | |
| last_error = NULL, | |
| last_error_at = NULL, | |
| last_error_type = NULL, | |
| last_error_source = NULL, | |
| error_code = NULL, | |
| rate_limited_until = NULL, | |
| backoff_level = 0, | |
| updated_at = @updatedAt | |
| WHERE id = @id` | |
| ) | |
| .run({ id: connectionId, apiKey: encryptedKey, updatedAt: now }); | |
| return result.changes; | |
| } | |
| export function updateProviderTestResult(db, connectionId, result) { | |
| ensureProviderSchema(db); | |
| const now = new Date().toISOString(); | |
| const valid = result?.valid === true; | |
| db.prepare( | |
| `UPDATE provider_connections SET | |
| test_status = @testStatus, | |
| last_error = @lastError, | |
| last_error_at = @lastErrorAt, | |
| last_error_type = @lastErrorType, | |
| last_error_source = @lastErrorSource, | |
| error_code = @errorCode, | |
| last_tested = @lastTested, | |
| updated_at = @updatedAt | |
| WHERE id = @id` | |
| ).run({ | |
| id: connectionId, | |
| testStatus: valid ? "active" : "error", | |
| lastError: valid ? null : result?.error || "Provider test failed", | |
| lastErrorAt: valid ? null : now, | |
| lastErrorType: valid ? null : "connection_test_failed", | |
| lastErrorSource: valid ? null : "upstream", | |
| errorCode: valid ? null : result?.statusCode || null, | |
| lastTested: now, | |
| updatedAt: now, | |
| }); | |
| return { | |
| ...result, | |
| testedAt: now, | |
| }; | |
| } | |