Spaces:
Runtime error
Runtime error
| import fs from "node:fs"; | |
| import { resolveDataDir, resolveStoragePath } from "./data-dir.mjs"; | |
| import { ensureProviderSchema } from "./provider-store.mjs"; | |
| import { ensureSettingsSchema, hashManagementPassword, updateSettings } from "./settings-store.mjs"; | |
| async function loadBetterSqlite() { | |
| try { | |
| return (await import("better-sqlite3")).default; | |
| } catch { | |
| throw new Error("better-sqlite3 is not installed. Run npm install before using setup."); | |
| } | |
| } | |
| export function createSqliteNativeError(error) { | |
| const message = error instanceof Error ? error.message : String(error); | |
| if (message.includes("NODE_MODULE_VERSION") || message.includes("ERR_DLOPEN_FAILED")) { | |
| return new Error( | |
| "better-sqlite3 native binding is incompatible with this Node.js runtime. " + | |
| "Run `npm rebuild better-sqlite3` in the OmniRoute project and try again. " + | |
| "Or run: omniroute runtime repair " + | |
| "(rebuilds into a user-writable runtime; works without a C++ toolchain)." | |
| ); | |
| } | |
| return error; | |
| } | |
| async function openSqliteDatabase(dbPath, options = {}) { | |
| const Database = await loadBetterSqlite(); | |
| try { | |
| return new Database(dbPath, options); | |
| } catch (error) { | |
| throw createSqliteNativeError(error); | |
| } | |
| } | |
| export async function openOmniRouteDb() { | |
| const dataDir = resolveDataDir(); | |
| const dbPath = resolveStoragePath(dataDir); | |
| fs.mkdirSync(dataDir, { recursive: true }); | |
| const db = await openSqliteDatabase(dbPath); | |
| db.pragma("journal_mode = WAL"); | |
| ensureSettingsSchema(db); | |
| ensureProviderSchema(db); | |
| return { db, dataDir, dbPath }; | |
| } | |
| export async function withReadonlySqlite(dbPath, callback) { | |
| const db = await openSqliteDatabase(dbPath, { readonly: true, fileMustExist: true }); | |
| try { | |
| return await callback(db); | |
| } finally { | |
| db.close(); | |
| } | |
| } | |
| export async function backupSqliteFile(sourcePath, destPath) { | |
| const db = await openSqliteDatabase(sourcePath, { readonly: true }); | |
| try { | |
| await db.backup(destPath); | |
| } finally { | |
| db.close(); | |
| } | |
| } | |
| export async function readDatabaseHealth(dbPath) { | |
| return withReadonlySqlite(dbPath, (db) => { | |
| const quickCheck = db.prepare("PRAGMA quick_check").get(); | |
| const quickCheckValue = Object.values(quickCheck || {})[0]; | |
| const hasMigrationTable = !!db | |
| .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?") | |
| .get("_omniroute_migrations"); | |
| const appliedMigrationVersions = hasMigrationTable | |
| ? db | |
| .prepare("SELECT version FROM _omniroute_migrations") | |
| .all() | |
| .map((row) => row.version) | |
| : []; | |
| return { quickCheckValue, hasMigrationTable, appliedMigrationVersions }; | |
| }); | |
| } | |
| export async function readEncryptedCredentialSamples(dbPath) { | |
| return withReadonlySqlite(dbPath, (db) => { | |
| const hasProviderTable = !!db | |
| .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?") | |
| .get("provider_connections"); | |
| if (!hasProviderTable) { | |
| return { hasProviderTable: false, encryptedValues: [] }; | |
| } | |
| const rows = db | |
| .prepare( | |
| `SELECT api_key, access_token, refresh_token, id_token | |
| FROM provider_connections | |
| WHERE api_key LIKE 'enc:v1:%' | |
| OR access_token LIKE 'enc:v1:%' | |
| OR refresh_token LIKE 'enc:v1:%' | |
| OR id_token LIKE 'enc:v1:%' | |
| LIMIT 20` | |
| ) | |
| .all(); | |
| const encryptedValues = rows.flatMap((row) => | |
| ["api_key", "access_token", "refresh_token", "id_token"] | |
| .filter((key) => typeof row[key] === "string" && row[key].startsWith("enc:v1:")) | |
| .map((key) => row[key]) | |
| ); | |
| return { hasProviderTable: true, encryptedValues }; | |
| }); | |
| } | |
| export async function readManagementPasswordState(dbPath = resolveStoragePath(resolveDataDir())) { | |
| if (!fs.existsSync(dbPath)) { | |
| return { exists: false, hasPassword: false }; | |
| } | |
| return withReadonlySqlite(dbPath, (db) => { | |
| const hasSettingsTable = !!db | |
| .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?") | |
| .get("key_value"); | |
| if (!hasSettingsTable) { | |
| return { exists: true, hasPassword: false }; | |
| } | |
| const row = db | |
| .prepare("SELECT value FROM key_value WHERE namespace = 'settings' AND key = ?") | |
| .get("password"); | |
| let password = row?.value; | |
| if (typeof password === "string") { | |
| try { | |
| password = JSON.parse(password); | |
| } catch {} | |
| } | |
| return { | |
| exists: true, | |
| hasPassword: typeof password === "string" && password.length > 0, | |
| }; | |
| }); | |
| } | |
| export async function resetManagementPassword( | |
| password, | |
| dbPath = resolveStoragePath(resolveDataDir()) | |
| ) { | |
| const db = await openSqliteDatabase(dbPath); | |
| try { | |
| db.pragma("journal_mode = WAL"); | |
| ensureSettingsSchema(db); | |
| const hashedPassword = await hashManagementPassword(password); | |
| updateSettings(db, { password: hashedPassword, requireLogin: true, setupComplete: true }); | |
| } finally { | |
| db.close(); | |
| } | |
| } | |