@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.
- package/CHANGELOG.md +93 -0
- package/README.md +10 -10
- package/index.js +52 -0
- package/lib/a2a.js +159 -34
- package/lib/acme.js +762 -0
- package/lib/ai-pref.js +166 -43
- package/lib/api-key.js +108 -47
- package/lib/api-snapshot.js +157 -40
- package/lib/app-shutdown.js +113 -77
- package/lib/archive.js +337 -40
- package/lib/arg-parser.js +697 -0
- package/lib/asyncapi.js +99 -55
- package/lib/atomic-file.js +465 -104
- package/lib/audit-chain.js +123 -34
- package/lib/audit-daily-review.js +389 -0
- package/lib/audit-sign.js +302 -56
- package/lib/audit-tools.js +412 -63
- package/lib/audit.js +656 -35
- package/lib/auth/jwt-external.js +17 -0
- package/lib/auth/oauth.js +7 -0
- package/lib/auth-bot-challenge.js +505 -0
- package/lib/auth-header.js +92 -25
- package/lib/backup/bundle.js +26 -0
- package/lib/backup/index.js +512 -89
- package/lib/backup/manifest.js +168 -7
- package/lib/break-glass.js +415 -39
- package/lib/budr.js +103 -30
- package/lib/bundler.js +86 -66
- package/lib/cache.js +192 -72
- package/lib/chain-writer.js +65 -40
- package/lib/circuit-breaker.js +56 -33
- package/lib/cli-helpers.js +106 -75
- package/lib/cli.js +6 -30
- package/lib/cloud-events.js +99 -32
- package/lib/cluster-storage.js +162 -37
- package/lib/cluster.js +340 -49
- package/lib/codepoint-class.js +66 -0
- package/lib/compliance.js +424 -24
- package/lib/config-drift.js +111 -46
- package/lib/config.js +94 -40
- package/lib/consent.js +165 -18
- package/lib/constants.js +1 -0
- package/lib/content-credentials.js +153 -48
- package/lib/cookies.js +154 -62
- package/lib/credential-hash.js +133 -61
- package/lib/crypto-field.js +702 -18
- package/lib/crypto-hpke.js +256 -0
- package/lib/crypto.js +744 -22
- package/lib/csv.js +178 -35
- package/lib/daemon.js +456 -0
- package/lib/dark-patterns.js +186 -55
- package/lib/db-query.js +79 -2
- package/lib/db.js +1431 -60
- package/lib/ddl-change-control.js +523 -0
- package/lib/deprecate.js +195 -40
- package/lib/dev.js +82 -39
- package/lib/dora.js +67 -48
- package/lib/dr-runbook.js +368 -0
- package/lib/dsr.js +142 -11
- package/lib/dual-control.js +91 -56
- package/lib/events.js +120 -41
- package/lib/external-db-migrate.js +192 -2
- package/lib/external-db.js +795 -50
- package/lib/fapi2.js +122 -1
- package/lib/fda-21cfr11.js +395 -0
- package/lib/fdx.js +132 -2
- package/lib/file-type.js +87 -0
- package/lib/file-upload.js +93 -0
- package/lib/flag.js +82 -20
- package/lib/forms.js +132 -29
- package/lib/framework-error.js +169 -0
- package/lib/framework-schema.js +163 -35
- package/lib/gate-contract.js +849 -175
- package/lib/graphql-federation.js +68 -7
- package/lib/guard-all.js +172 -55
- package/lib/guard-archive.js +286 -124
- package/lib/guard-auth.js +194 -21
- package/lib/guard-cidr.js +190 -28
- package/lib/guard-csv.js +397 -51
- package/lib/guard-domain.js +213 -91
- package/lib/guard-email.js +236 -29
- package/lib/guard-filename.js +307 -75
- package/lib/guard-graphql.js +263 -30
- package/lib/guard-html.js +310 -116
- package/lib/guard-image.js +243 -30
- package/lib/guard-json.js +260 -54
- package/lib/guard-jsonpath.js +235 -23
- package/lib/guard-jwt.js +284 -30
- package/lib/guard-markdown.js +204 -22
- package/lib/guard-mime.js +190 -26
- package/lib/guard-oauth.js +277 -28
- package/lib/guard-pdf.js +251 -27
- package/lib/guard-regex.js +226 -18
- package/lib/guard-shell.js +229 -26
- package/lib/guard-svg.js +177 -10
- package/lib/guard-template.js +232 -21
- package/lib/guard-time.js +195 -29
- package/lib/guard-uuid.js +189 -30
- package/lib/guard-xml.js +259 -36
- package/lib/guard-yaml.js +241 -44
- package/lib/honeytoken.js +63 -27
- package/lib/html-balance.js +83 -0
- package/lib/http-client.js +486 -59
- package/lib/http-message-signature.js +582 -0
- package/lib/i18n.js +102 -49
- package/lib/iab-mspa.js +112 -32
- package/lib/iab-tcf.js +107 -2
- package/lib/inbox.js +90 -52
- package/lib/keychain.js +865 -0
- package/lib/legal-hold.js +374 -0
- package/lib/local-db-thin.js +320 -0
- package/lib/log-stream.js +281 -51
- package/lib/log.js +184 -86
- package/lib/mail-bounce.js +107 -62
- package/lib/mail.js +295 -58
- package/lib/mcp.js +108 -27
- package/lib/metrics.js +98 -89
- package/lib/middleware/age-gate.js +36 -0
- package/lib/middleware/ai-act-disclosure.js +37 -0
- package/lib/middleware/api-encrypt.js +45 -0
- package/lib/middleware/assetlinks.js +40 -0
- package/lib/middleware/asyncapi-serve.js +35 -0
- package/lib/middleware/attach-user.js +40 -0
- package/lib/middleware/bearer-auth.js +40 -0
- package/lib/middleware/body-parser.js +230 -0
- package/lib/middleware/bot-disclose.js +34 -0
- package/lib/middleware/bot-guard.js +39 -0
- package/lib/middleware/compression.js +37 -0
- package/lib/middleware/cookies.js +32 -0
- package/lib/middleware/cors.js +40 -0
- package/lib/middleware/csp-nonce.js +40 -0
- package/lib/middleware/csp-report.js +34 -0
- package/lib/middleware/csrf-protect.js +43 -0
- package/lib/middleware/daily-byte-quota.js +53 -85
- package/lib/middleware/db-role-for.js +40 -0
- package/lib/middleware/dpop.js +40 -0
- package/lib/middleware/error-handler.js +37 -14
- package/lib/middleware/fetch-metadata.js +39 -0
- package/lib/middleware/flag-context.js +34 -0
- package/lib/middleware/gpc.js +33 -0
- package/lib/middleware/headers.js +35 -0
- package/lib/middleware/health.js +46 -0
- package/lib/middleware/host-allowlist.js +30 -0
- package/lib/middleware/network-allowlist.js +38 -0
- package/lib/middleware/openapi-serve.js +34 -0
- package/lib/middleware/rate-limit.js +160 -18
- package/lib/middleware/request-id.js +36 -18
- package/lib/middleware/request-log.js +37 -0
- package/lib/middleware/require-aal.js +29 -0
- package/lib/middleware/require-auth.js +32 -0
- package/lib/middleware/require-bound-key.js +41 -0
- package/lib/middleware/require-content-type.js +32 -0
- package/lib/middleware/require-methods.js +27 -0
- package/lib/middleware/require-mtls.js +33 -0
- package/lib/middleware/require-step-up.js +37 -0
- package/lib/middleware/security-headers.js +44 -0
- package/lib/middleware/security-txt.js +38 -0
- package/lib/middleware/span-http-server.js +37 -0
- package/lib/middleware/sse.js +36 -0
- package/lib/middleware/trace-log-correlation.js +33 -0
- package/lib/middleware/trace-propagate.js +32 -0
- package/lib/middleware/tus-upload.js +90 -0
- package/lib/middleware/web-app-manifest.js +53 -0
- package/lib/mtls-ca.js +100 -70
- package/lib/network-byte-quota.js +308 -0
- package/lib/network-heartbeat.js +135 -0
- package/lib/network-tls.js +534 -4
- package/lib/network.js +103 -0
- package/lib/notify.js +114 -43
- package/lib/ntp-check.js +192 -51
- package/lib/observability.js +145 -47
- package/lib/openapi.js +90 -44
- package/lib/outbox.js +99 -1
- package/lib/pagination.js +168 -86
- package/lib/parsers/index.js +16 -5
- package/lib/permissions.js +93 -40
- package/lib/pqc-agent.js +84 -8
- package/lib/pqc-software.js +94 -60
- package/lib/process-spawn.js +95 -21
- package/lib/pubsub.js +96 -66
- package/lib/queue.js +375 -54
- package/lib/redact.js +793 -21
- package/lib/render.js +139 -47
- package/lib/request-helpers.js +485 -121
- package/lib/restore-bundle.js +142 -39
- package/lib/restore-rollback.js +136 -45
- package/lib/retention.js +178 -50
- package/lib/retry.js +116 -33
- package/lib/router.js +475 -23
- package/lib/safe-async.js +543 -94
- package/lib/safe-buffer.js +337 -41
- package/lib/safe-json.js +467 -62
- package/lib/safe-jsonpath.js +285 -0
- package/lib/safe-schema.js +631 -87
- package/lib/safe-sql.js +221 -59
- package/lib/safe-url.js +278 -46
- package/lib/sandbox-worker.js +135 -0
- package/lib/sandbox.js +358 -0
- package/lib/scheduler.js +135 -70
- package/lib/self-update.js +647 -0
- package/lib/session-device-binding.js +431 -0
- package/lib/session.js +259 -49
- package/lib/slug.js +138 -26
- package/lib/ssrf-guard.js +316 -56
- package/lib/storage.js +433 -70
- package/lib/subject.js +405 -23
- package/lib/template.js +148 -8
- package/lib/tenant-quota.js +545 -0
- package/lib/testing.js +440 -53
- package/lib/time.js +291 -23
- package/lib/tls-exporter.js +239 -0
- package/lib/tracing.js +90 -74
- package/lib/uuid.js +97 -22
- package/lib/vault/index.js +284 -22
- package/lib/vault/seal-pem-file.js +66 -0
- package/lib/watcher.js +368 -0
- package/lib/webhook.js +196 -63
- package/lib/websocket.js +393 -68
- package/lib/wiki-concepts.js +338 -0
- package/lib/worker-pool.js +464 -0
- package/package.json +3 -3
- 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
|
+
};
|