@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
@@ -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,16 @@ 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
+ var built = sql.delete(_sessionSqlTable(), _sessionSqlOpts())
755
+ .where("userIdHash", lookup.value)
756
+ .toSql();
757
+ var result = await _currentStore().execute(built.sql, built.params);
731
758
  return result.rowCount || 0;
732
759
  }
733
760
 
@@ -776,18 +803,20 @@ async function touch(token, opts) {
776
803
  if (opts.extendBy !== undefined && opts.extendBy !== null) {
777
804
  _validateTtl(opts.extendBy, "session.touch");
778
805
  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
- );
806
+ var built = sql.update(_sessionSqlTable(), _sessionSqlOpts())
807
+ .set({ lastActivity: nowMs, expiresAt: newExpires })
808
+ .where("sidHash", sidHash)
809
+ .where("expiresAt", ">=", nowMs)
810
+ .toSql();
811
+ var result = await _currentStore().execute(built.sql, built.params);
784
812
  return (result.rowCount || 0) > 0;
785
813
  }
786
- var result2 = await _currentStore().execute(
787
- "UPDATE _blamejs_sessions SET lastActivity = ? " +
788
- "WHERE sidHash = ? AND expiresAt >= ?",
789
- [nowMs, sidHash, nowMs]
790
- );
814
+ var built2 = sql.update(_sessionSqlTable(), _sessionSqlOpts())
815
+ .set({ lastActivity: nowMs })
816
+ .where("sidHash", sidHash)
817
+ .where("expiresAt", ">=", nowMs)
818
+ .toSql();
819
+ var result2 = await _currentStore().execute(built2.sql, built2.params);
791
820
  return (result2.rowCount || 0) > 0;
792
821
  }
793
822
 
@@ -844,31 +873,31 @@ async function rotate(oldToken, opts) {
844
873
  newExpires = nowMs + opts.ttlMs;
845
874
  }
846
875
 
847
- var setParts = ['"sidHash" = ?', '"lastActivity" = ?'];
848
- var setParams = [newSidHash, nowMs];
876
+ var setCols = { sidHash: newSidHash, lastActivity: nowMs };
849
877
 
850
878
  if (opts.data !== undefined) {
851
879
  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);
880
+ var sealedRow = cryptoField.sealRow(SESSION_TABLE, { data: dataJson });
881
+ setCols.data = sealedRow.data;
855
882
  }
856
883
  if (newExpires !== null) {
857
- setParts.push('"expiresAt" = ?');
858
- setParams.push(newExpires);
884
+ setCols.expiresAt = newExpires;
859
885
  }
860
886
 
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);
887
+ var updBuilt = sql.update(_sessionSqlTable(), _sessionSqlOpts())
888
+ .set(setCols)
889
+ .where("sidHash", oldSidHash)
890
+ .where("expiresAt", ">=", nowMs)
891
+ .toSql();
892
+ var result = await _currentStore().execute(updBuilt.sql, updBuilt.params);
865
893
  if ((result.rowCount || 0) === 0) return null;
866
894
 
867
895
  // 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
- );
896
+ var rowBuilt = sql.select(_sessionSqlTable(), _sessionSqlOpts())
897
+ .columns(["expiresAt"])
898
+ .where("sidHash", newSidHash)
899
+ .toSql();
900
+ var row = await _currentStore().executeOne(rowBuilt.sql, rowBuilt.params);
872
901
  var expiresAt = row ? Number(row.expiresAt) : null;
873
902
 
874
903
  // Audit emit — best-effort. The framework's audit chain logs the
@@ -949,17 +978,18 @@ async function updateData(token, data, opts) {
949
978
  // wins on the same sid, which is the right shape for cart-style
950
979
  // writes; operators needing strict serialization wrap with
951
980
  // 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
- );
981
+ var selBuilt = sql.select(_sessionSqlTable(), _sessionSqlOpts())
982
+ .columns(["userId", "userIdHash", "data", "createdAt", "expiresAt", "lastActivity"])
983
+ .where("sidHash", sidHash)
984
+ .where("expiresAt", ">=", nowMs)
985
+ .toSql();
986
+ var row = await _currentStore().executeOne(selBuilt.sql, selBuilt.params);
957
987
  if (!row) return false;
