@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
@@ -63,9 +63,11 @@ var canonicalJson = require("./canonical-json");
63
63
  var auditSign = require("./audit-sign");
64
64
  var backupCrypto = require("./backup/crypto");
65
65
  var clusterStorage = require("./cluster-storage");
66
+ var frameworkFiles = require("./framework-files");
66
67
  var lazyRequire = require("./lazy-require");
67
68
  var validateOpts = require("./validate-opts");
68
69
  var safeJson = require("./safe-json");
70
+ var sql = require("./sql");
69
71
  var { defineClass } = require("./framework-error");
70
72
 
71
73
  var FRAMEWORK_VERSION = (pkg && pkg.version) || "unknown";
@@ -79,6 +81,17 @@ var audit = lazyRequire(function () { return require("./audit"); });
79
81
 
80
82
  var AuditToolsError = defineClass("AuditToolsError", { alwaysPermanent: true });
81
83
 
84
+ // b.sql opts for every framework-table statement: thread the ACTIVE backend
85
+ // dialect (clusterStorage.dialect() — "sqlite" single-node, "postgres" |
86
+ // "mysql" in cluster mode) so the emitted identifier quoting + dialect
87
+ // idioms (ON CONFLICT vs ON DUPLICATE KEY) match the backend the SQL
88
+ // dispatches to. Defaulting to "sqlite" works on Postgres only by accident
89
+ // (both double-quote identifiers) and emits the wrong quoting on MySQL.
90
+ // clusterStorage.execute still rewrites table names + translates `?`
91
+ // placeholders at dispatch; this controls only the builder-side quoting +
92
+ // idiom selection.
93
+ function _sqlOpts() { return { dialect: clusterStorage.dialect() }; }
94
+
82
95
  // Dual-control gate constants for the audit_log physical purge. The
83
96
  // purge erases signed audit history, so when an operator has declared
84
97
  // audit_log under b.db.declareRequireDualControl the deletion requires
