@blamejs/core 0.14.27 → 0.15.1

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 (134) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +2 -2
  3. package/index.js +4 -0
  4. package/lib/ai-content-detect.js +9 -10
  5. package/lib/api-key.js +158 -77
  6. package/lib/atomic-file.js +29 -1
  7. package/lib/audit-chain.js +47 -11
  8. package/lib/audit-sign.js +77 -2
  9. package/lib/audit-tools.js +79 -51
  10. package/lib/audit.js +228 -100
  11. package/lib/backup/index.js +13 -10
  12. package/lib/break-glass.js +202 -144
  13. package/lib/cache.js +174 -105
  14. package/lib/chain-writer.js +38 -16
  15. package/lib/cli.js +19 -14
  16. package/lib/cluster-provider-db.js +130 -104
  17. package/lib/cluster-storage.js +119 -22
  18. package/lib/cluster.js +119 -71
  19. package/lib/compliance.js +22 -0
  20. package/lib/consent.js +82 -29
  21. package/lib/constants.js +16 -11
  22. package/lib/crypto-field.js +387 -91
  23. package/lib/db-declare-row-policy.js +35 -22
  24. package/lib/db-file-lifecycle.js +3 -2
  25. package/lib/db-query.js +517 -256
  26. package/lib/db-schema.js +209 -44
  27. package/lib/db.js +202 -95
  28. package/lib/external-db-migrate.js +229 -139
  29. package/lib/external-db.js +25 -15
  30. package/lib/framework-error.js +11 -0
  31. package/lib/framework-files.js +73 -0
  32. package/lib/framework-schema.js +695 -394
  33. package/lib/gate-contract.js +596 -1
  34. package/lib/guard-agent-registry.js +26 -44
  35. package/lib/guard-all.js +1 -0
  36. package/lib/guard-auth.js +42 -112
  37. package/lib/guard-cidr.js +33 -154
  38. package/lib/guard-csv.js +46 -113
  39. package/lib/guard-domain.js +34 -157
  40. package/lib/guard-dsn.js +27 -43
  41. package/lib/guard-email.js +47 -69
  42. package/lib/guard-envelope.js +19 -32
  43. package/lib/guard-event-bus-payload.js +24 -42
  44. package/lib/guard-event-bus-topic.js +25 -43
  45. package/lib/guard-filename.js +42 -106
  46. package/lib/guard-graphql.js +42 -123
  47. package/lib/guard-html.js +53 -108
  48. package/lib/guard-idempotency-key.js +24 -42
  49. package/lib/guard-image.js +46 -103
  50. package/lib/guard-imap-command.js +18 -32
  51. package/lib/guard-jmap.js +16 -30
  52. package/lib/guard-json.js +38 -108
  53. package/lib/guard-jsonpath.js +38 -171
  54. package/lib/guard-jwt.js +49 -179
  55. package/lib/guard-list-id.js +25 -41
  56. package/lib/guard-list-unsubscribe.js +27 -43
  57. package/lib/guard-mail-compose.js +24 -42
  58. package/lib/guard-mail-move.js +26 -44
  59. package/lib/guard-mail-query.js +28 -46
  60. package/lib/guard-mail-reply.js +24 -42
  61. package/lib/guard-mail-sieve.js +24 -42
  62. package/lib/guard-managesieve-command.js +17 -31
  63. package/lib/guard-markdown.js +37 -104
  64. package/lib/guard-message-id.js +26 -45
  65. package/lib/guard-mime.js +39 -151
  66. package/lib/guard-oauth.js +54 -135
  67. package/lib/guard-pdf.js +45 -101
  68. package/lib/guard-pop3-command.js +21 -31
  69. package/lib/guard-posture-chain.js +24 -42
  70. package/lib/guard-regex.js +33 -107
  71. package/lib/guard-saga-config.js +24 -42
  72. package/lib/guard-shell.js +42 -172
  73. package/lib/guard-smtp-command.js +48 -54
  74. package/lib/guard-snapshot-envelope.js +24 -42
  75. package/lib/guard-sql.js +1491 -0
  76. package/lib/guard-stream-args.js +24 -43
  77. package/lib/guard-svg.js +47 -65
  78. package/lib/guard-template.js +35 -172
  79. package/lib/guard-tenant-id.js +26 -45
  80. package/lib/guard-time.js +32 -154
  81. package/lib/guard-trace-context.js +25 -44
  82. package/lib/guard-uuid.js +32 -153
  83. package/lib/guard-xml.js +38 -113
  84. package/lib/guard-yaml.js +51 -163
  85. package/lib/http-client.js +14 -0
  86. package/lib/inbox.js +120 -107
  87. package/lib/legal-hold.js +107 -50
  88. package/lib/log-stream-cloudwatch.js +47 -31
  89. package/lib/log-stream-otlp.js +32 -18
  90. package/lib/mail-crypto-smime.js +2 -6
  91. package/lib/mail-greylist.js +2 -6
  92. package/lib/mail-helo.js +2 -6
  93. package/lib/mail-journal.js +85 -64
  94. package/lib/mail-rbl.js +2 -6
  95. package/lib/mail-scan.js +2 -6
  96. package/lib/mail-spam-score.js +2 -6
  97. package/lib/mail-store.js +293 -154
  98. package/lib/middleware/fetch-metadata.js +17 -7
  99. package/lib/middleware/idempotency-key.js +54 -38
  100. package/lib/middleware/rate-limit.js +102 -32
  101. package/lib/middleware/security-headers.js +21 -5
  102. package/lib/migrations.js +108 -66
  103. package/lib/network-heartbeat.js +7 -0
  104. package/lib/nonce-store.js +31 -9
  105. package/lib/object-store/azure-blob-bucket-ops.js +9 -4
  106. package/lib/object-store/azure-blob.js +31 -3
  107. package/lib/object-store/sigv4.js +10 -0
  108. package/lib/outbox.js +136 -82
  109. package/lib/pqc-agent.js +44 -0
  110. package/lib/pubsub-cluster.js +42 -20
  111. package/lib/queue-local.js +202 -139
  112. package/lib/queue-redis.js +9 -1
  113. package/lib/queue-sqs.js +6 -0
  114. package/lib/retention.js +82 -39
  115. package/lib/safe-dns.js +29 -45
  116. package/lib/safe-ical.js +18 -33
  117. package/lib/safe-icap.js +27 -43
  118. package/lib/safe-sieve.js +21 -40
  119. package/lib/safe-sql.js +124 -3
  120. package/lib/safe-vcard.js +18 -33
  121. package/lib/scheduler.js +35 -12
  122. package/lib/seeders.js +122 -74
  123. package/lib/session-stores.js +42 -14
  124. package/lib/session.js +116 -72
  125. package/lib/sql.js +3885 -0
  126. package/lib/static.js +45 -7
  127. package/lib/subject.js +89 -49
  128. package/lib/vault/index.js +3 -2
  129. package/lib/vault/passphrase-ops.js +3 -2
  130. package/lib/vault/rotate.js +104 -64
  131. package/lib/vendor-data.js +2 -0
  132. package/lib/websocket.js +16 -0
  133. package/package.json +1 -1
  134. package/sbom.cdx.json +6 -6
