@blamejs/core 0.8.42 → 0.8.49

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 (222) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/README.md +10 -10
  3. package/index.js +52 -0
  4. package/lib/a2a.js +159 -34
  5. package/lib/acme.js +762 -0
  6. package/lib/ai-pref.js +166 -43
  7. package/lib/api-key.js +108 -47
  8. package/lib/api-snapshot.js +157 -40
  9. package/lib/app-shutdown.js +113 -77
  10. package/lib/archive.js +337 -40
  11. package/lib/arg-parser.js +697 -0
  12. package/lib/asyncapi.js +99 -55
  13. package/lib/atomic-file.js +465 -104
  14. package/lib/audit-chain.js +123 -34
  15. package/lib/audit-daily-review.js +389 -0
  16. package/lib/audit-sign.js +302 -56
  17. package/lib/audit-tools.js +412 -63
  18. package/lib/audit.js +656 -35
  19. package/lib/auth/jwt-external.js +17 -0
  20. package/lib/auth/oauth.js +7 -0
  21. package/lib/auth-bot-challenge.js +505 -0
  22. package/lib/auth-header.js +92 -25
  23. package/lib/backup/bundle.js +26 -0
  24. package/lib/backup/index.js +512 -89
  25. package/lib/backup/manifest.js +168 -7
  26. package/lib/break-glass.js +415 -39
  27. package/lib/budr.js +103 -30
  28. package/lib/bundler.js +86 -66
  29. package/lib/cache.js +192 -72
  30. package/lib/chain-writer.js +65 -40
  31. package/lib/circuit-breaker.js +56 -33
  32. package/lib/cli-helpers.js +106 -75
  33. package/lib/cli.js +6 -30
  34. package/lib/cloud-events.js +99 -32
  35. package/lib/cluster-storage.js +162 -37
  36. package/lib/cluster.js +340 -49
  37. package/lib/codepoint-class.js +66 -0
  38. package/lib/compliance.js +424 -24
  39. package/lib/config-drift.js +111 -46
  40. package/lib/config.js +94 -40
  41. package/lib/consent.js +165 -18
  42. package/lib/constants.js +1 -0
  43. package/lib/content-credentials.js +153 -48
  44. package/lib/cookies.js +154 -62
  45. package/lib/credential-hash.js +133 -61
  46. package/lib/crypto-field.js +702 -18
  47. package/lib/crypto-hpke.js +256 -0
  48. package/lib/crypto.js +744 -22
  49. package/lib/csv.js +178 -35
  50. package/lib/daemon.js +456 -0
  51. package/lib/dark-patterns.js +186 -55
  52. package/lib/db-query.js +79 -2
  53. package/lib/db.js +1431 -60
  54. package/lib/ddl-change-control.js +523 -0
  55. package/lib/deprecate.js +195 -40
  56. package/lib/dev.js +82 -39
  57. package/lib/dora.js +67 -48
  58. package/lib/dr-runbook.js +368 -0
  59. package/lib/dsr.js +142 -11
  60. package/lib/dual-control.js +91 -56
  61. package/lib/events.js +120 -41
  62. package/lib/external-db-migrate.js +192 -2
  63. package/lib/external-db.js +795 -50
  64. package/lib/fapi2.js +122 -1
  65. package/lib/fda-21cfr11.js +395 -0
  66. package/lib/fdx.js +132 -2
  67. package/lib/file-type.js +87 -0
  68. package/lib/file-upload.js +93 -0
  69. package/lib/flag.js +82 -20
  70. package/lib/forms.js +132 -29
  71. package/lib/framework-error.js +169 -0
  72. package/lib/framework-schema.js +163 -35
  73. package/lib/gate-contract.js +849 -175
  74. package/lib/graphql-federation.js +68 -7
  75. package/lib/guard-all.js +172 -55
  76. package/lib/guard-archive.js +286 -124
  77. package/lib/guard-auth.js +194 -21
  78. package/lib/guard-cidr.js +190 -28
  79. package/lib/guard-csv.js +397 -51
  80. package/lib/guard-domain.js +213 -91
  81. package/lib/guard-email.js +236 -29
  82. package/lib/guard-filename.js +307 -75
  83. package/lib/guard-graphql.js +263 -30
  84. package/lib/guard-html.js +310 -116
  85. package/lib/guard-image.js +243 -30
  86. package/lib/guard-json.js +260 -54
  87. package/lib/guard-jsonpath.js +235 -23
  88. package/lib/guard-jwt.js +284 -30
  89. package/lib/guard-markdown.js +204 -22
  90. package/lib/guard-mime.js +190 -26
  91. package/lib/guard-oauth.js +277 -28
  92. package/lib/guard-pdf.js +251 -27
  93. package/lib/guard-regex.js +226 -18
  94. package/lib/guard-shell.js +229 -26
  95. package/lib/guard-svg.js +177 -10
  96. package/lib/guard-template.js +232 -21
  97. package/lib/guard-time.js +195 -29
  98. package/lib/guard-uuid.js +189 -30
  99. package/lib/guard-xml.js +259 -36
  100. package/lib/guard-yaml.js +241 -44
  101. package/lib/honeytoken.js +63 -27
  102. package/lib/html-balance.js +83 -0
  103. package/lib/http-client.js +486 -59
  104. package/lib/http-message-signature.js +582 -0
  105. package/lib/i18n.js +102 -49
  106. package/lib/iab-mspa.js +112 -32
  107. package/lib/iab-tcf.js +107 -2
  108. package/lib/inbox.js +90 -52
  109. package/lib/keychain.js +865 -0
  110. package/lib/legal-hold.js +374 -0
  111. package/lib/local-db-thin.js +320 -0
  112. package/lib/log-stream.js +281 -51
  113. package/lib/log.js +184 -86
  114. package/lib/mail-bounce.js +107 -62
  115. package/lib/mail.js +295 -58
  116. package/lib/mcp.js +108 -27
  117. package/lib/metrics.js +98 -89
  118. package/lib/middleware/age-gate.js +36 -0
  119. package/lib/middleware/ai-act-disclosure.js +37 -0
  120. package/lib/middleware/api-encrypt.js +45 -0
  121. package/lib/middleware/assetlinks.js +40 -0
  122. package/lib/middleware/asyncapi-serve.js +35 -0
  123. package/lib/middleware/attach-user.js +40 -0
  124. package/lib/middleware/bearer-auth.js +40 -0
  125. package/lib/middleware/body-parser.js +230 -0
  126. package/lib/middleware/bot-disclose.js +34 -0
  127. package/lib/middleware/bot-guard.js +39 -0
  128. package/lib/middleware/compression.js +37 -0
  129. package/lib/middleware/cookies.js +32 -0
  130. package/lib/middleware/cors.js +40 -0
  131. package/lib/middleware/csp-nonce.js +40 -0
  132. package/lib/middleware/csp-report.js +34 -0
  133. package/lib/middleware/csrf-protect.js +43 -0
  134. package/lib/middleware/daily-byte-quota.js +53 -85
  135. package/lib/middleware/db-role-for.js +40 -0
  136. package/lib/middleware/dpop.js +40 -0
  137. package/lib/middleware/error-handler.js +37 -14
  138. package/lib/middleware/fetch-metadata.js +39 -0
  139. package/lib/middleware/flag-context.js +34 -0
  140. package/lib/middleware/gpc.js +33 -0
  141. package/lib/middleware/headers.js +35 -0
  142. package/lib/middleware/health.js +46 -0
  143. package/lib/middleware/host-allowlist.js +30 -0
  144. package/lib/middleware/network-allowlist.js +38 -0
  145. package/lib/middleware/openapi-serve.js +34 -0
  146. package/lib/middleware/rate-limit.js +160 -18
  147. package/lib/middleware/request-id.js +36 -18
  148. package/lib/middleware/request-log.js +37 -0
  149. package/lib/middleware/require-aal.js +29 -0
  150. package/lib/middleware/require-auth.js +32 -0
  151. package/lib/middleware/require-bound-key.js +41 -0
  152. package/lib/middleware/require-content-type.js +32 -0
  153. package/lib/middleware/require-methods.js +27 -0
  154. package/lib/middleware/require-mtls.js +33 -0
  155. package/lib/middleware/require-step-up.js +37 -0
  156. package/lib/middleware/security-headers.js +44 -0
  157. package/lib/middleware/security-txt.js +38 -0
  158. package/lib/middleware/span-http-server.js +37 -0
  159. package/lib/middleware/sse.js +36 -0
  160. package/lib/middleware/trace-log-correlation.js +33 -0
  161. package/lib/middleware/trace-propagate.js +32 -0
  162. package/lib/middleware/tus-upload.js +90 -0
  163. package/lib/middleware/web-app-manifest.js +53 -0
  164. package/lib/mtls-ca.js +100 -70
  165. package/lib/network-byte-quota.js +308 -0
  166. package/lib/network-heartbeat.js +135 -0
  167. package/lib/network-tls.js +534 -4
  168. package/lib/network.js +103 -0
  169. package/lib/notify.js +114 -43
  170. package/lib/ntp-check.js +192 -51
  171. package/lib/observability.js +145 -47
  172. package/lib/openapi.js +90 -44
  173. package/lib/outbox.js +99 -1
  174. package/lib/pagination.js +168 -86
  175. package/lib/parsers/index.js +16 -5
  176. package/lib/permissions.js +93 -40
  177. package/lib/pqc-agent.js +84 -8
  178. package/lib/pqc-software.js +94 -60
  179. package/lib/process-spawn.js +95 -21
  180. package/lib/pubsub.js +96 -66
  181. package/lib/queue.js +375 -54
  182. package/lib/redact.js +793 -21
  183. package/lib/render.js +139 -47
  184. package/lib/request-helpers.js +485 -121
  185. package/lib/restore-bundle.js +142 -39
  186. package/lib/restore-rollback.js +136 -45
  187. package/lib/retention.js +178 -50
  188. package/lib/retry.js +116 -33
  189. package/lib/router.js +475 -23
  190. package/lib/safe-async.js +543 -94
  191. package/lib/safe-buffer.js +337 -41
  192. package/lib/safe-json.js +467 -62
  193. package/lib/safe-jsonpath.js +285 -0
  194. package/lib/safe-schema.js +631 -87
  195. package/lib/safe-sql.js +221 -59
  196. package/lib/safe-url.js +278 -46
  197. package/lib/sandbox-worker.js +135 -0
  198. package/lib/sandbox.js +358 -0
  199. package/lib/scheduler.js +135 -70
  200. package/lib/self-update.js +647 -0
  201. package/lib/session-device-binding.js +431 -0
  202. package/lib/session.js +259 -49
  203. package/lib/slug.js +138 -26
  204. package/lib/ssrf-guard.js +316 -56
  205. package/lib/storage.js +433 -70
  206. package/lib/subject.js +405 -23
  207. package/lib/template.js +148 -8
  208. package/lib/tenant-quota.js +545 -0
  209. package/lib/testing.js +440 -53
  210. package/lib/time.js +291 -23
  211. package/lib/tls-exporter.js +239 -0
  212. package/lib/tracing.js +90 -74
  213. package/lib/uuid.js +97 -22
  214. package/lib/vault/index.js +284 -22
  215. package/lib/vault/seal-pem-file.js +66 -0
  216. package/lib/watcher.js +368 -0
  217. package/lib/webhook.js +196 -63
  218. package/lib/websocket.js +393 -68
  219. package/lib/wiki-concepts.js +338 -0
  220. package/lib/worker-pool.js +464 -0
  221. package/package.json +3 -3
  222. package/sbom.cyclonedx.json +7 -7
