@blamejs/core 0.14.26 → 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.
Files changed (150) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +2 -2
  3. package/index.js +4 -0
  4. package/lib/agent-envelope-mac.js +104 -0
  5. package/lib/agent-event-bus.js +105 -4
  6. package/lib/agent-posture-chain.js +8 -42
  7. package/lib/ai-content-detect.js +9 -10
  8. package/lib/api-key.js +107 -74
  9. package/lib/atomic-file.js +62 -4
  10. package/lib/audit-chain.js +47 -11
  11. package/lib/audit-sign.js +77 -2
  12. package/lib/audit-tools.js +79 -51
  13. package/lib/audit.js +249 -123
  14. package/lib/auth/openid-federation.js +108 -47
  15. package/lib/backup/index.js +13 -10
  16. package/lib/break-glass.js +202 -144
  17. package/lib/cache.js +174 -105
  18. package/lib/chain-writer.js +38 -16
  19. package/lib/cli.js +19 -14
  20. package/lib/cluster-provider-db.js +130 -104
  21. package/lib/cluster-storage.js +119 -22
  22. package/lib/cluster.js +119 -71
  23. package/lib/compliance.js +169 -4
  24. package/lib/consent.js +73 -24
  25. package/lib/constants.js +16 -11
  26. package/lib/crypto-field.js +474 -92
  27. package/lib/db-declare-row-policy.js +35 -22
  28. package/lib/db-file-lifecycle.js +3 -2
  29. package/lib/db-query.js +497 -255
  30. package/lib/db-schema.js +209 -44
  31. package/lib/db.js +176 -95
  32. package/lib/error-page.js +14 -1
  33. package/lib/external-db-migrate.js +229 -139
  34. package/lib/external-db.js +25 -15
  35. package/lib/file-upload.js +52 -7
  36. package/lib/framework-error.js +14 -1
  37. package/lib/framework-files.js +73 -0
  38. package/lib/framework-schema.js +695 -394
  39. package/lib/gate-contract.js +649 -1
  40. package/lib/guard-agent-registry.js +26 -44
  41. package/lib/guard-all.js +1 -0
  42. package/lib/guard-auth.js +42 -112
  43. package/lib/guard-cidr.js +33 -154
  44. package/lib/guard-csv.js +46 -113
  45. package/lib/guard-domain.js +34 -157
  46. package/lib/guard-dsn.js +27 -43
  47. package/lib/guard-email.js +47 -69
  48. package/lib/guard-envelope.js +19 -32
  49. package/lib/guard-event-bus-payload.js +24 -42
  50. package/lib/guard-event-bus-topic.js +25 -43
  51. package/lib/guard-filename.js +42 -106
  52. package/lib/guard-graphql.js +42 -123
  53. package/lib/guard-html.js +53 -108
  54. package/lib/guard-idempotency-key.js +24 -42
  55. package/lib/guard-image.js +46 -103
  56. package/lib/guard-imap-command.js +18 -32
  57. package/lib/guard-jmap.js +16 -30
  58. package/lib/guard-json.js +38 -108
  59. package/lib/guard-jsonpath.js +38 -171
  60. package/lib/guard-jwt.js +49 -179
  61. package/lib/guard-list-id.js +25 -41
  62. package/lib/guard-list-unsubscribe.js +27 -43
  63. package/lib/guard-mail-compose.js +24 -42
  64. package/lib/guard-mail-move.js +26 -44
  65. package/lib/guard-mail-query.js +28 -46
  66. package/lib/guard-mail-reply.js +24 -42
  67. package/lib/guard-mail-sieve.js +24 -42
  68. package/lib/guard-managesieve-command.js +17 -31
  69. package/lib/guard-markdown.js +37 -104
  70. package/lib/guard-message-id.js +26 -45
  71. package/lib/guard-mime.js +39 -151
  72. package/lib/guard-oauth.js +54 -135
  73. package/lib/guard-pdf.js +45 -101
  74. package/lib/guard-pop3-command.js +21 -31
  75. package/lib/guard-posture-chain.js +24 -42
  76. package/lib/guard-regex.js +33 -107
  77. package/lib/guard-saga-config.js +24 -42
  78. package/lib/guard-shell.js +42 -172
  79. package/lib/guard-smtp-command.js +48 -54
  80. package/lib/guard-snapshot-envelope.js +24 -42
  81. package/lib/guard-sql.js +1491 -0
  82. package/lib/guard-stream-args.js +24 -43
  83. package/lib/guard-svg.js +47 -65
  84. package/lib/guard-template.js +35 -172
  85. package/lib/guard-tenant-id.js +26 -45
  86. package/lib/guard-time.js +32 -154
  87. package/lib/guard-trace-context.js +25 -44
  88. package/lib/guard-uuid.js +32 -153
  89. package/lib/guard-xml.js +38 -113
  90. package/lib/guard-yaml.js +51 -163
  91. package/lib/http-client.js +37 -9
  92. package/lib/inbox.js +120 -107
  93. package/lib/legal-hold.js +107 -50
  94. package/lib/log-stream-cloudwatch.js +47 -31
  95. package/lib/log-stream-otlp.js +32 -18
  96. package/lib/mail-crypto-smime.js +2 -6
  97. package/lib/mail-greylist.js +2 -6
  98. package/lib/mail-helo.js +2 -6
  99. package/lib/mail-journal.js +85 -64
  100. package/lib/mail-rbl.js +2 -6
  101. package/lib/mail-scan.js +2 -6
  102. package/lib/mail-server-jmap.js +117 -12
  103. package/lib/mail-spam-score.js +2 -6
  104. package/lib/mail-store.js +287 -154
  105. package/lib/middleware/body-parser.js +71 -25
  106. package/lib/middleware/csrf-protect.js +19 -8
  107. package/lib/middleware/fetch-metadata.js +17 -7
  108. package/lib/middleware/idempotency-key.js +54 -38
  109. package/lib/middleware/rate-limit.js +102 -32
  110. package/lib/middleware/security-headers.js +21 -5
  111. package/lib/migrations.js +108 -66
  112. package/lib/network-heartbeat.js +7 -0
  113. package/lib/nonce-store.js +31 -9
  114. package/lib/object-store/azure-blob-bucket-ops.js +9 -4
  115. package/lib/object-store/azure-blob.js +57 -3
  116. package/lib/object-store/sigv4.js +10 -0
  117. package/lib/observability.js +87 -0
  118. package/lib/otel-export.js +25 -1
  119. package/lib/outbox.js +136 -82
  120. package/lib/parsers/safe-xml.js +47 -7
  121. package/lib/pqc-agent.js +44 -0
  122. package/lib/pubsub-cluster.js +42 -20
  123. package/lib/queue-local.js +202 -139
  124. package/lib/queue-redis.js +9 -1
  125. package/lib/queue-sqs.js +6 -0
  126. package/lib/redact.js +68 -11
  127. package/lib/redis-client.js +160 -31
  128. package/lib/retention.js +82 -39
  129. package/lib/router.js +212 -5
  130. package/lib/safe-dns.js +29 -45
  131. package/lib/safe-ical.js +18 -33
  132. package/lib/safe-icap.js +27 -43
  133. package/lib/safe-sieve.js +21 -40
  134. package/lib/safe-sql.js +124 -3
  135. package/lib/safe-vcard.js +18 -33
  136. package/lib/scheduler.js +35 -12
  137. package/lib/seeders.js +122 -74
  138. package/lib/session-stores.js +42 -14
  139. package/lib/session.js +109 -72
  140. package/lib/sql.js +3885 -0
  141. package/lib/ssrf-guard.js +51 -4
  142. package/lib/static.js +177 -34
  143. package/lib/subject.js +55 -17
  144. package/lib/vault/index.js +3 -2
  145. package/lib/vault/passphrase-ops.js +3 -2
  146. package/lib/vault/rotate.js +104 -64
  147. package/lib/vendor-data.js +2 -0
  148. package/lib/websocket.js +35 -5
  149. package/package.json +1 -1
  150. package/sbom.cdx.json +6 -6
