@blamejs/core 0.15.11 → 0.15.13

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 (305) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/index.js +2 -0
  3. package/lib/a2a-tasks.js +7 -23
  4. package/lib/acme.js +13 -16
  5. package/lib/agent-event-bus.js +5 -4
  6. package/lib/agent-idempotency.js +2 -5
  7. package/lib/agent-orchestrator.js +2 -5
  8. package/lib/agent-saga.js +3 -5
  9. package/lib/agent-tenant.js +2 -5
  10. package/lib/ai-adverse-decision.js +2 -15
  11. package/lib/ai-capability.js +1 -6
  12. package/lib/ai-dp.js +1 -5
  13. package/lib/ai-input.js +2 -2
  14. package/lib/ai-pref.js +3 -8
  15. package/lib/ai-quota.js +3 -14
  16. package/lib/api-key.js +37 -28
  17. package/lib/archive-adapters.js +2 -4
  18. package/lib/archive-entry-policy.js +32 -0
  19. package/lib/archive-read.js +5 -17
  20. package/lib/archive-tar-read.js +5 -16
  21. package/lib/archive.js +2 -10
  22. package/lib/arg-parser.js +7 -6
  23. package/lib/asyncapi-traits.js +2 -6
  24. package/lib/atomic-file.js +108 -31
  25. package/lib/audit-chain.js +133 -53
  26. package/lib/audit-daily-review.js +24 -14
  27. package/lib/audit-emit.js +82 -0
  28. package/lib/audit-sign.js +257 -0
  29. package/lib/audit.js +84 -0
  30. package/lib/auth/access-lock.js +5 -27
  31. package/lib/auth/dpop.js +23 -35
  32. package/lib/auth/fido-mds3.js +9 -15
  33. package/lib/auth/jwt-external.js +26 -8
  34. package/lib/auth/jwt.js +13 -15
  35. package/lib/auth/lockout.js +6 -25
  36. package/lib/auth/oauth.js +67 -45
  37. package/lib/auth/oid4vci.js +55 -32
  38. package/lib/auth/oid4vp.js +3 -2
  39. package/lib/auth/openid-federation.js +6 -6
  40. package/lib/auth/passkey.js +3 -3
  41. package/lib/auth/password.js +7 -1
  42. package/lib/auth/saml.js +37 -27
  43. package/lib/auth/sd-jwt-vc-holder.js +2 -14
  44. package/lib/auth/sd-jwt-vc-issuer.js +2 -14
  45. package/lib/auth/sd-jwt-vc.js +1 -1
  46. package/lib/auth/status-list.js +7 -7
  47. package/lib/auth/step-up.js +6 -12
  48. package/lib/auth-bot-challenge.js +6 -37
  49. package/lib/backup/bundle.js +11 -18
  50. package/lib/backup/index.js +14 -47
  51. package/lib/bounded-map.js +112 -1
  52. package/lib/breach-deadline.js +1 -11
  53. package/lib/cache.js +7 -18
  54. package/lib/cbor.js +2 -1
  55. package/lib/cdn-cache-control.js +8 -9
  56. package/lib/cert.js +3 -10
  57. package/lib/chain-writer.js +162 -47
  58. package/lib/cli.js +5 -1
  59. package/lib/client-hints.js +10 -10
  60. package/lib/cloud-events.js +40 -31
  61. package/lib/cluster-storage.js +2 -38
  62. package/lib/cluster.js +4 -2
  63. package/lib/cms-codec.js +2 -1
  64. package/lib/codepoint-class.js +67 -1
  65. package/lib/compliance-ai-act-logging.js +1 -6
  66. package/lib/compliance-ai-act-transparency.js +2 -4
  67. package/lib/compliance-eaa.js +1 -11
  68. package/lib/compliance-sanctions-fetcher.js +21 -28
  69. package/lib/compliance-sanctions.js +2 -14
  70. package/lib/compliance.js +2 -9
  71. package/lib/config-drift.js +2 -12
  72. package/lib/content-digest.js +10 -8
  73. package/lib/cookies.js +5 -11
  74. package/lib/cose.js +19 -11
  75. package/lib/cra-report.js +1 -11
  76. package/lib/crypto-field.js +5 -10
  77. package/lib/crypto.js +120 -3
  78. package/lib/csp.js +235 -3
  79. package/lib/daemon.js +42 -41
  80. package/lib/data-act.js +19 -9
  81. package/lib/db-query.js +6 -40
  82. package/lib/db.js +34 -12
  83. package/lib/dbsc.js +5 -6
  84. package/lib/ddl-change-control.js +18 -14
  85. package/lib/deprecate.js +4 -5
  86. package/lib/did.js +6 -2
  87. package/lib/dsr.js +8 -12
  88. package/lib/external-db-migrate.js +21 -22
  89. package/lib/external-db.js +14 -26
  90. package/lib/fda-21cfr11.js +12 -8
  91. package/lib/fdx.js +22 -18
  92. package/lib/file-upload.js +80 -66
  93. package/lib/flag-evaluation-context.js +5 -8
  94. package/lib/framework-error.js +12 -0
  95. package/lib/framework-schema.js +19 -0
  96. package/lib/fsm.js +7 -12
  97. package/lib/gate-contract.js +869 -38
  98. package/lib/gdpr-ropa.js +4 -13
  99. package/lib/graphql-federation.js +1 -1
  100. package/lib/guard-agent-registry.js +2 -1
  101. package/lib/guard-all.js +1 -0
  102. package/lib/guard-archive.js +9 -30
  103. package/lib/guard-auth.js +23 -80
  104. package/lib/guard-cidr.js +20 -96
  105. package/lib/guard-csv.js +135 -196
  106. package/lib/guard-domain.js +23 -106
  107. package/lib/guard-dsn.js +16 -13
  108. package/lib/guard-email.js +46 -53
  109. package/lib/guard-envelope.js +1 -1
  110. package/lib/guard-event-bus-topic.js +2 -1
  111. package/lib/guard-filename.js +18 -62
  112. package/lib/guard-graphql.js +28 -75
  113. package/lib/guard-html-wcag.js +15 -2
  114. package/lib/guard-html.js +65 -117
  115. package/lib/guard-idempotency-key.js +2 -1
  116. package/lib/guard-image.js +280 -77
  117. package/lib/guard-imap-command.js +8 -9
  118. package/lib/guard-json.js +87 -103
  119. package/lib/guard-jsonpath.js +20 -88
  120. package/lib/guard-jwt.js +32 -114
  121. package/lib/guard-list-id.js +2 -7
  122. package/lib/guard-list-unsubscribe.js +2 -7
  123. package/lib/guard-mail-compose.js +5 -6
  124. package/lib/guard-mail-move.js +2 -1
  125. package/lib/guard-mail-query.js +5 -3
  126. package/lib/guard-mail-sieve.js +6 -7
  127. package/lib/guard-managesieve-command.js +5 -4
  128. package/lib/guard-markdown.js +76 -110
  129. package/lib/guard-message-id.js +5 -6
  130. package/lib/guard-mime.js +20 -99
  131. package/lib/guard-oauth.js +19 -73
  132. package/lib/guard-pdf.js +65 -72
  133. package/lib/guard-pop3-command.js +12 -13
  134. package/lib/guard-posture-chain.js +2 -1
  135. package/lib/guard-regex.js +24 -99
  136. package/lib/guard-saga-config.js +2 -1
  137. package/lib/guard-shell.js +22 -87
  138. package/lib/guard-smtp-command.js +8 -11
  139. package/lib/guard-sql.js +15 -13
  140. package/lib/guard-stream-args.js +2 -1
  141. package/lib/guard-svg.js +95 -140
  142. package/lib/guard-template.js +23 -80
  143. package/lib/guard-tenant-id.js +2 -1
  144. package/lib/guard-text.js +592 -0
  145. package/lib/guard-time.js +27 -95
  146. package/lib/guard-uuid.js +21 -93
  147. package/lib/guard-xml.js +76 -106
  148. package/lib/guard-yaml.js +24 -60
  149. package/lib/http-client-cache.js +8 -13
  150. package/lib/http-client-cookie-jar.js +2 -4
  151. package/lib/http-client.js +8 -21
  152. package/lib/http-message-signature.js +26 -8
  153. package/lib/i18n-messageformat.js +5 -7
  154. package/lib/i18n.js +83 -26
  155. package/lib/inbox.js +8 -8
  156. package/lib/incident-report.js +3 -21
  157. package/lib/ip-utils.js +49 -6
  158. package/lib/jobs.js +3 -2
  159. package/lib/keychain.js +6 -18
  160. package/lib/legal-hold.js +6 -15
  161. package/lib/log-stream-cloudwatch.js +44 -112
  162. package/lib/log-stream-otlp-grpc.js +28 -14
  163. package/lib/log-stream-otlp.js +16 -80
  164. package/lib/log-stream-syslog.js +6 -0
  165. package/lib/log-stream-webhook.js +20 -92
  166. package/lib/log.js +24 -2
  167. package/lib/mail-arc-sign.js +8 -3
  168. package/lib/mail-arf.js +3 -2
  169. package/lib/mail-auth.js +40 -66
  170. package/lib/mail-bimi.js +19 -39
  171. package/lib/mail-crypto-pgp.js +3 -2
  172. package/lib/mail-dav.js +8 -35
  173. package/lib/mail-deploy.js +6 -9
  174. package/lib/mail-dkim.js +19 -38
  175. package/lib/mail-greylist.js +15 -26
  176. package/lib/mail-helo.js +2 -3
  177. package/lib/mail-journal.js +2 -1
  178. package/lib/mail-mdn.js +4 -3
  179. package/lib/mail-rbl.js +5 -8
  180. package/lib/mail-scan.js +2 -2
  181. package/lib/mail-send-deliver.js +33 -20
  182. package/lib/mail-server-imap.js +32 -87
  183. package/lib/mail-server-jmap.js +35 -51
  184. package/lib/mail-server-managesieve.js +29 -83
  185. package/lib/mail-server-mx.js +33 -74
  186. package/lib/mail-server-net.js +177 -0
  187. package/lib/mail-server-pop3.js +30 -83
  188. package/lib/mail-server-rate-limit.js +30 -6
  189. package/lib/mail-server-submission.js +34 -73
  190. package/lib/mail-server-tls.js +89 -0
  191. package/lib/mail-spam-score.js +7 -8
  192. package/lib/mail-store.js +3 -2
  193. package/lib/mail.js +11 -11
  194. package/lib/markup-escape.js +31 -0
  195. package/lib/markup-tokenizer.js +54 -0
  196. package/lib/mcp-tool-registry.js +26 -23
  197. package/lib/mcp.js +1 -1
  198. package/lib/mdoc.js +11 -12
  199. package/lib/metrics.js +32 -30
  200. package/lib/middleware/age-gate.js +3 -23
  201. package/lib/middleware/api-encrypt.js +11 -22
  202. package/lib/middleware/assetlinks.js +2 -7
  203. package/lib/middleware/bearer-auth.js +2 -1
  204. package/lib/middleware/body-parser.js +79 -54
  205. package/lib/middleware/bot-disclose.js +7 -10
  206. package/lib/middleware/bot-guard.js +2 -10
  207. package/lib/middleware/csrf-protect.js +13 -2
  208. package/lib/middleware/daily-byte-quota.js +6 -26
  209. package/lib/middleware/deny-response.js +19 -1
  210. package/lib/middleware/fetch-metadata.js +7 -0
  211. package/lib/middleware/idempotency-key.js +4 -20
  212. package/lib/middleware/rate-limit.js +20 -28
  213. package/lib/middleware/scim-server.js +3 -2
  214. package/lib/middleware/security-headers.js +9 -1
  215. package/lib/middleware/security-txt.js +2 -6
  216. package/lib/middleware/tus-upload.js +12 -27
  217. package/lib/middleware/web-app-manifest.js +2 -7
  218. package/lib/migrations.js +4 -13
  219. package/lib/mime-parse.js +34 -17
  220. package/lib/module-loader.js +44 -0
  221. package/lib/money.js +105 -0
  222. package/lib/mtls-ca.js +116 -36
  223. package/lib/network-byte-quota.js +4 -18
  224. package/lib/network-dane.js +8 -6
  225. package/lib/network-dns-resolver.js +97 -6
  226. package/lib/network-dns.js +22 -26
  227. package/lib/network-dnssec.js +16 -16
  228. package/lib/network-heartbeat.js +6 -5
  229. package/lib/network-proxy.js +3 -7
  230. package/lib/network-smtp-policy.js +29 -41
  231. package/lib/network-tls.js +74 -55
  232. package/lib/network.js +2 -6
  233. package/lib/nis2-report.js +1 -11
  234. package/lib/nonce-store.js +81 -3
  235. package/lib/notify.js +7 -12
  236. package/lib/numeric-bounds.js +22 -8
  237. package/lib/object-store/azure-blob-bucket-ops.js +2 -6
  238. package/lib/object-store/azure-blob.js +5 -45
  239. package/lib/object-store/gcs.js +8 -71
  240. package/lib/object-store/http-put.js +1 -5
  241. package/lib/object-store/http-request.js +128 -0
  242. package/lib/object-store/sigv4-bucket-ops.js +2 -1
  243. package/lib/object-store/sigv4.js +9 -77
  244. package/lib/observability-otlp-exporter.js +9 -25
  245. package/lib/observability.js +95 -0
  246. package/lib/openapi-paths-builder.js +7 -3
  247. package/lib/otel-export.js +7 -10
  248. package/lib/outbox.js +43 -37
  249. package/lib/pagination.js +11 -15
  250. package/lib/parsers/safe-env.js +5 -4
  251. package/lib/parsers/safe-ini.js +10 -4
  252. package/lib/parsers/safe-toml.js +11 -9
  253. package/lib/parsers/safe-xml.js +2 -2
  254. package/lib/parsers/safe-yaml.js +7 -6
  255. package/lib/pick.js +63 -5
  256. package/lib/pipl-cn.js +69 -59
  257. package/lib/privacy-pass.js +8 -6
  258. package/lib/problem-details.js +2 -5
  259. package/lib/pubsub.js +4 -8
  260. package/lib/redact.js +2 -1
  261. package/lib/render.js +4 -2
  262. package/lib/request-helpers.js +133 -6
  263. package/lib/restore.js +5 -22
  264. package/lib/retention.js +3 -5
  265. package/lib/safe-async.js +457 -0
  266. package/lib/safe-buffer.js +137 -6
  267. package/lib/safe-decompress.js +2 -1
  268. package/lib/safe-dns.js +2 -1
  269. package/lib/safe-ical.js +12 -26
  270. package/lib/safe-json.js +7 -8
  271. package/lib/safe-jsonpath.js +6 -5
  272. package/lib/safe-mime.js +25 -29
  273. package/lib/safe-redirect.js +2 -5
  274. package/lib/safe-schema.js +7 -6
  275. package/lib/safe-sql.js +126 -0
  276. package/lib/safe-vcard.js +12 -26
  277. package/lib/sandbox-worker.js +9 -2
  278. package/lib/sandbox.js +3 -2
  279. package/lib/scheduler.js +5 -12
  280. package/lib/sec-cyber.js +82 -37
  281. package/lib/seeders.js +9 -21
  282. package/lib/self-update.js +92 -69
  283. package/lib/session-device-binding.js +112 -66
  284. package/lib/session.js +194 -6
  285. package/lib/sql.js +225 -78
  286. package/lib/sse.js +6 -10
  287. package/lib/static.js +136 -98
  288. package/lib/storage.js +4 -2
  289. package/lib/structured-fields.js +313 -17
  290. package/lib/tenant-quota.js +2 -20
  291. package/lib/tsa.js +11 -15
  292. package/lib/validate-opts.js +154 -8
  293. package/lib/vault/seal-pem-file.js +30 -48
  294. package/lib/vault-aad.js +3 -2
  295. package/lib/vc.js +2 -7
  296. package/lib/vex.js +35 -13
  297. package/lib/web-push-vapid.js +23 -20
  298. package/lib/webhook-dispatcher.js +706 -0
  299. package/lib/webhook.js +43 -18
  300. package/lib/websocket-channels.js +9 -7
  301. package/lib/websocket.js +7 -3
  302. package/lib/worm.js +2 -3
  303. package/lib/ws-client.js +8 -10
  304. package/package.json +1 -1
  305. package/sbom.cdx.json +6 -6