958
988
 
959
989
  // Recover the existing data + reserved fingerprint key (vault-
960
990
  // sealed at rest). Operators that want a fresh fingerprint also
961
991
  // call b.session.rotate; updateData preserves the binding.
962
- var unsealed = cryptoField.unsealRow("_blamejs_sessions", row);
992
+ var unsealed = cryptoField.unsealRow(SESSION_TABLE, row);
963
993
  var existing = null;
964
994
  var storedFingerprint = null;
965
995
  if (unsealed.data) {
@@ -998,19 +1028,20 @@ async function updateData(token, data, opts) {
998
1028
 
999
1029
  // Re-seal the data column. cryptoField.sealRow handles the AAD
1000
1030
  // binding + sealedFields registration automatically.
1001
- var sealedRow = cryptoField.sealRow("_blamejs_sessions", {
1031
+ var sealedRow = cryptoField.sealRow(SESSION_TABLE, {
1002
1032
  data: next ? JSON.stringify(next) : null,
1003
1033
  });
1004
1034
 
1005
- var setParts = ['"data" = ?'];
1006
- var setParams = [sealedRow.data];
1035
+ var setCols = { data: sealedRow.data };
1007
1036
  if (opts.touchLastActivity !== false) {
1008
- setParts.push('"lastActivity" = ?');
1009
- setParams.push(nowMs);
1037
+ setCols.lastActivity = nowMs;
1010
1038
  }
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]));
1039
+ var updBuilt = sql.update(_sessionSqlTable(), _sessionSqlOpts())
1040
+ .set(setCols)
1041
+ .where("sidHash", sidHash)
1042
+ .where("expiresAt", ">=", nowMs)
1043
+ .toSql();
1044
+ var result = await _currentStore().execute(updBuilt.sql, updBuilt.params);
1014
1045
  return (result.rowCount || 0) > 0;
1015
1046
  }
1016
1047
 
@@ -1040,10 +1071,10 @@ async function updateData(token, data, opts) {
1040
1071
  */
1041
1072
  async function purgeExpired() {
1042
1073
  cluster.requireLeader();
1043
- var result = await _currentStore().execute(
1044
- "DELETE FROM _blamejs_sessions WHERE expiresAt < ?",
1045
- [Date.now()]
1046
- );
1074
+ var built = sql.delete(_sessionSqlTable(), _sessionSqlOpts())
1075
+ .where("expiresAt", "<", Date.now())
1076
+ .toSql();
1077
+ var result = await _currentStore().execute(built.sql, built.params);
1047
1078
  return result.rowCount || 0;
1048
1079
  }
1049
1080
 
@@ -1066,10 +1097,16 @@ async function purgeExpired() {
1066
1097
  * // → 482
1067
1098
  */
1068
1099
  async function count() {
1069
- var row = await _currentStore().executeOne(
1070
- "SELECT COUNT(*) AS c FROM _blamejs_sessions WHERE expiresAt >= ?",
1071
- [Date.now()]
1072
- );
1100
+ var built = sql.select(_sessionSqlTable(), _sessionSqlOpts())
1101
+ .count("*", "c")
1102
+ .where("expiresAt", ">=", Date.now())
1103
+ .toSql();
1104
+ var row = await _currentStore().executeOne(built.sql, built.params);
1105
+ // COUNT(*) aliased to `c` is not a framework-schema column, so
1106
+ // clusterStorage.coerceRows does not touch it; node-postgres / mysql2
1107
+ // hand a BIGINT count back as a decimal STRING. Number() at the read
1108
+ // boundary keeps single-node sqlite (native number) and the cluster
1109
+ // backends returning the same JS number.
1073
1110
  return row ? Number(row.c) : 0;
1074
1111
  }
1075
1112