@@ -0,0 +1,431 @@
1
+ "use strict";
2
+ /**
3
+ * b.sessionDeviceBinding — bind sessions to a device fingerprint and
4
+ * refuse-on-drift on every authenticated request.
5
+ *
6
+ * The fingerprint is a SHAKE256-derived digest over a stable subset of
7
+ * request signals:
8
+ *
9
+ * - User-Agent header (full string)
10
+ * - Accept-Language header (sorted preference list)
11
+ * - Accept-Encoding header (sorted set)
12
+ * - Client IP /24 (IPv4) or /48 (IPv6) prefix — tolerates carrier
13
+ * NAT churn and DHCP rotation while catching cross-region drift
14
+ * - WebAuthn AAGUID, when an authenticator is bound (operator passes
15
+ * it in via fingerprintExtras)
16
+ * - Operator-supplied bound key (b.auth.boundKey / mTLS cert hash /
17
+ * DPoP jkt) — when provided the binding is cryptographic, not
18
+ * just shape-based
19
+ *
20
+ * Operators choose the binding strength via the create-time opts:
21
+ *
22
+ * var binding = b.sessionDeviceBinding.create({
23
+ * session: b.session,
24
+ * audit: b.audit,
25
+ * requireBoundKey: true, // refuse if no key resolves
26
+ * boundKeyResolver: function (req) {
27
+ * // Return the cryptographic key bound to the session — DPoP
28
+ * // public key, mTLS cert SPKI hash, FIDO2 attestation hash.
29
+ * return req.dpop && req.dpop.jkt ? Buffer.from(req.dpop.jkt, "hex") : null;
30
+ * },
31
+ * });
32
+ *
33
+ * // After session.create:
34
+ * await binding.bind(token, req);
35
+ *
36
+ * // On every authenticated request:
37
+ * var verdict = await binding.verify(token, req);
38
+ * if (!verdict.ok) {
39
+ * // verdict.reason: "drift" | "missing-bind" | "missing-bound-key"
40
+ * return res.status(401).json({ error: verdict.reason });
41
+ * }
42
+ *
43
+ * Drift tolerance: the comparator does an EXACT match on UA + Accept-*,
44
+ * a /24-IPv4 (or /48-IPv6) match on IP, and an EXACT match on the
45
+ * bound-key when present. Operators with mobile clients that switch
46
+ * networks can pass `ipPrefixBits: { v4: 0, v6: 0 }` to skip the IP
47
+ * check entirely; the rest of the fingerprint still binds.
48
+ *
49
+ * Storage model: the fingerprint is stored under
50
+ * `bindingStore.set(token, fingerprintBytes, { ttlMs })` — operators
51
+ * pass any b.cache-shaped object. Without a separate store, the
52
+ * primitive falls back to b.session.touch metadata when the operator
53
+ * passes session=b.session AND opts in via storeInSession=true.
54
+ *
55
+ * Audit emissions:
56
+ *
57
+ * session.device.bound every successful bind()
58
+ * session.device.drift verify() found a mismatching fingerprint
59
+ * session.device.refused verify() refused (drift OR missing bind
60
+ * OR missing bound-key under requireBoundKey)
61
+ *
62
+ * Validation policy:
63
+ * - create() opts → throw at config time
64
+ * - bind / verify → throw on bad token / req shape (operator typo)
65
+ * - storage errors → fail-CLOSED on verify (drift indistinguishable
66
+ * from a wiped store, refuse rather than allow)
67
+ * fail-OPEN on bind (don't lose a fresh session
68
+ * to a transient cache outage)
69
+ */
70
+
71
+ var C = require("./constants");
72
+ var blamejsCrypto = require("./crypto");
73
+ var nodeCrypto = require("crypto");
74
+ var lazyRequire = require("./lazy-require");
75
+ var requestHelpers = require("./request-helpers");
76
+ var validateOpts = require("./validate-opts");
77
+ var { SessionDeviceBindingError } = require("./framework-error");
78
+
79
+ var observability = lazyRequire(function () { return require("./observability"); });
80
+
81
+ var DEFAULT_TTL_MS = C.TIME.days(7);
82
+ var DEFAULT_IP_V4_PREFIX = 24; // allow:raw-byte-literal — IPv4 /24 fingerprint mask in bits
83
+ var DEFAULT_IP_V6_PREFIX = 48; // allow:raw-byte-literal — IPv6 /48 fingerprint mask in bits
84
+ var FINGERPRINT_BYTES = C.BYTES.bytes(32);
85
+
86
+ var ALLOWED_OPTS = [
87
+ "session", "audit", "requireBoundKey", "boundKeyResolver",
88
+ "fingerprintExtras", "ipPrefixBits", "bindingStore", "ttlMs",
89
+ "storeInSession", "observability", "clock",
90
+ ];
91
+
92
+ function _requireFunction(name, val) {
93
+ if (typeof val !== "function") {
94
+ throw new SessionDeviceBindingError("session-device-binding/bad-opt",
95
+ name + ": expected function, got " + typeof val);
96
+ }
97
+ }
98
+
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
+ }
107
+ }
108
+
109
+ function _requireToken(token) {
110
+ if (typeof token !== "string" || token.length === 0) {
111
+ throw new SessionDeviceBindingError("session-device-binding/bad-token",
112
+ "token must be a non-empty string, got " + typeof token);
113
+ }
114
+ }
115
+
116
+ function _requireReq(req) {
117
+ if (!req || typeof req !== "object") {
118
+ throw new SessionDeviceBindingError("session-device-binding/bad-req",
119
+ "req must be a request-shaped object, got " + typeof req);
120
+ }
121
+ }
122
+
123
+ function _normalizeAcceptLanguage(value) {
124
+ if (typeof value !== "string" || value.length === 0) return "";
125
+ // Drop quality factors and sort tags so equivalent header orderings
126
+ // yield the same fingerprint.
127
+ return value.split(",")
128
+ .map(function (s) { return s.trim().split(";")[0].trim().toLowerCase(); })
129
+ .filter(function (s) { return s.length > 0; })
130
+ .sort()
131
+ .join(",");
132
+ }
133
+
134
+ function _normalizeAcceptEncoding(value) {
135
+ if (typeof value !== "string" || value.length === 0) return "";
136
+ return value.split(",")
137
+ .map(function (s) { return s.trim().split(";")[0].trim().toLowerCase(); })
138
+ .filter(function (s) { return s.length > 0; })
139
+ .sort()
140
+ .join(",");
141
+ }
142
+
143
+ function _ipPrefix(ip, bits) {
144
+ if (typeof ip !== "string" || ip.length === 0) return "";
145
+ if (bits === 0) return "";
146
+ // IPv6
147
+ if (ip.indexOf(":") !== -1) {
148
+ var v6Bits = bits;
149
+ var groups = ip.split(":");
150
+ // Naive expansion — keep the first ceil(v6Bits/16) groups intact
151
+ // and zero the rest. Sufficient for fingerprint stability; not a
152
+ // canonical IPv6 representation.
153
+ var keepGroups = Math.ceil(v6Bits / 16); // allow:raw-byte-literal — IPv6 group width in bits
154
+ var kept = groups.slice(0, keepGroups).join(":");
155
+ return "v6:" + kept + "/" + v6Bits;
156
+ }
157
+ // IPv4
158
+ var parts = ip.split(".");
159
+ if (parts.length !== 4) return "v4:" + ip + "/" + bits;
160
+ var v4Bits = bits;
161
+ var keepOctets = Math.floor(v4Bits / 8); // allow:raw-byte-literal — IPv4 octet width in bits
162
+ var maskedOctets = parts.slice(0, keepOctets);
163
+ while (maskedOctets.length < 4) maskedOctets.push("0");
164
+ return "v4:" + maskedOctets.join(".") + "/" + v4Bits;
165
+ }
166
+
167
+ function create(opts) {
168
+ opts = opts || {};
169
+ validateOpts(opts, ALLOWED_OPTS, "sessionDeviceBinding.create");
170
+
171
+ validateOpts.auditShape(opts.audit, "sessionDeviceBinding.create",
172
+ SessionDeviceBindingError);
173
+
174
+ if (opts.session !== undefined && (typeof opts.session !== "object" || opts.session === null)) {
175
+ throw new SessionDeviceBindingError("session-device-binding/bad-opt",
176
+ "session must be a b.session-shaped object or undefined");
177
+ }
178
+ if (opts.boundKeyResolver !== undefined) _requireFunction("boundKeyResolver", opts.boundKeyResolver);
179
+ if (opts.fingerprintExtras !== undefined) _requireFunction("fingerprintExtras", opts.fingerprintExtras);
180
+
181
+ var requireBoundKey = !!opts.requireBoundKey;
182
+ if (requireBoundKey && typeof opts.boundKeyResolver !== "function") {
183
+ throw new SessionDeviceBindingError("session-device-binding/bad-opt",
184
+ "requireBoundKey requires opts.boundKeyResolver");
185
+ }
186
+
187
+ var ipBits = opts.ipPrefixBits || {};
188
+ var v4Bits = typeof ipBits.v4 === "number" && isFinite(ipBits.v4) && ipBits.v4 >= 0 && ipBits.v4 <= 32 // allow:raw-byte-literal — IPv4 max prefix length in bits
189
+ ? ipBits.v4 : DEFAULT_IP_V4_PREFIX;
190
+ var v6Bits = typeof ipBits.v6 === "number" && isFinite(ipBits.v6) && ipBits.v6 >= 0 && ipBits.v6 <= 128 // allow:raw-byte-literal — IPv6 max prefix length in bits
191
+ ? ipBits.v6 : DEFAULT_IP_V6_PREFIX;
192
+
193
+ var ttlMs = opts.ttlMs !== undefined ? opts.ttlMs : DEFAULT_TTL_MS;
194
+ if (typeof ttlMs !== "number" || !isFinite(ttlMs) || ttlMs <= 0) {
195
+ throw new SessionDeviceBindingError("session-device-binding/bad-opt",
196
+ "ttlMs must be a positive finite number, got " + JSON.stringify(ttlMs));
197
+ }
198
+
199
+ var storeInSession = !!opts.storeInSession;
200
+ if (!storeInSession && !opts.bindingStore) {
201
+ throw new SessionDeviceBindingError("session-device-binding/bad-opt",
202
+ "either bindingStore (b.cache-shaped) or storeInSession=true must be set");
203
+ }
204
+ if (opts.bindingStore) _requireBindingStore(opts.bindingStore);
205
+ if (storeInSession && (!opts.session || typeof opts.session.touch !== "function")) {
206
+ throw new SessionDeviceBindingError("session-device-binding/bad-opt",
207
+ "storeInSession requires opts.session with a touch() function");
208
+ }
209
+
210
+ var sessionRef = opts.session || null;
211
+ var bindingStore = opts.bindingStore || null;
212
+ var auditInst = opts.audit || null;
213
+ var obsInst = opts.observability || null;
214
+ var clock = opts.clock || Date.now;
215
+ var boundKeyResolver = opts.boundKeyResolver || null;
216
+ var fingerprintExtras = opts.fingerprintExtras || null;
217
+
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
+ }
227
+
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
+ }
241
+
242
+ function _hashTokenForAudit(token) {
243
+ // Don't put the raw session id in the audit log. SHAKE256 to a
244
+ // stable short label.
245
+ return nodeCrypto.createHash("sha3-256").update("bj-session-device:" + token).digest("hex").slice(0, 16); // allow:raw-byte-literal — sha3-256 hex truncation length in chars
246
+ }
247
+
248
+ function _resolveBoundKey(req) {
249
+ if (!boundKeyResolver) return null;
250
+ var key;
251
+ try { key = boundKeyResolver(req); }
252
+ catch (_e) { return undefined; } // resolver threw — distinguishable
253
+ if (key === null || key === undefined) return null;
254
+ if (Buffer.isBuffer(key)) return key;
255
+ if (typeof key === "string" && key.length > 0) return Buffer.from(key, "utf8");
256
+ if (key instanceof Uint8Array) return Buffer.from(key);
257
+ throw new SessionDeviceBindingError("session-device-binding/bad-bound-key",
258
+ "boundKeyResolver returned a non-Buffer / non-string value (got " + typeof key + ")");
259
+ }
260
+
261
+ 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 ""; }
269
+ }
270
+
271
+ function _computeFingerprint(req) {
272
+ _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
+ var boundKeyMaybe = _resolveBoundKey(req);
284
+ if (requireBoundKey && (boundKeyMaybe === null || boundKeyMaybe === undefined)) {
285
+ return { ok: false, reason: "missing-bound-key" };
286
+ }
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
+ } };
307
+ }
308
+
309
+ async function bind(token, req) {
310
+ _requireToken(token);
311
+ var fp = _computeFingerprint(req);
312
+ if (!fp.ok) {
313
+ _emitObs("session.device.refused", { reason: fp.reason });
314
+ _emitAudit("session.device.refused", _hashTokenForAudit(token), "denied",
315
+ { reason: fp.reason, stage: "bind" }, req);
316
+ throw new SessionDeviceBindingError("session-device-binding/missing-bound-key",
317
+ "bind: requireBoundKey is true but no bound key resolved for this request");
318
+ }
319
+ var written = false;
320
+ if (bindingStore) {
321
+ try {
322
+ await bindingStore.set(token, fp.fingerprint, { ttlMs: ttlMs });
323
+ written = true;
324
+ } catch (_e) { /* fail-OPEN on bind: don't lose the fresh session */ }
325
+ }
326
+ if (!written && sessionRef && typeof sessionRef.touch === "function") {
327
+ // Best-effort: stash the fingerprint hex on the session row via
328
+ // touch metadata. Operators using storeInSession get this.
329
+ try {
330
+ await sessionRef.touch(token, {
331
+ metadata: { deviceFingerprint: fp.fingerprint.toString("hex"), boundAt: clock() },
332
+ });
333
+ written = true;
334
+ } catch (_e) { /* drop-silent */ }
335
+ }
336
+ _emitObs("session.device.bound", { stored: written ? "1" : "0" });
337
+ _emitAudit("session.device.bound", _hashTokenForAudit(token), "success",
338
+ { components: fp.components, stored: written }, req);
339
+ return fp.fingerprint;
340
+ }
341
+
342
+ async function _readBound(token) {
343
+ if (bindingStore) {
344
+ try {
345
+ var raw = await bindingStore.get(token);
346
+ if (Buffer.isBuffer(raw)) return raw;
347
+ if (typeof raw === "string" && raw.length > 0) return Buffer.from(raw, "hex");
348
+ if (raw instanceof Uint8Array) return Buffer.from(raw);
349
+ return null;
350
+ } catch (_e) { return undefined; } // fail-CLOSED on verify
351
+ }
352
+ if (sessionRef && typeof sessionRef.verify === "function") {
353
+ try {
354
+ var session = await sessionRef.verify(token);
355
+ if (session && session.data && typeof session.data.deviceFingerprint === "string") {
356
+ return Buffer.from(session.data.deviceFingerprint, "hex");
357
+ }
358
+ return null;
359
+ } catch (_e) { return undefined; }
360
+ }
361
+ return null;
362
+ }
363
+
364
+ async function verify(token, req) {
365
+ _requireToken(token);
366
+ var fpResult = _computeFingerprint(req);
367
+ if (!fpResult.ok) {
368
+ _emitObs("session.device.refused", { reason: fpResult.reason });
369
+ _emitAudit("session.device.refused", _hashTokenForAudit(token), "denied",
370
+ { reason: fpResult.reason, stage: "verify" }, req);
371
+ return { ok: false, reason: fpResult.reason };
372
+ }
373
+ var stored = await _readBound(token);
374
+ if (stored === undefined) {
375
+ // store error — fail closed
376
+ _emitObs("session.device.refused", { reason: "store-error" });
377
+ _emitAudit("session.device.refused", _hashTokenForAudit(token), "denied",
378
+ { reason: "store-error", stage: "verify" }, req);
379
+ return { ok: false, reason: "store-error" };
380
+ }
381
+ if (stored === null) {
382
+ // never bound — under requireBoundKey treat as refuse
383
+ _emitObs("session.device.refused", { reason: "missing-bind" });
384
+ _emitAudit("session.device.refused", _hashTokenForAudit(token), "denied",
385
+ { reason: "missing-bind", stage: "verify" }, req);
386
+ return { ok: false, reason: "missing-bind" };
387
+ }
388
+ if (!Buffer.isBuffer(stored) || stored.length !== fpResult.fingerprint.length ||
389
+ !blamejsCrypto.timingSafeEqual(stored, fpResult.fingerprint)) {
390
+ _emitObs("session.device.drift", {});
391
+ _emitAudit("session.device.drift", _hashTokenForAudit(token), "denied",
392
+ { components: fpResult.components, stage: "verify" }, req);
393
+ _emitAudit("session.device.refused", _hashTokenForAudit(token), "denied",
394
+ { reason: "drift", components: fpResult.components, stage: "verify" }, req);
395
+ return { ok: false, reason: "drift", components: fpResult.components };
396
+ }
397
+ return { ok: true, components: fpResult.components };
398
+ }
399
+
400
+ async function unbind(token) {
401
+ _requireToken(token);
402
+ if (bindingStore) {
403
+ try { await bindingStore.del(token); } catch (_e) { /* drop-silent */ }
404
+ }
405
+ return true;
406
+ }
407
+
408
+ function fingerprint(req) {
409
+ var fp = _computeFingerprint(req);
410
+ if (!fp.ok) return null;
411
+ return fp.fingerprint;
412
+ }
413
+
414
+ return {
415
+ bind: bind,
416
+ verify: verify,
417
+ unbind: unbind,
418
+ fingerprint: fingerprint,
419
+ };
420
+ }
421
+
422
+ module.exports = {
423
+ create: create,
424
+ SessionDeviceBindingError: SessionDeviceBindingError,
425
+ DEFAULTS: Object.freeze({
426
+ ttlMs: DEFAULT_TTL_MS,
427
+ ipV4Prefix: DEFAULT_IP_V4_PREFIX,
428
+ ipV6Prefix: DEFAULT_IP_V6_PREFIX,
429
+ fingerprintBytes: FINGERPRINT_BYTES,
430
+ }),
431
+ };