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