mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-18 21:02:50 +03:00
* mcp/audit: fall back when better-sqlite3 export is not callable
Dashboard MCP status polls reopen a failed native sqlite load every 30s
because a minified TypeError ("a is not a function") was not treated as
a native load failure and a failed open was not cached. Classify that
shape, fall back to node:sqlite, cache the miss, and refuse to ship a
Docker image without better_sqlite3.node.
Signed-off-by: Minxi Hou <houminxi@gmail.com>
* mcp/audit: force native better-sqlite3 compile in Docker
better-sqlite3 13 ships a linux prebuild. Bare `node-gyp rebuild`
then only TOUCHes stamp files and never writes
build/Release/better_sqlite3.node, so the new test -f gate fails the
image build. Pass --force_build=1, matching the package's own
build-release script.
Signed-off-by: Minxi Hou <houminxi@gmail.com>
* db/core: keep native-load classification under the file-size cap
The audit fallback added two TypeError fingerprints in core.ts and
crossed the frozen 1788-line cap. Move the classifier into
sqliteLoadError.ts and re-export it so existing importers stay stable.
Signed-off-by: Minxi Hou <houminxi@gmail.com>
* build/bootstrap: keep the encrypted-credentials probe narrow
The native-load classifier was copied into scripts/build/bootstrap-env.mjs
alongside the runtime one, but the two files consume its verdict in opposite
directions. In src/lib/db/sqliteLoadError.ts a true verdict means "the driver
is unusable, cascade to node:sqlite", so treating a non-callable export as a
load failure is what we want. In the bootstrap the verdict feeds
hasEncryptedCredentials, where true means "no encrypted credentials found" and
clears the way to generate a fresh STORAGE_ENCRYPTION_KEY.
With the TypeError patterns in the bootstrap copy, a binding that loads but
exports something non-callable over a database full of enc:v1: rows reads as an
empty database, and the operator silently loses access to every stored
credential. Drop those two patterns from the bootstrap copy only, and note in
both files why the pair is deliberately not identical.
A corrupt binding still fails loudly there, now with the database path, the
underlying message, and a rebuild hint, so the narrower classifier does not
cost any diagnosability.
Signed-off-by: Minxi Hou <houminxi@gmail.com>
* fix(mcp): keep audit logging recoverable when the database is created later
getDb() cached a null for the "storage.sqlite does not exist yet" branch, and
closeAuditDb() returns before clearing a falsy cache — so an MCP server started
before the app created the database stayed without audit logging for the whole
process lifetime. Only a genuine driver-load failure is cached now; the
not-found branch retries, which is how it recovers when the file appears.
Covered by a new test that fails without the change.
Also replace the fabricated minified TypeError text ("a is not a function")
thrown by the loader with "better-sqlite3 export is not a function": the
operator sees a diagnosable message and isNativeSqliteLoadError() still
classifies it (it matches on "is not a function").
---------
Signed-off-by: Minxi Hou <houminxi@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
84 lines
3.6 KiB
JavaScript
84 lines
3.6 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import test from "node:test";
|
|
|
|
// The two copies of isNativeSqliteLoadError are deliberately NOT identical.
|
|
//
|
|
// src/lib/db/sqliteLoadError.ts runs where a match means "this driver is
|
|
// unusable, cascade to the next one", so a non-callable better-sqlite3 export
|
|
// ("X is not a constructor") must match.
|
|
//
|
|
// scripts/build/bootstrap-env.mjs runs where the only consumer,
|
|
// hasEncryptedCredentials(), turns a match into `return false` — read by the
|
|
// caller as "no encrypted credentials exist" and used to mint a fresh
|
|
// STORAGE_ENCRYPTION_KEY. Matching a generic TypeError there would overwrite
|
|
// the key of a database that still holds enc:v1: rows, so that path stays loud.
|
|
//
|
|
// Sharing one widened classifier across both is what this test exists to catch.
|
|
|
|
const GENERIC_TYPE_ERROR_PATTERNS = ['"is not a function"', '"is not a constructor"'];
|
|
|
|
function readSource(relativePath) {
|
|
return readFileSync(new URL(relativePath, import.meta.url), "utf8");
|
|
}
|
|
|
|
function classifierBody(source) {
|
|
const start = source.indexOf("isNativeSqliteLoadError");
|
|
assert.notEqual(start, -1, "isNativeSqliteLoadError not found");
|
|
const end = source.indexOf("\n}", start);
|
|
assert.notEqual(end, -1, "could not delimit isNativeSqliteLoadError");
|
|
return source.slice(start, end);
|
|
}
|
|
|
|
test("the bootstrap classifier stays narrow so key generation cannot silently proceed", () => {
|
|
const body = classifierBody(readSource("../../../scripts/build/bootstrap-env.mjs"));
|
|
|
|
for (const pattern of GENERIC_TYPE_ERROR_PATTERNS) {
|
|
assert.ok(
|
|
!body.includes(pattern),
|
|
`scripts/build/bootstrap-env.mjs must not classify ${pattern} as a native SQLite load failure: ` +
|
|
"hasEncryptedCredentials() would then report no encrypted credentials and bootstrapEnv " +
|
|
"would generate a new STORAGE_ENCRYPTION_KEY over encrypted rows."
|
|
);
|
|
}
|
|
|
|
// The narrow patterns it does need must still be there.
|
|
assert.ok(body.includes('"ERR_DLOPEN_FAILED"'));
|
|
assert.ok(body.includes("\"Cannot find module 'better-sqlite3'\""));
|
|
});
|
|
|
|
test("the runtime classifier keeps matching a non-callable better-sqlite3 export", () => {
|
|
const body = classifierBody(readSource("../../../src/lib/db/sqliteLoadError.ts"));
|
|
|
|
for (const pattern of GENERIC_TYPE_ERROR_PATTERNS) {
|
|
assert.ok(
|
|
body.includes(pattern),
|
|
`src/lib/db/sqliteLoadError.ts must classify ${pattern} so a non-callable export ` +
|
|
"cascades to the node:sqlite/sql.js fallback instead of disabling the driver."
|
|
);
|
|
}
|
|
});
|
|
|
|
// Failing loudly is the point, but an operator staring at "Database is not a
|
|
// constructor" has no way to know a native rebuild is the remedy. The bootstrap
|
|
// wrapper appends that hint without widening what counts as a load failure.
|
|
test("a non-callable export still fails loudly, now with a rebuild hint", () => {
|
|
const source = readSource("../../../scripts/build/bootstrap-env.mjs");
|
|
|
|
assert.match(
|
|
source,
|
|
/function isLikelyBrokenNativeBinding\(error\)/,
|
|
"the hint predicate must exist and stay separate from isNativeSqliteLoadError"
|
|
);
|
|
assert.match(source, /npm rebuild better-sqlite3/, "the remediation hint text must be present");
|
|
|
|
// The hint decorates the throw; it must never turn into a `return false`.
|
|
const hintIndex = source.indexOf("npm rebuild better-sqlite3");
|
|
const tail = source.slice(hintIndex, hintIndex + 400);
|
|
assert.match(
|
|
tail,
|
|
/throw new Error\(`Unable to inspect existing database at \$\{dbPath\}: \$\{message\}\$\{hint\}`\)/,
|
|
"the broken-binding path must still throw, never report 'no encrypted credentials'"
|
|
);
|
|
});
|