@@ -37,22 +37,50 @@
37
37
  * b.session.useStore(sessionStore);
38
38
  */
39
39
 
40
+ var frameworkSchema = require("./framework-schema");
40
41
  var localDbThin = require("./local-db-thin");
42
+ var sql = require("./sql");
41
43
  var validateOpts = require("./validate-opts");
42
44
 
43
- var SESSION_SCHEMA_SQL = [
44
- "CREATE TABLE IF NOT EXISTS _blamejs_sessions (",
45
- ' "sidHash" TEXT PRIMARY KEY,',
46
- ' "userId" TEXT,',
47
- ' "userIdHash" TEXT,',
48
- ' "data" TEXT,',
49
- ' "createdAt" INTEGER,',
50
- ' "expiresAt" INTEGER,',
51
- ' "lastActivity" INTEGER',
52
- ");",
53
- 'CREATE INDEX IF NOT EXISTS "_blamejs_sessions_userIdHash_idx" ON _blamejs_sessions ("userIdHash");',
54
- 'CREATE INDEX IF NOT EXISTS "_blamejs_sessions_expiresAt_idx" ON _blamejs_sessions ("expiresAt");',
55
- ].join("\n");
45
+ // Logical session-table name — resolved through frameworkSchema.tableName
46
+ // so a configured table prefix (b.frameworkSchema.setTablePrefix) is
47
+ // honored. This isolated localDbThin file owns its own schema; the name
48
+ // must agree with the main-DB / cluster-mode session table b.session
49
+ // reads + the sealedFields registry key (db.js registers under the
50
+ // logical name).
51
+ var SESSION_LOGICAL = "_blamejs_sessions"; // allow:hand-rolled-sql — canonical logical table-name declaration
52
+
53
+ // b.sql opts for this adapter's schema DDL + every statement b.session
54
+ // builds against it. The localDbThin backend is a dedicated node:sqlite
55
+ // file (always sqlite, independent of cluster mode — see local-db-thin.js),
56
+ // so the dialect is the literal "sqlite": this store NEVER dispatches to an
57
+ // external Postgres / MySQL backend. Making the dialect explicit (rather than
58
+ // leaning on b.sql's "sqlite" default) keeps the quoting intent documented +
59
+ // matches the cluster-routed data-layer files threading
60
+ // clusterStorage.dialect() through the same opts seam.
61
+ var SQL_OPTS = { dialect: "sqlite" };
62
+
63
+ // CREATE TABLE + the two session-side indexes (userIdHash for
64
+ // destroyAllForUser, expiresAt for purgeExpired), built through b.sql so
65
+ // every identifier is quoted by construction and the table name resolves
66
+ // through the configurable prefix. DDL binds no values, so each builder
67
+ // returns { sql } only; the statements are joined for the adapter's
68
+ // schemaSql.
69
+ function _sessionSchemaSql() {
70
+ var table = frameworkSchema.tableName(SESSION_LOGICAL);
71
+ var create = sql.createTable(table, [
72
+ { name: "sidHash", type: "text", primaryKey: true },
73
+ { name: "userId", type: "text" },
74
+ { name: "userIdHash", type: "text" },
75
+ { name: "data", type: "text" },
76
+ { name: "createdAt", type: "int" },
77
+ { name: "expiresAt", type: "int" },
78
+ { name: "lastActivity", type: "int" },
79
+ ], SQL_OPTS).sql;
80
+ var idxUser = sql.createIndex(table + "_userIdHash_idx", table, ["userIdHash"], SQL_OPTS).sql;
81
+ var idxExp = sql.createIndex(table + "_expiresAt_idx", table, ["expiresAt"], SQL_OPTS).sql;
82
+ return [create + ";", idxUser + ";", idxExp + ";"].join("\n");
83
+ }
56
84
 