@@ -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 safeSql = require("../safe-sql");
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.prepare(
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
- return db.prepare("PRAGMA table_info(\"" + table.replace(/"/g, '""') + "\")").all()
111
- .map(function (c) { return c.name; });
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 quotedCols = unknown.map(function (n) { return '"' + n.replace(/"/g, '""') + '"'; }).join(", ");
205
- var sampleSql = "SELECT " + quotedCols +
206
- " FROM \"" + table.replace(/"/g, '""') + "\" LIMIT " + sampleLimit;
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.prepare(sampleSql).all();
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.prepare('SELECT COUNT(*) AS n FROM "' + table.replace(/"/g, '""') + '"').get();
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
- var sampled = db.prepare(
325
- 'SELECT * FROM "' + table.replace(/"/g, '""') + '" ORDER BY RANDOM() LIMIT ?'
326
- ).all(sampleN);
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
- function _runStmt(db, sql) { db.prepare(sql).run(); }
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
- // Identifiers reach SQL through safeSql.quoteIdentifier runs
536
- // validateIdentifier (rejects bad shape / reserved words /
537
- // sqlite_-prefix) + emits the dialect-correct quoted form.
538
- var qt = safeSql.quoteIdentifier(table, "sqlite");
539
- var qc = safeSql.quoteIdentifier(column, "sqlite");
540
- var total = db.prepare("SELECT COUNT(*) AS n FROM " + qt + " WHERE " + qc + " IS NOT NULL").get().n;
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
- var sel = db.prepare(
554
- "SELECT _id, " + qc + " AS v" + (qrid ? ", " + qrid + " AS rid" : "") + " FROM " + qt +
555
- " WHERE " + qc + " IS NOT NULL AND _id > ? ORDER BY _id LIMIT ?"
556
- );
557
- var upd = db.prepare("UPDATE " + qt + " SET " + qc + " = ? WHERE _id = ?");
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, batchSize);
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
- if (typeof row.v !== "string") continue;
569
- if (aadMode && vaultAad.isAadSealed(row.v)) {
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 (rid, or _id when rowIdField IS _id).
609
+ // selected (row[rowIdField], or _id when rowIdField IS _id).
573
610
  var rowForAad = {};
574
- rowForAad[rowIdField] = needRid ? row.rid : row._id;
611
+ rowForAad[rowIdField] = needRid ? row[rowIdField] : row._id;
575
612
  var aad = cryptoField._aadParts(schema, table, column, rowForAad);
576
- upd.run(vaultAad.resealRoot(row.v, aad, roots.oldRootJson, roots.newRootJson), row._id);
577
- } else if (row.v.indexOf(C.VAULT_PREFIX) === 0) {
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(row.v, roots.oldKeys, roots.newKeys), row._id);
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 qt = '"' + table.replace(/"/g, '""') + '"';
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.prepare("SELECT COUNT(*) AS n FROM " + qt + " WHERE data IS NOT NULL").get().n;
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
- var sel = db.prepare(
600
- "SELECT _id, data FROM " + qt +
601
- " WHERE data IS NOT NULL AND _id > ? ORDER BY _id LIMIT ?"
602
- );
603
- var upd = db.prepare("UPDATE " + qt + " SET data = ? WHERE _id = ?");
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, batchSize);
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: "db.enc",
693
- dbKeySealed: "db.key.enc",
694
- vaultKeyPlain: "vault.key",
695
- vaultKeySealed: "vault.key.sealed",
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
- _runStmt(db, "PRAGMA journal_mode=WAL");
853
- _runStmt(db, "PRAGMA synchronous=NORMAL");
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.prepare(
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.prepare(
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.prepare(
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
- _runStmt(db, "PRAGMA wal_checkpoint(TRUNCATE)");
937
+ db.prepare(sql.pragma("wal_checkpoint", "TRUNCATE").sql).run();
898
938
  } finally {
899
939
  db.close();
900
940
  }
@@ -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
@@ -530,22 +530,32 @@ function _parseExtensionHeader(header) {
530
530
  for (var i = 0; i < entries.length; i++) {
531
531
  var parts = structuredFields.splitTopLevel(entries[i], ";").map(function (s) { return s.trim(); });
532
532
  if (!parts[0]) continue;
533
- // `params` has no prototype chain `Object.create(null)` defends
534
- // against `__proto__` / `constructor` / `prototype` parameter names
535
- // in the Sec-WebSocket-Extensions header polluting downstream lookups.
536
- var ext = { name: parts[0].toLowerCase(), params: Object.create(null) };
533
+ // Collect [name, value] pairs, then materialize the params map via
534
+ // Object.fromEntries onto a null-prototype object. The extension-
535
+ // parameter name is taken from the client-supplied Sec-WebSocket-
536
+ // Extensions header, so it is never used as a computed-write key
537
+ // (`params[name] = value`) — that is the CWE-915 unsafe-reflection /
538
+ // CWE-1321 prototype-pollution sink. POISONED params (`__proto__` /
539
+ // `constructor` / `prototype`) are dropped, and the null-prototype
540
+ // accumulator means even a slipped name cannot reach Object.prototype.
541
+ var paramPairs = [];
537
542
  for (var j = 1; j < parts.length; j++) {
538
543
  var kv = parts[j].split("=");
539
544
  var k = kv[0].trim().toLowerCase();
540
545
  if (!k) continue;
546
+ if (k === "__proto__" || k === "constructor" || k === "prototype") continue;
541
547
  var v = kv.length > 1 ? kv.slice(1).join("=").trim() : true;
542
548
  // Strip surrounding quotes per the token-or-quoted-string grammar.
543
549
  if (typeof v === "string") {
544
550
  var _unq = structuredFields.unquoteSfString(v);
545
551
  if (_unq !== null) v = _unq;
546
552
  }
547
- ext.params[k] = v;
553
+ paramPairs.push([k, v]);
548
554
  }
555
+ var ext = {
556
+ name: parts[0].toLowerCase(),
557
+ params: Object.assign(Object.create(null), Object.fromEntries(paramPairs)),
558
+ };
549
559
  out.push(ext);
550
560
  }
551
561
  return out;
@@ -961,6 +971,22 @@ class WebSocketConnection extends EventEmitter {
961
971
  self._transitionToClosed(1006, "abnormal closure", false, null);
962
972
  }
963
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
+ });
964
990
  }
965
991
 
966
992
  // Single state-transition method. Idempotent — repeat calls after
@@ -1541,6 +1567,10 @@ module.exports = {
1541
1567
  // Server-side entrypoints
1542
1568
  handleUpgrade: handleUpgrade, // h1 — RFC 6455 HTTP upgrade
1543
1569
  handleExtendedConnect: handleExtendedConnect, // h2 — RFC 8441 Extended CONNECT
1570
+ // Internal helper exposed for tests — the Sec-WebSocket-Extensions
1571
+ // parser (RFC 7692 negotiation feeds off this). Underscore-prefixed so
1572
+ // it is not part of the public primitive surface.
1573
+ _parseExtensionHeader: _parseExtensionHeader,
1544
1574
  // Constants
1545
1575
  GUID: GUID,
1546
1576
  REFUSED_AUTH_QUERY_PARAMS: REFUSED_AUTH_QUERY_PARAMS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.14.26",
3
+ "version": "0.15.0",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
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:e0214cf9-5d77-475a-af9a-e3cedff9f6d7",
5
+ "serialNumber": "urn:uuid:d63a172d-d92b-4a98-ae64-7dcb04e82076",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-06-06T21:14:16.419Z",
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.14.26",
22
+ "bom-ref": "@blamejs/core@0.15.0",
23
23
  "type": "application",
24
24
  "name": "blamejs",
25
- "version": "0.14.26",
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.14.26",
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.14.26",
57
+ "ref": "@blamejs/core@0.15.0",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]