fix(db): open import-validation DB via resilient driver factory (#3025) (#3218)

The db-backups import route statically imported better-sqlite3, which is
stripped from the Next standalone server's node_modules in the packaged
Electron app. Loading the route then crashed with "Cannot find module
'better-sqlite3'" on the Windows installer, even though node:sqlite was
available. Route the upload integrity-check through openDatabaseAsync
(better-sqlite3 -> node:sqlite -> sql.js), matching every other DB path.

Adds a guard test so no API route can reintroduce a direct native import.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-05 09:23:17 -03:00
committed by GitHub
parent 5afb984425
commit c116bfbc7f
2 changed files with 82 additions and 4 deletions

View File

@@ -1,9 +1,10 @@
import { NextResponse } from "next/server";
import Database from "better-sqlite3";
import path from "path";
import fs from "fs";
import os from "os";
import { getDbInstance, resetDbInstance, SQLITE_FILE } from "@/lib/db/core";
import { openDatabaseAsync } from "@/lib/db/adapters/driverFactory";
import type { SqliteAdapter } from "@/lib/db/adapters/types";
import { backupDbFile } from "@/lib/db/backup";
import { isAuthRequired, isAuthenticated } from "@/shared/utils/apiAuth";
import { getSettings } from "@/lib/db/settings";
@@ -86,10 +87,14 @@ export async function POST(request: Request) {
tmpPath = path.join(os.tmpdir(), `omniroute-import-${Date.now()}.sqlite`);
fs.writeFileSync(tmpPath, fileBuffer!);
// Validate SQLite integrity
let testDb: InstanceType<typeof Database> | null = null;
// Validate SQLite integrity.
// Use the resilient driver factory (better-sqlite3 → node:sqlite → sql.js) rather than
// a direct `better-sqlite3` import: in the packaged Electron app that native module is
// absent from the standalone server's node_modules, so a hard import crashes the route
// with "Cannot find module 'better-sqlite3'" even though node:sqlite is available (#3025).
let testDb: SqliteAdapter | null = null;
try {
testDb = new Database(tmpPath, { readonly: true });
testDb = await openDatabaseAsync(tmpPath, { readonly: true });
const result = testDb.pragma("integrity_check") as any[];
if (result[0]?.integrity_check !== "ok") {
return NextResponse.json(

View File

@@ -0,0 +1,73 @@
/**
* #3025 — DB import route must not hard-depend on a static `better-sqlite3` import.
*
* In the packaged Electron app, `better-sqlite3` is stripped from the Next standalone
* server's `node_modules` (it is rebuilt for the Electron ABI elsewhere). Every other DB
* code path survives because it loads the driver through the resilient driver factory
* (`tryOpenSync` → better-sqlite3 → node:sqlite → sql.js). The db-backups *import* route
* was the lone exception: it did `import Database from "better-sqlite3"` at module scope,
* so loading the route crashed with `Cannot find module 'better-sqlite3'` (reported on
* Windows installer v3.8.10), even though node:sqlite was available.
*
* Guard: no API route under src/app/api may statically import/require better-sqlite3 —
* routes run inside the standalone server where the native module is not guaranteed.
* Behaviour: the resilient opener validates a real sqlite file via PRAGMA integrity_check
* and surfaces the same `[{ integrity_check: "ok" }]` shape the route relies on.
*/
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { readFileSync, readdirSync, statSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { openDatabaseAsync } from "../../src/lib/db/adapters/driverFactory.ts";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const API_DIR = path.join(__dirname, "..", "..", "src", "app", "api");
function walk(dir: string): string[] {
const out: string[] = [];
for (const entry of readdirSync(dir)) {
const full = path.join(dir, entry);
if (statSync(full).isDirectory()) out.push(...walk(full));
else if (/\.(ts|tsx|js|mjs)$/.test(entry)) out.push(full);
}
return out;
}
const DIRECT_IMPORT = /(?:from\s+|require\(\s*)["']better-sqlite3["']/;
test("no API route statically imports better-sqlite3 (must use the resilient driver factory)", () => {
const offenders = walk(API_DIR).filter((file) => DIRECT_IMPORT.test(readFileSync(file, "utf8")));
assert.deepEqual(
offenders.map((f) => path.relative(API_DIR, f)),
[],
"API routes run in the standalone server where better-sqlite3 may be absent; open " +
"databases via src/lib/db (openDatabaseAsync / getDbInstance), never a direct import."
);
});
test("openDatabaseAsync validates a real sqlite file with the integrity_check shape the route expects", async () => {
const tmp = path.join(os.tmpdir(), `omniroute-3025-${process.pid}-${process.hrtime.bigint()}.sqlite`);
// Seed a valid sqlite file through the same resilient adapter the route will now use.
const seed = await openDatabaseAsync(tmp);
seed.exec("CREATE TABLE api_keys (id INTEGER PRIMARY KEY)");
seed.close();
const db = await openDatabaseAsync(tmp, { readonly: true });
try {
const result = db.pragma("integrity_check") as Array<{ integrity_check?: string }>;
assert.equal(result[0]?.integrity_check, "ok");
const tables = (db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all() as Array<{
name: string;
}>).map((r) => r.name);
assert.ok(tables.includes("api_keys"));
} finally {
db.close();
for (const f of [tmp, `${tmp}-wal`, `${tmp}-shm`, `${tmp}-journal`]) {
if (fs.existsSync(f)) fs.unlinkSync(f);
}
}
});