@@ -97,13 +97,8 @@ function _requireFunction(name, val) {
97
97
  }
98
98
 
99
99
  function _requireBindingStore(s) {
100
- if (!s || typeof s !== "object" ||
101
- typeof s.get !== "function" ||
102
- typeof s.set !== "function" ||
103
- typeof s.del !== "function") {
104
- throw new SessionDeviceBindingError("session-device-binding/bad-opt",
105
- "bindingStore must be a b.cache-shaped object (get/set/del)");
106
- }
100
+ validateOpts.requireMethods(s, ["get", "set", "del"],
101
+ "bindingStore (b.cache-shaped)", SessionDeviceBindingError, "session-device-binding/bad-opt");
107
102
  }
108
103
 
109
104
  function _requireToken(token) {
@@ -164,6 +159,105 @@ function _ipPrefix(ip, bits) {
164
159
  return "v4:" + maskedOctets.join(".") + "/" + v4Bits;
165
160
  }
166
161
 
162
+ // Resolve operator-supplied fingerprintExtras(req) to a stable string. A
163
+ // throwing or non-serializable extractor drops to "" — extras only sharpen the
164
+ // digest, they must never crash fingerprinting.
165
+ function _resolveExtrasFn(fn, req) {
166
+ if (!fn) return "";
167
+ var v;
168
+ try { v = fn(req); } catch (_e) { return ""; }
169
+ if (v === null || v === undefined) return "";
170
+ if (typeof v === "string") return v;
171
+ try { return JSON.stringify(v); } catch (_e) { return ""; }
172
+ }
173
+
174
+ // Clamp an { v4, v6 } prefix-bits opt to valid IPv4/IPv6 ranges, defaulting
175
+ // each independently.
176
+ function _resolveIpBits(ipBits) {
177
+ ipBits = ipBits || {};
178
+ return {
179
+ v4: (typeof ipBits.v4 === "number" && isFinite(ipBits.v4) && ipBits.v4 >= 0 && ipBits.v4 <= 32) // IPv4 max prefix length in bits
180
+ ? ipBits.v4 : DEFAULT_IP_V4_PREFIX,
181
+ v6: (typeof ipBits.v6 === "number" && isFinite(ipBits.v6) && ipBits.v6 >= 0 && ipBits.v6 <= 128) // IPv6 max prefix length in bits
182
+ ? ipBits.v6 : DEFAULT_IP_V6_PREFIX,
183
+ };
184
+ }
185
+
186
+ // Pure request-shape device fingerprint — the side-effect-free core shared by
187
+ // the instance lookup and the static fingerprint() entry point. cfg: { v4Bits,
188
+ // v6Bits, extras (string), boundKey (Buffer|null) }. No store, no audit, no
189
+ // session — just headers + masked client IP (+ optional bound key) -> SHAKE256.
190
+ function _computeDeviceFingerprint(req, cfg) {
191
+ _requireReq(req);
192
+ var headers = req.headers || {};
193
+ var ua = typeof headers["user-agent"] === "string" ? headers["user-agent"] : "";
194
+ var al = _normalizeAcceptLanguage(headers["accept-language"]);
195
+ var ae = _normalizeAcceptEncoding(headers["accept-encoding"]);
196
+ var ip = "";
197
+ try { ip = requestHelpers.clientIp(req); } catch (_e) { ip = ""; }
198
+ var family = ip.indexOf(":") !== -1 ? "v6" : "v4";
199
+ var ipPart = _ipPrefix(ip, family === "v6" ? cfg.v6Bits : cfg.v4Bits);
200
+ var keyPart = "";
201
+ if (Buffer.isBuffer(cfg.boundKey)) {
202
+ keyPart = "k:" + nodeCrypto.createHash("sha3-256").update(cfg.boundKey).digest("hex");
203
+ }
204
+ var canonical = [
205
+ "ua=" + ua,
206
+ "al=" + al,
207
+ "ae=" + ae,
208
+ "ip=" + ipPart,
209
+ "ex=" + (cfg.extras || ""),
210
+ keyPart,
211
+ ].join("\n");
212
+ var hash = nodeCrypto.createHash("shake256", { outputLength: FINGERPRINT_BYTES })
213
+ .update(canonical)
214
+ .digest();
215
+ return { fingerprint: hash, components: {
216
+ ua: ua, al: al, ae: ae, ip: ipPart, hasBoundKey: !!keyPart,
217
+ } };
218
+ }
219
+
220
+ /**
221
+ * @primitive b.sessionDeviceBinding.fingerprint
222
+ * @signature b.sessionDeviceBinding.fingerprint(req, opts?)
223
+ * @since 0.15.13
224
+ * @status stable
225
+ * @related b.session.rotate, b.session.create
226
+ *
227
+ * Compute the stateless SHAKE256 device-shape digest for a request with no
228
+ * store, no session, and no side effects — the soft device-binding building
229
+ * block for self-validating tokens (a sealed cookie, a JWT) that carry the
230
+ * fingerprint inside the token and compare it themselves. `create()` requires a
231
+ * bindingStore for the persisted bind()/verify() lifecycle; this static form
232
+ * skips that gate because it touches no store. Returns a 32-byte Buffer derived
233
+ * from User-Agent + normalized Accept-Language / Accept-Encoding + the masked
234
+ * client-IP prefix (+ optional operator extras). Throws only on a missing or
235
+ * malformed request object.
236
+ *
237
+ * @opts
238
+ * ipPrefixBits: { v4: number, v6: number }, // default { v4: 24, v6: 48 } — mask width that survives a roaming IP
239
+ * fingerprintExtras: function, // (req) => string|object, optional extra signal folded into the digest
240
+ *
241
+ * @example
242
+ * var fp = b.sessionDeviceBinding.fingerprint(req);
243
+ * // seal fp inside the cookie/JWT; on the next request recompute + constant-time compare
244
+ */
245
+ function fingerprint(req, opts) {
246
+ opts = opts || {};
247
+ if (opts.fingerprintExtras !== undefined && opts.fingerprintExtras !== null &&
248
+ typeof opts.fingerprintExtras !== "function") {
249
+ throw new SessionDeviceBindingError("session-device-binding/bad-opt",
250
+ "fingerprint: fingerprintExtras must be a function (req) => string|object");
251
+ }
252
+ var bits = _resolveIpBits(opts.ipPrefixBits);
253
+ return _computeDeviceFingerprint(req, {
254
+ v4Bits: bits.v4,
255
+ v6Bits: bits.v6,
256
+ extras: _resolveExtrasFn(opts.fingerprintExtras, req),
257
+ boundKey: null,
258
+ }).fingerprint;
259
+ }
260
+
167
261
  function create(opts) {
168
262
  opts = opts || {};
169
263
  validateOpts(opts, ALLOWED_OPTS, "sessionDeviceBinding.create");
@@ -215,29 +309,9 @@ function create(opts) {
215
309
  var boundKeyResolver = opts.boundKeyResolver || null;
216
310
  var fingerprintExtras = opts.fingerprintExtras || null;
217
311
 
218
- function _emitObs(name, labels) {
219
- var sink = obsInst || _safeGlobalObs();
220
- if (!sink) return;
221
- try { sink.event(name, 1, labels); } catch (_e) { /* drop-silent */ }
222
- }
223
-
224
- function _safeGlobalObs() {
225
- try { return observability(); } catch (_e) { return null; }
226
- }
312
+ var _emitObs = observability().makeCounterEmitter(obsInst);
227
313
 
228
- function _emitAudit(action, tokenHash, outcome, metadata, req) {
229
- if (!auditInst) return;
230
- try {
231
- var event = {
232
- action: action,
233
- outcome: outcome,
234
- resource: { kind: "session.device", id: tokenHash },
235
- metadata: metadata || {},
236
- };
237
- if (req) event.actor = requestHelpers.extractActorContext(req);
238
- auditInst.safeEmit(event);
239
- } catch (_e) { /* drop-silent */ }
240
- }
314
+ var _emitAudit = requestHelpers.makeResourceAuditEmitter(auditInst, "session.device");
241
315
 
242
316
  function _hashTokenForAudit(token) {
243
317
  // Don't put the raw session id in the audit log. SHAKE256 to a
@@ -259,51 +333,22 @@ function create(opts) {
259
333
  }
260
334
 
261
335
  function _resolveExtras(req) {
262
- if (!fingerprintExtras) return "";
263
- var v;
264
- try { v = fingerprintExtras(req); }
265
- catch (_e) { return ""; }
266
- if (v === null || v === undefined) return "";
267
- if (typeof v === "string") return v;
268
- try { return JSON.stringify(v); } catch (_e) { return ""; }
336
+ return _resolveExtrasFn(fingerprintExtras, req);
269
337
  }
270
338
 
271
339
  function _computeFingerprint(req) {
272
340
  _requireReq(req);
273
- var headers = req.headers || {};
274
- var ua = typeof headers["user-agent"] === "string" ? headers["user-agent"] : "";
275
- var al = _normalizeAcceptLanguage(headers["accept-language"]);
276
- var ae = _normalizeAcceptEncoding(headers["accept-encoding"]);
277
- var ip = "";
278
- try { ip = requestHelpers.clientIp(req); } catch (_e) { ip = ""; }
279
- var family = ip.indexOf(":") !== -1 ? "v6" : "v4";
280
- var ipPart = _ipPrefix(ip, family === "v6" ? v6Bits : v4Bits);
281
- var extras = _resolveExtras(req);
282
-
283
341
  var boundKeyMaybe = _resolveBoundKey(req);
284
342
  if (requireBoundKey && (boundKeyMaybe === null || boundKeyMaybe === undefined)) {
285
343
  return { ok: false, reason: "missing-bound-key" };
286
344
  }
287
- var keyPart = "";
288
- if (Buffer.isBuffer(boundKeyMaybe)) {
289
- keyPart = "k:" + nodeCrypto.createHash("sha3-256").update(boundKeyMaybe).digest("hex");
290
- }
291
-
292
- var canonical = [
293
- "ua=" + ua,
294
- "al=" + al,
295
- "ae=" + ae,
296
- "ip=" + ipPart,
297
- "ex=" + extras,
298
- keyPart,
299
- ].join("\n");
300
-
301
- var hash = nodeCrypto.createHash("shake256", { outputLength: FINGERPRINT_BYTES })
302
- .update(canonical)
303
- .digest();
304
- return { ok: true, fingerprint: hash, components: {
305
- ua: ua, al: al, ae: ae, ip: ipPart, hasBoundKey: !!keyPart,
306
- } };
345
+ var r = _computeDeviceFingerprint(req, {
346
+ v4Bits: v4Bits,
347
+ v6Bits: v6Bits,
348
+ extras: _resolveExtras(req),
349
+ boundKey: Buffer.isBuffer(boundKeyMaybe) ? boundKeyMaybe : null,
350
+ });
351
+ return { ok: true, fingerprint: r.fingerprint, components: r.components };
307
352
  }
308
353
 
309
354
  async function bind(token, req) {
@@ -421,6 +466,7 @@ function create(opts) {
421
466
 
422
467
  module.exports = {
423
468
  create: create,
469
+ fingerprint: fingerprint,
424
470
  SessionDeviceBindingError: SessionDeviceBindingError,
425
471
  DEFAULTS: Object.freeze({
426
472
  ttlMs: DEFAULT_TTL_MS,
package/lib/session.js CHANGED
@@ -48,6 +48,7 @@
48
48
  */
49
49
  var audit = require("./audit");
50
50
  var canonicalJson = require("./canonical-json");
51
+ var validateOpts = require("./validate-opts");
51
52
  var cluster = require("./cluster");
52
53
  var clusterStorage = require("./cluster-storage");
53
54
  var C = require("./constants");
@@ -149,6 +150,38 @@ function _sessionSqlTable() { return frameworkSchema.tableName(SESSION_TABLE); }
149
150
  // dispatch; this controls only the builder-side quoting + idiom selection.
150
151
  function _sessionSqlOpts() { return { dialect: clusterStorage.dialect() }; }
151
152
 
153
+ // Per-subject valid-from boundary table. A monotonic epoch (ms) the issuer
154
+ // bumps to invalidate every STATELESS self-validating token (sealed cookie
155
+ // with no DB row, JWT) minted before the bump: log-out-everywhere, a
156
+ // right-to-erasure cutoff, a forced re-auth after a credential change. Token
157
+ // readers compare the token's iat against this boundary via check(). Same
158
+ // dual-storage shape as _blamejs_sessions — registered in both db.js's
159
+ // FRAMEWORK_SCHEMA (single-node SQLite) and framework-schema.js (cluster
160
+ // external-db); clusterStorage.execute routes by cluster mode. The subject id
161
+ // is hashed before it becomes the PRIMARY KEY, so the plaintext id never lands
162
+ // in the table (the same sidHash / userIdHash discipline the sessions table
163
+ // follows).
164
+ var VALID_FROM_TABLE = "_blamejs_session_valid_from"; // allow:hand-rolled-sql — canonical logical table-name + reserved schema name
165
+ var VALID_FROM_SUBJECT_NAMESPACE = "bj-session-valid-from-subject:";
166
+ function _validFromSqlTable() { return frameworkSchema.tableName(VALID_FROM_TABLE); }
167
+ function _hashSubjectId(subjectId) { return sha3Hash(VALID_FROM_SUBJECT_NAMESPACE + subjectId); }
168
+
169
+ // Dialect-aware conflict-row references for the monotonic-max upsert in
170
+ // bump(). Mirrors rate-limit.js's pattern: the proposed row is EXCLUDED
171
+ // (Postgres/SQLite) / VALUES() (MySQL); the existing row is a self-reference.
172
+ function _validFromConflictRefs(dialect, table) {
173
+ if (dialect === "mysql") {
174
+ return {
175
+ proposed: function (col) { return "VALUES(`" + col + "`)"; },
176
+ existing: function (col) { return "`" + table + "`.`" + col + "`"; },
177
+ };
178
+ }
179
+ return {
180
+ proposed: function (col) { return "EXCLUDED.\"" + col + "\""; },
181
+ existing: function (col) { return "\"" + table + "\".\"" + col + "\""; },
182
+ };
183
+ }
184
+
152
185
  // Column order used for INSERT — kept as a constant so the placeholders
153
186
  // list and the values list stay in sync. Must match the session table's
154
187
  // schema in db.js (single-node) and framework-schema.js (cluster mode).
@@ -825,6 +858,10 @@ async function destroyAllForUser(userId) {
825
858
  .whereIn("userIdHash", userHashes)
826
859
  .toSql();
827
860
  var result = await _currentStore().execute(built.sql, built.params);
861
+ // Also raise the stateless valid-from boundary so a "logout everywhere"
862
+ // revokes the operator's stateless tokens (sealed cookies / JWTs checked via
863
+ // b.session.check) too, not only the store-backed rows just deleted.
864
+ await bump(userId);
828
865
  return result.rowCount || 0;
829
866
  }
830
867
 
@@ -1272,12 +1309,8 @@ function useStore(store) {
1272
1309
  _store = null;
1273
1310
  return;
1274
1311
  }
1275
- if (typeof store !== "object" ||
1276
- typeof store.execute !== "function" ||
1277
- typeof store.executeOne !== "function") {
1278
- throw _err("INVALID_ARG",
1279
- "session.useStore: store must expose execute(sql,params) and executeOne(sql,params)", true);
1280
- }
1312
+ validateOpts.requireMethods(store, ["execute", "executeOne"],
1313
+ "session.useStore: store", SessionError, "INVALID_ARG", true);
1281
1314
  _store = store;
1282
1315
  }
1283
1316
 
@@ -1304,6 +1337,158 @@ function isAnonymous(userId) {
1304
1337
  return _isAnonymousUserId(userId);
1305
1338
  }
1306
1339
 
1340
+ /**
1341
+ * @primitive b.session.bump
1342
+ * @signature b.session.bump(subjectId, opts?)
1343
+ * @since 0.15.13
1344
+ * @status stable
1345
+ * @related b.session.check, b.session.validFrom, b.session.destroyAllForUser
1346
+ *
1347
+ * Revoke every STATELESS self-validating token (sealed cookie carrying no DB
1348
+ * row, JWT) for a subject by raising a durable per-subject valid-from boundary
1349
+ * to now. Any token whose issued-at (`iat`) predates the boundary fails
1350
+ * `b.session.check`. Unlike `destroyAllForUser` — which deletes server-side
1351
+ * session rows — this revokes tokens the framework never stored a row for:
1352
+ * log-out-everywhere, a right-to-erasure cutoff, a forced re-auth after a
1353
+ * password / key change. `destroyAllForUser` calls this for you, so a single
1354
+ * "logout everywhere" covers both store-backed and stateless tokens.
1355
+ *
1356
+ * The boundary is MONOTONIC: it only ever moves forward. A bump to an
1357
+ * `epochMs` at or below the stored value is a no-op — a replayed or
1358
+ * clock-skewed lower value can never widen a revoked window back open. Returns
1359
+ * the boundary in effect after the call. Leader-only. The subject id is stored
1360
+ * hashed; the plaintext id never lands in the table.
1361
+ *
1362
+ * @opts
1363
+ * epochMs: number, // boundary to set; default Date.now(). Tokens with iat < this are revoked.
1364
+ *
1365
+ * @example
1366
+ * // Force re-auth everywhere for a subject after a password change:
1367
+ * var boundary = await b.session.bump("user-42");
1368
+ * // Cut off at a specific instant (right-to-erasure effective time):
1369
+ * await b.session.bump("user-42", { epochMs: erasureEffectiveMs });
1370
+ */
1371
+ async function bump(subjectId, opts) {
1372
+ cluster.requireLeader();
1373
+ if (typeof subjectId !== "string" || subjectId.length === 0) {
1374
+ throw _err("INVALID_ARG", "session.bump requires a non-empty subjectId", true);
1375
+ }
1376
+ opts = opts || {};
1377
+ var epochMs = opts.epochMs === undefined ? Date.now() : opts.epochMs;
1378
+ if (typeof epochMs !== "number" || !isFinite(epochMs) || epochMs < 0) {
1379
+ throw _err("INVALID_ARG",
1380
+ "session.bump: epochMs must be a non-negative finite number, got " + JSON.stringify(epochMs), true);
1381
+ }
1382
+ var subjectHash = _hashSubjectId(subjectId);
1383
+ var t = _validFromSqlTable();
1384
+ var dialect = clusterStorage.dialect();
1385
+ var refs = _validFromConflictRefs(dialect, t);
1386
+
1387
+ // Monotonic-max conflict action: keep the LATER of the proposed and the
1388
+ // stored boundary, so a lower (replayed / clock-skewed) epoch can never move
1389
+ // the boundary backwards and re-open a revoked window.
1390
+ var validFromExpr = "CASE WHEN " + refs.proposed("validFromEpoch") + " > " +
1391
+ refs.existing("validFromEpoch") + " THEN " + refs.proposed("validFromEpoch") +
1392
+ " ELSE " + refs.existing("validFromEpoch") + " END";
1393
+ var built = sql.upsert(t, _sessionSqlOpts())
1394
+ .columns(["subjectHash", "validFromEpoch", "updatedAt"])
1395
+ .values({ subjectHash: subjectHash, validFromEpoch: epochMs, updatedAt: Date.now() })
1396
+ .onConflict(["subjectHash"])
1397
+ .doUpdate({ validFromEpoch: validFromExpr, updatedAt: "?" }, [Date.now()])
1398
+ .returning(["validFromEpoch"])
1399
+ .toSql();
1400
+ // The valid-from boundary is a FRAMEWORK table (FRAMEWORK_SCHEMA / cluster
1401
+ // DDL), NOT session data — it must execute against clusterStorage (the
1402
+ // framework db), never _currentStore(): a pluggable session-data store
1403
+ // (b.session.useStore / localDbThin) does not provision this table, so
1404
+ // routing the bump through it throws "no such table".
1405
+ var row;
1406
+ if (built.readbackSql) {
1407
+ // MySQL: ON DUPLICATE KEY UPDATE has no RETURNING — run the upsert, then
1408
+ // the readback SELECT b.sql emits (keyed on subjectHash).
1409
+ await clusterStorage.execute(built.sql, built.params);
1410
+ var readback = await clusterStorage.execute(built.readbackSql.sql, built.readbackSql.params);
1411
+ row = readback.rows && readback.rows[0];
1412
+ } else {
1413
+ var result = await clusterStorage.execute(built.sql, built.params);
1414
+ row = result.rows && result.rows[0];
1415
+ }
1416
+ var effective = row ? Number(row.validFromEpoch) : epochMs;
1417
+
1418
+ // Best-effort audit — matches the file's emit convention (safeEmit is
1419
+ // already drop-silent internally).
1420
+ try {
1421
+ audit.safeEmit({
1422
+ action: "auth.session.valid_from_bump",
1423
+ outcome: "success",
1424
+ metadata: { validFromEpoch: effective },
1425
+ });
1426
+ } catch (_ignored) { /* audit best-effort */ }
1427
+
1428
+ return effective;
1429
+ }
1430
+
1431
+ /**
1432
+ * @primitive b.session.validFrom
1433
+ * @signature b.session.validFrom(subjectId)
1434
+ * @since 0.15.13
1435
+ * @status stable
1436
+ * @related b.session.bump, b.session.check
1437
+ *
1438
+ * Read the current valid-from boundary (epoch ms) for a subject. Returns `0`
1439
+ * when the subject has never been bumped — no token is revoked by boundary, so
1440
+ * any non-negative token `iat` passes `b.session.check`. Runs anywhere (leader
1441
+ * or follower) — it only reads. The subject id is hashed before lookup; the
1442
+ * plaintext id never lands in the table.
1443
+ *
1444
+ * @example
1445
+ * var boundary = await b.session.validFrom("user-42");
1446
+ * // → 1735689600000 (last bump) or 0 (never bumped)
1447
+ */
1448
+ async function validFrom(subjectId) {
1449
+ if (typeof subjectId !== "string" || subjectId.length === 0) return 0;
1450
+ var built = sql.select(_validFromSqlTable(), _sessionSqlOpts())
1451
+ .columns(["validFromEpoch"])
1452
+ .where("subjectHash", _hashSubjectId(subjectId))
1453
+ .toSql();
1454
+ // Framework valid-from table — read from clusterStorage (the framework db),
1455
+ // not _currentStore(), so a pluggable session-data store cannot divert it (it
1456
+ // does not provision this table). See bump() for the full rationale.
1457
+ var row = await clusterStorage.executeOne(built.sql, built.params);
1458
+ return row ? Number(row.validFromEpoch) : 0;
1459
+ }
1460
+
1461
+ /**
1462
+ * @primitive b.session.check
1463
+ * @signature b.session.check(subjectId, tokenIatMs)
1464
+ * @since 0.15.13
1465
+ * @status stable
1466
+ * @related b.session.bump, b.session.validFrom
1467
+ *
1468
+ * Decide whether a stateless self-validating token is still valid against the
1469
+ * subject's valid-from boundary. Returns `true` when the token's issued-at
1470
+ * (`tokenIatMs`, epoch ms) is at or after the boundary; `false` when the token
1471
+ * was issued before the last `bump` (revoked). A subject that was never bumped
1472
+ * has boundary `0`, so any non-negative `iat` is valid. Runs anywhere.
1473
+ *
1474
+ * Fails CLOSED: a non-finite / negative / non-number `tokenIatMs` returns
1475
+ * `false` (treat an unparseable token as revoked rather than admit it). Call
1476
+ * this AFTER the token's own signature + expiry checks pass — it is the
1477
+ * server-side revocation layer those stateless checks otherwise lack.
1478
+ *
1479
+ * @example
1480
+ * // jwt already signature- and exp-verified; iat is in seconds → ms:
1481
+ * var ok = await b.session.check(claims.sub, claims.iat * 1000);
1482
+ * if (!ok) { res.statusCode = 401; res.end("session revoked"); return; }
1483
+ */
1484
+ async function check(subjectId, tokenIatMs) {
1485
+ if (typeof tokenIatMs !== "number" || !isFinite(tokenIatMs) || tokenIatMs < 0) {
1486
+ return false;
1487
+ }
1488
+ var boundary = await validFrom(subjectId);
1489
+ return tokenIatMs >= boundary;
1490
+ }
1491
+
1307
1492
  module.exports = {
1308
1493
  create: create,
1309
1494
  verify: verify,
@@ -1315,6 +1500,9 @@ module.exports = {
1315
1500
  updateData: updateData,
1316
1501
  purgeExpired: purgeExpired,
1317
1502
  count: count,
1503
+ bump: bump,
1504
+ validFrom: validFrom,
1505
+ check: check,
1318
1506
  useStore: useStore,
1319
1507
  isAnonymous: isAnonymous,
1320
1508
  stores: require("./session-stores"), // allow:inline-require — session-stores depends on local-db-thin which requires audit lazily; eager require is fine here