@blamejs/core 0.15.12 → 0.15.14

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 (365) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/index.js +2 -0
  3. package/lib/a2a-tasks.js +45 -29
  4. package/lib/acme.js +6 -5
  5. package/lib/agent-event-bus.js +18 -4
  6. package/lib/agent-idempotency.js +7 -6
  7. package/lib/agent-orchestrator.js +2 -5
  8. package/lib/agent-saga.js +3 -5
  9. package/lib/agent-snapshot.js +32 -2
  10. package/lib/agent-tenant.js +2 -5
  11. package/lib/ai-adverse-decision.js +2 -15
  12. package/lib/ai-aedt-bias-audit.js +2 -1
  13. package/lib/ai-capability.js +1 -6
  14. package/lib/ai-content-detect.js +1 -3
  15. package/lib/ai-dp.js +1 -5
  16. package/lib/ai-frontier-protocol.js +1 -1
  17. package/lib/ai-input.js +2 -2
  18. package/lib/ai-model-manifest.js +1 -1
  19. package/lib/ai-output.js +16 -7
  20. package/lib/ai-pref.js +3 -8
  21. package/lib/ai-quota.js +3 -14
  22. package/lib/api-key.js +37 -28
  23. package/lib/api-snapshot.js +4 -1
  24. package/lib/app-shutdown.js +7 -1
  25. package/lib/archive-adapters.js +2 -4
  26. package/lib/archive-entry-policy.js +32 -0
  27. package/lib/archive-gz.js +9 -0
  28. package/lib/archive-read.js +14 -24
  29. package/lib/archive-tar-read.js +56 -24
  30. package/lib/archive.js +6 -12
  31. package/lib/arg-parser.js +7 -6
  32. package/lib/asn1-der.js +70 -22
  33. package/lib/asyncapi-traits.js +2 -6
  34. package/lib/atomic-file.js +312 -33
  35. package/lib/audit-chain.js +183 -54
  36. package/lib/audit-daily-review.js +36 -16
  37. package/lib/audit-emit.js +82 -0
  38. package/lib/audit-sign.js +258 -0
  39. package/lib/audit-tools.js +108 -23
  40. package/lib/audit.js +91 -2
  41. package/lib/auth/access-lock.js +7 -29
  42. package/lib/auth/bot-challenge.js +1 -1
  43. package/lib/auth/ciba.js +43 -4
  44. package/lib/auth/dpop.js +29 -36
  45. package/lib/auth/fido-mds3.js +14 -16
  46. package/lib/auth/jwt-external.js +26 -8
  47. package/lib/auth/jwt.js +15 -17
  48. package/lib/auth/lockout.js +24 -27
  49. package/lib/auth/oauth.js +67 -45
  50. package/lib/auth/oid4vci.js +55 -32
  51. package/lib/auth/oid4vp.js +3 -2
  52. package/lib/auth/openid-federation.js +6 -6
  53. package/lib/auth/passkey.js +4 -4
  54. package/lib/auth/password.js +8 -2
  55. package/lib/auth/saml.js +70 -40
  56. package/lib/auth/sd-jwt-vc-holder.js +2 -14
  57. package/lib/auth/sd-jwt-vc-issuer.js +2 -14
  58. package/lib/auth/sd-jwt-vc.js +25 -5
  59. package/lib/auth/status-list.js +21 -9
  60. package/lib/auth/step-up.js +15 -13
  61. package/lib/auth-bot-challenge.js +27 -39
  62. package/lib/backup/bundle.js +18 -20
  63. package/lib/backup/crypto.js +23 -8
  64. package/lib/backup/index.js +55 -67
  65. package/lib/backup/manifest.js +7 -1
  66. package/lib/bounded-map.js +112 -1
  67. package/lib/breach-deadline.js +1 -11
  68. package/lib/break-glass.js +41 -22
  69. package/lib/cache.js +7 -18
  70. package/lib/cbor.js +36 -12
  71. package/lib/cdn-cache-control.js +15 -12
  72. package/lib/cert.js +8 -13
  73. package/lib/chain-writer.js +162 -47
  74. package/lib/cli.js +10 -2
  75. package/lib/client-hints.js +7 -9
  76. package/lib/cloud-events.js +43 -33
  77. package/lib/cluster-storage.js +9 -41
  78. package/lib/cms-codec.js +2 -1
  79. package/lib/codepoint-class.js +84 -1
  80. package/lib/compliance-ai-act-logging.js +1 -6
  81. package/lib/compliance-ai-act-transparency.js +2 -4
  82. package/lib/compliance-eaa.js +2 -12
  83. package/lib/compliance-sanctions-fetcher.js +21 -28
  84. package/lib/compliance-sanctions.js +11 -21
  85. package/lib/compliance.js +3 -10
  86. package/lib/config-drift.js +24 -20
  87. package/lib/content-credentials.js +11 -8
  88. package/lib/content-digest.js +21 -12
  89. package/lib/cookies.js +15 -13
  90. package/lib/cose.js +39 -11
  91. package/lib/cra-report.js +1 -11
  92. package/lib/crdt.js +2 -1
  93. package/lib/crypto-field.js +34 -33
  94. package/lib/crypto.js +138 -7
  95. package/lib/csp.js +239 -3
  96. package/lib/daemon.js +46 -42
  97. package/lib/data-act.js +46 -13
  98. package/lib/db-file-lifecycle.js +14 -6
  99. package/lib/db-query.js +75 -49
  100. package/lib/db.js +66 -27
  101. package/lib/dbsc.js +5 -6
  102. package/lib/ddl-change-control.js +18 -14
  103. package/lib/deprecate.js +4 -5
  104. package/lib/did.js +6 -2
  105. package/lib/dora.js +43 -13
  106. package/lib/dr-runbook.js +1 -1
  107. package/lib/dsa.js +2 -1
  108. package/lib/dsr.js +30 -20
  109. package/lib/early-hints.js +19 -0
  110. package/lib/eat.js +5 -1
  111. package/lib/external-db-migrate.js +21 -22
  112. package/lib/external-db.js +74 -30
  113. package/lib/fda-21cfr11.js +42 -13
  114. package/lib/fdx.js +22 -18
  115. package/lib/file-upload.js +80 -66
  116. package/lib/flag-evaluation-context.js +5 -8
  117. package/lib/flag-providers.js +6 -2
  118. package/lib/forms.js +1 -1
  119. package/lib/framework-error.js +12 -0
  120. package/lib/framework-schema.js +19 -0
  121. package/lib/fsm.js +7 -12
  122. package/lib/gate-contract.js +911 -39
  123. package/lib/gdpr-ropa.js +22 -22
  124. package/lib/graphql-federation.js +18 -5
  125. package/lib/guard-agent-registry.js +2 -1
  126. package/lib/guard-all.js +3 -2
  127. package/lib/guard-archive.js +9 -30
  128. package/lib/guard-auth.js +23 -80
  129. package/lib/guard-cidr.js +20 -96
  130. package/lib/guard-csv.js +135 -196
  131. package/lib/guard-domain.js +23 -106
  132. package/lib/guard-dsn.js +17 -14
  133. package/lib/guard-email.js +46 -53
  134. package/lib/guard-envelope.js +2 -2
  135. package/lib/guard-event-bus-topic.js +2 -1
  136. package/lib/guard-filename.js +12 -60
  137. package/lib/guard-graphql.js +28 -75
  138. package/lib/guard-html-wcag.js +15 -2
  139. package/lib/guard-html.js +74 -128
  140. package/lib/guard-idempotency-key.js +2 -1
  141. package/lib/guard-image.js +280 -77
  142. package/lib/guard-imap-command.js +9 -10
  143. package/lib/guard-jmap.js +1 -1
  144. package/lib/guard-json.js +101 -109
  145. package/lib/guard-jsonpath.js +20 -88
  146. package/lib/guard-jwt.js +32 -114
  147. package/lib/guard-list-id.js +2 -7
  148. package/lib/guard-list-unsubscribe.js +2 -7
  149. package/lib/guard-mail-compose.js +5 -6
  150. package/lib/guard-mail-move.js +3 -2
  151. package/lib/guard-mail-query.js +5 -3
  152. package/lib/guard-mail-sieve.js +6 -7
  153. package/lib/guard-managesieve-command.js +6 -5
  154. package/lib/guard-markdown.js +76 -110
  155. package/lib/guard-message-id.js +5 -6
  156. package/lib/guard-mime.js +20 -99
  157. package/lib/guard-oauth.js +19 -73
  158. package/lib/guard-pdf.js +65 -72
  159. package/lib/guard-pop3-command.js +13 -14
  160. package/lib/guard-posture-chain.js +2 -1
  161. package/lib/guard-regex.js +24 -99
  162. package/lib/guard-saga-config.js +2 -1
  163. package/lib/guard-shell.js +22 -87
  164. package/lib/guard-smtp-command.js +9 -12
  165. package/lib/guard-sql.js +15 -13
  166. package/lib/guard-stream-args.js +2 -1
  167. package/lib/guard-svg.js +103 -149
  168. package/lib/guard-template.js +23 -80
  169. package/lib/guard-tenant-id.js +2 -1
  170. package/lib/guard-text.js +592 -0
  171. package/lib/guard-time.js +27 -95
  172. package/lib/guard-uuid.js +21 -93
  173. package/lib/guard-xml.js +76 -106
  174. package/lib/guard-yaml.js +24 -60
  175. package/lib/html-balance.js +7 -3
  176. package/lib/http-client-cache.js +5 -12
  177. package/lib/http-client-cookie-jar.js +35 -16
  178. package/lib/http-client.js +233 -74
  179. package/lib/http-message-signature.js +3 -2
  180. package/lib/i18n-messageformat.js +5 -7
  181. package/lib/i18n.js +83 -26
  182. package/lib/iab-tcf.js +3 -2
  183. package/lib/importmap-integrity.js +41 -1
  184. package/lib/inbox.js +8 -8
  185. package/lib/incident-report.js +12 -27
  186. package/lib/ip-utils.js +49 -6
  187. package/lib/jobs.js +3 -2
  188. package/lib/json-patch.js +1 -1
  189. package/lib/json-path.js +24 -3
  190. package/lib/jtd.js +2 -2
  191. package/lib/keychain.js +6 -18
  192. package/lib/legal-hold.js +30 -23
  193. package/lib/log-stream-cloudwatch.js +44 -112
  194. package/lib/log-stream-otlp-grpc.js +17 -14
  195. package/lib/log-stream-otlp.js +16 -80
  196. package/lib/log-stream-webhook.js +20 -92
  197. package/lib/log.js +2 -2
  198. package/lib/mail-agent.js +2 -2
  199. package/lib/mail-arc-sign.js +8 -3
  200. package/lib/mail-arf.js +4 -3
  201. package/lib/mail-auth.js +43 -69
  202. package/lib/mail-bimi.js +35 -55
  203. package/lib/mail-bounce.js +3 -3
  204. package/lib/mail-crypto-pgp.js +3 -2
  205. package/lib/mail-crypto-smime.js +71 -6
  206. package/lib/mail-dav.js +8 -35
  207. package/lib/mail-deploy.js +15 -14
  208. package/lib/mail-dkim.js +19 -38
  209. package/lib/mail-greylist.js +17 -30
  210. package/lib/mail-helo.js +4 -7
  211. package/lib/mail-journal.js +13 -9
  212. package/lib/mail-mdn.js +12 -7
  213. package/lib/mail-rbl.js +7 -12
  214. package/lib/mail-scan.js +5 -7
  215. package/lib/mail-send-deliver.js +33 -20
  216. package/lib/mail-server-imap.js +32 -87
  217. package/lib/mail-server-jmap.js +39 -52
  218. package/lib/mail-server-managesieve.js +29 -83
  219. package/lib/mail-server-mx.js +33 -74
  220. package/lib/mail-server-net.js +177 -0
  221. package/lib/mail-server-pop3.js +30 -83
  222. package/lib/mail-server-rate-limit.js +30 -6
  223. package/lib/mail-server-registry.js +1 -1
  224. package/lib/mail-server-submission.js +34 -73
  225. package/lib/mail-server-tls.js +98 -2
  226. package/lib/mail-spam-score.js +9 -12
  227. package/lib/mail-store-fts.js +1 -1
  228. package/lib/mail-store.js +3 -2
  229. package/lib/mail.js +28 -13
  230. package/lib/markup-escape.js +31 -0
  231. package/lib/markup-tokenizer.js +78 -0
  232. package/lib/mcp-tool-registry.js +26 -23
  233. package/lib/mcp.js +7 -5
  234. package/lib/mdoc.js +36 -14
  235. package/lib/metrics.js +46 -33
  236. package/lib/middleware/age-gate.js +3 -23
  237. package/lib/middleware/api-encrypt.js +13 -24
  238. package/lib/middleware/assetlinks.js +2 -7
  239. package/lib/middleware/bearer-auth.js +2 -1
  240. package/lib/middleware/body-parser.js +41 -52
  241. package/lib/middleware/bot-disclose.js +7 -10
  242. package/lib/middleware/bot-guard.js +28 -28
  243. package/lib/middleware/clear-site-data.js +5 -1
  244. package/lib/middleware/compression.js +9 -0
  245. package/lib/middleware/cors.js +32 -23
  246. package/lib/middleware/csrf-protect.js +73 -23
  247. package/lib/middleware/daily-byte-quota.js +12 -30
  248. package/lib/middleware/deny-response.js +19 -1
  249. package/lib/middleware/fetch-metadata.js +35 -4
  250. package/lib/middleware/idempotency-key.js +4 -20
  251. package/lib/middleware/network-allowlist.js +61 -30
  252. package/lib/middleware/rate-limit.js +45 -44
  253. package/lib/middleware/scim-server.js +5 -3
  254. package/lib/middleware/security-headers.js +33 -7
  255. package/lib/middleware/security-txt.js +2 -6
  256. package/lib/middleware/speculation-rules.js +6 -3
  257. package/lib/middleware/tus-upload.js +14 -29
  258. package/lib/middleware/web-app-manifest.js +2 -7
  259. package/lib/migrations.js +4 -13
  260. package/lib/mime-parse.js +34 -17
  261. package/lib/module-loader.js +44 -0
  262. package/lib/money.js +105 -0
  263. package/lib/mtls-ca.js +125 -41
  264. package/lib/network-byte-quota.js +4 -18
  265. package/lib/network-dane.js +8 -6
  266. package/lib/network-dns-resolver.js +98 -7
  267. package/lib/network-dns.js +9 -2
  268. package/lib/network-dnssec.js +18 -17
  269. package/lib/network-heartbeat.js +3 -2
  270. package/lib/network-smtp-policy.js +52 -46
  271. package/lib/network-tls.js +67 -47
  272. package/lib/network-tsig.js +2 -2
  273. package/lib/nis2-report.js +2 -12
  274. package/lib/nist-crosswalk.js +1 -1
  275. package/lib/nonce-store.js +81 -3
  276. package/lib/ntp-check.js +28 -0
  277. package/lib/numeric-bounds.js +31 -8
  278. package/lib/object-store/azure-blob-bucket-ops.js +2 -6
  279. package/lib/object-store/azure-blob.js +6 -47
  280. package/lib/object-store/gcs-bucket-ops.js +4 -2
  281. package/lib/object-store/gcs.js +14 -75
  282. package/lib/object-store/http-put.js +2 -7
  283. package/lib/object-store/http-request.js +157 -0
  284. package/lib/object-store/local.js +37 -17
  285. package/lib/object-store/sigv4-bucket-ops.js +2 -1
  286. package/lib/object-store/sigv4.js +10 -79
  287. package/lib/observability-otlp-exporter.js +29 -29
  288. package/lib/observability.js +95 -0
  289. package/lib/openapi-paths-builder.js +7 -3
  290. package/lib/otel-export.js +7 -10
  291. package/lib/outbox.js +54 -41
  292. package/lib/pagination.js +11 -15
  293. package/lib/parsers/safe-env.js +5 -4
  294. package/lib/parsers/safe-ini.js +10 -4
  295. package/lib/parsers/safe-toml.js +11 -9
  296. package/lib/parsers/safe-xml.js +3 -3
  297. package/lib/parsers/safe-yaml.js +28 -9
  298. package/lib/pick.js +63 -5
  299. package/lib/pipl-cn.js +69 -59
  300. package/lib/privacy-pass.js +8 -6
  301. package/lib/problem-details.js +2 -5
  302. package/lib/pubsub.js +4 -8
  303. package/lib/queue-local.js +10 -3
  304. package/lib/redact.js +9 -4
  305. package/lib/render.js +4 -2
  306. package/lib/request-helpers.js +334 -29
  307. package/lib/resource-access-lock.js +3 -3
  308. package/lib/restore-bundle.js +46 -18
  309. package/lib/restore-rollback.js +10 -4
  310. package/lib/restore.js +24 -22
  311. package/lib/retention.js +23 -9
  312. package/lib/router.js +17 -4
  313. package/lib/safe-async.js +457 -0
  314. package/lib/safe-buffer.js +137 -6
  315. package/lib/safe-decompress.js +2 -1
  316. package/lib/safe-dns.js +2 -1
  317. package/lib/safe-ical.js +14 -28
  318. package/lib/safe-icap.js +1 -1
  319. package/lib/safe-json.js +51 -8
  320. package/lib/safe-jsonpath.js +6 -5
  321. package/lib/safe-mime.js +25 -29
  322. package/lib/safe-redirect.js +2 -5
  323. package/lib/safe-schema.js +7 -6
  324. package/lib/safe-sieve.js +1 -1
  325. package/lib/safe-sql.js +126 -0
  326. package/lib/safe-vcard.js +13 -27
  327. package/lib/sandbox-worker.js +15 -2
  328. package/lib/sandbox.js +4 -3
  329. package/lib/scheduler.js +22 -13
  330. package/lib/sec-cyber.js +82 -37
  331. package/lib/seeders.js +4 -11
  332. package/lib/self-update-standalone-verifier.js +16 -0
  333. package/lib/self-update.js +92 -69
  334. package/lib/session-device-binding.js +112 -66
  335. package/lib/session.js +219 -7
  336. package/lib/sql.js +228 -81
  337. package/lib/sse.js +6 -10
  338. package/lib/static.js +201 -111
  339. package/lib/storage.js +4 -2
  340. package/lib/structured-fields.js +275 -16
  341. package/lib/template.js +7 -5
  342. package/lib/tenant-quota.js +53 -38
  343. package/lib/tsa.js +16 -17
  344. package/lib/validate-opts.js +154 -8
  345. package/lib/vault/index.js +5 -0
  346. package/lib/vault/passphrase-ops.js +22 -26
  347. package/lib/vault/passphrase-source.js +8 -3
  348. package/lib/vault/rotate.js +13 -18
  349. package/lib/vault/seal-pem-file.js +34 -49
  350. package/lib/vault-aad.js +3 -2
  351. package/lib/vc.js +3 -8
  352. package/lib/vendor/MANIFEST.json +10 -10
  353. package/lib/vendor/public-suffix-list.dat +23 -10
  354. package/lib/vendor/public-suffix-list.data.js +5498 -5494
  355. package/lib/vex.js +35 -13
  356. package/lib/web-push-vapid.js +23 -20
  357. package/lib/webhook-dispatcher.js +706 -0
  358. package/lib/webhook.js +59 -19
  359. package/lib/websocket-channels.js +9 -7
  360. package/lib/websocket.js +8 -4
  361. package/lib/worm.js +3 -4
  362. package/lib/ws-client.js +65 -56
  363. package/lib/x509-chain.js +44 -0
  364. package/package.json +1 -1
  365. 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).