57
85
  /**
58
86
  * @primitive b.session.stores.localDbThin
@@ -99,7 +127,7 @@ function localDbThinStore(opts) {
99
127
  // logging out every user; operators wanting clear-on-corrupt opt in.
100
128
  var handle = localDbThin.thin({
101
129
  file: opts.file,
102
- schemaSql: SESSION_SCHEMA_SQL,
130
+ schemaSql: _sessionSchemaSql(),
103
131
  recovery: opts.recovery || "refuse",
104
132
  pragmas: opts.pragmas,
105
133
  audit: opts.audit !== false,
package/lib/session.js CHANGED
@@ -53,9 +53,11 @@ var clusterStorage = require("./cluster-storage");
53
53
  var C = require("./constants");
54
54
  var { generateToken, sha3Hash } = require("./crypto");
55
55
  var cryptoField = require("./crypto-field");
56
+ var frameworkSchema = require("./framework-schema");
56
57
  var lazyRequire = require("./lazy-require");
57
58
  var requestHelpers = require("./request-helpers");
58
59
  var safeJson = require("./safe-json");
60
+ var sql = require("./sql");
59
61
  var { SessionError } = require("./framework-error");
60
62
 
61
63
  // vault is initialized at boot before sessions; lazyRequire keeps the
@@ -119,8 +121,33 @@ var SID_NAMESPACE = "bj-session:";
119
121
  // behind the same helper.
120
122
  var SID_BYTES = C.BYTES.bytes(32);
121
123
 
124
+ // Logical session-table name. Two uses, deliberately distinct:
125
+ // - As the cryptoField registry key (sealRow / unsealRow / lookupHash),
126
+ // it stays the LOGICAL name — that is the key db.js registers the
127
+ // sealedFields + derivedHashes under, independent of any table prefix.
128
+ // - As the SQL table name, it is resolved through
129
+ // frameworkSchema.tableName(...) so a configured table prefix
130
+ // (b.frameworkSchema.setTablePrefix) is honored. The name is
131
+ // identity-mapped in LOCAL_TO_EXTERNAL, so clusterStorage's
132
+ // resolveTables leaves it untouched at dispatch.
133
+ var SESSION_TABLE = "_blamejs_sessions"; // allow:hand-rolled-sql — canonical logical table-name + cryptoField registry key
134
+ function _sessionSqlTable() { return frameworkSchema.tableName(SESSION_TABLE); }
135
+
136
+ // b.sql opts for every session statement: thread the ACTIVE backend dialect
137
+ // (clusterStorage.dialect() — "sqlite" single-node, "postgres" | "mysql" in
138
+ // cluster mode) so the emitted identifier quoting and dialect idioms match
139
+ // the backend the SQL dispatches to. b.sql defaults to "sqlite", which works
140
+ // on Postgres only by accident (both double-quote identifiers) and emits the
141
+ // wrong quoting + idioms on MySQL. The default store routes through
142
+ // clusterStorage, and an operator localDbThin store is single-node sqlite —
143
+ // in both single-node cases clusterStorage.dialect() resolves "sqlite", so
144
+ // the opts agree with the store the SQL reaches. clusterStorage.execute (the
145
+ // default store) still rewrites table names + translates `?` placeholders at
146
+ // dispatch; this controls only the builder-side quoting + idiom selection.
147
+ function _sessionSqlOpts() { return { dialect: clusterStorage.dialect() }; }
148
+
122
149
  // Column order used for INSERT — kept as a constant so the placeholders
123
- // list and the values list stay in sync. Must match _blamejs_sessions's
150
+ // list and the values list stay in sync. Must match the session table's
124
151
  // schema in db.js (single-node) and framework-schema.js (cluster mode).
125
152
  var SESSION_COLS = ["sidHash", "userId", "userIdHash", "data", "createdAt", "expiresAt", "lastActivity"];
126
153
 
@@ -167,7 +194,7 @@ function _unsealCookieToken(token) {
167
194
  // where not set). The cryptoField.sealRow call seals userId/data and
168
195
  // produces userIdHash from userId.
169
196
  function _sealForInsert(row) {
170
- var sealed = cryptoField.sealRow("_blamejs_sessions", row);
197
+ var sealed = cryptoField.sealRow(SESSION_TABLE, row);
171
198
  for (var i = 0; i < SESSION_COLS.length; i++) {
172
199
  if (!(SESSION_COLS[i] in sealed)) sealed[SESSION_COLS[i]] = null;
173
200
  }
@@ -433,13 +460,13 @@ async function create(opts) {
433
460
  expiresAt: expiresAt,
434
461
  lastActivity: nowMs,
435
462
  });
436
- var values = SESSION_COLS.map(function (c) { return sealed[c]; });
437
- var placeholders = SESSION_COLS.map(function () { return "?"; }).join(", ");
438
- var quoted = SESSION_COLS.map(function (c) { return '"' + c + '"'; }).join(", ");
439
- await _currentStore().execute(
440
- "INSERT INTO _blamejs_sessions (" + quoted + ") VALUES (" + placeholders + ")",
441
- values
442
- );
463
+ var insertRow = {};
464
+ for (var ci = 0; ci < SESSION_COLS.length; ci++) insertRow[SESSION_COLS[ci]] = sealed[SESSION_COLS[ci]];
465
+ var built = sql.insert(_sessionSqlTable(), _sessionSqlOpts())
466
+ .columns(SESSION_COLS)
467
+ .values(insertRow)
468
+ .toSql();
469
+ await _currentStore().execute(built.sql, built.params);
443
470
 
444
471
  return { token: _sealCookieToken(sid), expiresAt: expiresAt };
445
472
  }
@@ -496,11 +523,11 @@ async function verify(token, verifyOpts) {
496
523
  if (sid === null) return null;
497
524
  var sidHash = _hashSid(sid);
498
525
 
499
- var row = await _currentStore().executeOne(
500
- "SELECT sidHash, userId, userIdHash, data, createdAt, expiresAt, lastActivity " +
501
- "FROM _blamejs_sessions WHERE sidHash = ?",
502
- [sidHash]
503
- );
526
+ var selBuilt = sql.select(_sessionSqlTable(), _sessionSqlOpts())
527
+ .columns(["sidHash", "userId", "userIdHash", "data", "createdAt", "expiresAt", "lastActivity"])
528
+ .where("sidHash", sidHash)
529
+ .toSql();
530
+ var row = await _currentStore().executeOne(selBuilt.sql, selBuilt.params);
504
531
  if (!row) return null;
505
532
  var nowMs = Date.now();
506
533
  if (Number(row.expiresAt) < nowMs) {
@@ -554,7 +581,7 @@ async function verify(token, verifyOpts) {
554
581
  // Unseal sealed columns (userId, data) using the cryptoField pipeline
555
582
  // so we return cleartext to the caller — same shape as the previous
556
583
  // db().from(...).first() path delivered.
557
- var unsealed = cryptoField.unsealRow("_blamejs_sessions", row);
584
+ var unsealed = cryptoField.unsealRow(SESSION_TABLE, row);
558
585
  var data = null;
559
586
  var storedFingerprint = null;
560
587
  if (unsealed.data) {
@@ -679,10 +706,10 @@ async function destroy(token) {
679
706
  }
680
707
 
681
708
  async function _deleteBySidHash(sidHash) {
682
- var result = await _currentStore().execute(
683
- "DELETE FROM _blamejs_sessions WHERE sidHash = ?",
684
- [sidHash]
685
- );
709
+ var built = sql.delete(_sessionSqlTable(), _sessionSqlOpts())
710
+ .where("sidHash", sidHash)
711
+ .toSql();
712
+ var result = await _currentStore().execute(built.sql, built.params);
686
713
  return (result.rowCount || 0) > 0;
687
714
  }
688
715
 
@@ -718,16 +745,23 @@ async function destroyAllForUser(userId) {
718
745
  true);
719
746
  }
720
747
  // userId is sealed; look up via derived userIdHash.
721
- var lookup = cryptoField.lookupHash("_blamejs_sessions", "userId", userId);
748
+ var lookup = cryptoField.lookupHash(SESSION_TABLE, "userId", userId);
722
749
  if (!lookup) {
723
750
  throw _err("MISCONFIGURED",
724
- "_blamejs_sessions schema is missing the userIdHash derived hash — framework misconfigured",
751
+ "the session table schema is missing the userIdHash derived hash — framework misconfigured",
725
752
  true);
726
753
  }
727
- var result = await _currentStore().execute(
728
- "DELETE FROM _blamejs_sessions WHERE userIdHash = ?",
729
- [lookup.value]
730
- );
754
+ // Dual-read across the keyed-MAC flip: a pre-v0.15.0 session row carries
755
+ // the legacy salted-sha3 userIdHash, so destroy must match both digests
756
+ // or it leaves un-migrated sessions for the user un-revoked.
757
+ var userHashes = [lookup.value];
758
+ if (lookup.legacyValue != null && lookup.legacyValue !== lookup.value) {
759
+ userHashes.push(lookup.legacyValue);
760
+ }
761
+ var built = sql.delete(_sessionSqlTable(), _sessionSqlOpts())
762
+ .whereIn("userIdHash", userHashes)
763
+ .toSql();
764
+ var result = await _currentStore().execute(built.sql, built.params);
731
765
  return result.rowCount || 0;
732
766
  }
733
767
 
@@ -776,18 +810,20 @@ async function touch(token, opts) {
776
810
  if (opts.extendBy !== undefined && opts.extendBy !== null) {
777
811
  _validateTtl(opts.extendBy, "session.touch");
778
812
  var newExpires = nowMs + opts.extendBy;
779
- var result = await _currentStore().execute(
780
- "UPDATE _blamejs_sessions SET lastActivity = ?, expiresAt = ? " +
781
- "WHERE sidHash = ? AND expiresAt >= ?",
782
- [nowMs, newExpires, sidHash, nowMs]
783
- );
813
+ var built = sql.update(_sessionSqlTable(), _sessionSqlOpts())
814
+ .set({ lastActivity: nowMs, expiresAt: newExpires })
815
+ .where("sidHash", sidHash)
816
+ .where("expiresAt", ">=", nowMs)
817
+ .toSql();
818
+ var result = await _currentStore().execute(built.sql, built.params);
784
819
  return (result.rowCount || 0) > 0;
785
820
  }
786
- var result2 = await _currentStore().execute(
787
- "UPDATE _blamejs_sessions SET lastActivity = ? " +
788
- "WHERE sidHash = ? AND expiresAt >= ?",
789
- [nowMs, sidHash, nowMs]
790
- );
821
+ var built2 = sql.update(_sessionSqlTable(), _sessionSqlOpts())
822
+ .set({ lastActivity: nowMs })
823
+ .where("sidHash", sidHash)
824
+ .where("expiresAt", ">=", nowMs)
825
+ .toSql();
826
+ var result2 = await _currentStore().execute(built2.sql, built2.params);
791
827
  return (result2.rowCount || 0) > 0;
792
828
  }
793
829
 
@@ -844,31 +880,31 @@ async function rotate(oldToken, opts) {
844
880
  newExpires = nowMs + opts.ttlMs;
845
881
  }
846
882
 
847
- var setParts = ['"sidHash" = ?', '"lastActivity" = ?'];
848
- var setParams = [newSidHash, nowMs];
883
+ var setCols = { sidHash: newSidHash, lastActivity: nowMs };
849
884
 
850
885
  if (opts.data !== undefined) {
851
886
  var dataJson = opts.data ? JSON.stringify(opts.data) : null;
852
- var sealedRow = cryptoField.sealRow("_blamejs_sessions", { data: dataJson });
853
- setParts.push('"data" = ?');
854
- setParams.push(sealedRow.data);
887
+ var sealedRow = cryptoField.sealRow(SESSION_TABLE, { data: dataJson });
888
+ setCols.data = sealedRow.data;
855
889
  }
856
890
  if (newExpires !== null) {
857
- setParts.push('"expiresAt" = ?');
858
- setParams.push(newExpires);
891
+ setCols.expiresAt = newExpires;
859
892
  }
860
893
 
861
- var sql = "UPDATE _blamejs_sessions SET " + setParts.join(", ") +
862
- " WHERE sidHash = ? AND expiresAt >= ?";
863
- var params = setParams.concat([oldSidHash, nowMs]);
864
- var result = await _currentStore().execute(sql, params);
894
+ var updBuilt = sql.update(_sessionSqlTable(), _sessionSqlOpts())
895
+ .set(setCols)
896
+ .where("sidHash", oldSidHash)
897
+ .where("expiresAt", ">=", nowMs)
898
+ .toSql();
899
+ var result = await _currentStore().execute(updBuilt.sql, updBuilt.params);
865
900
  if ((result.rowCount || 0) === 0) return null;
866
901
 
867
902
  // Read the row's effective expiresAt to return — single source of truth.
868
- var row = await _currentStore().executeOne(
869
- 'SELECT "expiresAt" FROM _blamejs_sessions WHERE sidHash = ?',
870
- [newSidHash]
871
- );
903
+ var rowBuilt = sql.select(_sessionSqlTable(), _sessionSqlOpts())
904
+ .columns(["expiresAt"])
905
+ .where("sidHash", newSidHash)
906
+ .toSql();
907
+ var row = await _currentStore().executeOne(rowBuilt.sql, rowBuilt.params);
872
908
  var expiresAt = row ? Number(row.expiresAt) : null;
873
909
 
874
910
  // Audit emit — best-effort. The framework's audit chain logs the
@@ -949,17 +985,18 @@ async function updateData(token, data, opts) {
949
985
  // wins on the same sid, which is the right shape for cart-style
950
986
  // writes; operators needing strict serialization wrap with
951
987
  // b.resourceAccessLock.
952
- var row = await _currentStore().executeOne(
953
- 'SELECT "userId", "userIdHash", "data", "createdAt", "expiresAt", "lastActivity" ' +
954
- 'FROM _blamejs_sessions WHERE sidHash = ? AND expiresAt >= ?',
955
- [sidHash, nowMs]
956
- );
988
+ var selBuilt = sql.select(_sessionSqlTable(), _sessionSqlOpts())
989
+ .columns(["userId", "userIdHash", "data", "createdAt", "expiresAt", "lastActivity"])
990
+ .where("sidHash", sidHash)
991
+ .where("expiresAt", ">=", nowMs)
992
+ .toSql();
993
+ var row = await _currentStore().executeOne(selBuilt.sql, selBuilt.params);
957
994
  if (!row) return false;
958
995
 
959
996
  // Recover the existing data + reserved fingerprint key (vault-
960
997
  // sealed at rest). Operators that want a fresh fingerprint also
961
998
  // call b.session.rotate; updateData preserves the binding.
962
- var unsealed = cryptoField.unsealRow("_blamejs_sessions", row);
999
+ var unsealed = cryptoField.unsealRow(SESSION_TABLE, row);
963
1000
  var existing = null;
964
1001
  var storedFingerprint = null;
965
1002
  if (unsealed.data) {
@@ -998,19 +1035,20 @@ async function updateData(token, data, opts) {
998
1035
 
999
1036
  // Re-seal the data column. cryptoField.sealRow handles the AAD
1000
1037
  // binding + sealedFields registration automatically.
1001
- var sealedRow = cryptoField.sealRow("_blamejs_sessions", {
1038
+ var sealedRow = cryptoField.sealRow(SESSION_TABLE, {
1002
1039
  data: next ? JSON.stringify(next) : null,
1003
1040
  });
1004
1041
 
1005
- var setParts = ['"data" = ?'];
1006
- var setParams = [sealedRow.data];
1042
+ var setCols = { data: sealedRow.data };
1007
1043
  if (opts.touchLastActivity !== false) {
1008
- setParts.push('"lastActivity" = ?');
1009
- setParams.push(nowMs);
1044
+ setCols.lastActivity = nowMs;
1010
1045
  }
1011
- var sql = "UPDATE _blamejs_sessions SET " + setParts.join(", ") +
1012
- " WHERE sidHash = ? AND expiresAt >= ?";
1013
- var result = await _currentStore().execute(sql, setParams.concat([sidHash, nowMs]));
1046
+ var updBuilt = sql.update(_sessionSqlTable(), _sessionSqlOpts())
1047
+ .set(setCols)
1048
+ .where("sidHash", sidHash)
1049
+ .where("expiresAt", ">=", nowMs)
1050
+ .toSql();
1051
+ var result = await _currentStore().execute(updBuilt.sql, updBuilt.params);
1014
1052
  return (result.rowCount || 0) > 0;
1015
1053
  }
1016
1054
 
@@ -1040,10 +1078,10 @@ async function updateData(token, data, opts) {
1040
1078
  */
