@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
package/lib/cache.js CHANGED
@@ -74,15 +74,38 @@ var cacheRedis = require("./cache-redis");
74
74
  var redisClient = require("./redis-client");
75
75
  var clusterStorage = require("./cluster-storage");
76
76
  var C = require("./constants");
77
+ var frameworkSchema = require("./framework-schema");
77
78
  var lazyRequire = require("./lazy-require");
78
79
  var { boot } = require("./log");
79
80
  var numericChecks = require("./numeric-checks");
80
81
  var requestHelpers = require("./request-helpers");
81
82
  var safeAsync = require("./safe-async");
82
83
  var safeJson = require("./safe-json");
84
+ var sql = require("./sql");
83
85
  var validateOpts = require("./validate-opts");
84
86
  var { CacheError } = require("./framework-error");
85
87
 
88
+ // Cluster-backend tables — resolved through frameworkSchema.tableName so a
89
+ // configured table prefix (b.frameworkSchema.setTablePrefix) is honored.
90
+ // Both names are identity-mapped in LOCAL_TO_EXTERNAL, so clusterStorage's
91
+ // resolveTables leaves them untouched at dispatch and the resolved name is
92
+ // what reaches the backend on both single-node + cluster sides.
93
+ var CACHE_TABLE = "_blamejs_cache"; // allow:hand-rolled-sql — canonical logical table-name declaration
94
+ var CACHE_TAGS_TABLE = "_blamejs_cache_tags"; // allow:hand-rolled-sql — canonical logical table-name declaration
95
+ function _cacheSqlTable() { return frameworkSchema.tableName(CACHE_TABLE); }
96
+ function _cacheTagsSqlTable() { return frameworkSchema.tableName(CACHE_TAGS_TABLE); }
97
+ // b.sql opts for every cluster-backend statement: thread the ACTIVE
98
+ // backend dialect (clusterStorage.dialect() — "sqlite" single-node,
99
+ // "postgres" | "mysql" in cluster mode) so the emitted identifier quoting
100
+ // and dialect idioms (ON CONFLICT vs ON DUPLICATE KEY) match the backend
101
+ // the SQL dispatches to. Defaulting to "sqlite" works on Postgres only by
102
+ // accident (both double-quote identifiers) and emits the wrong quoting on
103
+ // MySQL, so this is the canonical resolver the data-layer threads into
104
+ // b.sql. clusterStorage.execute still rewrites table names + translates
105
+ // `?` placeholders at dispatch; this controls only the builder-side
106
+ // quoting + idiom selection.
107
+ function _cacheSqlOpts() { return { dialect: clusterStorage.dialect() }; }
108
+
86
109
  var log = boot("cache");
87
110
  var observability = lazyRequire(function () { return require("./observability"); });
88
111
  // Opt-in vault seal for cluster-backend cache values. Lazy so