@@ -810,8 +843,16 @@ async function destroyAllForUser(userId) {
810
843
  // userId is sealed; look up via derived userIdHash.
811
844
  var lookup = cryptoField.lookupHash(SESSION_TABLE, "userId", userId);
812
845
  if (!lookup) {
846
+ // The session table's userIdHash derived-hash schema is registered during
847
+ // b.db.init(). A pluggable-store consumer (b.session.useStore) who never
848
+ // called b.db.init() lands here first — surface that, not just an opaque
849
+ // "framework misconfigured", since destroyAllForUser still needs b.db for
850
+ // the userIdHash index + the stateless valid-from boundary.
813
851
  throw _err("MISCONFIGURED",
814
- "the session table schema is missing the userIdHash derived hash framework misconfigured",
852
+ "session.destroyAllForUser: the session table's userIdHash derived-hash schema is " +
853
+ "not registered. It is registered during b.db.init() — call b.db.init() at boot even " +
854
+ "when session data lives in a pluggable store (b.session.useStore). If b.db is already " +
855
+ "initialized, the session table schema is misconfigured.",
815
856
  true);
816
857
  }
817
858
  // Dual-read across the keyed-MAC flip: a pre-v0.15.0 session row carries
@@ -825,6 +866,26 @@ async function destroyAllForUser(userId) {
825
866
  .whereIn("userIdHash", userHashes)
826
867
  .toSql();
827
868
  var result = await _currentStore().execute(built.sql, built.params);
869
+ // Also raise the stateless valid-from boundary so a "logout everywhere"
870
+ // revokes the operator's stateless tokens (sealed cookies / JWTs checked via
871
+ // b.session.check) too, not only the store-backed rows just deleted. This
872
+ // bump writes to the FRAMEWORK db (clusterStorage); a pluggable-store consumer
873
+ // (b.session.useStore) who never ran b.db.init() would otherwise surface the
874
+ // opaque "db/not-initialized" here, after the store delete already succeeded.
875
+ // Rethrow it as a session-specific, actionable error.
876
+ try {
877
+ await bump(userId);
878
+ } catch (e) {
879
+ if (e && e.code === "db/not-initialized") {
880
+ throw _err("MISCONFIGURED",
881
+ "session.destroyAllForUser raises the stateless valid-from boundary (so a " +
882
+ "logout-everywhere also revokes sealed-cookie / JWT sessions), which requires " +
883
+ "b.db.init() — call it at boot even when session data lives in a pluggable " +
884
+ "store (b.session.useStore). The store-backed rows were already deleted; rerun " +
885
+ "after b.db.init() to also raise the stateless boundary.", true);
886
+ }
887
+ throw e;
888
+ }
828
889
  return result.rowCount || 0;
829
890
  }
830
891
 
@@ -1272,12 +1333,8 @@ function useStore(store) {
1272
1333
  _store = null;
1273
1334
  return;
1274
1335
  }
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
- }
1336
+ validateOpts.requireMethods(store, ["execute", "executeOne"],
1337
+ "session.useStore: store", SessionError, "INVALID_ARG", true);
1281
1338
  _store = store;
