159 lines
4.9 KiB
JavaScript
159 lines
4.9 KiB
JavaScript
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();
|
|
}
|
|
}
|