@blamejs/core 0.15.13 → 0.15.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/index.js +2 -0
- package/lib/a2a-tasks.js +38 -6
- package/lib/agent-event-bus.js +13 -0
- package/lib/agent-idempotency.js +5 -1
- package/lib/agent-snapshot.js +32 -2
- package/lib/ai-aedt-bias-audit.js +2 -1
- package/lib/ai-content-detect.js +1 -3
- package/lib/ai-frontier-protocol.js +1 -1
- package/lib/ai-model-manifest.js +1 -1
- package/lib/ai-output.js +16 -7
- package/lib/api-snapshot.js +4 -1
- package/lib/app-shutdown.js +7 -1
- package/lib/archive-gz.js +9 -0
- package/lib/archive-read.js +9 -7
- package/lib/archive-tar-read.js +51 -8
- package/lib/archive.js +4 -2
- package/lib/asn1-der.js +70 -22
- package/lib/atomic-file.js +204 -2
- package/lib/audit-chain.js +54 -5
- package/lib/audit-daily-review.js +12 -2
- package/lib/audit-sign.js +2 -1
- package/lib/audit-tools.js +108 -23
- package/lib/audit.js +7 -2
- package/lib/auth/access-lock.js +2 -2
- package/lib/auth/bot-challenge.js +1 -1
- package/lib/auth/ciba.js +71 -8
- package/lib/auth/dpop.js +15 -1
- package/lib/auth/fido-mds3.js +30 -13
- package/lib/auth/jwt.js +21 -5
- package/lib/auth/lockout.js +18 -2
- package/lib/auth/oauth.js +8 -2
- package/lib/auth/passkey.js +1 -1
- package/lib/auth/password.js +1 -1
- package/lib/auth/saml.js +42 -12
- package/lib/auth/sd-jwt-vc.js +24 -4
- package/lib/auth/status-list.js +14 -2
- package/lib/auth/step-up.js +9 -1
- package/lib/auth-bot-challenge.js +21 -2
- package/lib/backup/bundle.js +7 -2
- package/lib/backup/crypto.js +23 -8
- package/lib/backup/index.js +41 -20
- package/lib/backup/manifest.js +7 -1
- package/lib/break-glass.js +41 -22
- package/lib/cbor.js +34 -11
- package/lib/cdn-cache-control.js +7 -3
- package/lib/cert.js +5 -3
- package/lib/cli.js +5 -1
- package/lib/cloud-events.js +3 -2
- package/lib/cluster-storage.js +7 -3
- package/lib/codepoint-class.js +17 -0
- package/lib/compliance-eaa.js +1 -1
- package/lib/compliance-sanctions.js +9 -7
- package/lib/compliance.js +1 -1
- package/lib/config-drift.js +22 -8
- package/lib/content-credentials.js +11 -8
- package/lib/content-digest.js +11 -4
- package/lib/cookies.js +10 -2
- package/lib/cose.js +20 -0
- package/lib/crdt.js +2 -1
- package/lib/crypto-field.js +48 -24
- package/lib/crypto.js +18 -4
- package/lib/csp.js +13 -0
- package/lib/daemon.js +4 -1
- package/lib/data-act.js +27 -4
- package/lib/db-file-lifecycle.js +14 -6
- package/lib/db-query.js +102 -11
- package/lib/db.js +32 -15
- package/lib/dora.js +43 -13
- package/lib/dr-runbook.js +1 -1
- package/lib/dsa.js +2 -1
- package/lib/dsr.js +22 -8
- package/lib/early-hints.js +19 -0
- package/lib/eat.js +5 -1
- package/lib/external-db.js +60 -4
- package/lib/fda-21cfr11.js +30 -5
- package/lib/flag-providers.js +6 -2
- package/lib/forms.js +1 -1
- package/lib/gate-contract.js +46 -5
- package/lib/gdpr-ropa.js +18 -9
- package/lib/graphql-federation.js +17 -4
- package/lib/guard-all.js +2 -2
- package/lib/guard-dsn.js +1 -1
- package/lib/guard-envelope.js +1 -1
- package/lib/guard-html.js +9 -11
- package/lib/guard-imap-command.js +1 -1
- package/lib/guard-jmap.js +1 -1
- package/lib/guard-json.js +14 -6
- package/lib/guard-mail-move.js +1 -1
- package/lib/guard-managesieve-command.js +1 -1
- package/lib/guard-pop3-command.js +1 -1
- package/lib/guard-smtp-command.js +1 -1
- package/lib/guard-svg.js +8 -9
- package/lib/html-balance.js +7 -3
- package/lib/http-client-cookie-jar.js +33 -12
- package/lib/http-client.js +225 -53
- package/lib/iab-tcf.js +3 -2
- package/lib/importmap-integrity.js +41 -1
- package/lib/incident-report.js +9 -6
- package/lib/json-patch.js +1 -1
- package/lib/json-path.js +24 -3
- package/lib/jtd.js +2 -2
- package/lib/legal-hold.js +24 -8
- package/lib/log.js +2 -2
- package/lib/mail-agent.js +2 -2
- package/lib/mail-arf.js +1 -1
- package/lib/mail-auth.js +27 -4
- package/lib/mail-bimi.js +16 -16
- package/lib/mail-bounce.js +3 -3
- package/lib/mail-crypto-smime.js +71 -6
- package/lib/mail-deploy.js +9 -5
- package/lib/mail-dkim.js +20 -7
- package/lib/mail-greylist.js +2 -4
- package/lib/mail-helo.js +2 -4
- package/lib/mail-journal.js +11 -8
- package/lib/mail-mdn.js +8 -4
- package/lib/mail-rbl.js +2 -4
- package/lib/mail-scan.js +3 -5
- package/lib/mail-server-jmap.js +4 -1
- package/lib/mail-server-registry.js +1 -1
- package/lib/mail-server-tls.js +9 -2
- package/lib/mail-spam-score.js +2 -4
- package/lib/mail-store-fts.js +1 -1
- package/lib/mail.js +22 -2
- package/lib/markup-tokenizer.js +24 -0
- package/lib/mcp.js +6 -4
- package/lib/mdoc.js +26 -3
- package/lib/metrics.js +14 -3
- package/lib/middleware/api-encrypt.js +2 -2
- package/lib/middleware/body-parser.js +10 -4
- package/lib/middleware/bot-guard.js +26 -18
- package/lib/middleware/clear-site-data.js +5 -1
- package/lib/middleware/compose-pipeline.js +39 -5
- package/lib/middleware/compression.js +9 -0
- package/lib/middleware/cors.js +32 -23
- package/lib/middleware/csrf-protect.js +60 -21
- package/lib/middleware/daily-byte-quota.js +6 -4
- package/lib/middleware/fetch-metadata.js +28 -4
- package/lib/middleware/network-allowlist.js +61 -30
- package/lib/middleware/rate-limit.js +25 -16
- package/lib/middleware/scim-server.js +2 -1
- package/lib/middleware/security-headers.js +24 -6
- package/lib/middleware/speculation-rules.js +6 -3
- package/lib/middleware/tus-upload.js +2 -2
- package/lib/money.js +1 -1
- package/lib/mtls-ca.js +10 -6
- package/lib/network-dns-resolver.js +1 -1
- package/lib/network-dns.js +9 -2
- package/lib/network-dnssec.js +2 -1
- package/lib/network-smtp-policy.js +23 -5
- package/lib/network-tls.js +27 -5
- package/lib/network-tsig.js +2 -2
- package/lib/nis2-report.js +1 -1
- package/lib/nist-crosswalk.js +1 -1
- package/lib/ntp-check.js +28 -0
- package/lib/numeric-bounds.js +9 -0
- package/lib/object-store/azure-blob.js +1 -2
- package/lib/object-store/gcs-bucket-ops.js +4 -2
- package/lib/object-store/gcs.js +6 -4
- package/lib/object-store/http-put.js +1 -2
- package/lib/object-store/http-request.js +30 -1
- package/lib/object-store/local.js +37 -17
- package/lib/object-store/sigv4.js +1 -2
- package/lib/observability-otlp-exporter.js +20 -4
- package/lib/outbox.js +11 -4
- package/lib/parsers/safe-xml.js +1 -1
- package/lib/parsers/safe-yaml.js +21 -3
- package/lib/pipl-cn.js +11 -8
- package/lib/queue-local.js +10 -3
- package/lib/redact.js +7 -3
- package/lib/request-helpers.js +347 -36
- package/lib/resource-access-lock.js +3 -3
- package/lib/restore-bundle.js +46 -18
- package/lib/restore-rollback.js +10 -4
- package/lib/restore.js +19 -0
- package/lib/retention.js +20 -4
- package/lib/router.js +17 -4
- package/lib/safe-ical.js +2 -2
- package/lib/safe-icap.js +1 -1
- package/lib/safe-json.js +70 -0
- package/lib/safe-sieve.js +1 -1
- package/lib/safe-vcard.js +1 -1
- package/lib/sandbox-worker.js +6 -0
- package/lib/sandbox.js +1 -1
- package/lib/scheduler.js +17 -1
- package/lib/self-update-standalone-verifier.js +16 -0
- package/lib/session.js +62 -120
- package/lib/sql.js +25 -3
- package/lib/static.js +65 -13
- package/lib/template.js +7 -5
- package/lib/tenant-quota.js +52 -19
- package/lib/tsa.js +5 -2
- package/lib/vault/index.js +5 -0
- package/lib/vault/passphrase-ops.js +22 -26
- package/lib/vault/passphrase-source.js +8 -3
- package/lib/vault/rotate.js +13 -18
- package/lib/vault/seal-pem-file.js +4 -1
- package/lib/vc.js +1 -1
- package/lib/vendor/MANIFEST.json +10 -10
- package/lib/vendor/public-suffix-list.dat +23 -10
- package/lib/vendor/public-suffix-list.data.js +5498 -5494
- package/lib/webhook.js +16 -1
- package/lib/websocket.js +1 -1
- package/lib/worm.js +1 -1
- package/lib/ws-client.js +83 -46
- package/lib/x509-chain.js +91 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/crypto-field.js
CHANGED
|
@@ -62,7 +62,6 @@ var vaultAad = require("./vault-aad");
|
|
|
62
62
|
var validateOpts = require("./validate-opts");
|
|
63
63
|
var numericBounds = require("./numeric-bounds");
|
|
64
64
|
var safeJson = require("./safe-json");
|
|
65
|
-
var frameworkSchema = require("./framework-schema");
|
|
66
65
|
var sql = require("./sql");
|
|
67
66
|
var { defineClass } = require("./framework-error");
|
|
68
67
|
var { sha3Hash, kdf, generateBytes, encryptPacked, decryptPacked, generateToken } = require("./crypto");
|
|
@@ -226,14 +225,19 @@ var PER_ROW_KEYS_TABLE = "_blamejs_per_row_keys"; // allow:hand-rolled-sql
|
|
|
226
225
|
var PER_ROW_KEYS_COLUMN = "wrappedKey";
|
|
227
226
|
var PER_ROW_KEYS_SCHEMA_VERSION = "1";
|
|
228
227
|
|
|
229
|
-
// The per-row-key registry
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
//
|
|
228
|
+
// The per-row-key registry ALWAYS lives in the LOCAL sqlite — reconcile creates
|
|
229
|
+
// it under its RAW schema name, and per-row keys are never dispatched to an
|
|
230
|
+
// external backend (see _kRowOnce's db() fallback). It is read/written against
|
|
231
|
+
// the local db() / dbHandle directly, NOT through clusterStorage, so it must use
|
|
232
|
+
// the RAW table name. frameworkSchema.tableName() resolves to the cluster-mode
|
|
233
|
+
// EXTERNAL prefixed name (the prefix applies only to the external backend, via
|
|
234
|
+
// resolveTables — local DDL stays raw); under a custom tablePrefix that names a
|
|
235
|
+
// table that does not exist locally, which SILENTLY breaks crypto-shred:
|
|
236
|
+
// destroyPerRowKey deletes 0 rows and sealed cells stay decryptable after
|
|
237
|
+
// eraseHard. quoteName so b.sql emits the quoted identifier the local path expects.
|
|
234
238
|
var _PER_ROW_SQL_OPTS = { dialect: "sqlite", quoteName: true };
|
|
235
239
|
function _perRowKeysTableName() {
|
|
236
|
-
return
|
|
240
|
+
return PER_ROW_KEYS_TABLE;
|
|
237
241
|
}
|
|
238
242
|
|
|
239
243
|
// Build the canonical AAD parts for a row-secret wrap in
|
|
@@ -1271,19 +1275,19 @@ function unsealRow(table, row, actor, dbHandle) {
|
|
|
1271
1275
|
unsealed = out[field];
|
|
1272
1276
|
}
|
|
1273
1277
|
} catch (e) {
|
|
1274
|
-
// A
|
|
1275
|
-
//
|
|
1276
|
-
//
|
|
1277
|
-
//
|
|
1278
|
-
//
|
|
1279
|
-
//
|
|
1280
|
-
//
|
|
1281
|
-
//
|
|
1282
|
-
|
|
1278
|
+
// A crypto-shredded (or never-materialized) per-row key is an EXPECTED
|
|
1279
|
+
// absence, not a decryption-oracle attack: the wrapped secret is gone,
|
|
1280
|
+
// so there is no oracle to brute-force. Reading such a row must read as
|
|
1281
|
+
// "no value" WITHOUT counting against the rate cap — otherwise a bulk
|
|
1282
|
+
// read over a table with many erased rows (GDPR eraseHard) trips the
|
|
1283
|
+
// cap and DoS's the live rows (self-DoS, CWE-307 mis-applied). It is
|
|
1284
|
+
// audited under a distinct, non-failure action so operators don't alert
|
|
1285
|
+
// on routine post-erasure reads as forged-ciphertext bursts.
|
|
1286
|
+
var _shredded = e && e.code === "crypto-field/row-key-unavailable";
|
|
1283
1287
|
try {
|
|
1284
1288
|
audit().safeEmit({
|
|
1285
|
-
action: "system.crypto.unseal_failed",
|
|
1286
|
-
outcome: "failure",
|
|
1289
|
+
action: _shredded ? "system.crypto.shredded_read" : "system.crypto.unseal_failed",
|
|
1290
|
+
outcome: _shredded ? "success" : "failure",
|
|
1287
1291
|
metadata: {
|
|
1288
1292
|
table: table,
|
|
1289
1293
|
field: field,
|
|
@@ -1293,11 +1297,13 @@ function unsealRow(table, row, actor, dbHandle) {
|
|
|
1293
1297
|
},
|
|
1294
1298
|
});
|
|
1295
1299
|
} catch (_e) { /* drop-silent */ }
|
|
1296
|
-
// Default-on rate cap: account
|
|
1297
|
-
//
|
|
1298
|
-
//
|
|
1299
|
-
//
|
|
1300
|
-
|
|
1300
|
+
// Default-on rate cap: account a genuine decryption / AEAD-verify /
|
|
1301
|
+
// AAD-downgrade failure (a possible forged-ciphertext attack) against
|
|
1302
|
+
// the (actor, table, column) tuple. A shredded-key read is exempt (see
|
|
1303
|
+
// above). When the cap trips the threshold, arm the cooldown + emit the
|
|
1304
|
+
// distinct rate-exceeded audit once on the transition. No-op when the
|
|
1305
|
+
// cap is disabled.
|
|
1306
|
+
if (!_shredded && _rateNoteFailure(capActor, table, field)) {
|
|
1301
1307
|
_emitRateAudit({
|
|
1302
1308
|
table: table, field: field, actor: capActor, shape: shape,
|
|
1303
1309
|
threshold: _rateCap.threshold, windowMs: _rateCap.windowMs, cooldownMs: _rateCap.cooldownMs,
|
|
@@ -1741,9 +1747,27 @@ function assertColumnResidency(table, row, args) {
|
|
|
1741
1747
|
var entry = columnResidency[table];
|
|
1742
1748
|
if (!entry || !row || !args) return null;
|
|
1743
1749
|
var backendTag = args.backendTag || "unrestricted";
|
|
1750
|
+
// SQL unquoted identifiers are case-insensitive; a raw-SQL-parsed row keeps
|
|
1751
|
+
// the column token's case, so resolve each mapped column case-insensitively
|
|
1752
|
+
// (a case-sensitive `row[col]` let a differently-cased column skip the gate —
|
|
1753
|
+
// CWE-178). Lazily index the row's keys by lowercase; exact match wins first.
|
|
1754
|
+
var rowLcIndex = null;
|
|
1755
|
+
function _rowVal(c) {
|
|
1756
|
+
if (Object.prototype.hasOwnProperty.call(row, c)) return row[c];
|
|
1757
|
+
if (rowLcIndex === null) {
|
|
1758
|
+
rowLcIndex = {};
|
|
1759
|
+
var ks = Object.keys(row);
|
|
1760
|
+
for (var i = 0; i < ks.length; i++) {
|
|
1761
|
+
var lk = ks[i].toLowerCase();
|
|
1762
|
+
if (!Object.prototype.hasOwnProperty.call(rowLcIndex, lk)) rowLcIndex[lk] = row[ks[i]];
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
return rowLcIndex[String(c).toLowerCase()];
|
|
1766
|
+
}
|
|
1744
1767
|
for (var col in entry) {
|
|
1745
1768
|
var want = entry[col];
|
|
1746
|
-
|
|
1769
|
+
var cellVal = _rowVal(col);
|
|
1770
|
+
if (cellVal === undefined || cellVal === null) continue;
|
|
1747
1771
|
if (want === "global" || want === "unrestricted") continue;
|
|
1748
1772
|
if (backendTag === "unrestricted") continue;
|
|
1749
1773
|
if (backendTag !== want) {
|
package/lib/crypto.js
CHANGED
|
@@ -963,7 +963,7 @@ var SRI_ALGORITHMS = { "sha256": "sha256", "sha384": "sha384", "sha512": "sha512
|
|
|
963
963
|
function sri(content, opts) {
|
|
964
964
|
opts = opts || {};
|
|
965
965
|
var algorithm = (opts.algorithm || "sha384").toLowerCase();
|
|
966
|
-
if (!SRI_ALGORITHMS
|
|
966
|
+
if (!Object.prototype.hasOwnProperty.call(SRI_ALGORITHMS, algorithm)) {
|
|
967
967
|
throw new Error("crypto.sri: unsupported algorithm '" + algorithm +
|
|
968
968
|
"' (W3C SRI 1.0 §3.2 supports sha256/sha384/sha512)");
|
|
969
969
|
}
|
|
@@ -1356,7 +1356,21 @@ function decrypt(ciphertext, privateKeys, opts) {
|
|
|
1356
1356
|
opts && opts.raw === true ? { raw: true } : undefined);
|
|
1357
1357
|
}
|
|
1358
1358
|
|
|
1359
|
+
// Bounds-checked 2-byte length read — a bare _envU16(packed, pos) on a
|
|
1360
|
+
// truncated envelope throws a raw Node RangeError that escapes the documented
|
|
1361
|
+
// "Invalid envelope: ..." error contract (untrusted ciphertext must fail with
|
|
1362
|
+
// the typed envelope error, not a stack-trace-leaking RangeError).
|
|
1363
|
+
function _envU16(buf, at) {
|
|
1364
|
+
if (at + 2 > buf.length) {
|
|
1365
|
+
throw new Error("Invalid envelope: truncated (expected a 2-byte length at offset " + at + ")");
|
|
1366
|
+
}
|
|
1367
|
+
return buf.readUInt16BE(at);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1359
1370
|
function decryptEnvelope(packed, privateKeys, internalOpts) {
|
|
1371
|
+
if (!Buffer.isBuffer(packed) || packed.length < 4) {
|
|
1372
|
+
throw new Error("Invalid envelope: too short (need at least the 4-byte suite header)");
|
|
1373
|
+
}
|
|
1360
1374
|
var kemId = packed[1], cipherId = packed[2], kdfId = packed[3], pos = 4;
|
|
1361
1375
|
|
|
1362
1376
|
// The legacy 0xE1 envelope predates the FixedInfo / suite-binding
|
|
@@ -1372,7 +1386,7 @@ function decryptEnvelope(packed, privateKeys, internalOpts) {
|
|
|
1372
1386
|
throw new Error("Invalid envelope: unsupported KDF (only SHAKE256 supported)");
|
|
1373
1387
|
}
|
|
1374
1388
|
|
|
1375
|
-
var kemCtLen = packed
|
|
1389
|
+
var kemCtLen = _envU16(packed, pos); pos += 2;
|
|
1376
1390
|
var kemCt = packed.subarray(pos, pos + kemCtLen); pos += kemCtLen;
|
|
1377
1391
|
|
|
1378
1392
|
var mlkemPriv = nodeCrypto.createPrivateKey(
|
|
@@ -1383,7 +1397,7 @@ function decryptEnvelope(packed, privateKeys, internalOpts) {
|
|
|
1383
1397
|
var fixedInfo = omitFixedInfo ? Buffer.alloc(0) : _suiteFixedInfo(kemId, cipherId, kdfId);
|
|
1384
1398
|
|
|
1385
1399
|
if (kemId === C.KEM_IDS.ML_KEM_1024_P384) {
|
|
1386
|
-
var ecEphLen = packed
|
|
1400
|
+
var ecEphLen = _envU16(packed, pos); pos += 2;
|
|
1387
1401
|
var ecEphDer = packed.subarray(pos, pos + ecEphLen); pos += ecEphLen;
|
|
1388
1402
|
var ecPrivPem = typeof privateKeys === "string" ? null : privateKeys.ecPrivateKey;
|
|
1389
1403
|
if (!ecPrivPem) throw new Error("Hybrid KEM requires EC private key");
|
|
@@ -1400,7 +1414,7 @@ function decryptEnvelope(packed, privateKeys, internalOpts) {
|
|
|
1400
1414
|
// the correct keypair via privateKeys when the envelope was sealed
|
|
1401
1415
|
// with this algorithm. Same length-prefixed shape as the P-384
|
|
1402
1416
|
// hybrid: 2-byte ec-eph-len + DER X25519 pubkey + nonce + ct.
|
|
1403
|
-
var x25519EphLen = packed
|
|
1417
|
+
var x25519EphLen = _envU16(packed, pos); pos += 2;
|
|
1404
1418
|
var x25519EphDer = packed.subarray(pos, pos + x25519EphLen); pos += x25519EphLen;
|
|
1405
1419
|
var x25519PrivPem = typeof privateKeys === "string" ? null : privateKeys.x25519PrivateKey;
|
|
1406
1420
|
if (!x25519PrivPem) throw new Error("ML-KEM-768 + X25519 hybrid envelope requires x25519PrivateKey");
|
package/lib/csp.js
CHANGED
|
@@ -210,6 +210,15 @@ function build(directives, opts) {
|
|
|
210
210
|
throw new CspError("csp/header-injection",
|
|
211
211
|
"csp.build: source '" + src + "' contains CR/LF/NUL");
|
|
212
212
|
}
|
|
213
|
+
// A CSP source-expression is a single non-whitespace token. The emitter
|
|
214
|
+
// space-joins sources within a directive and ';'-joins directives, so a
|
|
215
|
+
// source containing ';' or ASCII whitespace would inject a brand-new live
|
|
216
|
+
// directive (e.g. "https://x; script-src https://evil") — refuse both.
|
|
217
|
+
if (/[\s;]/.test(src)) {
|
|
218
|
+
throw new CspError("csp/bad-source",
|
|
219
|
+
"csp.build: source '" + src + "' contains whitespace or ';' — a CSP source " +
|
|
220
|
+
"must be a single token (directive-injection defense)");
|
|
221
|
+
}
|
|
213
222
|
if (!acknowledgeUnsafe && SCRIPT_DIRECTIVES.indexOf(name) !== -1 &&
|
|
214
223
|
UNSAFE_KEYWORDS.indexOf(src) !== -1) {
|
|
215
224
|
throw new CspError("csp/unsafe-keyword",
|
|
@@ -416,6 +425,10 @@ function mergeDirectives(base, additions, opts) {
|
|
|
416
425
|
? directives[name].slice()
|
|
417
426
|
: (directives["default-src"] ? directives["default-src"].slice() : ["'self'"]);
|
|
418
427
|
var added = additions[name];
|
|
428
|
+
// A directive value of 'none' MUST stand alone (CSP3 §2.3.1) — appending a
|
|
429
|
+
// host to it emits the malformed "'none' https://x". When merging real
|
|
430
|
+
// sources in, the added sources supersede 'none', so drop it first.
|
|
431
|
+
if (added.length && existing.length === 1 && existing[0] === "'none'") existing = [];
|
|
419
432
|
for (var si = 0; si < added.length; si += 1) {
|
|
420
433
|
if (existing.indexOf(added[si]) === -1) existing.push(added[si]);
|
|
421
434
|
}
|
package/lib/daemon.js
CHANGED
|
@@ -73,7 +73,10 @@ function _isLivePid(pid) {
|
|
|
73
73
|
|
|
74
74
|
function _readPidFile(pidFile) {
|
|
75
75
|
try {
|
|
76
|
-
|
|
76
|
+
// Same fd-safe + capped + symlink-refusing read as app-shutdown's lockfile
|
|
77
|
+
// reader (one shape): a PID file is never a legit symlink mount, so
|
|
78
|
+
// refuseSymlink is safe; any throw → null ("nothing live there").
|
|
79
|
+
var raw = atomicFile.fdSafeReadSync(pidFile, { maxBytes: C.BYTES.kib(1), refuseSymlink: true, encoding: "utf8" });
|
|
77
80
|
var pid = parseInt(String(raw).trim(), 10);
|
|
78
81
|
return isFinite(pid) && pid > 0 ? pid : null;
|
|
79
82
|
} catch (_e) { return null; }
|
package/lib/data-act.js
CHANGED
|
@@ -89,6 +89,32 @@ var DESIGNATED_GATEKEEPERS = Object.freeze([
|
|
|
89
89
|
"booking",
|
|
90
90
|
]);
|
|
91
91
|
|
|
92
|
+
// Is `recipient` a designated DMA gatekeeper? Match the gatekeeper token as a
|
|
93
|
+
// DNS LABEL of the recipient's host authority — any position, including the
|
|
94
|
+
// FINAL label. A substring `indexOf(g + ".")` test missed a gatekeeper that is
|
|
95
|
+
// the last label (e.g. "blog.google" / "drive.google" on the real ".google"
|
|
96
|
+
// TLD, or "data.amazon"), letting an Art 32 §1 third-party share through — and
|
|
97
|
+
// also false-flagged a non-gatekeeper whose name merely contained the token
|
|
98
|
+
// ("notgoogle.com"). Label matching closes the bypass and drops the false
|
|
99
|
+
// positive. A bare name ("Google") still matches exactly. URL / userinfo / port
|
|
100
|
+
// forms are normalized to the host authority first.
|
|
101
|
+
function _isGatekeeperRecipient(recipient) {
|
|
102
|
+
var s = String(recipient).toLowerCase().trim();
|
|
103
|
+
var scheme = s.indexOf("://");
|
|
104
|
+
if (scheme !== -1) s = s.slice(scheme + 3);
|
|
105
|
+
s = s.split("/")[0].split("?")[0].split("#")[0]; // drop path / query / fragment
|
|
106
|
+
var at = s.lastIndexOf("@");
|
|
107
|
+
if (at !== -1) s = s.slice(at + 1); // drop userinfo
|
|
108
|
+
s = s.split(":")[0]; // drop port
|
|
109
|
+
if (s.length === 0) return false;
|
|
110
|
+
var labels = s.split(".");
|
|
111
|
+
return DESIGNATED_GATEKEEPERS.some(function (g) {
|
|
112
|
+
if (s === g) return true;
|
|
113
|
+
for (var i = 0; i < labels.length; i++) { if (labels[i] === g) return true; }
|
|
114
|
+
return false;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
92
118
|
/**
|
|
93
119
|
* @primitive b.dataAct.declareProduct
|
|
94
120
|
* @signature b.dataAct.declareProduct(opts)
|
|
@@ -229,10 +255,7 @@ function shareWithThirdParty(opts) {
|
|
|
229
255
|
acceptGatekeeper: "optional-plain-object",
|
|
230
256
|
}, "dataAct.shareWithThirdParty", DataActError, "dataact/bad-opts");
|
|
231
257
|
|
|
232
|
-
var
|
|
233
|
-
var isGatekeeper = DESIGNATED_GATEKEEPERS.some(function (g) {
|
|
234
|
-
return recipientLower === g || recipientLower.indexOf(g + ".") !== -1;
|
|
235
|
-
});
|
|
258
|
+
var isGatekeeper = _isGatekeeperRecipient(opts.recipient);
|
|
236
259
|
if (isGatekeeper) {
|
|
237
260
|
if (!opts.acceptGatekeeper || typeof opts.acceptGatekeeper !== "object" ||
|
|
238
261
|
typeof opts.acceptGatekeeper.reason !== "string" ||
|
package/lib/db-file-lifecycle.js
CHANGED
|
@@ -172,8 +172,11 @@ function fileLifecycle(opts) {
|
|
|
172
172
|
: nodePath.join(opts.dataDir, frameworkFiles.fileName("dbKeyEnc"));
|
|
173
173
|
var flushIntervalMs = opts.flushIntervalMs || DEFAULT_FLUSH_INTERVAL_MS;
|
|
174
174
|
var tmpDir = _resolveTmpDir(opts.tmpDir, opts.allowDiskFallback === true);
|
|
175
|
-
|
|
176
|
-
|
|
175
|
+
// 0o700 — these hold the DB encryption tmpfs scratch + the data dir
|
|
176
|
+
// (db.key.enc, the decrypted DB while open); a default-mode mkdir leaves
|
|
177
|
+
// them group/other-traversable.
|
|
178
|
+
if (!nodeFs.existsSync(tmpDir)) nodeFs.mkdirSync(tmpDir, { recursive: true, mode: 0o700 });
|
|
179
|
+
if (!nodeFs.existsSync(opts.dataDir)) nodeFs.mkdirSync(opts.dataDir, { recursive: true, mode: 0o700 });
|
|
177
180
|
|
|
178
181
|
var dbPath = null;
|
|
179
182
|
var encKey = null;
|
|
@@ -183,7 +186,9 @@ function fileLifecycle(opts) {
|
|
|
183
186
|
function _loadOrGenerateKey() {
|
|
184
187
|
if (encKey) return encKey;
|
|
185
188
|
if (nodeFs.existsSync(keyPath)) {
|
|
186
|
-
|
|
189
|
+
// Cap + fd-bound. NO refuseSymlink: dbKeyPath may be an operator-absolute
|
|
190
|
+
// KMS-fronted volume that legitimately symlinks the sealed key.
|
|
191
|
+
var sealedKey = atomicFile.fdSafeReadSync(keyPath, { maxBytes: C.BYTES.kib(64), encoding: "utf8" });
|
|
187
192
|
var keyB64;
|
|
188
193
|
try { keyB64 = opts.vault.unseal(sealedKey); }
|
|
189
194
|
catch (e) {
|
|
@@ -212,7 +217,10 @@ function fileLifecycle(opts) {
|
|
|
212
217
|
dbPath = nodePath.join(tmpDir, "blamejs-fl-" + label + "-" +
|
|
213
218
|
generateToken(TMP_NAME_BYTES) + ".db");
|
|
214
219
|
if (nodeFs.existsSync(encPath)) {
|
|
215
|
-
|
|
220
|
+
// Cap + fd-bound (deployment-tunable ceiling). NO refuseSymlink: the data
|
|
221
|
+
// dir may be a symlink-mounted volume (k8s PVC). db.enc is AEAD-checked
|
|
222
|
+
// downstream, so the cap is the OOM-before-cap defense.
|
|
223
|
+
var packed = atomicFile.fdSafeReadSync(encPath, { maxBytes: (opts.maxDbBytes) || C.BYTES.gib(2) });
|
|
216
224
|
if (packed.length < 26) { // minimum envelope length
|
|
217
225
|
throw new DbFileLifecycleError("db-file-lifecycle/short-envelope",
|
|
218
226
|
"fileLifecycle: " + encPath + " too short to be a valid envelope (" + packed.length + " bytes)");
|
|
@@ -248,7 +256,7 @@ function fileLifecycle(opts) {
|
|
|
248
256
|
catch (_e) { /* best-effort — operators on read-only handles or pre-init still flush */ }
|
|
249
257
|
}
|
|
250
258
|
if (!nodeFs.existsSync(dbPath)) return null;
|
|
251
|
-
var plain =
|
|
259
|
+
var plain = atomicFile.fdSafeReadSync(dbPath, { maxBytes: (opts.maxDbBytes) || C.BYTES.gib(2) });
|
|
252
260
|
var packed = encryptPacked(plain, encKey, _aad(opts.dataDir, label));
|
|
253
261
|
atomicFile.writeSync(encPath, packed);
|
|
254
262
|
_emitAudit("flushed", "success", { label: label, bytes: plain.length });
|
|
@@ -269,7 +277,7 @@ function fileLifecycle(opts) {
|
|
|
269
277
|
throw new DbFileLifecycleError("db-file-lifecycle/no-source",
|
|
270
278
|
"fileLifecycle.snapshot: " + dbPath + " is missing");
|
|
271
279
|
}
|
|
272
|
-
var plain =
|
|
280
|
+
var plain = atomicFile.fdSafeReadSync(dbPath, { maxBytes: (opts.maxDbBytes) || C.BYTES.gib(2) });
|
|
273
281
|
return encryptPacked(plain, encKey, _aad(opts.dataDir, label));
|
|
274
282
|
}
|
|
275
283
|
|
package/lib/db-query.js
CHANGED
|
@@ -89,6 +89,19 @@ function _postureState() {
|
|
|
89
89
|
//
|
|
90
90
|
// Unregulated postures audit (drop-silent) and pass; tables with no
|
|
91
91
|
// declaration are untouched.
|
|
92
|
+
// Resolve a column in a raw-SQL-parsed row case-insensitively (SQL unquoted
|
|
93
|
+
// identifiers fold case; the parser preserves the token). Exact match wins
|
|
94
|
+
// first so a structured-builder row (keys already canonical) is unaffected.
|
|
95
|
+
function _ciColumn(row, col) {
|
|
96
|
+
if (Object.prototype.hasOwnProperty.call(row, col)) return { present: true, value: row[col] };
|
|
97
|
+
var lc = String(col).toLowerCase();
|
|
98
|
+
var keys = Object.keys(row);
|
|
99
|
+
for (var i = 0; i < keys.length; i++) {
|
|
100
|
+
if (keys[i].toLowerCase() === lc) return { present: true, value: row[keys[i]] };
|
|
101
|
+
}
|
|
102
|
+
return { present: false, value: undefined };
|
|
103
|
+
}
|
|
104
|
+
|
|
92
105
|
function _assertLocalResidency(table, plaintextRow, op) {
|
|
93
106
|
var spec = cryptoField.getPerRowResidency(table);
|
|
94
107
|
var colMap = cryptoField.getColumnResidency(table);
|
|
@@ -106,9 +119,16 @@ function _assertLocalResidency(table, plaintextRow, op) {
|
|
|
106
119
|
var regulated = state.regulated;
|
|
107
120
|
|
|
108
121
|
if (spec) {
|
|
109
|
-
|
|
122
|
+
// SQL unquoted identifiers are case-insensitive, and the raw-SQL parser
|
|
123
|
+
// preserves the column token's case — so resolve the residency column
|
|
124
|
+
// case-insensitively. A case-sensitive lookup let `UPDATE t SET REGION=...`
|
|
125
|
+
// miss a `residencyColumn: "region"` declaration, skipping the gate and
|
|
126
|
+
// admitting a cross-border write (CWE-178 / CWE-863). Fail-safe: any
|
|
127
|
+
// spelling that could be the residency column engages the gate.
|
|
128
|
+
var resolved = _ciColumn(plaintextRow, spec.residencyColumn);
|
|
129
|
+
var tag = resolved.value;
|
|
110
130
|
var tagPresent = tag !== undefined && tag !== null;
|
|
111
|
-
var colInChangeSet =
|
|
131
|
+
var colInChangeSet = resolved.present;
|
|
112
132
|
if (op === "insert" && !tagPresent) {
|
|
113
133
|
throw new DbQueryError("db-query/row-residency-tag-missing",
|
|
114
134
|
op + ": table '" + table + "' declares per-row residency on column '" +
|
|
@@ -368,6 +388,17 @@ class Query {
|
|
|
368
388
|
return this.where(field, "IN", values);
|
|
369
389
|
}
|
|
370
390
|
|
|
391
|
+
// whereNull / whereNotNull — explicit NULL predicates (IS NULL / IS NOT
|
|
392
|
+
// NULL). `where({ field: null })` / `where(field, "=", null)` is refused
|
|
393
|
+
// because `col = NULL` is UNKNOWN in SQL (never true); these are the
|
|
394
|
+
// intended way to test a column for NULL.
|
|
395
|
+
whereNull(field) {
|
|
396
|
+
return this.where(field, "IS", null);
|
|
397
|
+
}
|
|
398
|
+
whereNotNull(field) {
|
|
399
|
+
return this.where(field, "IS NOT", null);
|
|
400
|
+
}
|
|
401
|
+
|
|
371
402
|
// Resolve a (field, op, value) predicate through the framework gates
|
|
372
403
|
// (JSONB value guard, sealed-field → derived-hash rewrite, column
|
|
373
404
|
// membership) and return the post-rewrite { field, op, value } that
|
|
@@ -1378,12 +1409,62 @@ function _unquoteIdent(s) {
|
|
|
1378
1409
|
return s;
|
|
1379
1410
|
}
|
|
1380
1411
|
|
|
1412
|
+
// Strip LEADING SQL comments + whitespace so the ^-anchored write-detection
|
|
1413
|
+
// regexes see the real statement head. Without this, a residency write smuggled
|
|
1414
|
+
// behind a leading "/* x */" or "-- x\n" comment is NOT recognized as a write →
|
|
1415
|
+
// the residency gate is skipped entirely (a cross-border-write BYPASS). The
|
|
1416
|
+
// executed SQL is unchanged; this normalized copy is only for the gate's parse.
|
|
1417
|
+
// Each replace is ^-anchored single-pass; the loop terminates when nothing more
|
|
1418
|
+
// is stripped (an unterminated /* leaves the head intact → write still detected
|
|
1419
|
+
// or fails closed downstream).
|
|
1420
|
+
function _stripLeadingSqlComments(sql) {
|
|
1421
|
+
var s = String(sql), prev;
|
|
1422
|
+
do {
|
|
1423
|
+
prev = s;
|
|
1424
|
+
s = s.replace(/^\s+/, ""); // allow:regex-no-length-cap — anchored, single leading run
|
|
1425
|
+
s = s.replace(/^--[^\n]*\r?\n?/, ""); // allow:regex-no-length-cap — anchored leading line comment
|
|
1426
|
+
s = s.replace(/^\/\*[\s\S]*?\*\//, ""); // allow:regex-no-length-cap — anchored leading block comment (lazy, single scan)
|
|
1427
|
+
} while (s !== prev);
|
|
1428
|
+
return s;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
// Non-anchored write-target scan for the writable-CTE / EXPLAIN-prefixed case.
|
|
1432
|
+
// A SQLite `WITH c AS (...) INSERT INTO residents ...` / `WITH ... UPDATE residents
|
|
1433
|
+
// SET ...` is a real write, but its effective verb is hidden behind the prefix so
|
|
1434
|
+
// the ^-anchored _RAW_WRITE_KEYWORD_RE misses it. This matches every INSERT/REPLACE
|
|
1435
|
+
// INTO, MERGE INTO, and UPDATE ... SET target token anywhere in the statement and
|
|
1436
|
+
// returns the first that names a residency table — so such a write still ENGAGES
|
|
1437
|
+
// the residency gate (_assertRawWriteResidency then fails CLOSED: the ^-anchored
|
|
1438
|
+
// body parsers can't read a CTE body, so it throws row-residency-raw-unparseable
|
|
1439
|
+
// directing the operator to b.db.from().insertOne/.updateOne). DELETE moves no
|
|
1440
|
+
// residency value across a border, so it is not a residency write. Linear scan.
|
|
1441
|
+
var _CTE_WRITE_TARGET_RE = /(?:\b(?:INSERT|REPLACE)\s+(?:OR\s+[A-Za-z]+\s+)?INTO|\bMERGE\s+INTO)\s+(?:[\x22\x27\x60]?[A-Za-z_]\w*[\x22\x27\x60]?\s*\.\s*){0,3}[\x22\x27\x60]?([A-Za-z_]\w*)[\x22\x27\x60]?|\bUPDATE\s+(?:[\x22\x27\x60]?[A-Za-z_]\w*[\x22\x27\x60]?\s*\.\s*){0,3}[\x22\x27\x60]?([A-Za-z_]\w*)[\x22\x27\x60]?\s+SET\b/ig; // allow:regex-no-length-cap — alternation, no nested quantifiers; linear
|
|
1442
|
+
function _firstResidencyWriteTarget(s) {
|
|
1443
|
+
_CTE_WRITE_TARGET_RE.lastIndex = 0;
|
|
1444
|
+
var m;
|
|
1445
|
+
while ((m = _CTE_WRITE_TARGET_RE.exec(s)) !== null) { // allow:regex-no-length-cap
|
|
1446
|
+
var t = _unquoteIdent(m[1] || m[2]);
|
|
1447
|
+
if (t && (cryptoField.getPerRowResidency(t) || cryptoField.getColumnResidency(t))) return t;
|
|
1448
|
+
}
|
|
1449
|
+
return null;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1381
1452
|
function _rawWriteTable(sql) {
|
|
1382
|
-
//
|
|
1383
|
-
//
|
|
1384
|
-
if (typeof sql !== "string"
|
|
1385
|
-
var
|
|
1386
|
-
|
|
1453
|
+
// The ^-anchored regexes scan only the statement head (constant-time). Strip
|
|
1454
|
+
// leading comments first so a commented-out head can't hide the write.
|
|
1455
|
+
if (typeof sql !== "string") return null;
|
|
1456
|
+
var s = _stripLeadingSqlComments(sql);
|
|
1457
|
+
if (_RAW_WRITE_KEYWORD_RE.test(s)) { // allow:regex-no-length-cap
|
|
1458
|
+
var m = _RAW_TABLE_RE.exec(s); // allow:regex-no-length-cap
|
|
1459
|
+
return m ? _unquoteIdent(m[1] || m[2]) : null;
|
|
1460
|
+
}
|
|
1461
|
+
// Writable-CTE / EXPLAIN-prefixed write: the effective write verb is hidden
|
|
1462
|
+
// behind the prefix the ^-anchored test misses. If it writes a residency table,
|
|
1463
|
+
// return it so the gate engages and then fails closed on the unparseable body.
|
|
1464
|
+
if (/^\s*(?:WITH|EXPLAIN)\b/i.test(s)) { // allow:regex-no-length-cap
|
|
1465
|
+
return _firstResidencyWriteTarget(s);
|
|
1466
|
+
}
|
|
1467
|
+
return null;
|
|
1387
1468
|
}
|
|
1388
1469
|
|
|
1389
1470
|
// Cheap prepare-time pre-check so only writes to a residency table get wrapped.
|
|
@@ -1463,17 +1544,23 @@ function _assertRawWriteResidency(sql, boundParams) {
|
|
|
1463
1544
|
if (!cryptoField.getPerRowResidency(table) && !cryptoField.getColumnResidency(table)) return;
|
|
1464
1545
|
boundParams = _flattenRunParams(boundParams);
|
|
1465
1546
|
|
|
1547
|
+
// Parse the comment-stripped head: a leading "/* x */" / "-- x" comment must
|
|
1548
|
+
// not hide the INSERT/UPDATE body from the ^-anchored regexes below (that would
|
|
1549
|
+
// let a residency-restricted write through the gate). The executed SQL is
|
|
1550
|
+
// unchanged; this normalized copy is only for residency parsing.
|
|
1551
|
+
var norm = _stripLeadingSqlComments(sql);
|
|
1552
|
+
|
|
1466
1553
|
// The INSERT/UPDATE body regexes below scan with [\s\S]+; bound the input
|
|
1467
1554
|
// first and fail CLOSED on an over-long statement - a residency write the
|
|
1468
1555
|
// framework cannot safely parse must be refused, never let past the gate.
|
|
1469
|
-
if (
|
|
1556
|
+
if (norm.length > 100000) {
|
|
1470
1557
|
throw new DbQueryError("db-query/row-residency-raw-unparseable",
|
|
1471
1558
|
"raw write to residency table '" + table + "' exceeds the parse limit (" +
|
|
1472
|
-
|
|
1559
|
+
norm.length + " chars) - use b.db.from(\"" + table + "\") so residency is validated", true);
|
|
1473
1560
|
}
|
|
1474
1561
|
|
|
1475
|
-
var mi = _RAW_INSERT_RE.exec(
|
|
1476
|
-
var mu = mi ? null : _RAW_UPDATE_RE.exec(
|
|
1562
|
+
var mi = _RAW_INSERT_RE.exec(norm); // allow:regex-no-length-cap — input length-capped above
|
|
1563
|
+
var mu = mi ? null : _RAW_UPDATE_RE.exec(norm); // allow:regex-no-length-cap — input length-capped above
|
|
1477
1564
|
if (!mi && !mu) {
|
|
1478
1565
|
throw new DbQueryError("db-query/row-residency-raw-unparseable",
|
|
1479
1566
|
"raw write to residency table '" + table + "' cannot be parsed to validate its " +
|
|
@@ -1509,4 +1596,8 @@ module.exports = {
|
|
|
1509
1596
|
Query: Query,
|
|
1510
1597
|
_isRawWriteToResidencyTable: _isRawWriteToResidencyTable,
|
|
1511
1598
|
_assertRawWriteResidency: _assertRawWriteResidency,
|
|
1599
|
+
// Shared leading-comment stripper so db.js's storage-low write-gate sees the
|
|
1600
|
+
// real statement head (a `/* x */ INSERT` / WITH-prefixed write must not slip
|
|
1601
|
+
// the ENOSPC gate the way it slipped the residency gate).
|
|
1602
|
+
_stripLeadingSqlComments: _stripLeadingSqlComments,
|
|
1512
1603
|
};
|
package/lib/db.js
CHANGED
|
@@ -57,7 +57,7 @@ var { generateToken, generateBytes, encryptPacked, decryptPacked, sha3Hash } = r
|
|
|
57
57
|
var cryptoField = require("./crypto-field");
|
|
58
58
|
var dbDeclareRowPolicy = require("./db-declare-row-policy");
|
|
59
59
|
var dbDeclareView = require("./db-declare-view");
|
|
60
|
-
var { Query, _isRawWriteToResidencyTable, _assertRawWriteResidency } = require("./db-query");
|
|
60
|
+
var { Query, _isRawWriteToResidencyTable, _assertRawWriteResidency, _stripLeadingSqlComments } = require("./db-query");
|
|
61
61
|
var dbSchema = require("./db-schema");
|
|
62
62
|
var { defineClass } = require("./framework-error");
|
|
63
63
|
var frameworkFiles = require("./framework-files");
|
|
@@ -748,7 +748,7 @@ function loadOrCreateDbKey(dataDirPath, keyPathOverride) {
|
|
|
748
748
|
var keyPath = keyPathOverride || nodePath.join(dataDirPath, frameworkFiles.fileName("dbKeyEnc"));
|
|
749
749
|
var aad = _dbKeyAad(dataDirPath, keyPath);
|
|
750
750
|
if (nodeFs.existsSync(keyPath)) {
|
|
751
|
-
var sealed = atomicFile.readSync(keyPath, { encoding: "utf8" }).trim();
|
|
751
|
+
var sealed = atomicFile.readSync(keyPath, { encoding: "utf8", maxBytes: C.BYTES.kib(64) }).trim();
|
|
752
752
|
var b64;
|
|
753
753
|
// isAadSealed is checked FIRST and is load-bearing: AAD_PREFIX
|
|
754
754
|
// ("vault.aad:") is NOT a prefix of VAULT_PREFIX ("vault:"), so a
|
|
@@ -811,7 +811,10 @@ function decryptToTmp() {
|
|
|
811
811
|
try { nodeFs.unlinkSync(dbPath + "-shm"); } catch (_e) { /* may not exist */ }
|
|
812
812
|
}
|
|
813
813
|
}
|
|
814
|
-
|
|
814
|
+
// Cap + fd-bound db.enc read at db.init. NO refuseSymlink: the data dir may be
|
|
815
|
+
// a symlink-mounted volume (k8s PVC). db.enc is AEAD-verified downstream, so
|
|
816
|
+
// the cap is the OOM-before-cap defense (was an uncapped read).
|
|
817
|
+
var packed = atomicFile.fdSafeReadSync(encPath, { maxBytes: C.BYTES.gib(2) });
|
|
815
818
|
if (packed.length < 26) return; // too short to be a valid envelope
|
|
816
819
|
// AAD binds the envelope to this deployment's data dir so two
|
|
817
820
|
// installs sharing the same operator passphrase can't swap each
|
|
@@ -890,11 +893,27 @@ function _probeStorageHeadroom() {
|
|
|
890
893
|
// DELETE, PRAGMA, and DDL pass through ungated. Called once in init() after
|
|
891
894
|
// schema setup so init's own writes are never gated (writesRefused is false
|
|
892
895
|
// until the first probe anyway).
|
|
896
|
+
// A growth write for the storage-low gate: INSERT/UPDATE/REPLACE at the head
|
|
897
|
+
// AFTER stripping leading comments, OR a writable-CTE / EXPLAIN-prefixed write
|
|
898
|
+
// (`WITH c AS (...) INSERT INTO t ...`) whose effective verb places rows. The
|
|
899
|
+
// leading-keyword-only test missed both a `/* x */ INSERT` and a WITH-prefixed
|
|
900
|
+
// growth write, letting them proceed into a near-full tmpfs (the ENOSPC the gate
|
|
901
|
+
// exists to prevent). Matches write SYNTAX (INTO / UPDATE..SET) so a WITH..SELECT
|
|
902
|
+
// carrying the word "insert" in a value is not refused.
|
|
903
|
+
function _isGrowthWrite(sql) {
|
|
904
|
+
if (typeof sql !== "string") return false;
|
|
905
|
+
var s = _stripLeadingSqlComments(sql);
|
|
906
|
+
if (/^\s*(?:INSERT|UPDATE|REPLACE)\b/i.test(s)) return true;
|
|
907
|
+
if (/^\s*(?:WITH|EXPLAIN)\b/i.test(s) &&
|
|
908
|
+
/\b(?:(?:INSERT|REPLACE|MERGE)\s+(?:OR\s+[A-Za-z]+\s+)?INTO|UPDATE\s+[\w".`]+\s+SET)\b/i.test(s)) return true;
|
|
909
|
+
return false;
|
|
910
|
+
}
|
|
911
|
+
|
|
893
912
|
function _installWriteGate() {
|
|
894
913
|
var rawPrepare = database.prepare.bind(database);
|
|
895
914
|
database.prepare = function (sql) {
|
|
896
915
|
var stmt = rawPrepare(sql);
|
|
897
|
-
if (
|
|
916
|
+
if (_isGrowthWrite(sql)) {
|
|
898
917
|
var rawRun = stmt.run.bind(stmt);
|
|
899
918
|
stmt.run = function () {
|
|
900
919
|
if (writesRefused) {
|
|
@@ -915,7 +934,7 @@ function encryptToDisk() {
|
|
|
915
934
|
// Force WAL checkpoint so the .db file holds all committed transactions.
|
|
916
935
|
try { runSql(database, "PRAGMA wal_checkpoint(TRUNCATE)"); } catch (_e) { /* best effort */ }
|
|
917
936
|
if (!nodeFs.existsSync(dbPath)) return;
|
|
918
|
-
atomicFile.writeSync(encPath, encryptPacked(
|
|
937
|
+
atomicFile.writeSync(encPath, encryptPacked(atomicFile.fdSafeReadSync(dbPath, { maxBytes: C.BYTES.gib(2) }), encKey, _dbEncAad(dataDir)));
|
|
919
938
|
}
|
|
920
939
|
|
|
921
940
|
/**
|
|
@@ -952,7 +971,7 @@ function snapshot() {
|
|
|
952
971
|
throw _dbErr("db/snapshot-no-source",
|
|
953
972
|
"snapshot: plaintext DB at " + dbPath + " is missing — did init complete?");
|
|
954
973
|
}
|
|
955
|
-
var plain =
|
|
974
|
+
var plain = atomicFile.fdSafeReadSync(dbPath, { maxBytes: C.BYTES.gib(2) });
|
|
956
975
|
if (!encPath || !encKey) {
|
|
957
976
|
// atRest: 'plain' — return the raw bytes. Operators wanting an
|
|
958
977
|
// encrypted snapshot under plain mode wrap with their own
|
|
@@ -2611,14 +2630,12 @@ function declareRequireDualControl(args) {
|
|
|
2611
2630
|
}
|
|
2612
2631
|
var m = args.m === undefined ? 2 : args.m;
|
|
2613
2632
|
var n = args.n === undefined ? Math.max(2, m) : args.n;
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
"declareRequireDualControl: n must be an integer >= m");
|
|
2621
|
-
}
|
|
2633
|
+
// m >= 2, n >= m, both integers — via numericBounds (matches db.js's existing
|
|
2634
|
+
// inline numeric-bounds usage; throws DbError("db/dual-control-bad-quorum")).
|
|
2635
|
+
require("./numeric-bounds").requirePositiveFiniteInt(m,
|
|
2636
|
+
"declareRequireDualControl: m", DbError, "db/dual-control-bad-quorum", { min: 2 });
|
|
2637
|
+
require("./numeric-bounds").requirePositiveFiniteInt(n,
|
|
2638
|
+
"declareRequireDualControl: n", DbError, "db/dual-control-bad-quorum", { min: m });
|
|
2622
2639
|
if (args.posture !== undefined && args.posture !== null &&
|
|
2623
2640
|
(typeof args.posture !== "string" || args.posture.length === 0)) {
|
|
2624
2641
|
throw new DbError("db/dual-control-bad-posture",
|
|
@@ -2785,7 +2802,7 @@ function _checkRollback(dataDirPath) {
|
|
|
2785
2802
|
}
|
|
2786
2803
|
var tip;
|
|
2787
2804
|
try {
|
|
2788
|
-
tip = safeJson.parse(atomicFile.readSync(tipPath), { schema: AUDIT_TIP_SCHEMA });
|
|
2805
|
+
tip = safeJson.parse(atomicFile.readSync(tipPath, { maxBytes: C.BYTES.kib(64) }), { schema: AUDIT_TIP_SCHEMA });
|
|
2789
2806
|
} catch (e) {
|
|
2790
2807
|
throw _dbErr("db/audit-tip-unreadable",
|
|
2791
2808
|
"FATAL: audit.tip unreadable or schema-invalid at " + tipPath + " — " + e.message +
|