1282
1339
  }
1283
1340
 
@@ -1304,6 +1361,158 @@ function isAnonymous(userId) {
1304
1361
  return _isAnonymousUserId(userId);
1305
1362
  }
1306
1363
 
1364
+ /**
1365
+ * @primitive b.session.bump
1366
+ * @signature b.session.bump(subjectId, opts?)
1367
+ * @since 0.15.13
1368
+ * @status stable
1369
+ * @related b.session.check, b.session.validFrom, b.session.destroyAllForUser
1370
+ *
1371
+ * Revoke every STATELESS self-validating token (sealed cookie carrying no DB
1372
+ * row, JWT) for a subject by raising a durable per-subject valid-from boundary
1373
+ * to now. Any token whose issued-at (`iat`) predates the boundary fails
1374
+ * `b.session.check`. Unlike `destroyAllForUser` — which deletes server-side
1375
+ * session rows — this revokes tokens the framework never stored a row for:
1376
+ * log-out-everywhere, a right-to-erasure cutoff, a forced re-auth after a
1377
+ * password / key change. `destroyAllForUser` calls this for you, so a single
1378
+ * "logout everywhere" covers both store-backed and stateless tokens.
1379
+ *
1380
+ * The boundary is MONOTONIC: it only ever moves forward. A bump to an
1381
+ * `epochMs` at or below the stored value is a no-op — a replayed or
1382
+ * clock-skewed lower value can never widen a revoked window back open. Returns
1383
+ * the boundary in effect after the call. Leader-only. The subject id is stored
1384
+ * hashed; the plaintext id never lands in the table.
1385
+ *
1386
+ * @opts
1387
+ * epochMs: number, // boundary to set; default Date.now(). Tokens with iat < this are revoked.
1388
+ *
1389
+ * @example
1390
+ * // Force re-auth everywhere for a subject after a password change:
1391
+ * var boundary = await b.session.bump("user-42");
1392
+ * // Cut off at a specific instant (right-to-erasure effective time):
1393
+ * await b.session.bump("user-42", { epochMs: erasureEffectiveMs });
1394
+ */
1395
+ async function bump(subjectId, opts) {
1396
+ cluster.requireLeader();
1397
+ if (typeof subjectId !== "string" || subjectId.length === 0) {
1398
+ throw _err("INVALID_ARG", "session.bump requires a non-empty subjectId", true);
1399
+ }
1400
+ opts = opts || {};
1401
+ var epochMs = opts.epochMs === undefined ? Date.now() : opts.epochMs;
1402
+ if (typeof epochMs !== "number" || !isFinite(epochMs) || epochMs < 0) {
1403
+ throw _err("INVALID_ARG",
1404
+ "session.bump: epochMs must be a non-negative finite number, got " + JSON.stringify(epochMs), true);
1405
+ }
1406
+ var subjectHash = _hashSubjectId(subjectId);
1407
+ var t = _validFromSqlTable();
1408
+ var dialect = clusterStorage.dialect();
1409
+ var refs = _validFromConflictRefs(dialect, t);
1410
+
1411
+ // Monotonic-max conflict action: keep the LATER of the proposed and the
1412
+ // stored boundary, so a lower (replayed / clock-skewed) epoch can never move
1413
+ // the boundary backwards and re-open a revoked window.
1414
+ var validFromExpr = "CASE WHEN " + refs.proposed("validFromEpoch") + " > " +
1415
+ refs.existing("validFromEpoch") + " THEN " + refs.proposed("validFromEpoch") +
1416
+ " ELSE " + refs.existing("validFromEpoch") + " END";
1417
+ var built = sql.upsert(t, _sessionSqlOpts())
1418
+ .columns(["subjectHash", "validFromEpoch", "updatedAt"])
1419
+ .values({ subjectHash: subjectHash, validFromEpoch: epochMs, updatedAt: Date.now() })
1420
+ .onConflict(["subjectHash"])
1421
+ .doUpdate({ validFromEpoch: validFromExpr, updatedAt: "?" }, [Date.now()])
1422
+ .returning(["validFromEpoch"])
1423
+ .toSql();
1424
+ // The valid-from boundary is a FRAMEWORK table (FRAMEWORK_SCHEMA / cluster
1425
+ // DDL), NOT session data — it must execute against clusterStorage (the
1426
+ // framework db), never _currentStore(): a pluggable session-data store
1427
+ // (b.session.useStore / localDbThin) does not provision this table, so
1428
+ // routing the bump through it throws "no such table".
1429
+ var row;
1430
+ if (built.readbackSql) {
1431
+ // MySQL: ON DUPLICATE KEY UPDATE has no RETURNING — run the upsert, then
1432
+ // the readback SELECT b.sql emits (keyed on subjectHash).
1433
+ await clusterStorage.execute(built.sql, built.params);
1434
+ var readback = await clusterStorage.execute(built.readbackSql.sql, built.readbackSql.params);
1435
+ row = readback.rows && readback.rows[0];
1436
+ } else {
1437
+ var result = await clusterStorage.execute(built.sql, built.params);
1438
+ row = result.rows && result.rows[0];
1439
+ }
1440
+ var effective = row ? Number(row.validFromEpoch) : epochMs;
1441
+
1442
+ // Best-effort audit — matches the file's emit convention (safeEmit is
1443
+ // already drop-silent internally).
1444
+ try {
1445
+ audit.safeEmit({
1446
+ action: "auth.session.valid_from_bump",
1447
+ outcome: "success",
1448
+ metadata: { validFromEpoch: effective },
1449
+ });
1450
+ } catch (_ignored) { /* audit best-effort */ }
1451
+
1452
+ return effective;
1453
+ }
1454
+
1455
+ /**
1456
+ * @primitive b.session.validFrom
1457
+ * @signature b.session.validFrom(subjectId)
1458
+ * @since 0.15.13
1459
+ * @status stable
1460
+ * @related b.session.bump, b.session.check
1461
+ *
1462
+ * Read the current valid-from boundary (epoch ms) for a subject. Returns `0`
1463
+ * when the subject has never been bumped — no token is revoked by boundary, so
1464
+ * any non-negative token `iat` passes `b.session.check`. Runs anywhere (leader
1465
+ * or follower) — it only reads. The subject id is hashed before lookup; the
1466
+ * plaintext id never lands in the table.
1467
+ *
1468
+ * @example
1469
+ * var boundary = await b.session.validFrom("user-42");
1470
+ * // → 1735689600000 (last bump) or 0 (never bumped)
1471
+ */
1472
+ async function validFrom(subjectId) {
1473
+ if (typeof subjectId !== "string" || subjectId.length === 0) return 0;
1474
+ var built = sql.select(_validFromSqlTable(), _sessionSqlOpts())
1475
+ .columns(["validFromEpoch"])
1476
+ .where("subjectHash", _hashSubjectId(subjectId))
1477
+ .toSql();
1478
+ // Framework valid-from table — read from clusterStorage (the framework db),
1479
+ // not _currentStore(), so a pluggable session-data store cannot divert it (it
1480
+ // does not provision this table). See bump() for the full rationale.
1481
+ var row = await clusterStorage.executeOne(built.sql, built.params);
1482
+ return row ? Number(row.validFromEpoch) : 0;
1483
+ }
1484
+
1485
+ /**
1486
+ * @primitive b.session.check
1487
+ * @signature b.session.check(subjectId, tokenIatMs)
1488
+ * @since 0.15.13
1489
+ * @status stable
1490
+ * @related b.session.bump, b.session.validFrom
1491
+ *
1492
+ * Decide whether a stateless self-validating token is still valid against the
1493
+ * subject's valid-from boundary. Returns `true` when the token's issued-at
1494
+ * (`tokenIatMs`, epoch ms) is at or after the boundary; `false` when the token
1495
+ * was issued before the last `bump` (revoked). A subject that was never bumped
1496
+ * has boundary `0`, so any non-negative `iat` is valid. Runs anywhere.
1497
+ *
1498
+ * Fails CLOSED: a non-finite / negative / non-number `tokenIatMs` returns
1499
+ * `false` (treat an unparseable token as revoked rather than admit it). Call
1500
+ * this AFTER the token's own signature + expiry checks pass — it is the
1501
+ * server-side revocation layer those stateless checks otherwise lack.
1502
+ *
1503
+ * @example
1504
+ * // jwt already signature- and exp-verified; iat is in seconds → ms:
1505
+ * var ok = await b.session.check(claims.sub, claims.iat * 1000);
1506
+ * if (!ok) { res.statusCode = 401; res.end("session revoked"); return; }
1507
+ */
1508
+ async function check(subjectId, tokenIatMs) {
1509
+ if (typeof tokenIatMs !== "number" || !isFinite(tokenIatMs) || tokenIatMs < 0) {
1510
+ return false;
1511
+ }
1512
+ var boundary = await validFrom(subjectId);
1513
+ return tokenIatMs >= boundary;
1514
+ }
1515
+
1307
1516
  module.exports = {
1308
1517
  create: create,
1309
1518
  verify: verify,
@@ -1315,6 +1524,9 @@ module.exports = {
1315
1524
  updateData: updateData,
1316
1525
  purgeExpired: purgeExpired,
1317
1526
  count: count,
1527
+ bump: bump,
1528
+ validFrom: validFrom,
1529
+ check: check,
1318
1530
  useStore: useStore,
1319
1531
  isAnonymous: isAnonymous,
1320
1532
  stores: require("./session-stores"), // allow:inline-require — session-stores depends on local-db-thin which requires audit lazily; eager require is fine here