@blamejs/core 0.14.27 → 0.15.0
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/README.md +2 -2
- package/index.js +4 -0
- package/lib/ai-content-detect.js +9 -10
- package/lib/api-key.js +107 -74
- package/lib/atomic-file.js +29 -1
- package/lib/audit-chain.js +47 -11
- package/lib/audit-sign.js +77 -2
- package/lib/audit-tools.js +79 -51
- package/lib/audit.js +218 -100
- package/lib/backup/index.js +13 -10
- package/lib/break-glass.js +202 -144
- package/lib/cache.js +174 -105
- package/lib/chain-writer.js +38 -16
- package/lib/cli.js +19 -14
- package/lib/cluster-provider-db.js +130 -104
- package/lib/cluster-storage.js +119 -22
- package/lib/cluster.js +119 -71
- package/lib/compliance.js +22 -0
- package/lib/consent.js +73 -24
- package/lib/constants.js +16 -11
- package/lib/crypto-field.js +387 -91
- package/lib/db-declare-row-policy.js +35 -22
- package/lib/db-file-lifecycle.js +3 -2
- package/lib/db-query.js +497 -255
- package/lib/db-schema.js +209 -44
- package/lib/db.js +176 -95
- package/lib/external-db-migrate.js +229 -139
- package/lib/external-db.js +25 -15
- package/lib/framework-error.js +11 -0
- package/lib/framework-files.js +73 -0
- package/lib/framework-schema.js +695 -394
- package/lib/gate-contract.js +596 -1
- package/lib/guard-agent-registry.js +26 -44
- package/lib/guard-all.js +1 -0
- package/lib/guard-auth.js +42 -112
- package/lib/guard-cidr.js +33 -154
- package/lib/guard-csv.js +46 -113
- package/lib/guard-domain.js +34 -157
- package/lib/guard-dsn.js +27 -43
- package/lib/guard-email.js +47 -69
- package/lib/guard-envelope.js +19 -32
- package/lib/guard-event-bus-payload.js +24 -42
- package/lib/guard-event-bus-topic.js +25 -43
- package/lib/guard-filename.js +42 -106
- package/lib/guard-graphql.js +42 -123
- package/lib/guard-html.js +53 -108
- package/lib/guard-idempotency-key.js +24 -42
- package/lib/guard-image.js +46 -103
- package/lib/guard-imap-command.js +18 -32
- package/lib/guard-jmap.js +16 -30
- package/lib/guard-json.js +38 -108
- package/lib/guard-jsonpath.js +38 -171
- package/lib/guard-jwt.js +49 -179
- package/lib/guard-list-id.js +25 -41
- package/lib/guard-list-unsubscribe.js +27 -43
- package/lib/guard-mail-compose.js +24 -42
- package/lib/guard-mail-move.js +26 -44
- package/lib/guard-mail-query.js +28 -46
- package/lib/guard-mail-reply.js +24 -42
- package/lib/guard-mail-sieve.js +24 -42
- package/lib/guard-managesieve-command.js +17 -31
- package/lib/guard-markdown.js +37 -104
- package/lib/guard-message-id.js +26 -45
- package/lib/guard-mime.js +39 -151
- package/lib/guard-oauth.js +54 -135
- package/lib/guard-pdf.js +45 -101
- package/lib/guard-pop3-command.js +21 -31
- package/lib/guard-posture-chain.js +24 -42
- package/lib/guard-regex.js +33 -107
- package/lib/guard-saga-config.js +24 -42
- package/lib/guard-shell.js +42 -172
- package/lib/guard-smtp-command.js +48 -54
- package/lib/guard-snapshot-envelope.js +24 -42
- package/lib/guard-sql.js +1491 -0
- package/lib/guard-stream-args.js +24 -43
- package/lib/guard-svg.js +47 -65
- package/lib/guard-template.js +35 -172
- package/lib/guard-tenant-id.js +26 -45
- package/lib/guard-time.js +32 -154
- package/lib/guard-trace-context.js +25 -44
- package/lib/guard-uuid.js +32 -153
- package/lib/guard-xml.js +38 -113
- package/lib/guard-yaml.js +51 -163
- package/lib/http-client.js +14 -0
- package/lib/inbox.js +120 -107
- package/lib/legal-hold.js +107 -50
- package/lib/log-stream-cloudwatch.js +47 -31
- package/lib/log-stream-otlp.js +32 -18
- package/lib/mail-crypto-smime.js +2 -6
- package/lib/mail-greylist.js +2 -6
- package/lib/mail-helo.js +2 -6
- package/lib/mail-journal.js +85 -64
- package/lib/mail-rbl.js +2 -6
- package/lib/mail-scan.js +2 -6
- package/lib/mail-spam-score.js +2 -6
- package/lib/mail-store.js +287 -154
- package/lib/middleware/fetch-metadata.js +17 -7
- package/lib/middleware/idempotency-key.js +54 -38
- package/lib/middleware/rate-limit.js +102 -32
- package/lib/middleware/security-headers.js +21 -5
- package/lib/migrations.js +108 -66
- package/lib/network-heartbeat.js +7 -0
- package/lib/nonce-store.js +31 -9
- package/lib/object-store/azure-blob-bucket-ops.js +9 -4
- package/lib/object-store/azure-blob.js +31 -3
- package/lib/object-store/sigv4.js +10 -0
- package/lib/outbox.js +136 -82
- package/lib/pqc-agent.js +44 -0
- package/lib/pubsub-cluster.js +42 -20
- package/lib/queue-local.js +202 -139
- package/lib/queue-redis.js +9 -1
- package/lib/queue-sqs.js +6 -0
- package/lib/retention.js +82 -39
- package/lib/safe-dns.js +29 -45
- package/lib/safe-ical.js +18 -33
- package/lib/safe-icap.js +27 -43
- package/lib/safe-sieve.js +21 -40
- package/lib/safe-sql.js +124 -3
- package/lib/safe-vcard.js +18 -33
- package/lib/scheduler.js +35 -12
- package/lib/seeders.js +122 -74
- package/lib/session-stores.js +42 -14
- package/lib/session.js +109 -72
- package/lib/sql.js +3885 -0
- package/lib/static.js +45 -7
- package/lib/subject.js +55 -17
- package/lib/vault/index.js +3 -2
- package/lib/vault/passphrase-ops.js +3 -2
- package/lib/vault/rotate.js +104 -64
- package/lib/vendor-data.js +2 -0
- package/lib/websocket.js +16 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/static.js
CHANGED
|
@@ -126,12 +126,32 @@ var DEFAULTS = Object.freeze({
|
|
|
126
126
|
// serve event is the audit-worthy act, not a precursor.
|
|
127
127
|
auditSuccess: true,
|
|
128
128
|
auditFailures: true,
|
|
129
|
+
// mountType — declares what KIND of content this mount serves, so
|
|
130
|
+
// the stored-XSS-relevant defaults follow the typing instead of being
|
|
131
|
+
// hand-flipped per mount (v0.15.0):
|
|
132
|
+
// "curated" (default) — operator-controlled assets (CSS / JS
|
|
133
|
+
// bundles / fonts / images). Inline render is required
|
|
134
|
+
// and safe because the operator authored the bytes;
|
|
135
|
+
// forceAttachmentForNonText defaults OFF.
|
|
136
|
+
// "user-content" — files written by end users / untrusted uploaders.
|
|
137
|
+
// A served .html / .js / .svg here is a stored-XSS
|
|
138
|
+
// vector, so forceAttachmentForNonText defaults ON —
|
|
139
|
+
// risky inline MIMEs are forced to download unless a
|
|
140
|
+
// sanitizer gate vouches for them (see
|
|
141
|
+
// `_shouldForceAttachment`). This is the conditional
|
|
142
|
+
// flip: a curated asset dir is never blindly forced to
|
|
143
|
+
// download; only a mount the operator TYPED as
|
|
144
|
+
// user-content gets the strict default.
|
|
145
|
+
// An explicit forceAttachmentForNonText always overrides the
|
|
146
|
+
// mountType-derived default.
|
|
147
|
+
mountType: "curated",
|
|
129
148
|
// forceAttachmentForNonText — stored-XSS defense for user-upload
|
|
130
|
-
// directories. Default OFF
|
|
131
|
-
// (CSS / JS bundles / fonts
|
|
132
|
-
// user-
|
|
133
|
-
//
|
|
134
|
-
//
|
|
149
|
+
// directories. Default follows mountType: OFF for "curated" mounts
|
|
150
|
+
// (operator-curated CSS / JS bundles / fonts need inline render), ON for
|
|
151
|
+
// "user-content" mounts so HTML / JS / SVG without a sanitizer / PDF /
|
|
152
|
+
// archives are forced to download. Set explicitly to override the
|
|
153
|
+
// mountType-derived default either way. See `_shouldForceAttachment`
|
|
154
|
+
// below for the safe-render allowlist.
|
|
135
155
|
forceAttachmentForNonText: false,
|
|
136
156
|
// Companion knobs — when forceAttachmentForNonText is on, allow
|
|
137
157
|
// image/svg+xml inline render IF an SVG sanitizer gate is wired
|
|
@@ -547,6 +567,15 @@ function _validateCreateOpts(opts) {
|
|
|
547
567
|
validateOpts.optionalBoolean(opts.auditFailures, "staticServe.create: auditFailures", StaticServeError);
|
|
548
568
|
validateOpts.optionalBoolean(opts.safeAttachmentForRiskyMimes,
|
|
549
569
|
"staticServe.create: safeAttachmentForRiskyMimes", StaticServeError);
|
|
570
|
+
// mountType — config-time enum. A typo ("usercontent", "uploads")
|
|
571
|
+
// would silently fall back to the curated default and serve untrusted
|
|
572
|
+
// HTML inline, so THROW at boot rather than mis-type the mount.
|
|
573
|
+
if (opts.mountType !== undefined &&
|
|
574
|
+
opts.mountType !== "curated" && opts.mountType !== "user-content") {
|
|
575
|
+
throw _err("BAD_OPT",
|
|
576
|
+
"staticServe.create: mountType must be 'curated' (default) or " +
|
|
577
|
+
"'user-content'; got " + JSON.stringify(opts.mountType));
|
|
578
|
+
}
|
|
550
579
|
validateOpts.optionalBoolean(opts.forceAttachmentForNonText,
|
|
551
580
|
"staticServe.create: forceAttachmentForNonText", StaticServeError);
|
|
552
581
|
validateOpts.optionalBoolean(opts.safeRenderSvg,
|
|
@@ -684,7 +713,7 @@ function create(opts) {
|
|
|
684
713
|
"maxBytesPerActorPerWindowMs", "maxBytesAllActorsPerWindowMs",
|
|
685
714
|
"bandwidthWindowMs", "maxConcurrentDownloadsPerActor", "maxIdleMs",
|
|
686
715
|
"contentSafety", "contentSafetyDisabledReason",
|
|
687
|
-
"forceAttachmentForNonText", "safeRenderSvg", "safeRenderPdf",
|
|
716
|
+
"mountType", "forceAttachmentForNonText", "safeRenderSvg", "safeRenderPdf",
|
|
688
717
|
], "staticServe.create");
|
|
689
718
|
_validateCreateOpts(opts);
|
|
690
719
|
var cfg = validateOpts.applyDefaults(opts, DEFAULTS);
|
|
@@ -738,7 +767,16 @@ function create(opts) {
|
|
|
738
767
|
var auditFailures = cfg.auditFailures;
|
|
739
768
|
var acceptRanges = cfg.acceptRanges;
|
|
740
769
|
var safeAttachment = !!cfg.safeAttachmentForRiskyMimes;
|
|
741
|
-
|
|
770
|
+
// forceAttachmentForNonText default follows mountType (v0.15.0): a
|
|
771
|
+
// mount TYPED "user-content" forces risky inline MIMEs to download by
|
|
772
|
+
// default (stored-XSS defense for untrusted uploads); a "curated" mount
|
|
773
|
+
// keeps inline render. An explicit forceAttachmentForNonText overrides
|
|
774
|
+
// the mountType-derived default either way. The conditional flip never
|
|
775
|
+
// blindly force-attaches a curated asset dir.
|
|
776
|
+
var mountType = opts.mountType || "curated";
|
|
777
|
+
var forceAttachmentForNonText = opts.forceAttachmentForNonText !== undefined
|
|
778
|
+
? !!opts.forceAttachmentForNonText
|
|
779
|
+
: (mountType === "user-content");
|
|
742
780
|
var allowSvgRender = cfg.safeRenderSvg !== false;
|
|
743
781
|
var allowPdfRender = !!cfg.safeRenderPdf;
|
|
744
782
|
var perActorCap = cfg.maxBytesPerActorPerWindowMs;
|
package/lib/subject.js
CHANGED
|
@@ -40,11 +40,22 @@ var { sha3Hash } = require("./crypto");
|
|
|
40
40
|
var cryptoField = require("./crypto-field");
|
|
41
41
|
var audit = require("./audit");
|
|
42
42
|
var cluster = require("./cluster");
|
|
43
|
+
var safeSql = require("./safe-sql");
|
|
44
|
+
var sql = require("./sql");
|
|
43
45
|
var lazyRequire = require("./lazy-require");
|
|
44
46
|
|
|
45
47
|
var db = lazyRequire(function () { return require("./db"); });
|
|
46
48
|
var legalHold = lazyRequire(function () { return require("./legal-hold"); });
|
|
47
49
|
|
|
50
|
+
// Local-SQLite framework tables for the Art. 18 restriction flag + the
|
|
51
|
+
// erasure marker. These run against the b.db() handle directly, so the
|
|
52
|
+
// b.sql builders carry { quoteName: true } to emit the quoted local name
|
|
53
|
+
// (no clusterStorage prefix rewrite on this path). The names are literals
|
|
54
|
+
// for the same reason db.js declares them as literals — they ARE the
|
|
55
|
+
// canonical local table identifiers.
|
|
56
|
+
var RESTRICTIONS_TABLE = "_blamejs_subject_restrictions"; // allow:hand-rolled-sql — canonical local table-name; passed to b.sql with quoteName
|
|
57
|
+
var ERASURES_TABLE = "_blamejs_subject_erasures"; // allow:hand-rolled-sql — canonical local table-name; passed to b.sql with quoteName
|
|
58
|
+
|
|
48
59
|
// Required acknowledgements before subject.erase will run. Operator must
|
|
49
60
|
// explicitly attest each one to confirm no statutory retention or active
|
|
50
61
|
// litigation hold blocks the deletion.
|
|
@@ -211,7 +222,7 @@ function rectify(subjectId, opts) {
|
|
|
211
222
|
rowId: opts.id,
|
|
212
223
|
requestReason: opts.reason,
|
|
213
224
|
});
|
|
214
|
-
throw new Error("subject.rectify: row not found in '" + opts.table + "'
|
|
225
|
+
throw new Error("subject.rectify: row not found in '" + opts.table + "' for _id '" + opts.id + "'");
|
|
215
226
|
}
|
|
216
227
|
|
|
217
228
|
var changedKeys = Object.keys(opts.changes);
|
|
@@ -478,7 +489,10 @@ function eraseHard(subjectId, opts) {
|
|
|
478
489
|
perTable[spec.name] = deleted;
|
|
479
490
|
// REINDEX the table so B-tree pages holding the deleted row's
|
|
480
491
|
// index entries are rebuilt — closes the erase-vacuum residual class.
|
|
481
|
-
|
|
492
|
+
// REINDEX is a sqlite maintenance verb with no b.sql builder; the
|
|
493
|
+
// table identifier is quoted through b.safeSql so the name is safe by
|
|
494
|
+
// construction (it comes from FRAMEWORK_SCHEMA / the subject-table set).
|
|
495
|
+
try { db().runSql("REINDEX " + safeSql.quoteIdentifier(spec.name, "sqlite", { allowReserved: true })); }
|
|
482
496
|
catch (_e) { /* cluster mode / unsupported dialect */ }
|
|
483
497
|
}
|
|
484
498
|
_markErased(subjectId);
|
|
@@ -536,20 +550,31 @@ function restrict(subjectId, opts) {
|
|
|
536
550
|
if (!opts || typeof opts.on !== "boolean") {
|
|
537
551
|
throw new Error("subject.restrict requires { on: true|false }");
|
|
538
552
|
}
|
|
539
|
-
var
|
|
540
|
-
"
|
|
541
|
-
|
|
553
|
+
var restrictSelBuilt = sql.select(RESTRICTIONS_TABLE, { dialect: "sqlite", quoteName: true })
|
|
554
|
+
.columns(["subjectIdHash"])
|
|
555
|
+
.where("subjectIdHash", _subjectHash(subjectId))
|
|
556
|
+
.toSql();
|
|
557
|
+
var restrictSelStmt = db().prepare(restrictSelBuilt.sql);
|
|
558
|
+
var existing = restrictSelStmt.get.apply(restrictSelStmt, restrictSelBuilt.params);
|
|
542
559
|
|
|
543
560
|
if (opts.on) {
|
|
544
561
|
if (!existing) {
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
562
|
+
var restrictInsBuilt = sql.insert(RESTRICTIONS_TABLE, { dialect: "sqlite", quoteName: true })
|
|
563
|
+
.values({
|
|
564
|
+
subjectIdHash: _subjectHash(subjectId),
|
|
565
|
+
since: Date.now(),
|
|
566
|
+
reason: opts.reason || null,
|
|
567
|
+
})
|
|
568
|
+
.toSql();
|
|
569
|
+
var restrictInsStmt = db().prepare(restrictInsBuilt.sql);
|
|
570
|
+
restrictInsStmt.run.apply(restrictInsStmt, restrictInsBuilt.params);
|
|
548
571
|
}
|
|
549
572
|
} else if (existing) {
|
|
550
|
-
|
|
551
|
-
"
|
|
552
|
-
|
|
573
|
+
var restrictDelBuilt = sql.delete(RESTRICTIONS_TABLE, { dialect: "sqlite", quoteName: true })
|
|
574
|
+
.where("subjectIdHash", _subjectHash(subjectId))
|
|
575
|
+
.toSql();
|
|
576
|
+
var restrictDelStmt = db().prepare(restrictDelBuilt.sql);
|
|
577
|
+
restrictDelStmt.run.apply(restrictDelStmt, restrictDelBuilt.params);
|
|
553
578
|
}
|
|
554
579
|
|
|
555
580
|
_writeAudit("subject.restrict", subjectId, "success", {
|
|
@@ -581,9 +606,15 @@ function restrict(subjectId, opts) {
|
|
|
581
606
|
*/
|
|
582
607
|
function isRestricted(subjectId) {
|
|
583
608
|
if (!subjectId) return false;
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
609
|
+
// Presence check — project the PK column (b.sql columns must be real
|
|
610
|
+
// identifiers, not a `SELECT 1` literal); a matched row is truthy.
|
|
611
|
+
var built = sql.select(RESTRICTIONS_TABLE, { dialect: "sqlite", quoteName: true })
|
|
612
|
+
.columns(["subjectIdHash"])
|
|
613
|
+
.where("subjectIdHash", _subjectHash(subjectId))
|
|
614
|
+
.limit(1)
|
|
615
|
+
.toSql();
|
|
616
|
+
var stmt = db().prepare(built.sql);
|
|
617
|
+
var row = stmt.get.apply(stmt, built.params);
|
|
587
618
|
return !!row;
|
|
588
619
|
}
|
|
589
620
|
|
|
@@ -629,9 +660,16 @@ function recordObjection(subjectId, opts) {
|
|
|
629
660
|
// ---- Internal helpers ----
|
|
630
661
|
|
|
631
662
|
function _markErased(subjectId) {
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
663
|
+
// "INSERT OR REPLACE" is the sqlite upsert idiom — express it portably as
|
|
664
|
+
// INSERT … ON CONFLICT(subjectIdHash) DO UPDATE SET erasedAt = EXCLUDED.erasedAt
|
|
665
|
+
// (the row is keyed by subjectIdHash; a re-erase just refreshes the timestamp).
|
|
666
|
+
var built = sql.upsert(ERASURES_TABLE, { dialect: "sqlite", quoteName: true })
|
|
667
|
+
.values({ subjectIdHash: _subjectHash(subjectId), erasedAt: Date.now() })
|
|
668
|
+
.onConflict(["subjectIdHash"])
|
|
669
|
+
.doUpdateFromExcluded(["erasedAt"])
|
|
670
|
+
.toSql();
|
|
671
|
+
var stmt = db().prepare(built.sql);
|
|
672
|
+
stmt.run.apply(stmt, built.params);
|
|
635
673
|
}
|
|
636
674
|
|
|
637
675
|
function _subjectHash(subjectId) {
|
package/lib/vault/index.js
CHANGED
|
@@ -71,6 +71,7 @@ var { boot } = require("../log");
|
|
|
71
71
|
var safeBuffer = require("../safe-buffer");
|
|
72
72
|
var safeJson = require("../safe-json");
|
|
73
73
|
var observability = require("../observability");
|
|
74
|
+
var frameworkFiles = require("../framework-files");
|
|
74
75
|
var vaultPassphraseSource = require("./passphrase-source");
|
|
75
76
|
var vaultWrap = require("./wrap");
|
|
76
77
|
var { defineClass } = require("../framework-error");
|
|
@@ -99,8 +100,8 @@ var log = boot("vault");
|
|
|
99
100
|
function resolvePaths(dataDir) {
|
|
100
101
|
return {
|
|
101
102
|
dataDir: dataDir,
|
|
102
|
-
plaintext: nodePath.join(dataDir,
|
|
103
|
-
sealed: nodePath.join(dataDir, "
|
|
103
|
+
plaintext: nodePath.join(dataDir, frameworkFiles.fileName("vaultKey")),
|
|
104
|
+
sealed: nodePath.join(dataDir, frameworkFiles.fileName("vaultKey") + ".sealed"),
|
|
104
105
|
derivedHashSalt: nodePath.join(dataDir, "vault.derived-hash-salt"),
|
|
105
106
|
derivedHashMacKey: nodePath.join(dataDir, "vault.derived-hash-mac.sealed"),
|
|
106
107
|
};
|
|
@@ -38,13 +38,14 @@
|
|
|
38
38
|
var nodeFs = require("node:fs");
|
|
39
39
|
var nodePath = require("node:path");
|
|
40
40
|
var atomicFile = require("../atomic-file");
|
|
41
|
+
var frameworkFiles = require("../framework-files");
|
|
41
42
|
var vaultWrap = require("./wrap");
|
|
42
43
|
var { defineClass } = require("../framework-error");
|
|
43
44
|
|
|
44
45
|
var VaultPassphraseError = defineClass("VaultPassphraseError", { alwaysPermanent: true });
|
|
45
46
|
|
|
46
|
-
var PLAINTEXT_NAME =
|
|
47
|
-
var SEALED_NAME = "
|
|
47
|
+
var PLAINTEXT_NAME = frameworkFiles.fileName("vaultKey");
|
|
48
|
+
var SEALED_NAME = frameworkFiles.fileName("vaultKey") + ".sealed";
|
|
48
49
|
|
|
49
50
|
function _paths(dataDir) {
|
|
50
51
|
return {
|
package/lib/vault/rotate.js
CHANGED
|
@@ -52,12 +52,13 @@ var nodeFs = require("node:fs");
|
|
|
52
52
|
var nodePath = require("node:path");
|
|
53
53
|
var { DatabaseSync } = require("node:sqlite");
|
|
54
54
|
var atomicFile = require("../atomic-file");
|
|
55
|
-
var
|
|
55
|
+
var sql = require("../sql");
|
|
56
56
|
var C = require("../constants");
|
|
57
57
|
var cryptoField = require("../crypto-field");
|
|
58
58
|
var bCrypto = require("../crypto");
|
|
59
59
|
var vaultAad = require("../vault-aad");
|
|
60
60
|
var dbSchema = require("../db-schema");
|
|
61
|
+
var frameworkFiles = require("../framework-files");
|
|
61
62
|
var lazyRequire = require("../lazy-require");
|
|
62
63
|
var { boot } = require("../log");
|
|
63
64
|
var numericBounds = require("../numeric-bounds");
|
|
@@ -97,18 +98,30 @@ var DEFAULT_DRIFT_SAMPLE_LIMIT = 100;
|
|
|
97
98
|
var DEFAULT_VERIFY_SAMPLE_MIN = 5;
|
|
98
99
|
var DEFAULT_VERIFY_SAMPLE_FRAC = 0.01;
|
|
99
100
|
|
|
101
|
+
// The catalog/PRAGMA statements all compose through b.sql's narrow audited
|
|
102
|
+
// catalog sub-API (b.sql.catalog / b.sql.pragma) - the only path that emits
|
|
103
|
+
// an sqlite_master reference or a PRAGMA verb, allowlisting exactly the
|
|
104
|
+
// statements the key-rotation walk needs and refusing every other internal
|
|
105
|
+
// identifier / PRAGMA verb. Each returns { sql, params }; the node:sqlite
|
|
106
|
+
// handle takes the params positionally.
|
|
107
|
+
function _all(db, built) {
|
|
108
|
+
var stmt = db.prepare(built.sql);
|
|
109
|
+
return built.params.length > 0 ? stmt.all.apply(stmt, built.params) : stmt.all();
|
|
110
|
+
}
|
|
111
|
+
function _get(db, built) {
|
|
112
|
+
var stmt = db.prepare(built.sql);
|
|
113
|
+
return built.params.length > 0 ? stmt.get.apply(stmt, built.params) : stmt.get();
|
|
114
|
+
}
|
|
115
|
+
|
|
100
116
|
function _listLiveTables(db) {
|
|
101
|
-
return db.
|
|
102
|
-
"SELECT name FROM sqlite_master " +
|
|
103
|
-
"WHERE type='table' AND name NOT LIKE 'sqlite_%'"
|
|
104
|
-
).all().map(function (r) { return r.name; });
|
|
117
|
+
return _all(db, sql.catalog.listTables()).map(function (r) { return r.name; });
|
|
105
118
|
}
|
|
106
119
|
|
|
107
120
|
function _listLiveColumns(db, table) {
|
|
108
121
|
// PRAGMA table_info — table name comes from sqlite_master so it's
|
|
109
|
-
// already validated as an existing identifier.
|
|
110
|
-
|
|
111
|
-
|
|
122
|
+
// already validated as an existing identifier; b.sql.catalog.tableInfo
|
|
123
|
+
// quotes it by construction.
|
|
124
|
+
return _all(db, sql.catalog.tableInfo(table)).map(function (c) { return c.name; });
|
|
112
125
|
}
|
|
113
126
|
|
|
114
127
|
function _knownColumnsFor(schema, infraColumns) {
|
|
@@ -201,12 +214,13 @@ function validateSchemaMatch(db, opts) {
|
|
|
201
214
|
}
|
|
202
215
|
if (unknown.length === 0) continue;
|
|
203
216
|
|
|
204
|
-
var
|
|
205
|
-
|
|
206
|
-
|
|
217
|
+
var sampleBuilt = sql.select(table, { dialect: "sqlite", quoteName: true })
|
|
218
|
+
.columns(unknown)
|
|
219
|
+
.limit(sampleLimit)
|
|
220
|
+
.toSql();
|
|
207
221
|
var sampled;
|
|
208
222
|
try {
|
|
209
|
-
sampled = db
|
|
223
|
+
sampled = _all(db, sampleBuilt);
|
|
210
224
|
} catch (e) {
|
|
211
225
|
warnings.push({
|
|
212
226
|
kind: "sample_failed",
|
|
@@ -312,7 +326,8 @@ function verify(opts) {
|
|
|
312
326
|
var schema = cryptoField.getSchema(table);
|
|
313
327
|
if (!schema || !Array.isArray(schema.sealedFields) || schema.sealedFields.length === 0) continue;
|
|
314
328
|
|
|
315
|
-
var totalRow = db.
|
|
329
|
+
var totalRow = _get(db, sql.select(table, { dialect: "sqlite", quoteName: true })
|
|
330
|
+
.count("*", "n").toSql());
|
|
316
331
|
var total = totalRow ? totalRow.n : 0;
|
|
317
332
|
if (total === 0) continue;
|
|
318
333
|
|
|
@@ -320,10 +335,10 @@ function verify(opts) {
|
|
|
320
335
|
if (sampleN > total) sampleN = total;
|
|
321
336
|
|
|
322
337
|
// RANDOM() is fine for a sampler — we're picking representative rows,
|
|
323
|
-
// not building cryptographic randomness.
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
338
|
+
// not building cryptographic randomness. b.sql.catalog.sampleRandom is
|
|
339
|
+
// the audited ORDER BY RANDOM() form (the general builder has no random-
|
|
340
|
+
// order clause); columns omitted -> `*`.
|
|
341
|
+
var sampled = _all(db, sql.catalog.sampleRandom(table, null, { limit: sampleN }));
|
|
327
342
|
|
|
328
343
|
var foundOldFail = !oldKeys; // when no oldKeys supplied, this check is N/A
|
|
329
344
|
var verifiedRows = 0;
|
|
@@ -529,15 +544,19 @@ function _walkAndReSeal(node, oldKeys, newKeys) {
|
|
|
529
544
|
return { value: node, changed: false };
|
|
530
545
|
}
|
|
531
546
|
|
|
532
|
-
|
|
547
|
+
// Transaction-control statements only (BEGIN / COMMIT / ROLLBACK) - fixed
|
|
548
|
+
// keywords, no identifier / value, so they stay verbatim rather than route
|
|
549
|
+
// through b.sql (the builder has no transaction-control verb). The param is
|
|
550
|
+
// named `stmtText` so it does not shadow the module-level `sql` builder.
|
|
551
|
+
function _runStmt(db, stmtText) { db.prepare(stmtText).run(); }
|
|
533
552
|
|
|
534
553
|
function _rotateColumn(db, table, column, schema, roots, batchSize, progress) {
|
|
535
|
-
//
|
|
536
|
-
//
|
|
537
|
-
//
|
|
538
|
-
|
|
539
|
-
var
|
|
540
|
-
|
|
554
|
+
// Every statement composes through b.sql (sqlite dialect, quoteName so
|
|
555
|
+
// the concrete handle's table is quoted, not left bare for a cluster
|
|
556
|
+
// rewrite that does not apply here). Identifiers are validated + quoted
|
|
557
|
+
// by construction; the cursor bound (_id) + LIMIT bind as ? placeholders.
|
|
558
|
+
var total = _get(db, sql.select(table, { dialect: "sqlite", quoteName: true })
|
|
559
|
+
.count("*", "n").whereNotNull(column).toSql()).n;
|
|
541
560
|
if (total === 0) return 0;
|
|
542
561
|
|
|
543
562
|
// AAD-bound tables (registerTable({aad:true})) seal each cell under a
|
|
@@ -548,36 +567,54 @@ function _rotateColumn(db, table, column, schema, roots, batchSize, progress) {
|
|
|
548
567
|
var aadMode = !!(schema && schema.aad);
|
|
549
568
|
var rowIdField = aadMode ? schema.rowIdField : null;
|
|
550
569
|
var needRid = aadMode && rowIdField && rowIdField !== "_id";
|
|
551
|
-
var qrid = needRid ? safeSql.quoteIdentifier(rowIdField, "sqlite") : null;
|
|
552
570
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
571
|
+
// Keyset-cursor page over (_id) ascending. The projected columns are read
|
|
572
|
+
// by their REAL names off the result row (no AS alias) - the column value
|
|
573
|
+
// is row[column], the row-id value is row[rowIdField]. The SQL text is
|
|
574
|
+
// constant across the loop (only the bound _id-cursor changes; LIMIT is a
|
|
575
|
+
// builder-inlined integer literal, validated non-negative), so prepare
|
|
576
|
+
// once + re-run with the fresh cursor param positionally. The SELECT
|
|
577
|
+
// carries exactly one `?` (the _id cursor); the UPDATE carries two (the
|
|
578
|
+
// resealed value + the _id).
|
|
579
|
+
var selCols = ["_id", column];
|
|
580
|
+
if (needRid) selCols.push(rowIdField);
|
|
581
|
+
var selBuilt = sql.select(table, { dialect: "sqlite", quoteName: true })
|
|
582
|
+
.columns(selCols)
|
|
583
|
+
.whereNotNull(column)
|
|
584
|
+
.whereOp("_id", ">", "")
|
|
585
|
+
.orderBy("_id")
|
|
586
|
+
.limit(batchSize)
|
|
587
|
+
.toSql();
|
|
588
|
+
var sel = db.prepare(selBuilt.sql);
|
|
589
|
+
var updBuilt = sql.update(table, { dialect: "sqlite", quoteName: true })
|
|
590
|
+
.set(column, "")
|
|
591
|
+
.where("_id", "")
|
|
592
|
+
.toSql();
|
|
593
|
+
var upd = db.prepare(updBuilt.sql);
|
|
558
594
|
|
|
559
595
|
var processed = 0;
|
|
560
596
|
var lastId = "";
|
|
561
597
|
while (true) {
|
|
562
|
-
var rows = sel.all(lastId
|
|
598
|
+
var rows = sel.all(lastId);
|
|
563
599
|
if (rows.length === 0) break;
|
|
564
600
|
|
|
565
601
|
dbSchema.runInTransaction(db, function () {
|
|
566
602
|
for (var i = 0; i < rows.length; i++) {
|
|
567
603
|
var row = rows[i];
|
|
568
|
-
|
|
569
|
-
if (
|
|
604
|
+
var cellVal = row[column];
|
|
605
|
+
if (typeof cellVal !== "string") continue;
|
|
606
|
+
if (aadMode && vaultAad.isAadSealed(cellVal)) {
|
|
570
607
|
// Rebuild the exact AAD the seal side used. cryptoField._aadParts
|
|
571
608
|
// reads row[schema.rowIdField]; feed it the rowIdField value we
|
|
572
|
-
// selected (
|
|
609
|
+
// selected (row[rowIdField], or _id when rowIdField IS _id).
|
|
573
610
|
var rowForAad = {};
|
|
574
|
-
rowForAad[rowIdField] = needRid ? row
|
|
611
|
+
rowForAad[rowIdField] = needRid ? row[rowIdField] : row._id;
|
|
575
612
|
var aad = cryptoField._aadParts(schema, table, column, rowForAad);
|
|
576
|
-
upd.run(vaultAad.resealRoot(
|
|
577
|
-
} else if (
|
|
613
|
+
upd.run(vaultAad.resealRoot(cellVal, aad, roots.oldRootJson, roots.newRootJson), row._id);
|
|
614
|
+
} else if (cellVal.indexOf(C.VAULT_PREFIX) === 0) {
|
|
578
615
|
// Plain vault: cell (non-AAD table, or a legacy pre-AAD cell in
|
|
579
616
|
// an AAD table that the next sealRow upgrades).
|
|
580
|
-
upd.run(_reSealValue(
|
|
617
|
+
upd.run(_reSealValue(cellVal, roots.oldKeys, roots.newKeys), row._id);
|
|
581
618
|
}
|
|
582
619
|
}
|
|
583
620
|
});
|
|
@@ -589,23 +626,33 @@ function _rotateColumn(db, table, column, schema, roots, batchSize, progress) {
|
|
|
589
626
|
}
|
|
590
627
|
|
|
591
628
|
function _rotateOverflow(db, table, oldKeys, newKeys, batchSize, progress, warnings) {
|
|
592
|
-
var
|
|
593
|
-
var cols = db.prepare("PRAGMA table_info(" + qt + ")").all();
|
|
629
|
+
var cols = _all(db, sql.catalog.tableInfo(table));
|
|
594
630
|
if (!cols.some(function (c) { return c.name === "data"; })) return 0;
|
|
595
631
|
|
|
596
|
-
var total = db.
|
|
632
|
+
var total = _get(db, sql.select(table, { dialect: "sqlite", quoteName: true })
|
|
633
|
+
.count("*", "n").whereNotNull("data").toSql()).n;
|
|
597
634
|
if (total === 0) return 0;
|
|
598
635
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
636
|
+
// Same keyset cursor as _rotateColumn over the overflow `data` JSON
|
|
637
|
+
// column: one bound `?` (the _id cursor), builder-inlined LIMIT literal.
|
|
638
|
+
var selBuilt = sql.select(table, { dialect: "sqlite", quoteName: true })
|
|
639
|
+
.columns(["_id", "data"])
|
|
640
|
+
.whereNotNull("data")
|
|
641
|
+
.whereOp("_id", ">", "")
|
|
642
|
+
.orderBy("_id")
|
|
643
|
+
.limit(batchSize)
|
|
644
|
+
.toSql();
|
|
645
|
+
var sel = db.prepare(selBuilt.sql);
|
|
646
|
+
var updBuilt = sql.update(table, { dialect: "sqlite", quoteName: true })
|
|
647
|
+
.set("data", "")
|
|
648
|
+
.where("_id", "")
|
|
649
|
+
.toSql();
|
|
650
|
+
var upd = db.prepare(updBuilt.sql);
|
|
604
651
|
|
|
605
652
|
var processed = 0;
|
|
606
653
|
var lastId = "";
|
|
607
654
|
while (true) {
|
|
608
|
-
var rows = sel.all(lastId
|
|
655
|
+
var rows = sel.all(lastId);
|
|
609
656
|
if (rows.length === 0) break;
|
|
610
657
|
|
|
611
658
|
_runStmt(db, "BEGIN");
|
|
@@ -689,10 +736,10 @@ async function rotate(opts) {
|
|
|
689
736
|
var progress = opts.progressCallback;
|
|
690
737
|
var warnings = [];
|
|
691
738
|
var paths = Object.assign({
|
|
692
|
-
encryptedDb:
|
|
693
|
-
dbKeySealed:
|
|
694
|
-
vaultKeyPlain:
|
|
695
|
-
vaultKeySealed: "
|
|
739
|
+
encryptedDb: frameworkFiles.fileName("dbEnc"),
|
|
740
|
+
dbKeySealed: frameworkFiles.fileName("dbKeyEnc"),
|
|
741
|
+
vaultKeyPlain: frameworkFiles.fileName("vaultKey"),
|
|
742
|
+
vaultKeySealed: frameworkFiles.fileName("vaultKey") + ".sealed",
|
|
696
743
|
additionalSealed: [],
|
|
697
744
|
verbatimFiles: [],
|
|
698
745
|
verbatimDirs: [],
|
|
@@ -849,18 +896,15 @@ async function rotate(opts) {
|
|
|
849
896
|
|
|
850
897
|
var db = new DatabaseSync(tmpDbPath);
|
|
851
898
|
try {
|
|
852
|
-
|
|
853
|
-
|
|
899
|
+
db.prepare(sql.pragma("journal_mode", "WAL").sql).run();
|
|
900
|
+
db.prepare(sql.pragma("synchronous", "NORMAL").sql).run();
|
|
854
901
|
|
|
855
902
|
// Walk tables. For each, re-seal every column declared sealed
|
|
856
903
|
// by the field-crypto registry, plus the overflow `data` JSON
|
|
857
904
|
// column if present.
|
|
858
905
|
var tablesToRotate = Array.isArray(opts.tables) && opts.tables.length > 0
|
|
859
906
|
? opts.tables.slice()
|
|
860
|
-
: db
|
|
861
|
-
"SELECT name FROM sqlite_master " +
|
|
862
|
-
"WHERE type='table' AND name NOT LIKE 'sqlite_%'"
|
|
863
|
-
).all().map(function (r) { return r.name; });
|
|
907
|
+
: _listLiveTables(db);
|
|
864
908
|
|
|
865
909
|
// Serialized roots threaded to the AAD reseal path; oldRootJson /
|
|
866
910
|
// newRootJson match b.vault.getKeysJson() so rotated AAD cells unseal
|
|
@@ -869,15 +913,11 @@ async function rotate(opts) {
|
|
|
869
913
|
|
|
870
914
|
for (var ti = 0; ti < tablesToRotate.length; ti++) {
|
|
871
915
|
var table = tablesToRotate[ti];
|
|
872
|
-
var tableExists = db.
|
|
873
|
-
"SELECT name FROM sqlite_master WHERE type='table' AND name = ?"
|
|
874
|
-
).get(table);
|
|
916
|
+
var tableExists = _get(db, sql.catalog.tableExists(table));
|
|
875
917
|
if (!tableExists) continue;
|
|
876
918
|
|
|
877
919
|
var schema = cryptoField.getSchema(table);
|
|
878
|
-
var liveCols = db
|
|
879
|
-
'PRAGMA table_info("' + table.replace(/"/g, '""') + '")'
|
|
880
|
-
).all().map(function (c) { return c.name; });
|
|
920
|
+
var liveCols = _listLiveColumns(db, table);
|
|
881
921
|
var liveColSet = Object.create(null);
|
|
882
922
|
for (var lc = 0; lc < liveCols.length; lc++) liveColSet[liveCols[lc]] = true;
|
|
883
923
|
|
|
@@ -894,7 +934,7 @@ async function rotate(opts) {
|
|
|
894
934
|
if (tableRows > 0) { tablesProcessed++; totalRowsProcessed += tableRows; }
|
|
895
935
|
}
|
|
896
936
|
|
|
897
|
-
|
|
937
|
+
db.prepare(sql.pragma("wal_checkpoint", "TRUNCATE").sql).run();
|
|
898
938
|
} finally {
|
|
899
939
|
db.close();
|
|
900
940
|
}
|
package/lib/vendor-data.js
CHANGED
|
@@ -130,6 +130,7 @@ function _timingSafeHexEqual(a, b) {
|
|
|
130
130
|
var KNOWN_VENDOR_DATA = Object.freeze({
|
|
131
131
|
"public-suffix-list": {
|
|
132
132
|
module: "./vendor/public-suffix-list.data",
|
|
133
|
+
// allow:hand-rolled-sql — `_blamejs_canary_*` is an in-payload tamper-canary token, not a SQL table name (no DB sink in this file)
|
|
133
134
|
canary: "_blamejs_canary_v0_9_8_.local",
|
|
134
135
|
// Canary parse check — operator-side `b.publicSuffix.isPublicSuffix(canary)`
|
|
135
136
|
// MUST return true after the PSL parser ingests the data. The check
|
|
@@ -138,6 +139,7 @@ var KNOWN_VENDOR_DATA = Object.freeze({
|
|
|
138
139
|
},
|
|
139
140
|
"common-passwords-top-10000": {
|
|
140
141
|
module: "./vendor/common-passwords-top-10000.data",
|
|
142
|
+
// allow:hand-rolled-sql — `_blamejs_canary_*` is an in-payload tamper-canary token, not a SQL table name (no DB sink in this file)
|
|
141
143
|
canary: "_blamejs_canary_password_2026_05_13_blamejs_internal_",
|
|
142
144
|
description: "Top-10000 most common passwords (SecLists). Used by b.auth.password to refuse known-breached credentials.",
|
|
143
145
|
},
|
package/lib/websocket.js
CHANGED
|
@@ -971,6 +971,22 @@ class WebSocketConnection extends EventEmitter {
|
|
|
971
971
|
self._transitionToClosed(1006, "abnormal closure", false, null);
|
|
972
972
|
}
|
|
973
973
|
});
|
|
974
|
+
socket.on("end", function () {
|
|
975
|
+
// Peer half-closed (TCP FIN) without sending a Close frame. HTTP
|
|
976
|
+
// 'upgrade' sockets default to allowHalfOpen=true, so this arrives
|
|
977
|
+
// as 'end' (readable side ended) while the writable side stays
|
|
978
|
+
// open — the 'close' handler above never fires and the connection
|
|
979
|
+
// would otherwise wedge open (ping timer running, no 'close' event,
|
|
980
|
+
// peer's socket never destroyed). RFC 6455 §7.1.1 treats a TCP
|
|
981
|
+
// close without a prior Close frame as abnormal closure: surface
|
|
982
|
+
// the lifecycle event and end our writable side so the socket
|
|
983
|
+
// actually tears down. _transitionToClosed is idempotent, so the
|
|
984
|
+
// native 'close' that follows is a no-op.
|
|
985
|
+
if (self._state !== STATE_CLOSED) {
|
|
986
|
+
self._transitionToClosed(1006, "abnormal closure", false, null);
|
|
987
|
+
}
|
|
988
|
+
try { socket.end(); } catch (_e) { /* socket already closing */ }
|
|
989
|
+
});
|
|
974
990
|
}
|
|
975
991
|
|
|
976
992
|
// Single state-transition method. Idempotent — repeat calls after
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:d63a172d-d92b-4a98-ae64-7dcb04e82076",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-06-
|
|
8
|
+
"timestamp": "2026-06-08T20:30:45.961Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/core@0.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.15.0",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.
|
|
25
|
+
"version": "0.15.0",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "The Node framework that owns its stack.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/core@0.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.15.0",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/core@0.
|
|
57
|
+
"ref": "@blamejs/core@0.15.0",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|