@blamejs/blamejs-shop 0.4.32 → 0.4.37
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 +10 -0
- package/README.md +1 -1
- package/lib/asset-manifest.json +1 -1
- package/lib/vendor/MANIFEST.json +72 -52
- package/lib/vendor/blamejs/.github/workflows/ci.yml +12 -12
- package/lib/vendor/blamejs/.github/workflows/npm-publish.yml +37 -5
- package/lib/vendor/blamejs/.github/workflows/release-container.yml +2 -2
- package/lib/vendor/blamejs/CHANGELOG.md +6 -0
- package/lib/vendor/blamejs/MIGRATING.md +12 -0
- package/lib/vendor/blamejs/README.md +5 -2
- package/lib/vendor/blamejs/SECURITY.md +4 -2
- package/lib/vendor/blamejs/api-snapshot.json +137 -2
- package/lib/vendor/blamejs/examples/wiki/lib/source-comment-block-validator.js +1 -0
- package/lib/vendor/blamejs/index.js +4 -0
- package/lib/vendor/blamejs/lib/archive-read.js +2 -1
- package/lib/vendor/blamejs/lib/archive-tar-read.js +2 -1
- package/lib/vendor/blamejs/lib/atomic-file.js +5 -0
- package/lib/vendor/blamejs/lib/audit.js +2 -0
- package/lib/vendor/blamejs/lib/auth/elevation-grant.js +6 -2
- package/lib/vendor/blamejs/lib/auth/oauth.js +13 -0
- package/lib/vendor/blamejs/lib/auth/sd-jwt-vc.js +5 -2
- package/lib/vendor/blamejs/lib/cli.js +8 -1
- package/lib/vendor/blamejs/lib/compliance.js +4 -0
- package/lib/vendor/blamejs/lib/config-drift.js +2 -1
- package/lib/vendor/blamejs/lib/credential-hash.js +9 -0
- package/lib/vendor/blamejs/lib/db.js +15 -2
- package/lib/vendor/blamejs/lib/dsa.js +482 -0
- package/lib/vendor/blamejs/lib/framework-error.js +14 -0
- package/lib/vendor/blamejs/lib/http-client.js +5 -2
- package/lib/vendor/blamejs/lib/local-db-thin.js +3 -2
- package/lib/vendor/blamejs/lib/log-stream-local.js +1 -1
- package/lib/vendor/blamejs/lib/log-stream-otlp-grpc.js +9 -2
- package/lib/vendor/blamejs/lib/log-stream-otlp.js +16 -7
- package/lib/vendor/blamejs/lib/middleware/clear-site-data.js +36 -11
- package/lib/vendor/blamejs/lib/mtls-ca.js +2 -2
- package/lib/vendor/blamejs/lib/observability.js +3 -2
- package/lib/vendor/blamejs/lib/pipl-cn.js +377 -0
- package/lib/vendor/blamejs/lib/restore-rollback.js +5 -5
- package/lib/vendor/blamejs/lib/retention.js +16 -2
- package/lib/vendor/blamejs/lib/scheduler.js +12 -0
- package/lib/vendor/blamejs/lib/self-update.js +1 -1
- package/lib/vendor/blamejs/lib/session.js +64 -0
- package/lib/vendor/blamejs/lib/ssrf-guard.js +25 -7
- package/lib/vendor/blamejs/lib/vault/passphrase-ops.js +3 -3
- package/lib/vendor/blamejs/lib/watcher.js +8 -0
- package/lib/vendor/blamejs/package.json +2 -2
- package/lib/vendor/blamejs/release-notes/v0.15.7.json +43 -0
- package/lib/vendor/blamejs/release-notes/v0.15.8.json +48 -0
- package/lib/vendor/blamejs/release-notes/v0.15.9.json +58 -0
- package/lib/vendor/blamejs/scripts/gen-migrating.js +16 -0
- package/lib/vendor/blamejs/scripts/generate-ssdf-attestation.js +338 -0
- package/lib/vendor/blamejs/test/00-primitives.js +51 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-rename-retry.test.js +70 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +250 -3
- package/lib/vendor/blamejs/test/layer-0-primitives/credential-hash.test.js +18 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-init-extensions.test.js +32 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dsa.test.js +169 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/otlp-attr-redaction.test.js +40 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/pipl-cn.test.js +172 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/retention-floor.test.js +59 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-url-canonicalize.test.js +64 -11
- package/lib/vendor/blamejs/test/layer-0-primitives/scheduler-watchdog-stale-settle.test.js +71 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/session-extensions.test.js +57 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/watcher.test.js +7 -3
- package/package.json +2 -2
|
@@ -614,9 +614,16 @@ var COMPLIANCE_RETENTION_FLOOR_MS = Object.freeze({
|
|
|
614
614
|
* // → 220752000000 (Sarbanes-Oxley §802 — 7 years)
|
|
615
615
|
*/
|
|
616
616
|
function complianceFloor(posture, candidateTtlMs) {
|
|
617
|
+
// Optional posture: omit it to inherit the active posture recorded by
|
|
618
|
+
// applyPosture (the b.compliance.set cascade). A numeric first argument is
|
|
619
|
+
// taken as candidateTtlMs so complianceFloor(ttl) works; an explicit posture
|
|
620
|
+
// always overrides the active one.
|
|
621
|
+
if (typeof posture === "number") { candidateTtlMs = posture; posture = undefined; }
|
|
622
|
+
if (posture === undefined || posture === null) { posture = STATE.activePosture; }
|
|
617
623
|
if (typeof posture !== "string") {
|
|
618
624
|
throw new RetentionError("retention/bad-posture",
|
|
619
|
-
"complianceFloor: posture must be a string,
|
|
625
|
+
"complianceFloor: posture must be a string (pass one, or set the active " +
|
|
626
|
+
"posture via applyPosture / b.compliance.set), got " + JSON.stringify(posture));
|
|
620
627
|
}
|
|
621
628
|
var floor = COMPLIANCE_RETENTION_FLOOR_MS[posture];
|
|
622
629
|
if (floor === undefined) {
|
|
@@ -660,7 +667,14 @@ function complianceFloor(posture, candidateTtlMs) {
|
|
|
660
667
|
* // → "hipaa"
|
|
661
668
|
*/
|
|
662
669
|
function applyPosture(posture) {
|
|
663
|
-
if (typeof posture !== "string" || posture.length === 0)
|
|
670
|
+
if (typeof posture !== "string" || posture.length === 0) {
|
|
671
|
+
// Clear the active posture (the inverse of a set) so b.compliance.clear
|
|
672
|
+
// and operators can reset the inherited floor; complianceFloor then falls
|
|
673
|
+
// back to requiring an explicit posture again.
|
|
674
|
+
STATE.activePosture = null;
|
|
675
|
+
STATE.activeFloorMs = null;
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
664
678
|
var floor = COMPLIANCE_RETENTION_FLOOR_MS[posture];
|
|
665
679
|
STATE.activePosture = posture;
|
|
666
680
|
STATE.activeFloorMs = (typeof floor === "number") ? floor : null;
|
|
@@ -488,6 +488,10 @@ function create(opts) {
|
|
|
488
488
|
lastError: null,
|
|
489
489
|
running: false,
|
|
490
490
|
runningSince: 0,
|
|
491
|
+
// Monotonic run tag. The watchdog and each fire bump it, so a run the
|
|
492
|
+
// watchdog abandoned can't clobber state / emit a stale settle event
|
|
493
|
+
// when its slow promise finally resolves.
|
|
494
|
+
runGeneration: 0,
|
|
491
495
|
fires: 0,
|
|
492
496
|
misses: 0, // skipped because previous run still in-flight
|
|
493
497
|
nonLeaderSkips: 0,
|
|
@@ -540,6 +544,8 @@ function create(opts) {
|
|
|
540
544
|
(maxJobMs / C.TIME.seconds(1)) + "s — forcing reset");
|
|
541
545
|
} catch (_e) { /* logger best-effort */ }
|
|
542
546
|
_emit("system.scheduler.task.watchdog", { name: task.name }, "failure");
|
|
547
|
+
// Supersede the abandoned run so its late settle is ignored.
|
|
548
|
+
task.runGeneration++;
|
|
543
549
|
task.running = false;
|
|
544
550
|
} else {
|
|
545
551
|
task.misses++;
|
|
@@ -665,6 +671,10 @@ function create(opts) {
|
|
|
665
671
|
task.runningSince = Date.now();
|
|
666
672
|
task.lastRun = new Date().toISOString();
|
|
667
673
|
var startedAt = Date.now();
|
|
674
|
+
// Tag this run. The settle handlers below only write back if the tag still
|
|
675
|
+
// matches — so a run the watchdog reset (or a newer fire) can't clobber the
|
|
676
|
+
// current run's state or emit a stale success/failure when it settles late.
|
|
677
|
+
var gen = (task.runGeneration = (task.runGeneration || 0) + 1);
|
|
668
678
|
|
|
669
679
|
var promise;
|
|
670
680
|
try {
|
|
@@ -678,6 +688,7 @@ function create(opts) {
|
|
|
678
688
|
}
|
|
679
689
|
|
|
680
690
|
Promise.resolve(promise).then(function (_v) {
|
|
691
|
+
if (task.runGeneration !== gen) return; // watchdog/newer fire superseded this run
|
|
681
692
|
task.running = false;
|
|
682
693
|
task.runningSince = 0;
|
|
683
694
|
task.lastFinish = new Date().toISOString();
|
|
@@ -689,6 +700,7 @@ function create(opts) {
|
|
|
689
700
|
viaJob: !!task.job,
|
|
690
701
|
});
|
|
691
702
|
}, function (e) {
|
|
703
|
+
if (task.runGeneration !== gen) return; // watchdog/newer fire superseded this run
|
|
692
704
|
task.running = false;
|
|
693
705
|
task.runningSince = 0;
|
|
694
706
|
task.lastFinish = new Date().toISOString();
|
|
@@ -676,7 +676,7 @@ async function swap(opts) {
|
|
|
676
676
|
// don't silently lose both binaries (the prior best-effort comment
|
|
677
677
|
// swallowed the rollback exception — SSDF RV.1 violation).
|
|
678
678
|
try {
|
|
679
|
-
|
|
679
|
+
atomicFile.renameWithRetry(from, to);
|
|
680
680
|
} catch (e) {
|
|
681
681
|
if (e && e.code === "EXDEV") {
|
|
682
682
|
// Cross-device — copy + unlink. Use atomicFile.copy for the safety
|
|
@@ -65,6 +65,9 @@ var { SessionError } = require("./framework-error");
|
|
|
65
65
|
// the cookie-side sid so the wire token is ciphertext rather than
|
|
66
66
|
// plaintext (sealed-cookie default since v0.8.61).
|
|
67
67
|
var vault = lazyRequire(function () { return require("./vault"); });
|
|
68
|
+
// Lazy — b.session.logout composes the Clear-Site-Data header builder; keep it
|
|
69
|
+
// out of the boot require graph (no cycle, but session is a low-level primitive).
|
|
70
|
+
var clearSiteData = lazyRequire(function () { return require("./middleware/clear-site-data"); });
|
|
68
71
|
|
|
69
72
|
// Pluggable session-storage backend. Default uses cluster-storage (which
|
|
70
73
|
// in turn dispatches to the framework's main DB or external DB). An
|
|
@@ -705,6 +708,66 @@ async function destroy(token) {
|
|
|
705
708
|
return await _deleteBySidHash(_hashSid(sid));
|
|
706
709
|
}
|
|
707
710
|
|
|
711
|
+
/**
|
|
712
|
+
* @primitive b.session.logout
|
|
713
|
+
* @signature b.session.logout(res, token, opts?)
|
|
714
|
+
* @since 0.15.9
|
|
715
|
+
* @status stable
|
|
716
|
+
* @related b.session.destroy, b.middleware.clearSiteData
|
|
717
|
+
*
|
|
718
|
+
* Secure logout in one call: destroy the server-side session AND tell the
|
|
719
|
+
* browser to wipe its client-side state. It emits an RFC 9527 Clear-Site-Data
|
|
720
|
+
* response header (cookies + storage + cache + executionContexts by default)
|
|
721
|
+
* and expires the session cookie, then destroys the session row. `destroy()`
|
|
722
|
+
* alone is a store operation with no `res`, so it cannot wipe the browser's
|
|
723
|
+
* cached pages / storage / any stale tab still holding the now-revoked cookie;
|
|
724
|
+
* this composes the secure-default logout the middleware otherwise had to be
|
|
725
|
+
* mounted by hand. Returns whether a session was destroyed. Leader-only.
|
|
726
|
+
*
|
|
727
|
+
* @opts
|
|
728
|
+
* cookieName: string, // default: "sid" — the session cookie to expire
|
|
729
|
+
* types: string[], // default: the RFC 9527 Clear-Site-Data directive set
|
|
730
|
+
*
|
|
731
|
+
* @example
|
|
732
|
+
* app.post("/logout", async function (req, res) {
|
|
733
|
+
* await b.session.logout(res, req.cookies.sid);
|
|
734
|
+
* res.end("logged out");
|
|
735
|
+
* });
|
|
736
|
+
* // → emits Clear-Site-Data + expires the sid cookie + destroys the session
|
|
737
|
+
*/
|
|
738
|
+
async function logout(res, token, opts) {
|
|
739
|
+
if (!res || typeof res.setHeader !== "function") {
|
|
740
|
+
throw new SessionError("session/bad-res",
|
|
741
|
+
"b.session.logout: res must be an HTTP response with setHeader()");
|
|
742
|
+
}
|
|
743
|
+
opts = opts || {};
|
|
744
|
+
var cookieName = opts.cookieName === undefined ? "sid" : opts.cookieName;
|
|
745
|
+
if (typeof cookieName !== "string" || cookieName.length === 0) {
|
|
746
|
+
throw new SessionError("session/bad-cookie-name",
|
|
747
|
+
"b.session.logout: opts.cookieName must be a non-empty string");
|
|
748
|
+
}
|
|
749
|
+
var csd = clearSiteData();
|
|
750
|
+
var types = opts.types === undefined ? csd.DEFAULT_TYPES : opts.types;
|
|
751
|
+
// Build (and validate) the RFC 9527 header BEFORE any side effect — an
|
|
752
|
+
// unknown directive throws here, queuing nothing.
|
|
753
|
+
var clearSiteDataValue = csd.headerValue(types, "b.session.logout");
|
|
754
|
+
|
|
755
|
+
// Revoke the server-side session FIRST. If destroy() throws (a follower
|
|
756
|
+
// failing cluster.requireLeader(), or a store/DB error), no client-wipe
|
|
757
|
+
// headers have been queued — an error response can't then expire the
|
|
758
|
+
// browser cookie + Clear-Site-Data while the session row is still live,
|
|
759
|
+
// which would leave a copied token usable server-side.
|
|
760
|
+
var destroyed = await destroy(token);
|
|
761
|
+
|
|
762
|
+
// Now wipe the client-side state: RFC 9527 Clear-Site-Data (cookies /
|
|
763
|
+
// storage / cache) + expire the session cookie (belt-and-suspenders with the
|
|
764
|
+
// "cookies" directive, and effective even if the client ignores the header).
|
|
765
|
+
res.setHeader("Clear-Site-Data", clearSiteDataValue);
|
|
766
|
+
res.setHeader("Set-Cookie",
|
|
767
|
+
cookieName + "=; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=0");
|
|
768
|
+
return destroyed;
|
|
769
|
+
}
|
|
770
|
+
|
|
708
771
|
async function _deleteBySidHash(sidHash) {
|
|
709
772
|
var built = sql.delete(_sessionSqlTable(), _sessionSqlOpts())
|
|
710
773
|
.where("sidHash", sidHash)
|
|
@@ -1245,6 +1308,7 @@ module.exports = {
|
|
|
1245
1308
|
create: create,
|
|
1246
1309
|
verify: verify,
|
|
1247
1310
|
destroy: destroy,
|
|
1311
|
+
logout: logout,
|
|
1248
1312
|
destroyAllForUser: destroyAllForUser,
|
|
1249
1313
|
touch: touch,
|
|
1250
1314
|
rotate: rotate,
|
|
@@ -335,14 +335,32 @@ function canonicalizeHost(host) {
|
|
|
335
335
|
return bare.toLowerCase();
|
|
336
336
|
}
|
|
337
337
|
if (family === 6) {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
338
|
+
var v6bytes = _ipv6ToBytes(bare);
|
|
339
|
+
// An IPv4-mapped IPv6 address (::ffff:a.b.c.d, the ::ffff:0:0/96 block) IS
|
|
340
|
+
// the IPv4 address a.b.c.d for routing / access control — classify() already
|
|
341
|
+
// re-classifies it by the embedded v4, and a dual-stack peer arriving on
|
|
342
|
+
// ::ffff:1.2.3.4 reaches the same host as 1.2.3.4. Fold it to the dotted
|
|
343
|
+
// IPv4 form so a dual-stack peer and an operator's IPv4 allowlist entry
|
|
344
|
+
// canonicalize equal. ONLY the IPv4-mapped block (::ffff:0:0/96) folds,
|
|
345
|
+
// because classify(::ffff:x) === classify(x) — its classify branch returns
|
|
346
|
+
// the embedded-v4 verdict with no reserved fallback, so folding can't change
|
|
347
|
+
// an SSRF verdict. NAT64 (64:ff9b::/96) and 6to4 (2002::/16) are NOT folded:
|
|
348
|
+
// classify treats a NAT64 literal as `classify(v4) || "reserved"`, so a
|
|
349
|
+
// public NAT64 address classifies as "reserved" while its embedded v4 is
|
|
350
|
+
// null — folding would flip a blocked verdict to an allowed public IPv4.
|
|
351
|
+
// classify still reaches the embedded v4 for the deny side; the canonical
|
|
352
|
+
// form keeps NAT64 / 6to4 as IPv6 so canonicalize-then-classify agrees with
|
|
353
|
+
// classify alone.
|
|
354
|
+
if (_ipv6PrefixMatch(IPV6_V4_MAPPED_PREFIX, C.BYTES.bytes(96), v6bytes)) {
|
|
355
|
+
return v6bytes[12] + "." + v6bytes[13] + "." + v6bytes[14] + "." + v6bytes[15];
|
|
356
|
+
}
|
|
357
|
+
return _ipv6BytesToString(v6bytes);
|
|
345
358
|
}
|
|
359
|
+
// Not an IP literal — DNS name. Lowercase + strip ALL trailing dots: a
|
|
360
|
+
// hostname's trailing-dot count is not significant for identity (the root
|
|
361
|
+
// label is empty), so host / host. / host.. must collapse to one form or a
|
|
362
|
+
// trailing-dot count bypasses a host allow/deny comparison.
|
|
363
|
+
var name = bare.toLowerCase().replace(/\.+$/, "");
|
|
346
364
|
return name;
|
|
347
365
|
}
|
|
348
366
|
|
|
@@ -169,7 +169,7 @@ async function seal(opts) {
|
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
// Step 3: atomic rename sealed.tmp → sealed
|
|
172
|
-
|
|
172
|
+
atomicFile.renameWithRetry(p.sealedTmp, p.sealed);
|
|
173
173
|
atomicFile.fsyncDir(opts.dataDir);
|
|
174
174
|
|
|
175
175
|
// Step 4: delete plaintext (unless keepPlaintext)
|
|
@@ -220,7 +220,7 @@ async function unseal(opts) {
|
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
// Step 3: atomic rename plaintext.tmp → plaintext
|
|
223
|
-
|
|
223
|
+
atomicFile.renameWithRetry(p.plaintextTmp, p.plaintext);
|
|
224
224
|
atomicFile.fsyncDir(opts.dataDir);
|
|
225
225
|
|
|
226
226
|
// Step 4: delete sealed file
|
|
@@ -293,7 +293,7 @@ async function rotate(opts) {
|
|
|
293
293
|
}
|
|
294
294
|
|
|
295
295
|
// Step 3: atomic rename — swap in the new sealed file
|
|
296
|
-
|
|
296
|
+
atomicFile.renameWithRetry(p.sealedTmp, p.sealed);
|
|
297
297
|
atomicFile.fsyncDir(opts.dataDir);
|
|
298
298
|
|
|
299
299
|
return { sealedPath: p.sealed };
|
|
@@ -316,6 +316,14 @@ function create(opts) {
|
|
|
316
316
|
_validateOpts(opts);
|
|
317
317
|
|
|
318
318
|
var root = nodePath.resolve(opts.root);
|
|
319
|
+
// Canonicalize to the real long path. On Windows a path with an 8.3
|
|
320
|
+
// short-name component (os.tmpdir() commonly resolves to C:\Users\RUNNER~1\…)
|
|
321
|
+
// makes the native recursive backend (ReadDirectoryChangesW) deliver
|
|
322
|
+
// long-name event paths that no longer prefix-match the watched root, which
|
|
323
|
+
// trips a libuv fs-event assertion and aborts the process on some Node builds.
|
|
324
|
+
// realpathSync.native expands short names and resolves symlinks; guarded so a
|
|
325
|
+
// non-existent root still falls through to the watcher's own not-found error.
|
|
326
|
+
try { root = nodeFs.realpathSync.native(root); } catch (_e) { /* keep resolved path */ }
|
|
319
327
|
var debounceMs = (opts.debounceMs !== undefined) ? opts.debounceMs : DEFAULT_DEBOUNCE_MS;
|
|
320
328
|
var maxPending = (opts.maxPending !== undefined) ? opts.maxPending : DEFAULT_MAX_PENDING;
|
|
321
329
|
var requestedMode = opts.mode || "fs";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blamejs/core",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.9",
|
|
4
4
|
"description": "The Node framework that owns its stack.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "blamejs contributors",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"owns-its-stack"
|
|
55
55
|
],
|
|
56
56
|
"engines": {
|
|
57
|
-
"node": ">=24.
|
|
57
|
+
"node": ">=24.16.0"
|
|
58
58
|
},
|
|
59
59
|
"files": [
|
|
60
60
|
"index.js",
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.15.7",
|
|
4
|
+
"date": "2026-06-13",
|
|
5
|
+
"headline": "Hardens the new URL canonicalizer against an IPv4-mapped allowlist bypass, enforces the OIDC azp authorized-party check, and closes a set of audited correctness gaps in retention, credential rehashing, the scheduler, and SD-JWT key binding",
|
|
6
|
+
"summary": "This release deepens the URL/host canonicalizer shipped in 0.15.6 and clears a batch of audited correctness gaps. The canonicalizer now folds an IPv4-mapped IPv6 address (::ffff:1.2.3.4) to its embedded IPv4 and strips every trailing dot from a host, so a dual-stack peer can no longer slip past an operator's dotted-IPv4 allowlist and host./host.. no longer evade a host comparison. (NAT64 and 6to4 hosts are deliberately kept as IPv6 so canonicalizing then classifying agrees with the SSRF classifier, which treats them as reserved.) On the OIDC side, verifyIdToken now enforces OIDC Core 3.1.3.7: a multi-audience ID token must carry an azp (authorized party) and a present azp must equal the client_id, closing a confused-deputy hole where a token minted for a different client but listing this RP in its audience array verified clean. The rest are audited fixes: retention.complianceFloor now honors the active posture set via applyPosture (the documented inheritance was unimplemented); credentialHash.needsRehash now drives the advertised SHAKE256 length-rotation (raising the output length now triggers a rehash, upgrade-only); the task scheduler no longer lets a run abandoned by its watchdog clobber the next run's state or emit a stale success when its slow promise settles late; and the SD-JWT key-binding JWT compares its audience and nonce in constant time.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Security",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "The URL canonicalizer folds IPv4-mapped IPv6 addresses to IPv4",
|
|
13
|
+
"body": "`b.safeUrl.canonicalize` / `b.ssrfGuard.canonicalizeHost` now fold an IPv4-mapped IPv6 address (`::ffff:1.2.3.4`, the `::ffff:0:0/96` block) to its embedded IPv4 dotted form. Previously it canonicalized to an IPv6 string, so a dual-stack peer never unified with an operator's `1.2.3.4` allow/deny entry — an allowlist bypass. Only the IPv4-mapped block folds, because the SSRF classifier maps it to the embedded v4 verdict directly; NAT64 (`64:ff9b::/96`) and 6to4 (`2002::/16`) are deliberately kept as IPv6, since the classifier treats a NAT64 literal as reserved and folding it would turn a blocked verdict into an allowed public IPv4. The host canonicalizer also now strips every trailing dot, so `host`, `host.`, and `host..` collapse to one form."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"title": "verifyIdToken enforces the OIDC azp (authorized party) check",
|
|
17
|
+
"body": "`b.auth.oauth`'s `verifyIdToken` validated only that its `client_id` was present in the token's `aud`, ignoring `azp`. Per OIDC Core 3.1.3.7, a multi-audience ID token must carry an `azp` and a present `azp` must equal the RP's `client_id`. Without it, a token whose authorized party is a different client — but whose `aud` array also lists this RP — verified clean (a confused-deputy / token-substitution hole). The verifier now rejects a multi-audience token with no `azp` (`auth-oauth/azp-required`) and any token whose `azp` is not the `client_id` (`auth-oauth/azp-mismatch`). A single-audience token with no `azp` (the common case) is unaffected."
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"title": "SD-JWT key-binding audience and nonce compare in constant time",
|
|
21
|
+
"body": "`b.auth.sdJwtVc.verify` compared the key-binding JWT's `aud` and `nonce` with a short-circuiting `!==`, while the adjacent `sd_hash` check already used a constant-time compare. The `nonce` is a verifier-issued replay-defense value, so a non-constant-time compare leaks a matching-prefix timing oracle; both checks now use the constant-time helper."
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"heading": "Fixed",
|
|
27
|
+
"items": [
|
|
28
|
+
{
|
|
29
|
+
"title": "retention.complianceFloor honors the active compliance posture",
|
|
30
|
+
"body": "`b.retention.complianceFloor` required an explicit posture and never read the active posture set by `applyPosture` (the `b.compliance.set` cascade), so the documented inheritance was unimplemented dead state. It now inherits the active posture when none is passed — `complianceFloor(candidateTtlMs)` uses the active posture, an explicit posture still overrides it — and `applyPosture(null)` now clears the active posture (it was a silent no-op, so `b.compliance.clear` could not reset it)."
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"title": "credentialHash.needsRehash drives the SHAKE256 length-rotation",
|
|
34
|
+
"body": "`b.credentialHash.needsRehash` never compared the stored SHAKE256 digest length against the configured length, so raising the output length never triggered a rehash and the advertised length-rotation was a silent no-op. It now flags a digest shorter than the configured/default length for rehash (upgrade-only — a longer-than-target digest is never shortened, matching the Argon2 convention)."
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"title": "The scheduler no longer lets a watchdog-abandoned run corrupt the next run",
|
|
38
|
+
"body": "`b.scheduler`'s watchdog force-clears a task's running flag after `maxJobMs` so a hung handler can't lock out future fires, and the next tick re-fires. The original slow promise then settled late and unconditionally overwrote the task's running / lastFinish / lastError state and emitted a `system.scheduler.task.success`|`failure` for a run the watchdog had already given up on — clobbering the new run and double-counting. Each run is now tagged with a generation that the watchdog and every fire bump; a settle whose tag is stale is ignored."
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.15.8",
|
|
4
|
+
"date": "2026-06-13",
|
|
5
|
+
"headline": "Redacts OTLP log-sink attributes to close a secret/PII egress the span fix missed, adds EU DSA and China PIPL cross-border compliance record-builders, ships an SSDF producer self-attestation with every release, and makes the published tarball reproducible",
|
|
6
|
+
"summary": "This release closes a telemetry egress hole, adds two compliance record-builder namespaces, and hardens the release supply chain. The OTLP log sinks (HTTP-JSON and gRPC) shipped a log record's meta attributes and the resource attributes to the collector unredacted — a log line carrying a bearer token, password, or API key reached the wire verbatim (CWE-532). The 0.15.4 fix wired the telemetry redactor into the span and metric exporters but the log sinks were missed; both now run record and resource attributes through the same redactor before serialization. New b.dsa builds the EU Digital Services Act (Reg 2022/2065) records an intermediary or platform must keep — Art. 16 notice-and-action, Art. 17 statement of reasons, and the Art. 15 / 24(3) transparency report. New b.pipl builds the China PIPL cross-border transfer records — an Art. 38/40 assessment that determines whether a CAC security assessment is mandatory (CIIO, important data, or the volume / sensitive-PI thresholds), and an Art. 40 security-assessment certificate. On the supply-chain side, every release now ships ssdf-attestation.json, a machine-readable NIST SP 800-218 / OMB M-22-18 producer self-attestation mapping each secure-development practice to its implementing control, and the published tarball is now packed with SOURCE_DATE_EPOCH so an operator can rebuild it byte-for-byte from the release commit.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Added",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "b.dsa — EU Digital Services Act compliance record-builders",
|
|
13
|
+
"body": "`b.dsa` builds the dated, frozen records the EU Digital Services Act (Regulation (EU) 2022/2065) requires an online intermediary or platform to keep: `b.dsa.noticeAndAction` (Art. 16) records a notice against a piece of content and computes the action-due window; `b.dsa.statementOfReasons` (Art. 17) records a moderation decision with its legal or contractual ground (exactly one is required), the facts, whether it was automated, and the redress routes offered; `b.dsa.transparencyReport` (Art. 15 / 24(3)) aggregates the period counts into a report with the next-due date. The builders perform no network I/O and emit a best-effort audit event; they map to the `dsa` compliance posture."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"title": "b.pipl — China PIPL cross-border transfer record-builders",
|
|
17
|
+
"body": "`b.pipl.sccFilingAssessment` builds a PIPL Art. 38/40/55 cross-border transfer assessment and determines the lawful mechanism: it forces a CAC security assessment (over a self-selected standard contract or certification) when the exporter is a critical-information-infrastructure operator, exports important data, handles personal information of more than 1,000,000 individuals, or crosses the cumulative volume / sensitive-PI thresholds. `b.pipl.securityAssessmentCertificate` records an Art. 40 security-assessment self-declaration with a 3-year validity clock. Both return frozen dated records and map to the `pipl-cn` posture."
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"title": "SSDF producer self-attestation shipped with every release",
|
|
21
|
+
"body": "Every release now attaches `ssdf-attestation.json` — a machine-readable NIST SP 800-218 (SSDF v1.1) / OMB M-22-18 producer self-attestation. It maps each secure-software-development practice to its implementing control in the tree (SLSA L3 provenance, SSH-signed tags, vendored zero-runtime-dep supply chain, OSV-Scanner gating, coordinated disclosure) and is deterministic from the release commit. Its sha256 is a subject of the SLSA L3 provenance, so verifying the provenance verifies the attestation has not been tampered with. Downstream consumers who require SSDF supplier-compliance evidence can download it from the release page."
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"heading": "Security",
|
|
27
|
+
"items": [
|
|
28
|
+
{
|
|
29
|
+
"title": "OTLP log sinks redact record and resource attributes before export",
|
|
30
|
+
"body": "`b.logStream`'s OTLP log sinks (HTTP-JSON and gRPC) shipped a log record's `meta` attributes and the sink's resource attributes to the collector UNREDACTED, so a log line whose meta carried a bearer token, password, or API key — or a credential placed in a resource attribute — reached the OTLP wire verbatim (CWE-532). The 0.15.4 change baked the telemetry redactor into the span and metric exporters but its detector was anchored on the span/metric encoder function names, leaving the log sinks uncovered. Both log sinks now run record and resource attributes through `b.observability.redactAttrs` before serialization, the same egress contract the span and metric exporters already hold."
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"title": "Reproducible published tarball (SOURCE_DATE_EPOCH)",
|
|
34
|
+
"body": "The release workflow now exports `SOURCE_DATE_EPOCH` (derived from the tagged commit's author date) before `npm pack`, so the mtime stamped into every tar header is deterministic. An operator can re-pack the package from the same commit and match the published tarball's sha256 byte-for-byte, strengthening the source-to-artifact verification path alongside the existing SLSA L3 provenance and PQC signatures."
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"heading": "Detectors",
|
|
40
|
+
"items": [
|
|
41
|
+
{
|
|
42
|
+
"title": "otlp-log-sink-encodes-attrs-without-redactor",
|
|
43
|
+
"body": "Fires when an OTLP log-sink encoder hands a raw `record.meta` or resource-attribute map to serialization without routing it through `observability.redactAttrs` — the class the span/metric detector could not see because the log sinks carry the OTLP-logs schema function names."
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.15.9",
|
|
4
|
+
"date": "2026-06-13",
|
|
5
|
+
"headline": "Raises the Node floor to 24.16, adds SQLite parse-time resource caps, retries Windows rename locks on every atomic write and download, and ships a one-call secure logout that wipes client-side state",
|
|
6
|
+
"summary": "This release moves the engines floor to the current Node 24 LTS patch level and adds three hardening primitives. node:sqlite handles now construct with SQLITE_LIMIT_* caps: a statement over 1 MiB is rejected at parse time (a DoS floor on the raw-SQL surface, complementary to the existing row-count gate) and ATTACH DATABASE is denied. Every final temp-to-destination rename — the file written by an atomic write, a downloaded file, a sealed vault key, a rotated log, an extracted archive entry — now routes through a single retry that rides out a transient Windows lock (antivirus, the search indexer, or a file-sync client briefly holding the destination), instead of surfacing the lock as a hard failure; the retry, previously hand-rolled and unreachable, is now the reusable b.atomicFile.renameWithRetry. And b.session.logout destroys a session and tells the browser to wipe its client-side state in one call: it emits an RFC 9527 Clear-Site-Data header and expires the session cookie before destroying the row, the secure-default logout that previously had to be assembled by hand.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Changed",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "Node engines floor raised to >=24.16.0",
|
|
13
|
+
"body": "The minimum supported Node is now 24.16.0 (the current Node 24 LTS patch level), up from 24.14.1. This is an LTS-currency bump — there are no Node CVE fixes between 24.14.1 and 24.16.0 (24.14.1 already carried the CVE-2026-21713 HMAC fix); it keeps the framework on the latest patched LTS and makes the node:sqlite resource-cap hardening below available everywhere. Pre-1.0, operators upgrade across the floor; Node 26 continues to satisfy it."
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"heading": "Security",
|
|
19
|
+
"items": [
|
|
20
|
+
{
|
|
21
|
+
"title": "SQLite parse-time statement-size cap",
|
|
22
|
+
"body": "Every node:sqlite database the framework opens — the main db handle and the CLI's handle — now constructs with a SQLITE_LIMIT_LENGTH cap: a SQL statement over 1 MiB is rejected at parse time. Because the query builder parameterizes every value, the size cap guards the raw-SQL surface (`b.db.runSql`) against an attacker-influenced megaquery the parser would otherwise process (SQLite's default is 1 GB); it is a parse-time DoS floor complementary to the existing row-count gate. Legitimate framework and operator statements are far under the cap."
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"title": "Windows rename-lock retry on every atomic rename and download",
|
|
26
|
+
"body": "On Windows a freshly-written file's destination is briefly held by antivirus, the search indexer, or a file-sync client (Dropbox, OneDrive), surfacing as a transient EPERM / EACCES / EBUSY on rename even though the temp file is fine. `b.atomicFile.writeSync` already retried this, but `b.httpClient.downloadStream` did not — a download into a cloud-synced or AV-scanned directory could fail hard on the lock. The retry is now the reusable `b.atomicFile.renameWithRetry`, and every final temp-to-destination rename in the framework routes through it: downloads, sealed vault keys, CA key/cert writes, log rotation, archive extraction, config-drift sidecars, the self-update binary swap, and restore/rollback moves. A non-transient error still throws immediately; POSIX renames are unaffected."
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"heading": "Added",
|
|
32
|
+
"items": [
|
|
33
|
+
{
|
|
34
|
+
"title": "b.session.logout — one-call secure logout",
|
|
35
|
+
"body": "`b.session.logout(res, token, opts?)` destroys the server-side session AND tells the browser to wipe its client-side state in one call: it emits an RFC 9527 Clear-Site-Data response header (cookies + storage + cache + execution contexts by default) and expires the session cookie, then destroys the session row. `b.session.destroy` alone is a store operation with no response object, so it could not wipe the browser's cached pages, storage, or a stale tab still holding the now-revoked cookie — that wiring previously had to be mounted by hand. Pass `cookieName` to match a non-default cookie and `types` to choose the Clear-Site-Data directives."
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"heading": "Fixed",
|
|
41
|
+
"items": [
|
|
42
|
+
{
|
|
43
|
+
"title": "b.watcher canonicalizes its root on Windows",
|
|
44
|
+
"body": "`b.watcher.create` now resolves its `root` to the real long path before watching. On Windows a root with an 8.3 short-name component (the system temp directory commonly resolves to one) made the native recursive backend deliver long-name event paths that no longer prefix-matched the watched root, which could abort the process under a strict libuv fs-event assertion. The watcher now canonicalizes the root (expanding short names and resolving symlinks), so events match the watched directory on Windows."
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"heading": "Detectors",
|
|
50
|
+
"items": [
|
|
51
|
+
{
|
|
52
|
+
"title": "Rename-retry, SQLite-limits, and Clear-Site-Data guards",
|
|
53
|
+
"body": "Three recurrence detectors ship with the fixes: a bare `nodeFs.renameSync` final rename that doesn't route through `atomicFile.renameWithRetry`; a main `DatabaseSync` handle constructed without the SQLITE_LIMIT_LENGTH `limits`; and a hand-rolled Clear-Site-Data header value that skips the shared RFC 9527 builder."
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
}
|
|
@@ -277,6 +277,22 @@ var OUT_OF_BAND_BREAKS = [
|
|
|
277
277
|
"- If you pinned, cached, or asserted on the raw signature bytes of this library's ES256 / ES384 output, update the fixture — the bytes are now `ieee-p1363`. EdDSA / ML-DSA signatures are unchanged.",
|
|
278
278
|
].join("\n"),
|
|
279
279
|
},
|
|
280
|
+
{
|
|
281
|
+
release: "v0.15.7",
|
|
282
|
+
surface: "b.auth.oauth verifyIdToken — azp (authorized party) is now enforced",
|
|
283
|
+
summary: "verifyIdToken now applies OIDC Core 3.1.3.7: a multi-audience ID token (aud is an array with more than one entry) MUST carry an azp claim, and a present azp MUST equal the configured client_id. A token whose azp is a different client, or a multi-audience token with no azp, now throws (auth-oauth/azp-mismatch / auth-oauth/azp-required). Previously only `aud contains client_id` was checked, so a token authorized for a different party but also listing this RP verified clean.",
|
|
284
|
+
migration: [
|
|
285
|
+
"No change for the common single-audience ID token with no azp. If your IdP issues multi-audience ID tokens, ensure it sets azp to your client_id (it should, per the spec) — otherwise verifyIdToken will now reject them. This is a security fix; a token that fails the new check was authorized for a different client.",
|
|
286
|
+
].join("\n"),
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
release: "v0.15.7",
|
|
290
|
+
surface: "b.safeUrl.canonicalize — IPv4-mapped hosts fold to IPv4",
|
|
291
|
+
summary: "b.safeUrl.canonicalize / b.ssrfGuard.canonicalizeHost now fold an IPv4-mapped IPv6 host (::ffff:1.2.3.4) to its embedded IPv4 dotted form, and strip every trailing dot from a host. In 0.15.6 it canonicalized to an IPv6 string and only one trailing dot was stripped. NAT64 / 6to4 hosts stay IPv6.",
|
|
292
|
+
migration: [
|
|
293
|
+
"No code change is needed — this makes a dual-stack / NAT64 peer unify with a dotted-IPv4 allow/deny entry as intended. If you persisted canonical host strings produced by 0.15.6 (e.g. as cache or dedup keys) and compare them against freshly-canonicalized hosts, recompute them: an IPv4-mapped host now yields the dotted IPv4 instead of the bracketed IPv6, and a multi-trailing-dot host yields the bare name.",
|
|
294
|
+
].join("\n"),
|
|
295
|
+
},
|
|
280
296
|
{
|
|
281
297
|
release: "v0.15.6",
|
|
282
298
|
surface: "b.auth.oauth verifyIdToken — skipExpCheck is restricted to logout tokens",
|