import { v4 as uuidv4 } from "uuid"; import { getAdapter } from "../driver.js"; import { parseJson, stringifyJson } from "../helpers/jsonCol.js"; const OPTIONAL_FIELDS = [ "displayName", "email", "globalPriority", "defaultModel", "accessToken", "refreshToken", "expiresAt", "tokenType", "scope", "projectId", "apiKey", "testStatus", "lastTested", "lastError", "lastErrorAt", "rateLimitedUntil", "expiresIn", "errorCode", "consecutiveUseCount", "idToken", "lastRefreshAt", ]; function rowToConn(row) { if (!row) return null; const extra = parseJson(row.data, {}); return { ...extra, id: row.id, provider: row.provider, authType: row.authType, name: row.name, email: row.email, ownerId: row.ownerId, priority: row.priority, isActive: row.isActive === 1 || row.isActive === true, createdAt: row.createdAt, updatedAt: row.updatedAt, }; } function connToRow(c) { const { id, provider, authType, name, email, ownerId, priority, isActive, createdAt, updatedAt, ...rest } = c; return { id, provider, authType, name: name ?? null, email: email ?? null, ownerId: ownerId ?? null, priority: priority ?? null, isActive: isActive === false ? 0 : 1, data: stringifyJson(rest), createdAt, updatedAt, }; } function upsert(db, c) { const r = connToRow(c); db.run( `INSERT INTO providerConnections(id, provider, authType, name, email, ownerId, priority, isActive, data, createdAt, updatedAt) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET provider=excluded.provider, authType=excluded.authType, name=excluded.name, email=excluded.email, ownerId=excluded.ownerId, priority=excluded.priority, isActive=excluded.isActive, data=excluded.data, updatedAt=excluded.updatedAt`, [r.id, r.provider, r.authType, r.name, r.email, r.ownerId, r.priority, r.isActive, r.data, r.createdAt, r.updatedAt] ); } function deriveConnectionName(data, fallbackName) { if (data.provider === "github") { return data.providerSpecificData?.githubLogin || data.providerSpecificData?.githubEmail || data.email || data.providerSpecificData?.githubName || fallbackName; } return fallbackName; } function findDuplicateAccount(connections, data) { const incomingCredential = data.accessToken || data.apiKey || data.refreshToken; if (incomingCredential) { const credentialMatch = connections.find((connection) => ( connection.accessToken === incomingCredential || connection.apiKey === incomingCredential || connection.refreshToken === incomingCredential )); if (credentialMatch) return credentialMatch; } if (!data.email) return null; const incomingUsername = data.providerSpecificData?.username; const incomingWorkspace = data.providerSpecificData?.chatgptAccountId; return connections.find((connection) => { if (connection.email !== data.email) return false; const existingWorkspace = connection.providerSpecificData?.chatgptAccountId; if (incomingWorkspace && existingWorkspace) { return incomingWorkspace === existingWorkspace; } if (incomingWorkspace || existingWorkspace) return false; const existingUsername = connection.providerSpecificData?.username; if (incomingUsername && existingUsername) { return incomingUsername === existingUsername; } if (incomingUsername || existingUsername) return false; return true; }); } function duplicateAccountError() { const error = new Error("Account already exists in the system"); error.code = "PROVIDER_ACCOUNT_EXISTS"; error.status = 409; return error; } export async function getProviderConnections(filter = {}) { const db = await getAdapter(); const where = []; const params = []; if (filter.provider) { where.push("provider = ?"); params.push(filter.provider); } // `undefined` is an explicit administrative, unscoped query. `null` scopes // to legacy/global connections rather than exposing every user's accounts. if (Object.prototype.hasOwnProperty.call(filter, "ownerId")) { where.push("ownerId IS ?"); params.push(filter.ownerId); } if (filter.isActive !== undefined) { where.push("isActive = ?"); params.push(filter.isActive ? 1 : 0); } const sql = `SELECT * FROM providerConnections${where.length ? ` WHERE ${where.join(" AND ")}` : ""}`; const rows = db.all(sql, params); const list = rows.map(rowToConn); list.sort((a, b) => (a.priority || 999) - (b.priority || 999)); return list; } export async function getProviderConnectionById(id, ownerId = null) { const db = await getAdapter(); const where = ["id = ?"]; const params = [id]; if (ownerId) { where.push("ownerId = ?"); params.push(ownerId); } const row = db.get(`SELECT * FROM providerConnections WHERE ${where.join(" AND ")}`, params); return rowToConn(row); } // Internal sync reorder — must be called INSIDE a transaction function reorderInTx(db, providerId) { const list = db.all(`SELECT * FROM providerConnections WHERE provider = ?`, [providerId]).map(rowToConn); list.sort((a, b) => { const pDiff = (a.priority || 0) - (b.priority || 0); if (pDiff !== 0) return pDiff; return new Date(b.updatedAt || 0) - new Date(a.updatedAt || 0); }); list.forEach((c, i) => { db.run(`UPDATE providerConnections SET priority = ? WHERE id = ?`, [i + 1, c.id]); }); } export async function createProviderConnection(data) { const db = await getAdapter(); const now = new Date().toISOString(); let result; db.transaction(() => { const allProviderConnections = db.all(`SELECT * FROM providerConnections WHERE provider = ?`, [data.provider]).map(rowToConn); const all = data.ownerId ? allProviderConnections.filter((connection) => connection.ownerId === data.ownerId) : allProviderConnections; // Account identities are global: two dashboard users must not register the // same provider account. API-key connections without an account identity // or matching credential remain private and may use the same display name. const existingAccount = findDuplicateAccount(allProviderConnections, data); if (existingAccount) { throw duplicateAccountError(); } let connectionName = data.name || null; if (!connectionName && (data.authType === "oauth" || data.authType === "access_token")) { connectionName = deriveConnectionName(data, data.email || `Account ${all.length + 1}`); } let connectionPriority = data.priority; if (!connectionPriority) { connectionPriority = all.reduce((m, c) => Math.max(m, c.priority || 0), 0) + 1; } const conn = { id: uuidv4(), provider: data.provider, authType: data.authType || "oauth", name: connectionName, ownerId: data.ownerId || null, priority: connectionPriority, isActive: data.isActive !== undefined ? data.isActive : true, createdAt: now, updatedAt: now, }; for (const f of OPTIONAL_FIELDS) { if (data[f] !== undefined && data[f] !== null) conn[f] = data[f]; } if (data.providerSpecificData && Object.keys(data.providerSpecificData).length > 0) { conn.providerSpecificData = data.providerSpecificData; } if (data.email !== undefined) conn.email = data.email; upsert(db, conn); reorderInTx(db, data.provider); result = conn; }); return result; } // Critical: OAuth refresh token race — atomic merge inside transaction export async function updateProviderConnection(id, data) { const db = await getAdapter(); let result; db.transaction(() => { const row = db.get(`SELECT * FROM providerConnections WHERE id = ?`, [id]); if (!row) { result = null; return; } const existing = rowToConn(row); const merged = { ...existing, ...data, updatedAt: new Date().toISOString() }; upsert(db, merged); if (data.priority !== undefined) reorderInTx(db, existing.provider); result = merged; }); return result; } export async function deleteProviderConnection(id) { const db = await getAdapter(); let ok = false; db.transaction(() => { const row = db.get(`SELECT provider FROM providerConnections WHERE id = ?`, [id]); if (!row) return; db.run(`DELETE FROM providerConnections WHERE id = ?`, [id]); reorderInTx(db, row.provider); ok = true; }); return ok; } export async function deleteProviderConnectionsByProvider(providerId) { const db = await getAdapter(); const before = db.get(`SELECT COUNT(*) AS n FROM providerConnections WHERE provider = ?`, [providerId]); db.run(`DELETE FROM providerConnections WHERE provider = ?`, [providerId]); return before?.n || 0; } export async function reorderProviderConnections(providerId) { const db = await getAdapter(); db.transaction(() => reorderInTx(db, providerId)); } export async function cleanupProviderConnections() { const db = await getAdapter(); const fieldsToCheck = [ "displayName", "email", "globalPriority", "defaultModel", "accessToken", "refreshToken", "expiresAt", "tokenType", "scope", "projectId", "apiKey", "testStatus", "lastTested", "lastError", "lastErrorAt", "rateLimitedUntil", "expiresIn", "consecutiveUseCount", ]; let cleaned = 0; db.transaction(() => { const rows = db.all(`SELECT * FROM providerConnections`); for (const row of rows) { const conn = rowToConn(row); let dirty = false; for (const f of fieldsToCheck) { if (conn[f] === null || conn[f] === undefined) { if (f in conn) { delete conn[f]; cleaned++; dirty = true; } } } if (conn.providerSpecificData && Object.keys(conn.providerSpecificData).length === 0) { delete conn.providerSpecificData; cleaned++; dirty = true; } if (dirty) upsert(db, conn); } }); return cleaned; }