@@ -471,19 +494,21 @@ function _clusterBackend(cfg) {
471
494
 
472
495
  async function get(key) {
473
496
  var now = clock();
474
- var result = await clusterStorage.execute(
475
- "SELECT valueJson, expiresAt FROM _blamejs_cache WHERE cacheKey = ?",
476
- [_composedKey(key)]
477
- );
497
+ var getBuilt = sql.select(_cacheSqlTable(), _cacheSqlOpts())
498
+ .columns(["valueJson", "expiresAt"])
499
+ .where("cacheKey", _composedKey(key))
500
+ .toSql();
501
+ var result = await clusterStorage.execute(getBuilt.sql, getBuilt.params);
478
502
  if (!result || !result.rows || result.rows.length === 0) return undefined;
479
503
  var row = result.rows[0];
480
504
  if (row.expiresAt <= now) {
481
505
  // Lazy purge: opportunistic delete on stale read.
482
506
  try {
483
- await clusterStorage.execute(
484
- "DELETE FROM _blamejs_cache WHERE cacheKey = ? AND expiresAt <= ?",
485
- [_composedKey(key), now]
486
- );
507
+ var delBuilt = sql.delete(_cacheSqlTable(), _cacheSqlOpts())
508
+ .where("cacheKey", _composedKey(key))
509
+ .where("expiresAt", "<=", now)
510
+ .toSql();
511
+ await clusterStorage.execute(delBuilt.sql, delBuilt.params);
487
512
  } catch (_e) { /* sweeper will catch it next pass */ }
488
513
  emitObs("cache.eviction.expired", { namespace: namespace });
489
514
  return undefined;
@@ -494,11 +519,13 @@ function _clusterBackend(cfg) {
494
519
  // Fire-and-forget — best-effort lifetime extension.
495
520
  if (slidingTtl && defaultTtlMs !== Infinity && typeof defaultTtlMs === "number") {
496
521
  var newExpires = now + defaultTtlMs;
497
- clusterStorage.execute(
498
- "UPDATE _blamejs_cache SET expiresAt = ?, updatedAt = ? " +
499
- "WHERE cacheKey = ? AND expiresAt > ?",
500
- [newExpires, now, _composedKey(key), now]
501
- ).catch(function () { /* best-effort */ });
522
+ var slideBuilt = sql.update(_cacheSqlTable(), _cacheSqlOpts())
523
+ .set({ expiresAt: newExpires, updatedAt: now })
524
+ .where("cacheKey", _composedKey(key))
525
+ .where("expiresAt", ">", now)
526
+ .toSql();
527
+ clusterStorage.execute(slideBuilt.sql, slideBuilt.params)
528
+ .catch(function () { /* best-effort */ });
502
529
  }
503
530
  var stored = row.valueJson;
504
531
  // Sealed-row decode. Sealed entries are prefixed at write
@@ -541,28 +568,30 @@ function _clusterBackend(cfg) {
541
568
  // Wrapping them in a transaction makes a concurrent set see either the
542
569
  // whole prior state or the whole new state, never a mix.
543
570
  // SQLite + Postgres both honor ON CONFLICT (cacheKey) DO UPDATE.
571
+ var upsertBuilt = sql.upsert(_cacheSqlTable(), _cacheSqlOpts())
572
+ .columns(["cacheKey", "valueJson", "expiresAt", "updatedAt"])
573
+ .values({ cacheKey: ck, valueJson: json, expiresAt: storedExpires, updatedAt: now })
574
+ .onConflict(["cacheKey"])
575
+ .doUpdate({ valueJson: "?", expiresAt: "?", updatedAt: "?" }, [json, storedExpires, now])
576
+ .toSql();
577
+ var tagsDelBuilt = sql.delete(_cacheTagsSqlTable(), _cacheSqlOpts())
578
+ .where("cacheKey", ck)
579
+ .toSql();
544
580
  await clusterStorage.transaction(async function (tx) {
545
- await tx.execute(
546
- "INSERT INTO _blamejs_cache (cacheKey, valueJson, expiresAt, updatedAt) " +
547
- "VALUES (?, ?, ?, ?) " +
548
- "ON CONFLICT (cacheKey) DO UPDATE SET " +
549
- "valueJson = ?, expiresAt = ?, updatedAt = ?",
550
- [ck, json, storedExpires, now, json, storedExpires, now]
551
- );
581
+ await tx.execute(upsertBuilt.sql, upsertBuilt.params);
552
582
  // Drop any prior tags for this key (tags can change across sets),
553
583
  // then INSERT the new ones. The PRIMARY KEY on (cacheKey, tag) makes
554
584
  // the INSERT idempotent if duplicate tags sneak in.
555
- await tx.execute(
556
- "DELETE FROM _blamejs_cache_tags WHERE cacheKey = ?",
557
- [ck]
558
- );
585
+ await tx.execute(tagsDelBuilt.sql, tagsDelBuilt.params);
559
586
  if (tags && tags.length > 0) {
560
587
  for (var i = 0; i < tags.length; i++) {
561
- await tx.execute(
562
- "INSERT INTO _blamejs_cache_tags (cacheKey, tag) VALUES (?, ?) " +
563
- "ON CONFLICT (cacheKey, tag) DO NOTHING",
564
- [ck, tags[i]]
565
- );
588
+ var tagInsBuilt = sql.upsert(_cacheTagsSqlTable(), _cacheSqlOpts())
589
+ .columns(["cacheKey", "tag"])
590
+ .values({ cacheKey: ck, tag: tags[i] })
591
+ .onConflict(["cacheKey", "tag"])
592
+ .doNothing()
593
+ .toSql();
594
+ await tx.execute(tagInsBuilt.sql, tagInsBuilt.params);
566
595
  }
567
596
  }
568
597
  });
@@ -583,8 +612,11 @@ function _clusterBackend(cfg) {
583
612
  for (var attempt = 0; attempt < maxRetries; attempt++) {
584
613
  var outcome = await clusterStorage.transaction(async function (tx) {
585
614
  var now = clock();
586
- var row = await tx.executeOne(
587
- "SELECT valueJson, expiresAt FROM _blamejs_cache WHERE cacheKey = ?", [ck]);
615
+ var rowSelBuilt = sql.select(_cacheSqlTable(), _cacheSqlOpts())
616
+ .columns(["valueJson", "expiresAt"])
617
+ .where("cacheKey", ck)
618
+ .toSql();
619
+ var row = await tx.executeOne(rowSelBuilt.sql, rowSelBuilt.params);
588
620
  var oldRaw = null;
589
621
  var current = null;
590
622
  if (row && row.expiresAt > now) {
@@ -599,8 +631,15 @@ function _clusterBackend(cfg) {
599
631
  if (decision && decision.abort !== undefined) return { aborted: decision.abort };
600
632
  if (decision && decision.delete === true) {
601
633
  if (oldRaw !== null) {
602
- await tx.execute("DELETE FROM _blamejs_cache WHERE cacheKey = ? AND valueJson = ?", [ck, oldRaw]);
603
- await tx.execute("DELETE FROM _blamejs_cache_tags WHERE cacheKey = ?", [ck]);
634
+ var casDelBuilt = sql.delete(_cacheSqlTable(), _cacheSqlOpts())
635
+ .where("cacheKey", ck)
636
+ .where("valueJson", oldRaw)
637
+ .toSql();
638
+ await tx.execute(casDelBuilt.sql, casDelBuilt.params);
639
+ var casTagDelBuilt = sql.delete(_cacheTagsSqlTable(), _cacheSqlOpts())
640
+ .where("cacheKey", ck)
641
+ .toSql();
642
+ await tx.execute(casTagDelBuilt.sql, casTagDelBuilt.params);
604
643
  }
605
644
  return { updated: true, deleted: true };
606
645
  }
@@ -614,17 +653,22 @@ function _clusterBackend(cfg) {
614
653
  if (oldRaw === null) {
615
654
  // Row was absent/expired — insert, but lose the race if another
616
655
  // writer inserted concurrently (ON CONFLICT DO NOTHING → 0 rows).
617
- var ins = await tx.execute(
618
- "INSERT INTO _blamejs_cache (cacheKey, valueJson, expiresAt, updatedAt) " +
619
- "VALUES (?, ?, ?, ?) ON CONFLICT (cacheKey) DO NOTHING",
620
- [ck, json, storedExpires, now]);
656
+ var insBuilt = sql.upsert(_cacheSqlTable(), _cacheSqlOpts())
657
+ .columns(["cacheKey", "valueJson", "expiresAt", "updatedAt"])
658
+ .values({ cacheKey: ck, valueJson: json, expiresAt: storedExpires, updatedAt: now })
659
+ .onConflict(["cacheKey"])
660
+ .doNothing()
661
+ .toSql();
662
+ var ins = await tx.execute(insBuilt.sql, insBuilt.params);
621
663
  if (!ins || ins.rowCount !== 1) return { conflict: true };
622
664
  } else {
623
665
  // CAS: only commit if the row still holds the exact bytes we read.
624
- var upd = await tx.execute(
625
- "UPDATE _blamejs_cache SET valueJson = ?, expiresAt = ?, updatedAt = ? " +
626
- "WHERE cacheKey = ? AND valueJson = ?",
627
- [json, storedExpires, now, ck, oldRaw]);
666
+ var updBuilt = sql.update(_cacheSqlTable(), _cacheSqlOpts())
667
+ .set({ valueJson: json, expiresAt: storedExpires, updatedAt: now })
668
+ .where("cacheKey", ck)
669
+ .where("valueJson", oldRaw)
670
+ .toSql();
671
+ var upd = await tx.execute(updBuilt.sql, updBuilt.params);
628
672
  if (!upd || upd.rowCount !== 1) return { conflict: true };
629
673
  }
630
674
  return { updated: true, value: decision.value };
@@ -638,59 +682,67 @@ function _clusterBackend(cfg) {
638
682
 
639
683
  async function del(key) {
640
684
  var ck = _composedKey(key);
641
- var result = await clusterStorage.execute(
642
- "DELETE FROM _blamejs_cache WHERE cacheKey = ?",
643
- [ck]
644
- );
685
+ var delBuilt = sql.delete(_cacheSqlTable(), _cacheSqlOpts())
686
+ .where("cacheKey", ck)
687
+ .toSql();
688
+ var result = await clusterStorage.execute(delBuilt.sql, delBuilt.params);
645
689
  // Drop any matching tag rows. Best-effort: a stale tag row pointing
646
690
  // at a non-existent cacheKey is dropped on the next invalidateTag
647
691
  // sweep (by the JOIN-shape DELETE) anyway.
648
- await clusterStorage.execute(
649
- "DELETE FROM _blamejs_cache_tags WHERE cacheKey = ?",
650
- [ck]
651
- ).catch(function () { /* best-effort */ });
692
+ var tagDelBuilt = sql.delete(_cacheTagsSqlTable(), _cacheSqlOpts())
693
+ .where("cacheKey", ck)
694
+ .toSql();
695
+ await clusterStorage.execute(tagDelBuilt.sql, tagDelBuilt.params)
696
+ .catch(function () { /* best-effort */ });
652
697
  return !!(result && result.rowCount && result.rowCount > 0);
653
698
  }
654
699
 
655
700
  async function invalidateTag(tag) {
656
- // Find every cacheKey carrying the tag (namespace-scoped via the LIKE
657
- // on the composed key), delete from the cache table + the junction.
658
- var like = namespace + ":%";
659
- var keysResult = await clusterStorage.execute(
660
- "SELECT cacheKey FROM _blamejs_cache_tags WHERE tag = ? AND cacheKey LIKE ?",
661
- [tag, like]
662
- );
701
+ // Find every cacheKey carrying the tag (namespace-scoped via a prefix
702
+ // LIKE on the composed "<namespace>:<key>" form), delete from the
703
+ // cache table + the junction. whereLike(..., "prefix") escapes the
704
+ // namespace's own metacharacters and appends the live wildcard, so the
705
+ // scope stays "this namespace's keys" without a hand-rolled LIKE.
706
+ var prefix = namespace + ":";
707
+ var keysBuilt = sql.select(_cacheTagsSqlTable(), _cacheSqlOpts())
708
+ .columns(["cacheKey"])
709
+ .where("tag", tag)
710
+ .whereLike("cacheKey", prefix, "prefix")
711
+ .toSql();
712
+ var keysResult = await clusterStorage.execute(keysBuilt.sql, keysBuilt.params);
663
713
  var keys = (keysResult && keysResult.rows) || [];
664
714
  if (keys.length === 0) {
665
715
  // Nothing to invalidate; still drop any orphan tag rows for
666
716
  // this tag scoped to our namespace.
667
- await clusterStorage.execute(
668
- "DELETE FROM _blamejs_cache_tags WHERE tag = ? AND cacheKey LIKE ?",
669
- [tag, like]
670
- );
717
+ var orphanBuilt = sql.delete(_cacheTagsSqlTable(), _cacheSqlOpts())
718
+ .where("tag", tag)
719
+ .whereLike("cacheKey", prefix, "prefix")
720
+ .toSql();
721
+ await clusterStorage.execute(orphanBuilt.sql, orphanBuilt.params);
671
722
  return 0;
672
723
  }
673
724
  var purged = 0;
674
725
  for (var i = 0; i < keys.length; i++) {
675
726
  var ck = keys[i].cacheKey;
676
- var r = await clusterStorage.execute(
677
- "DELETE FROM _blamejs_cache WHERE cacheKey = ?",
678
- [ck]
679
- );
727
+ var rBuilt = sql.delete(_cacheSqlTable(), _cacheSqlOpts())
728
+ .where("cacheKey", ck)
729
+ .toSql();
730
+ var r = await clusterStorage.execute(rBuilt.sql, rBuilt.params);
680
731
  if (r && r.rowCount > 0) purged += r.rowCount;
681
- await clusterStorage.execute(
682
- "DELETE FROM _blamejs_cache_tags WHERE cacheKey = ?",
683
- [ck]
684
- );
732
+ var tagDelBuilt = sql.delete(_cacheTagsSqlTable(), _cacheSqlOpts())
733
+ .where("cacheKey", ck)
734
+ .toSql();
735
+ await clusterStorage.execute(tagDelBuilt.sql, tagDelBuilt.params);
685
736
  }
686
737
  return purged;
687
738
  }
688
739
 
689
740
  async function getTags(key) {
690
- var result = await clusterStorage.execute(
691
- "SELECT tag FROM _blamejs_cache_tags WHERE cacheKey = ?",
692
- [_composedKey(key)]
693
- );
741
+ var built = sql.select(_cacheTagsSqlTable(), _cacheSqlOpts())
742
+ .columns(["tag"])
743
+ .where("cacheKey", _composedKey(key))
744
+ .toSql();
745
+ var result = await clusterStorage.execute(built.sql, built.params);
694
746
  if (!result || !result.rows) return [];
695
747
  return result.rows.map(function (r) { return r.tag; });
696
748
  }
@@ -700,60 +752,77 @@ function _clusterBackend(cfg) {
700
752
  // track LRU at all, so "without bumping" is automatic. Honors
701
753
  // expiresAt the same as get().
702
754
  var now = clock();
703
- var result = await clusterStorage.execute(
704
- "SELECT expiresAt FROM _blamejs_cache WHERE cacheKey = ? AND expiresAt > ?",
705
- [_composedKey(key), now]
706
- );
755
+ var built = sql.select(_cacheSqlTable(), _cacheSqlOpts())
756
+ .columns(["expiresAt"])
757
+ .where("cacheKey", _composedKey(key))
758
+ .where("expiresAt", ">", now)
759
+ .toSql();
760
+ var result = await clusterStorage.execute(built.sql, built.params);
707
761
  return !!(result && result.rows && result.rows.length > 0);
708
762
  }
709
763
 
710
764
  async function clear() {
711
765
  // Namespace-scoped wipe so two CacheInstance instances sharing the
712
- // table don't cross-purge each other.
713
- var like = namespace + ":%";
714
- var result = await clusterStorage.execute(
715
- "DELETE FROM _blamejs_cache WHERE cacheKey LIKE ?",
716
- [like]
717
- );
766
+ // table don't cross-purge each other. Prefix LIKE on the composed
767
+ // "<namespace>:<key>" form scopes the wipe to this instance.
768
+ var prefix = namespace + ":";
769
+ var clrBuilt = sql.delete(_cacheSqlTable(), _cacheSqlOpts())
770
+ .whereLike("cacheKey", prefix, "prefix")
771
+ .toSql();
772
+ var result = await clusterStorage.execute(clrBuilt.sql, clrBuilt.params);
718
773
  // Drop matching tag rows in the same namespace.
719
- await clusterStorage.execute(
720
- "DELETE FROM _blamejs_cache_tags WHERE cacheKey LIKE ?",
721
- [like]
722
- ).catch(function () { /* best-effort */ });
774
+ var tagClrBuilt = sql.delete(_cacheTagsSqlTable(), _cacheSqlOpts())
775
+ .whereLike("cacheKey", prefix, "prefix")
776
+ .toSql();
777
+ await clusterStorage.execute(tagClrBuilt.sql, tagClrBuilt.params)
778
+ .catch(function () { /* best-effort */ });
723
779
  return (result && result.rowCount) || 0;
724
780
  }
725
781
 
726
782
  async function size() {
727
783
  var now = clock();
728
- var like = namespace + ":%";
729
- var result = await clusterStorage.execute(
730
- "SELECT COUNT(*) AS n FROM _blamejs_cache WHERE cacheKey LIKE ? AND expiresAt > ?",
731
- [like, now]
732
- );
784
+ var prefix = namespace + ":";
785
+ var built = sql.select(_cacheSqlTable(), _cacheSqlOpts())
786
+ .count("*", "n")
787
+ .whereLike("cacheKey", prefix, "prefix")
788
+ .where("expiresAt", ">", now)
789
+ .toSql();
790
+ var result = await clusterStorage.execute(built.sql, built.params);
733
791
  if (!result || !result.rows || result.rows.length === 0) return 0;
734
- return result.rows[0].n || 0;
792
+ // COUNT(*) comes back as a BIGINT — node-postgres / mysql2 hand that
793
+ // back as a decimal STRING, and "5" || 0 stays the string "5". A caller
794
+ // comparing size() numerically (>= maxEntries, capacity math) would
795
+ // then compare a string and silently mis-decide. The COUNT alias `n`
796
+ // is not a framework-schema column, so coerceRows does not touch it —
797
+ // coerce it here at the read boundary.
798
+ var n = result.rows[0].n;
799
+ return Number(n) || 0;
735
800
  }
736
801
 
737
802
  async function _sweep() {
738
803
  var now = clock();
739
- var like = namespace + ":%";
804
+ var prefix = namespace + ":";
740
805
  // Capture the to-be-purged keys first so we can drop matching tag
741
806
  // rows in the same sweep — keeps the junction table free of orphans
742
807
  // pointing at expired cacheKeys.
743
- var expiredResult = await clusterStorage.execute(
744
- "SELECT cacheKey FROM _blamejs_cache WHERE cacheKey LIKE ? AND expiresAt <= ?",
745
- [like, now]
746
- );
808
+ var expiredBuilt = sql.select(_cacheSqlTable(), _cacheSqlOpts())
809
+ .columns(["cacheKey"])
810
+ .whereLike("cacheKey", prefix, "prefix")
811
+ .where("expiresAt", "<=", now)
812
+ .toSql();
813
+ var expiredResult = await clusterStorage.execute(expiredBuilt.sql, expiredBuilt.params);
747
814
  var expiredKeys = (expiredResult && expiredResult.rows) || [];
748
- await clusterStorage.execute(
749
- "DELETE FROM _blamejs_cache WHERE cacheKey LIKE ? AND expiresAt <= ?",
750
- [like, now]
751
- );
815
+ var sweepDelBuilt = sql.delete(_cacheSqlTable(), _cacheSqlOpts())
816
+ .whereLike("cacheKey", prefix, "prefix")
817
+ .where("expiresAt", "<=", now)
818
+ .toSql();
819
+ await clusterStorage.execute(sweepDelBuilt.sql, sweepDelBuilt.params);
752
820
  for (var i = 0; i < expiredKeys.length; i++) {
753
- await clusterStorage.execute(
754
- "DELETE FROM _blamejs_cache_tags WHERE cacheKey = ?",
755
- [expiredKeys[i].cacheKey]
756
- ).catch(function () { /* best-effort */ });
821
+ var tagSweepBuilt = sql.delete(_cacheTagsSqlTable(), _cacheSqlOpts())
822
+ .where("cacheKey", expiredKeys[i].cacheKey)
823
+ .toSql();
824
+ await clusterStorage.execute(tagSweepBuilt.sql, tagSweepBuilt.params)
825
+ .catch(function () { /* best-effort */ });
757
826
  }
758
827
  }
759
828
 
@@ -43,6 +43,7 @@ var cluster = require("./cluster");
43
43
  var clusterStorage = require("./cluster-storage");
44
44
  var safeAsync = require("./safe-async");
45
45
  var safeSql = require("./safe-sql");
46
+ var sql = require("./sql");
46
47
  var C = require("./constants");
47
48
  var { FrameworkError } = require("./framework-error");
48
49
 
@@ -54,6 +55,16 @@ var ALLOWED_CHAIN_TABLES = new Set(["audit_log", "consent_log"]);
54
55
 
55
56
  var FRAMEWORK_SQL_TIMEOUT_MS = C.TIME.seconds(30);
56
57
 
58
+ // b.sql opts for every chain-table statement: thread the ACTIVE backend
59
+ // dialect (clusterStorage.dialect() — "sqlite" single-node, "postgres" |
60
+ // "mysql" in cluster mode) so the emitted identifier quoting + dialect
61
+ // idioms match the backend the SQL dispatches to. Defaulting to "sqlite"
62
+ // works on Postgres only by accident (both double-quote identifiers) and
63
+ // emits the wrong quoting on MySQL. clusterStorage.execute still rewrites
64
+ // table names + translates `?` placeholders at dispatch; this controls only
65
+ // the builder-side quoting + idiom selection.
66
+ function _sqlOpts() { return { dialect: clusterStorage.dialect() }; }
67
+
57
68
  class ChainWriterError extends FrameworkError {
58
69
  constructor(message, code) {
59
70
  super(message);
@@ -140,11 +151,15 @@ function create(opts) {
140
151
  function _ensureCounterInit() {
141
152
  if (!_counterInit) {
142
153
  _counterInit = new safeAsync.Once(async function () {
154
+ // BARE logical table name — clusterStorage rewrites the framework
155
+ // name to the configured-prefix form and placeholderizes; b.sql
156
+ // quotes the camelCase column + emits the MAX aggregate.
157
+ var maxBuilt = sql.select(table, _sqlOpts())
158
+ .max("monotonicCounter", "m")
159
+ .toSql();
143
160
  var row = await safeAsync.withTimeout(
144
161
  safeAsync.asyncRetry(function () {
145
- return clusterStorage.executeOne(
146
- "SELECT MAX(monotonicCounter) AS m FROM " + safeSql.quoteIdentifier(table)
147
- );
162
+ return clusterStorage.executeOne(maxBuilt.sql, maxBuilt.params);
148
163
  }),
149
164
  FRAMEWORK_SQL_TIMEOUT_MS,
150
165
  { name: table + ".readMaxCounter" }
@@ -156,12 +171,14 @@ function create(opts) {
156
171
  }
157
172
 
158
173
  async function _readChainTipRow() {
174
+ var tipBuilt = sql.select(table, _sqlOpts())
175
+ .columns(["rowHash"])
176
+ .orderBy("monotonicCounter", "desc")
177
+ .limit(1)
178
+ .toSql();
159
179
  return await safeAsync.withTimeout(
160
180
  safeAsync.asyncRetry(function () {
161
- return clusterStorage.executeOne(
162
- "SELECT rowHash FROM " + safeSql.quoteIdentifier(table) +
163
- " ORDER BY monotonicCounter DESC LIMIT 1"
164
- );
181
+ return clusterStorage.executeOne(tipBuilt.sql, tipBuilt.params);
165
182
  }),
166
183
  FRAMEWORK_SQL_TIMEOUT_MS,
167
184
  { name: table + ".readChainTip" }
@@ -169,16 +186,21 @@ function create(opts) {
169
186
  }
170
187
 
171
188
  async function _insertRow(values) {
172
- // Build INSERT with quoted identifiers + ? placeholders. cluster-
173
- // storage handles dialect-specific placeholder translation.
174
- var quoted = columnsForInsert.map(function (c) { return safeSql.quoteIdentifier(c); }).join(", ");
175
- var placeholders = columnsForInsert.map(function () { return "?"; }).join(", ");
189
+ // b.sql INSERT: map each column (identifier-validated at create()) to
190
+ // its positional value and bind as a row object — the unambiguous form
191
+ // (a flat value array whose first element is a Buffer/object would be
192
+ // misread as an array-of-rows). BARE logical table name — clusterStorage
193
+ // rewrites + placeholderizes per dialect.
194
+ var rowObj = {};
195
+ for (var ci = 0; ci < columnsForInsert.length; ci++) {
196
+ rowObj[columnsForInsert[ci]] = values[ci];
197
+ }
198
+ var insBuilt = sql.insert(table, _sqlOpts())
199
+ .columns(columnsForInsert)
200
+ .values(rowObj)
201
+ .toSql();
176
202
  return await safeAsync.withTimeout(
177
- clusterStorage.execute(
178
- "INSERT INTO " + safeSql.quoteIdentifier(table) +
179
- " (" + quoted + ") VALUES (" + placeholders + ")",
180
- values
181
- ),
203
+ clusterStorage.execute(insBuilt.sql, insBuilt.params),
182
204
  FRAMEWORK_SQL_TIMEOUT_MS,
183
205
  { name: table + ".insertRow" }
184
206
  );
package/lib/cli.js CHANGED
@@ -2030,7 +2030,14 @@ async function _runErase(args, ctx) {
2030
2030
  }
2031
2031
  var row;
2032
2032
  try {
2033
- row = b.db.prepare("SELECT * FROM \"" + safeTable + "\" WHERE _id = ?").get(String(rowId));
2033
+ // Compose the lookup through b.sql so the identifier is quoted by
2034
+ // construction and the _id binds as a placeholder. quoteName: true
2035
+ // emits the local-sqlite `"table"` form (this runs against the
2036
+ // bootstrapped single-node b.db handle, no clusterStorage rewrite).
2037
+ var selBuilt = b.sql.select(safeTable, { quoteName: true })
2038
+ .where("_id", String(rowId)).toSql();
2039
+ var selStmt = b.db.prepare(selBuilt.sql);
2040
+ row = selStmt.get.apply(selStmt, selBuilt.params);
2034
2041
  } catch (e) {
2035
2042
  return report.error("row lookup failed: " + ((e && e.message) || String(e)));
2036
2043
  }
@@ -2044,20 +2051,18 @@ async function _runErase(args, ctx) {
2044
2051
  return report.error("table " + safeTable + " has no sealed columns or derived hashes; " +
2045
2052
  "use a regular DELETE for non-sealed rows");
2046
2053
  }
2047
- var setClauses = [];
2048
- var values = [];
2049
- for (var si = 0; si < sealedFields.length; si++) {
2050
- setClauses.push('"' + sealedFields[si] + '" = ?');
2051
- values.push(null);
2052
- }
2053
- for (var di = 0; di < derivedHashes.length; di++) {
2054
- setClauses.push('"' + derivedHashes[di] + '" = ?');
2055
- values.push(null);
2056
- }
2057
- values.push(String(rowId));
2054
+ // NULL every sealed column + derived hash. Build the SET map for
2055
+ // b.sql.update each column binds NULL as a placeholder, the
2056
+ // identifiers quote by construction, and the WHERE keeps the write
2057
+ // scoped to the single _id (b.sql refuses an unconditional update).
2058
+ var eraseSet = {};
2059
+ for (var si = 0; si < sealedFields.length; si++) eraseSet[sealedFields[si]] = null;
2060
+ for (var di = 0; di < derivedHashes.length; di++) eraseSet[derivedHashes[di]] = null;
2058
2061
  try {
2059
- var upd = b.db.prepare("UPDATE \"" + safeTable + "\" SET " + setClauses.join(", ") + " WHERE _id = ?");
2060
- upd.run.apply(upd, values);
2062
+ var updBuilt = b.sql.update(safeTable, { quoteName: true })
2063
+ .set(eraseSet).where("_id", String(rowId)).toSql();
2064
+ var upd = b.db.prepare(updBuilt.sql);
2065
+ upd.run.apply(upd, updBuilt.params);
2061
2066
  } catch (e) {
2062
2067
  return report.error("UPDATE failed: " + ((e && e.message) || String(e)));
2063
2068
  }