@@ -274,42 +287,48 @@ function _verifyChainSlice(rows, startPrevHash) {
274
287
  // cluster-storage reader so the tooling works in both single-node and
275
288
  // cluster deployments without the caller knowing which mode is active.
276
289
  async function _defaultReadRows(criteria) {
277
- var sql = 'SELECT * FROM "audit_log"';
278
- var conds = [];
279
- var params = [];
280
- if (criteria.fromMs != null) { conds.push("recordedAt >= ?"); params.push(criteria.fromMs); }
281
- if (criteria.toMs != null) { conds.push("recordedAt <= ?"); params.push(criteria.toMs); }
282
- if (criteria.beforeMs != null) { conds.push("recordedAt < ?"); params.push(criteria.beforeMs); }
283
- if (criteria.action) { conds.push("action = ?"); params.push(criteria.action); }
284
- if (criteria.firstCounter != null) { conds.push("monotonicCounter >= ?"); params.push(criteria.firstCounter); }
285
- if (criteria.lastCounter != null) { conds.push("monotonicCounter <= ?"); params.push(criteria.lastCounter); }
286
- if (conds.length > 0) sql += " WHERE " + conds.join(" AND ");
287
- sql += " ORDER BY monotonicCounter ASC";
288
- return clusterStorage.executeAll(sql, params);
290
+ // Compose the criteria onto a b.sql SELECT with a BARE logical table name
291
+ // (clusterStorage rewrites the framework name + placeholderizes); b.sql
292
+ // quotes the camelCase columns + binds every value.
293
+ var qb = sql.select("audit_log", _sqlOpts());
294
+ if (criteria.fromMs != null) qb.whereOp("recordedAt", ">=", criteria.fromMs);
295
+ if (criteria.toMs != null) qb.whereOp("recordedAt", "<=", criteria.toMs);
296
+ if (criteria.beforeMs != null) qb.whereOp("recordedAt", "<", criteria.beforeMs);
297
+ if (criteria.action) qb.where("action", criteria.action);
298
+ if (criteria.firstCounter != null) qb.whereOp("monotonicCounter", ">=", criteria.firstCounter);
299
+ if (criteria.lastCounter != null) qb.whereOp("monotonicCounter", "<=", criteria.lastCounter);
300
+ qb.orderBy("monotonicCounter", "asc");
301
+ var built = qb.toSql();
302
+ return clusterStorage.executeAll(built.sql, built.params);
289
303
  }
290
304
 
291
305
  async function _defaultReadCoveringCheckpoint(lastCounter) {
292
- return clusterStorage.executeOne(
293
- "SELECT * FROM audit_checkpoints " +
294
- "WHERE atMonotonicCounter >= ? " +
295
- "ORDER BY atMonotonicCounter ASC LIMIT 1",
296
- [lastCounter]
297
- );
306
+ var built = sql.select("audit_checkpoints", _sqlOpts())
307
+ .whereOp("atMonotonicCounter", ">=", lastCounter)
308
+ .orderBy("atMonotonicCounter", "asc")
309
+ .limit(1)
310
+ .toSql();
311
+ return clusterStorage.executeOne(built.sql, built.params);
298
312
  }
299
313
 
300
314
  async function _defaultReadPredecessorRowHash(firstCounter) {
301
315
  if (firstCounter <= 1) return auditChain.ZERO_HASH;
302
- var row = await clusterStorage.executeOne(
303
- "SELECT rowHash FROM audit_log WHERE monotonicCounter = ?",
304
- [firstCounter - 1]
305
- );
316
+ var rowBuilt = sql.select("audit_log", _sqlOpts())
317
+ .columns(["rowHash"])
318
+ .where("monotonicCounter", firstCounter - 1)
319
+ .toSql();
320
+ var row = await clusterStorage.executeOne(rowBuilt.sql, rowBuilt.params);
306
321
  if (!row) {
307
322
  // First row of the slice is right after a purged range. Read the
308
- // purge anchor's lastRowHash instead.
309
- var anchor = await clusterStorage.executeOne(
310
- "SELECT lastPurgedRowHash, lastPurgedCounter FROM _blamejs_audit_purge_anchor " +
311
- "WHERE scope = 'audit'"
312
- );
323
+ // purge anchor's lastRowHash instead. The anchor is an external-only
324
+ // table whose LOGICAL name IS the `_blamejs_`-prefixed name (it maps
325
+ // to itself in LOCAL_TO_EXTERNAL); b.sql must receive it bare so
326
+ // clusterStorage rewrites it. allow:hand-rolled-sql — bare logical key.
327
+ var anchorBuilt = sql.select("_blamejs_audit_purge_anchor", _sqlOpts()) // allow:hand-rolled-sql
328
+ .columns(["lastPurgedRowHash", "lastPurgedCounter"])
329
+ .where("scope", "audit")
330
+ .toSql();
331
+ var anchor = await clusterStorage.executeOne(anchorBuilt.sql, anchorBuilt.params);
313
332
  if (anchor && Number(anchor.lastPurgedCounter) === firstCounter - 1) {
314
333
  return anchor.lastPurgedRowHash;
315
334
  }
@@ -343,7 +362,7 @@ async function _buildBundle(args) {
343
362
  return JSON.stringify(_rowToWireForm(r));
344
363
  }).join("\n") + "\n";
345
364
  var rowsEnc = await backupCrypto.encryptWithFreshSalt(jsonl, passphrase);
346
- files["rows.enc"] = rowsEnc.encrypted;
365
+ files[frameworkFiles.fileName("rowsEnc")] = rowsEnc.encrypted;
347
366
 
348
367
  // 2. (archive) Encrypt the checkpoint JSON
349
368
  var checkpointSalt = null;
@@ -351,7 +370,7 @@ async function _buildBundle(args) {
351
370
  if (checkpoint) {
352
371
  var ckptJson = _canonicalize(_rowToWireForm(checkpoint));
353
372
  var ckptEnc = await backupCrypto.encryptWithFreshSalt(ckptJson, passphrase);
354
- files["checkpoint.enc"] = ckptEnc.encrypted;
373
+ files[frameworkFiles.fileName("checkpointEnc")] = ckptEnc.encrypted;
355
374
  checkpointSalt = ckptEnc.salt;
356
375
  checkpointEncrypted = ckptEnc.encrypted;
357
376
  }
@@ -401,9 +420,9 @@ async function _writeBundle(args) {
401
420
  var built = await _buildBundle(args);
402
421
 
403
422
  atomicFile.ensureDir(outDir);
404
- atomicFile.writeSync(nodePath.join(outDir, "rows.enc"), built.files["rows.enc"], { fileMode: 0o600 });
405
- if (built.files["checkpoint.enc"]) {
406
- atomicFile.writeSync(nodePath.join(outDir, "checkpoint.enc"), built.files["checkpoint.enc"], { fileMode: 0o600 });
423
+ atomicFile.writeSync(nodePath.join(outDir, frameworkFiles.fileName("rowsEnc")), built.files[frameworkFiles.fileName("rowsEnc")], { fileMode: 0o600 });
424
+ if (built.files[frameworkFiles.fileName("checkpointEnc")]) {
425
+ atomicFile.writeSync(nodePath.join(outDir, frameworkFiles.fileName("checkpointEnc")), built.files[frameworkFiles.fileName("checkpointEnc")], { fileMode: 0o600 });
407
426
  }
408
427
  var manifestPath = nodePath.join(outDir, "manifest.json");
409
428
  atomicFile.writeSync(manifestPath, built.files["manifest.json"], { fileMode: 0o600 });
@@ -432,7 +451,7 @@ async function _readBundle(inDir, passphrase) {
432
451
  "manifest.kind must be one of " + Object.keys(VALID_KINDS).join(", "));
433
452
  }
434
453
 
435
- var rowsEncPath = nodePath.join(inDir, "rows.enc");
454
+ var rowsEncPath = nodePath.join(inDir, frameworkFiles.fileName("rowsEnc"));
436
455
  if (!nodeFs.existsSync(rowsEncPath)) {
437
456
  throw new AuditToolsError("audit-tools/no-rows-blob",
438
457
  "rows.enc missing in " + inDir);
@@ -450,7 +469,7 @@ async function _readBundle(inDir, passphrase) {
450
469
 
451
470
  var checkpoint = null;
452
471
  if (manifest.kind === KIND_ARCHIVE) {
453
- var ckptPath = nodePath.join(inDir, "checkpoint.enc");
472
+ var ckptPath = nodePath.join(inDir, frameworkFiles.fileName("checkpointEnc"));
454
473
  if (!nodeFs.existsSync(ckptPath)) {
455
474
  throw new AuditToolsError("audit-tools/no-checkpoint-blob",
456
475
  "checkpoint.enc missing in " + inDir + " (archive bundles must include the covering checkpoint)");
@@ -945,26 +964,35 @@ async function purge(opts) {
945
964
  }
946
965
 
947
966
  async function _defaultReadPurgeAnchor() {
948
- return clusterStorage.executeOne(
949
- "SELECT * FROM _blamejs_audit_purge_anchor WHERE scope = 'audit'"
950
- );
967
+ // External-only table — its logical name IS the `_blamejs_`-prefixed name
968
+ // (self-mapped in LOCAL_TO_EXTERNAL); b.sql receives it bare so
969
+ // clusterStorage rewrites it. allow:hand-rolled-sql — bare logical key.
970
+ var built = sql.select("_blamejs_audit_purge_anchor", _sqlOpts()) // allow:hand-rolled-sql
971
+ .where("scope", "audit")
972
+ .toSql();
973
+ return clusterStorage.executeOne(built.sql, built.params);
951
974
  }
952
975
 
953
976
  async function _defaultApplyPurge(args) {
954
977
  var del = await db().purgeAuditChain({ lastPurgedCounter: args.lastPurgedCounter });
955
- // UPSERT the single-row anchor. SQLite + Postgres both support
956
- // INSERT ... ON CONFLICT(scope) DO UPDATE.
957
- await clusterStorage.execute(
958
- "INSERT INTO _blamejs_audit_purge_anchor " +
959
- "(scope, lastPurgedCounter, lastPurgedRowHash, archiveBundleId, purgedAt) " +
960
- "VALUES ('audit', ?, ?, ?, ?) " +
961
- "ON CONFLICT(scope) DO UPDATE SET " +
962
- "lastPurgedCounter = excluded.lastPurgedCounter, " +
963
- "lastPurgedRowHash = excluded.lastPurgedRowHash, " +
964
- "archiveBundleId = excluded.archiveBundleId, " +
965
- "purgedAt = excluded.purgedAt",
966
- [args.lastPurgedCounter, args.lastPurgedRowHash, args.archiveBundleId, args.purgedAt]
967
- );
978
+ // UPSERT the single-row anchor via b.sql ON CONFLICT(scope) DO UPDATE
979
+ // (SQLite + Postgres). The anchor is external-only; its logical name IS
980
+ // the `_blamejs_`-prefixed name (self-mapped), passed bare so
981
+ // clusterStorage rewrites + placeholderizes. b.sql quotes the camelCase
982
+ // columns + binds 'audit'. allow:hand-rolled-sql — bare logical key.
983
+ var built = sql.upsert("_blamejs_audit_purge_anchor", _sqlOpts()) // allow:hand-rolled-sql
984
+ .columns(["scope", "lastPurgedCounter", "lastPurgedRowHash", "archiveBundleId", "purgedAt"])
985
+ .values({
986
+ scope: "audit",
987
+ lastPurgedCounter: args.lastPurgedCounter,
988
+ lastPurgedRowHash: args.lastPurgedRowHash,
989
+ archiveBundleId: args.archiveBundleId,
990
+ purgedAt: args.purgedAt,
991
+ })
992
+ .onConflict(["scope"])
993
+ .doUpdateFromExcluded(["lastPurgedCounter", "lastPurgedRowHash", "archiveBundleId", "purgedAt"])
994
+ .toSql();
995
+ await clusterStorage.execute(built.sql, built.params);
968
996
  return {
969
997
  rowsDeleted: del.rowsDeleted,
970
998
  checkpointsDeleted: del.checkpointsDeleted,
@@ -1047,7 +1075,7 @@ async function forensicSnapshot(opts) {
1047
1075
  reason: opts.reason,
1048
1076
  actor: opts.actor || null,
1049
1077
  composedAt: new Date().toISOString(),
1050
- auditSliceFile: returnBytes ? "rows.enc" : (sliceResult && sliceResult.manifestPath),
1078
+ auditSliceFile: returnBytes ? frameworkFiles.fileName("rowsEnc") : (sliceResult && sliceResult.manifestPath),
1051
1079
  auditSliceCount: sliceResult && sliceResult.rowCount,
1052
1080
  runtime: {
1053
1081
  nodeVersion: process.version,