1041
1079
  async function purgeExpired() {
1042
1080
  cluster.requireLeader();
1043
- var result = await _currentStore().execute(
1044
- "DELETE FROM _blamejs_sessions WHERE expiresAt < ?",
1045
- [Date.now()]
1046
- );
1081
+ var built = sql.delete(_sessionSqlTable(), _sessionSqlOpts())
1082
+ .where("expiresAt", "<", Date.now())
1083
+ .toSql();
1084
+ var result = await _currentStore().execute(built.sql, built.params);
1047
1085
  return result.rowCount || 0;
1048
1086
  }
1049
1087
 
@@ -1066,10 +1104,16 @@ async function purgeExpired() {
1066
1104
  * // → 482
1067
1105
  */
1068
1106
  async function count() {
1069
- var row = await _currentStore().executeOne(
1070
- "SELECT COUNT(*) AS c FROM _blamejs_sessions WHERE expiresAt >= ?",
1071
- [Date.now()]
1072
- );
1107
+ var built = sql.select(_sessionSqlTable(), _sessionSqlOpts())
1108
+ .count("*", "c")
1109
+ .where("expiresAt", ">=", Date.now())
1110
+ .toSql();
1111
+ var row = await _currentStore().executeOne(built.sql, built.params);
1112
+ // COUNT(*) aliased to `c` is not a framework-schema column, so
1113
+ // clusterStorage.coerceRows does not touch it; node-postgres / mysql2
1114
+ // hand a BIGINT count back as a decimal STRING. Number() at the read
1115
+ // boundary keeps single-node sqlite (native number) and the cluster
1116
+ // backends returning the same JS number.
1073
1117
  return row ? Number(row.c) : 0;
1074
1118
  }
1075
1119