@blamejs/blamejs-shop 0.5.15 → 0.5.17
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/lib/asset-manifest.json +1 -1
- package/lib/backorder.js +29 -9
- package/lib/stock-transfers.js +10 -2
- package/lib/vendor/MANIFEST.json +19 -13
- package/lib/vendor/blamejs/CHANGELOG.md +2 -0
- package/lib/vendor/blamejs/NOTICE +1 -1
- package/lib/vendor/blamejs/api-snapshot.json +2 -2
- package/lib/vendor/blamejs/examples/wiki/package-lock.json +1 -1
- package/lib/vendor/blamejs/lib/audit-sign.js +10 -1
- package/lib/vendor/blamejs/lib/session.js +65 -18
- package/lib/vendor/blamejs/lib/vendor/MANIFEST.json +11 -11
- package/lib/vendor/blamejs/lib/vendor/public-suffix-list.dat +2 -6
- package/lib/vendor/blamejs/lib/vendor/public-suffix-list.data.js +2215 -2217
- package/lib/vendor/blamejs/package-lock.json +2 -2
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.17.23.json +40 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-sign.test.js +480 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/session.test.js +593 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.5.x
|
|
10
10
|
|
|
11
|
+
- v0.5.17 (2026-07-25) — **Backorder capacity is enforced atomically under concurrent requests, and a stock-transfer over-receipt no longer mints net inventory.** Two inventory-quantity integrity fixes. recordBackorder checked the per-SKU supplier cap (max_backorder_quantity) against a snapshot read and then incremented the pending counter unconditionally, so two concurrent backorders of the same SKU could both pass the check on the same starting value and both apply their increment, pushing the pending total past the cap and over-committing to the supplier. The increment now folds the cap into a single guarded update — only an increment that keeps the pending total within the cap applies — and is gated on the row count: a losing claim removes the line it had inserted and refuses, so the counter can never exceed the cap regardless of interleaving. Separately, stock-transfer reconcile credited the destination location with the full received quantity, so a mis-scan that received MORE than was shipped credited the destination beyond what the origin was debited at ship time — minting net sellable inventory the transfer created nowhere. The destination credit is now clamped to the shipped (debited) quantity, and the over-count is recorded as a negative discrepancy for the operator; a normal or short receipt is unchanged. **Fixed:** *Backorder supplier cap holds under concurrent requests* — recordBackorder enforced max_backorder_quantity by reading the current pending total, checking pending + requested <= cap, and then unconditionally incrementing the counter. Two concurrent backorders of the same SKU could each read the same pending value, each pass the check, and each apply its increment, leaving the pending total above the cap — over-committing beyond supplier capacity. The counter increment now carries the cap in its WHERE clause so only an increment that keeps the total within the cap applies, and the call checks that exactly one row changed; a losing claim (a concurrent line took the remaining capacity, or the SKU was deactivated) removes the line it inserted and refuses with the current reason. The early snapshot check is kept as a fast-fail for the common case. · *Stock-transfer over-receipt no longer mints net inventory* — When a transfer was reconciled, the destination location was credited with the full received quantity. A mis-scan (or a genuinely found unit) that recorded more received than was shipped therefore credited the destination beyond what the origin had been debited when the transfer opened — creating net sellable stock the transfer never moved. The destination credit is now clamped to the shipped quantity (what the origin was debited), so an over-receipt can no longer mint inventory; the discrepancy column records the true shipped-minus-received delta (negative for an over-receipt) so the operator still sees the mis-count. Exact and short receipts are unaffected.
|
|
12
|
+
|
|
13
|
+
- v0.5.16 (2026-07-25) — **Vendored framework refreshed to 0.17.23: session refresh paths enforce the idle/absolute timeout floor and two verifier fail-closed contracts are hardened.** Refreshes the vendored framework from 0.17.22 to 0.17.23, a security patch to primitives the store composes; all of the following are inherited without shop code changes. The encrypted-session primitive now enforces its idle and absolute timeout floors on the refresh paths, not just on read: rotating a session or writing session data previously reset the activity clock without re-checking the idle floor, so a session already past its idle timeout — one a normal verify would expire and delete — could be revived with a fresh idle window; both refresh paths now fail closed on a breach. A strict device-fingerprint policy configured without a request to compute the fingerprint now fails closed instead of silently admitting the session from any device. The audit-signature verifier now returns false on a malformed public key instead of throwing, honoring its never-throw contract for callers that branch on the boolean. The bundled Public Suffix List is refreshed to the current snapshot. The minimum Node.js engine is unchanged at 24.18.0. **Changed:** *Vendored framework refreshed to 0.17.23* — Advances the vendored framework from 0.17.22 to 0.17.23. Beyond the session and verifier fixes above, the bundled Public Suffix List is refreshed to the current upstream snapshot so registrable-domain and public-suffix checks across URL, cookie, and SSRF handling reflect the latest registry delegations. The minimum Node.js engine is unchanged at 24.18.0. **Security:** *Session refresh paths can no longer resurrect an idle-expired session* — The encrypted-session primitive enforces two floors independent of the configured TTL — an idle timeout and an absolute timeout. Read paths already enforced them, but rotating a session (on a privilege change) or writing session data reset the activity clock without re-checking the idle floor, so a session already past its idle timeout — which a normal verify would expire and delete — could be revived with a fresh window by rotating or writing to it. Both refresh paths now route through the same floor check and fail closed on a breach, and the idle/absolute-expiry audit signal is emitted consistently. Inherited from the framework refresh; the store's sessions gain the tighter enforcement with no configuration change. · *A strict device-fingerprint policy with no request now fails closed; audit-signature verify no longer throws on a malformed key* — Two verifier fail-closed fixes ride in with the refresh. A strict device-binding policy (require-fingerprint-match / max-anomaly-score) that was configured without a request to compute the current fingerprint previously skipped the binding check and admitted the session from any device; it now fails closed. And the audit-signature verifier, documented never to throw so callers can branch on its boolean, threw on a malformed / non-PEM public key (an untrusted key can reach it from a signature block); it now catches that and returns false, so a verifier-only consumer cleanly rejects instead of crashing. Inherited from the framework refresh.
|
|
14
|
+
|
|
11
15
|
- v0.5.15 (2026-07-25) — **`giftCardLedger.history` and `loyaltyRedemption.redemptionsForCustomer` no longer return a next-page cursor that resolves to an empty page.** Fixes a cursor-pagination contract defect in two ledger/history primitives. giftCardLedger.history and loyaltyRedemption.redemptionsForCustomer emitted a next-page cursor whenever a page came back full — its row count equalled the requested limit — instead of first peeking one row past the page. When the history length is an exact multiple of the page size, the last full page still handed back a cursor; following it queried for strictly older rows and returned nothing, so a caller paginating with the cursor would land on an empty page. Both methods now fetch one extra row, emit the cursor only when a genuinely older row exists, and return the page trimmed to the requested size — matching the store-credit and loyalty-transaction history that already peek. This is an API-level correctness fix: the shop's current callers of these two methods read only the returned rows (the admin gift-card ledger view and the account loyalty-redemptions list do not paginate on this cursor), so no live page changes today; the fix corrects the primitives' cursor contract for any consumer that does paginate and removes a latent phantom-page. A codebase check now covers each method so the peek can't be dropped again. **Fixed:** *Gift-card and loyalty-redemption history return an honest next-page cursor* — giftCardLedger.history and loyaltyRedemption.redemptionsForCustomer keyed their next-page cursor off "the page came back full" (row count === limit) rather than peeking one row beyond the page, so a history whose length is an exact multiple of the page size returned a cursor on its last full page that resolves to an empty page when followed. Both now fetch limit + 1 rows, emit the cursor only when an older row actually exists, and return the page trimmed to the requested limit — the same peek the store-credit and loyalty-transaction history already use. The change is to the primitives' pagination contract (the shop's present callers of these two methods consume only the rows, so no rendered page changes); it removes a latent phantom next page for any consumer that paginates on the cursor. A codebase-patterns check now guards each method against a regression that drops the peek.
|
|
12
16
|
|
|
13
17
|
- v0.5.14 (2026-07-25) — **A sales-tax refund is now credited in the filing period it occurs even when the refunded order was placed in an earlier period, closing a systematic over-remittance.** Fixes a sales-tax over-remittance (CWE-682, incorrect calculation) in sales_tax_filings.computeFiling. A partial or full refund reduces the filing for the period it occurs in, but the computation only consulted refunds for orders that were ALSO created inside that filing window. Because a refund almost always lands in a later period than the sale — the norm under monthly filing — a refund of a prior-period order was silently dropped from every filing: the sale period had already remitted the full tax, and the refund period never subtracted it. The operator over-remitted the refunded tax to the jurisdiction. computeFiling now nets every in-window refund against the current period regardless of when the refunded order was placed, apportioning the refunded cash to subtotal and tax by the order's own structure, matching it to the order's jurisdiction and exemption, and attributing the reduction to the tax rate effective at the order's sale time so both collected and owed reflect it. A window whose refunds exceed its own sales floors at zero rather than storing a negative filing (the schema does not carry a net-credit forward; the guarantee is that the fix never over-remits and never violates the constraint). **Fixed:** *Cross-period sales-tax refunds are credited in the period they occur* — computeFiling netted a refund only when the refunded order fell inside the same filing window as the refund. A refund recorded in a later period than the sale — the common case under monthly or quarterly filing — was therefore attributed to no period at all: the sale period had already remitted the tax in full, and the refund period skipped the order because it was created earlier. The refunded tax was remitted and never credited, so the operator over-paid the jurisdiction on every returned prior-period sale. The filing now processes every refund recorded inside the window against the current period, whether or not the refunded order was created in it — apportioning the refunded cash to taxable subtotal and tax by the order's own totals, honoring the order's jurisdiction and customer exemption, and reducing the rate bucket effective at the order's sale time so tax_collected and tax_owed both net the refund. A window whose in-window sales are outweighed by refunds of earlier-period orders reports zero rather than a negative filing.
|
package/lib/asset-manifest.json
CHANGED
package/lib/backorder.js
CHANGED
|
@@ -271,6 +271,27 @@ function create(opts) {
|
|
|
271
271
|
|
|
272
272
|
var id = b.uuid.v7();
|
|
273
273
|
var ts = _now();
|
|
274
|
+
// Claim capacity FIRST, atomically, THEN create the line. The early
|
|
275
|
+
// check above reads a snapshot; two concurrent calls can both pass it,
|
|
276
|
+
// so the cap is folded into this guarded update — only an increment that
|
|
277
|
+
// keeps pending within the cap (on an active sku) applies, gated on
|
|
278
|
+
// rowCount. Claiming before the INSERT means a line only exists once its
|
|
279
|
+
// capacity is committed: a concurrent retry's dedup check never sees a
|
|
280
|
+
// line that a cap loss would then have to delete (which would let the
|
|
281
|
+
// retry report a phantom success for a backorder that no longer exists).
|
|
282
|
+
var claim = await query(
|
|
283
|
+
"UPDATE backorder_skus SET pending_quantity = pending_quantity + ?1, updated_at = ?2 " +
|
|
284
|
+
"WHERE sku = ?3 AND active = 1 AND (max_quantity IS NULL OR pending_quantity + ?1 <= max_quantity)",
|
|
285
|
+
[input.quantity, ts, input.sku],
|
|
286
|
+
);
|
|
287
|
+
if (Number((claim && claim.rowCount) || 0) !== 1) {
|
|
288
|
+
var cur = await _getConfig(input.sku);
|
|
289
|
+
if (!cur || cur.active !== 1) {
|
|
290
|
+
throw new TypeError("backorder.recordBackorder: sku " + JSON.stringify(input.sku) + " is not currently backorderable");
|
|
291
|
+
}
|
|
292
|
+
throw new TypeError("backorder.recordBackorder: would exceed max_backorder_quantity " +
|
|
293
|
+
"(cap=" + cur.max_quantity + ", pending=" + cur.pending_quantity + ", requested=" + input.quantity + ")");
|
|
294
|
+
}
|
|
274
295
|
try {
|
|
275
296
|
await query(
|
|
276
297
|
"INSERT INTO backorder_lines (id, order_id, customer_id, sku, quantity, status, " +
|
|
@@ -278,10 +299,14 @@ function create(opts) {
|
|
|
278
299
|
[id, input.order_id, input.customer_id, input.sku, input.quantity, ts],
|
|
279
300
|
);
|
|
280
301
|
} catch (e) {
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
//
|
|
302
|
+
// A concurrent call for the same (order_id, sku) already recorded the
|
|
303
|
+
// line (UNIQUE violation) after we claimed capacity. Release the
|
|
304
|
+
// capacity we just claimed so the line is counted exactly once, then
|
|
305
|
+
// return the dedup shape (or re-raise a non-duplicate error).
|
|
306
|
+
await query(
|
|
307
|
+
"UPDATE backorder_skus SET pending_quantity = pending_quantity - ?1, updated_at = ?2 WHERE sku = ?3",
|
|
308
|
+
[input.quantity, _now(), input.sku],
|
|
309
|
+
);
|
|
285
310
|
var redup = await query(
|
|
286
311
|
"SELECT id, status FROM backorder_lines WHERE order_id = ?1 AND sku = ?2 LIMIT 1",
|
|
287
312
|
[input.order_id, input.sku],
|
|
@@ -291,11 +316,6 @@ function create(opts) {
|
|
|
291
316
|
}
|
|
292
317
|
throw e;
|
|
293
318
|
}
|
|
294
|
-
await query(
|
|
295
|
-
"UPDATE backorder_skus SET pending_quantity = pending_quantity + ?1, updated_at = ?2 " +
|
|
296
|
-
"WHERE sku = ?3",
|
|
297
|
-
[input.quantity, ts, input.sku],
|
|
298
|
-
);
|
|
299
319
|
return { id: id, status: "recorded" };
|
|
300
320
|
},
|
|
301
321
|
|
package/lib/stock-transfers.js
CHANGED
|
@@ -688,11 +688,19 @@ function create(opts) {
|
|
|
688
688
|
var line = transfer.lines[i];
|
|
689
689
|
if (line.discrepancy != null) continue; // already credited + stamped on a prior attempt
|
|
690
690
|
var rx = line.quantity_received == null ? 0 : line.quantity_received;
|
|
691
|
-
|
|
691
|
+
// Credit the destination only up to what origin was debited at open
|
|
692
|
+
// (quantity_shipped). An over-receipt (a mis-scan, or a genuinely
|
|
693
|
+
// found unit) is a real discrepancy worth recording, but crediting
|
|
694
|
+
// the full received amount would mint net inventory the transfer
|
|
695
|
+
// debited nowhere. The discrepancy below still records the true
|
|
696
|
+
// shipped − received delta (negative for an over-receipt) so the
|
|
697
|
+
// operator sees the mis-count.
|
|
698
|
+
var credited = rx > line.quantity_shipped ? line.quantity_shipped : rx;
|
|
699
|
+
if (credited > 0) {
|
|
692
700
|
await locations.creditOnce({
|
|
693
701
|
sku: line.sku,
|
|
694
702
|
location_code: transfer.to_location,
|
|
695
|
-
delta:
|
|
703
|
+
delta: credited,
|
|
696
704
|
reason: "stock-transfer:reconcile:" + id,
|
|
697
705
|
idempotency_key: "stock-transfer:reconcile:" + id + ":" + line.id,
|
|
698
706
|
});
|
package/lib/vendor/MANIFEST.json
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
"_about": "blamejs.shop vendors a single framework — blamejs — which itself bundles every server-side crypto/identity dependency. The transitive packages blamejs ships are surfaced in its own MANIFEST.json at lib/vendor/blamejs/lib/vendor/MANIFEST.json — Trivy / Grype rely on that nested data for CVE attribution.",
|
|
4
4
|
"packages": {
|
|
5
5
|
"blamejs": {
|
|
6
|
-
"version": "0.17.
|
|
7
|
-
"tag": "v0.17.
|
|
6
|
+
"version": "0.17.23",
|
|
7
|
+
"tag": "v0.17.23",
|
|
8
8
|
"license": "Apache-2.0",
|
|
9
9
|
"author": "blamejs contributors",
|
|
10
10
|
"source": "https://github.com/blamejs/blamejs",
|
|
@@ -858,6 +858,7 @@
|
|
|
858
858
|
"release-notes/v0.17.20.json": "lib/vendor/blamejs/release-notes/v0.17.20.json",
|
|
859
859
|
"release-notes/v0.17.21.json": "lib/vendor/blamejs/release-notes/v0.17.21.json",
|
|
860
860
|
"release-notes/v0.17.22.json": "lib/vendor/blamejs/release-notes/v0.17.22.json",
|
|
861
|
+
"release-notes/v0.17.23.json": "lib/vendor/blamejs/release-notes/v0.17.23.json",
|
|
861
862
|
"release-notes/v0.17.3.json": "lib/vendor/blamejs/release-notes/v0.17.3.json",
|
|
862
863
|
"release-notes/v0.17.4.json": "lib/vendor/blamejs/release-notes/v0.17.4.json",
|
|
863
864
|
"release-notes/v0.17.5.json": "lib/vendor/blamejs/release-notes/v0.17.5.json",
|
|
@@ -1046,6 +1047,7 @@
|
|
|
1046
1047
|
"test/layer-0-primitives/audit-segregation.test.js": "lib/vendor/blamejs/test/layer-0-primitives/audit-segregation.test.js",
|
|
1047
1048
|
"test/layer-0-primitives/audit-sign-anchor.test.js": "lib/vendor/blamejs/test/layer-0-primitives/audit-sign-anchor.test.js",
|
|
1048
1049
|
"test/layer-0-primitives/audit-sign-ml-dsa-65.test.js": "lib/vendor/blamejs/test/layer-0-primitives/audit-sign-ml-dsa-65.test.js",
|
|
1050
|
+
"test/layer-0-primitives/audit-sign.test.js": "lib/vendor/blamejs/test/layer-0-primitives/audit-sign.test.js",
|
|
1049
1051
|
"test/layer-0-primitives/audit-signing-key-rotation.test.js": "lib/vendor/blamejs/test/layer-0-primitives/audit-signing-key-rotation.test.js",
|
|
1050
1052
|
"test/layer-0-primitives/audit-tools-dual-control.test.js": "lib/vendor/blamejs/test/layer-0-primitives/audit-tools-dual-control.test.js",
|
|
1051
1053
|
"test/layer-0-primitives/audit-tools-return-bytes.test.js": "lib/vendor/blamejs/test/layer-0-primitives/audit-tools-return-bytes.test.js",
|
|
@@ -1572,6 +1574,7 @@
|
|
|
1572
1574
|
"test/layer-0-primitives/session-extensions.test.js": "lib/vendor/blamejs/test/layer-0-primitives/session-extensions.test.js",
|
|
1573
1575
|
"test/layer-0-primitives/session-strict-binding-missing-failclosed.test.js": "lib/vendor/blamejs/test/layer-0-primitives/session-strict-binding-missing-failclosed.test.js",
|
|
1574
1576
|
"test/layer-0-primitives/session-valid-from.test.js": "lib/vendor/blamejs/test/layer-0-primitives/session-valid-from.test.js",
|
|
1577
|
+
"test/layer-0-primitives/session.test.js": "lib/vendor/blamejs/test/layer-0-primitives/session.test.js",
|
|
1575
1578
|
"test/layer-0-primitives/shape-match.test.js": "lib/vendor/blamejs/test/layer-0-primitives/shape-match.test.js",
|
|
1576
1579
|
"test/layer-0-primitives/sigv4-bucket-ops.test.js": "lib/vendor/blamejs/test/layer-0-primitives/sigv4-bucket-ops.test.js",
|
|
1577
1580
|
"test/layer-0-primitives/sigv4-multipart-sse.test.js": "lib/vendor/blamejs/test/layer-0-primitives/sigv4-multipart-sse.test.js",
|
|
@@ -1678,18 +1681,18 @@
|
|
|
1678
1681
|
".npmrc": "sha256:66f104e7d07c496d2d0409988225e8c0e4ceb8d247dbcac3be75b2128d20ce66",
|
|
1679
1682
|
".pinact.yaml": "sha256:0213ffda55961dc49b64c0a5dfa3c0567419633b1499d57eaf7c8d842d7da6c7",
|
|
1680
1683
|
"ARCHITECTURE.md": "sha256:9b1c8d2b1b7a41838eb348b0a008e4b4369718fd72bfe2974b37155f7536d35b",
|
|
1681
|
-
"CHANGELOG.md": "sha256:
|
|
1684
|
+
"CHANGELOG.md": "sha256:bdae4688ee7a4641dff4a3e6c3f46ae585cfd8018b8af1cb367d16b6e1c09845",
|
|
1682
1685
|
"CODE_OF_CONDUCT.md": "sha256:148a281960fff7c2fe6554dab66da572c72245ddeb00b0d14811558397bff386",
|
|
1683
1686
|
"CONTRIBUTING.md": "sha256:3e4575c3cd57e72317773a80faabcabb611133ace5ee523db4b8d631e80bdaa8",
|
|
1684
1687
|
"GOVERNANCE.md": "sha256:906df6afb1f552b27b9acb50f7f96c47b917a2f1021cd4e987dbf4ee0e0a821b",
|
|
1685
1688
|
"LICENSE": "sha256:d1b40781c0774cb3b2936beb466709994d164b0f7466000be5279b5ed522af65",
|
|
1686
1689
|
"LTS-CALENDAR.md": "sha256:8ed8c0051c3d4e14637a24555751b07758fbac2678688d9e1aca2ce312bcf585",
|
|
1687
1690
|
"MIGRATING.md": "sha256:3dcc952a3d4a77d53ff60fb67cb5eb5c3a3db2449d7c71f9c4dc7f868097153c",
|
|
1688
|
-
"NOTICE": "sha256:
|
|
1691
|
+
"NOTICE": "sha256:cd95a020eeddf2b0d808fd34c3aab74ab15d43ed3ec5fcb67c0472b712860983",
|
|
1689
1692
|
"README.md": "sha256:e1c6d401d81c959a0f264d73b3640a26465921f0ceb1103be6936816d99d21b4",
|
|
1690
1693
|
"ROADMAP.md": "sha256:d66be1de21603794608395c57ecc17381b673452ea74159bd805c85684821ecd",
|
|
1691
1694
|
"SECURITY.md": "sha256:f7f853de9d344624cdaddfc1b5b1782eee3626dd855b32318340ec9a2aa4c6bd",
|
|
1692
|
-
"api-snapshot.json": "sha256:
|
|
1695
|
+
"api-snapshot.json": "sha256:feff37cc033c1220d79702bcafcbb43402f6ddab463f23ad0a5a5ee4060a603d",
|
|
1693
1696
|
"assets/BlameJS_Logo.png": "sha256:3c65699753c771b48ef9ac7f45bb40815ec19a23afcdd0cd30ef4601bbbe293e",
|
|
1694
1697
|
"assets/BlameJS_Logo.svg": "sha256:dda44f3fb1343d5de9db6b1fcdb75fc649c57e7a99a8e8239fcf852e3841e1a8",
|
|
1695
1698
|
"bench/README.md": "sha256:74202f2507fd840bfc1ac6c681975d9273cf36cca6e0f72655f138337304033c",
|
|
@@ -1749,7 +1752,7 @@
|
|
|
1749
1752
|
"examples/wiki/lib/symbol-index.js": "sha256:abd133b4aa8e582cf72b7c92fafe7f342b2d06d1a9f3d1082eb0c4df8a743b0e",
|
|
1750
1753
|
"examples/wiki/migrations/0001-pages-schema.js": "sha256:b4459ee6d1d4f1130dbf0c7a22b39ffa0f972a7ca3a0ef19215abe4bf35e46a5",
|
|
1751
1754
|
"examples/wiki/nginx.conf": "sha256:27763c094e7a753a37e52f9227bfe8b2b9f36bdd382bc7e1d1e0892b1f5781e1",
|
|
1752
|
-
"examples/wiki/package-lock.json": "sha256:
|
|
1755
|
+
"examples/wiki/package-lock.json": "sha256:1ca6a8f857a3eab57637ec684c898298c2b96040797923360fe73c5f388ab316",
|
|
1753
1756
|
"examples/wiki/package.json": "sha256:8efb9ee3012107f9631ce23e7846ff2b10cadf643ec449e7940de496728aaedb",
|
|
1754
1757
|
"examples/wiki/public/img/blamejs-logo.png": "sha256:3c65699753c771b48ef9ac7f45bb40815ec19a23afcdd0cd30ef4601bbbe293e",
|
|
1755
1758
|
"examples/wiki/public/img/blamejs-logo.svg": "sha256:dda44f3fb1343d5de9db6b1fcdb75fc649c57e7a99a8e8239fcf852e3841e1a8",
|
|
@@ -1945,7 +1948,7 @@
|
|
|
1945
1948
|
"lib/audit-chain.js": "sha256:1982abb2fa50a5fb99eaac144db907c696ca1332dae08068f49055e5b9a688f3",
|
|
1946
1949
|
"lib/audit-daily-review.js": "sha256:ae9c90652c87ac11122409d9358d2b302fd76ec09091d4a887c16b6758dc585b",
|
|
1947
1950
|
"lib/audit-emit.js": "sha256:40aa17c9e78fedb2314774888e613ef1457c8c09d73b915b4af83989f31ff369",
|
|
1948
|
-
"lib/audit-sign.js": "sha256:
|
|
1951
|
+
"lib/audit-sign.js": "sha256:ebfe0558309ab7cda4eac87bb560f4a5e9a80700d3624741700d94bd3fb4dfc4",
|
|
1949
1952
|
"lib/audit-tools.js": "sha256:9d7ea2a9164b851f1c9bb5dea6727e5a6a1f4689a392d31b6815213f24228886",
|
|
1950
1953
|
"lib/audit.js": "sha256:c4c361432920dfce4f49953ad67929f0b50a405cacfede7b956733e38b8f3534",
|
|
1951
1954
|
"lib/auth-bot-challenge.js": "sha256:bfc9d3f2d0e943098d0e0f88fd30f081bc8185845b67679a0a5363a8b42916f2",
|
|
@@ -2406,7 +2409,7 @@
|
|
|
2406
2409
|
"lib/server-timing.js": "sha256:556034593eab2842cb6bd1990c8939c43e1ef55151c532118196003d0580617c",
|
|
2407
2410
|
"lib/session-device-binding.js": "sha256:8df2e56ad886dff05e8d534b19f1ea74a0b07b1f505c78d7ff259166bae90e9f",
|
|
2408
2411
|
"lib/session-stores.js": "sha256:33306898c178c072972d09c751b65caf292769ea8e2a4c0e4c3a1f7085a1b951",
|
|
2409
|
-
"lib/session.js": "sha256:
|
|
2412
|
+
"lib/session.js": "sha256:1c19b97dc3907a8c1708c4674e38912a92e3ea146aed7be42910ba1333490741",
|
|
2410
2413
|
"lib/slug.js": "sha256:5ce2d3c85bec0cfbe82301039813938072de63b2ee170ba74207d1ac714aeec6",
|
|
2411
2414
|
"lib/sql.js": "sha256:65cc66d7f7079a338b6cc572d92b0cfe4c3d038996eaa9e1abfac8b13459385b",
|
|
2412
2415
|
"lib/sse.js": "sha256:e13f452aa1b099ef798a207f83612bf7739cd7745931f7e7e225748ae68bb6c1",
|
|
@@ -2440,7 +2443,7 @@
|
|
|
2440
2443
|
"lib/vc.js": "sha256:fdbb5044c9cfbc8cf728b5fad0fa93ab2ec4c14f4ad8eb33b9c71553e50c5dc4",
|
|
2441
2444
|
"lib/vendor-data.js": "sha256:a4e4b9d51b36765c5d528cbb87578a7e720d2b2327bef68f64a3455b4ecc0364",
|
|
2442
2445
|
"lib/vendor/.vendor-data-pubkey": "sha256:73a935b8c72f55d821c22ddb8188453bab324f80d229d033a0a1549819a1f096",
|
|
2443
|
-
"lib/vendor/MANIFEST.json": "sha256:
|
|
2446
|
+
"lib/vendor/MANIFEST.json": "sha256:5e605c631a133c57e2f0cb9d562352b5b456d1df19ab8385069532866c026e95",
|
|
2444
2447
|
"lib/vendor/bimi-trust-anchors.data.js": "sha256:aa7a4d33b65a68422a2a2c1670177689f66fdcaa08bd2514d78798b827bd1608",
|
|
2445
2448
|
"lib/vendor/bimi-trust-anchors.pem": "sha256:81ff9f5ab3c9774132c845684e783be95cf73146f8b670d964105f0a3765b4b4",
|
|
2446
2449
|
"lib/vendor/common-passwords-top-10000.data.js": "sha256:87b223beca89f33d2c2c32a2cfda0bc187e58061de40e7127bb5ffc4258c6e2a",
|
|
@@ -2449,8 +2452,8 @@
|
|
|
2449
2452
|
"lib/vendor/noble-curves.cjs": "sha256:2880c288b1285ef51d356d057bee6f0c8a00de36638cf47b47617e8c1faf10d5",
|
|
2450
2453
|
"lib/vendor/noble-post-quantum.cjs": "sha256:f9c94094b3c10fe73dac5343289da582454ea6053494fab2bf66099d9103d6c3",
|
|
2451
2454
|
"lib/vendor/pki.cjs": "sha256:2307ef65e070757ffb13442b377e45efb9fa1a10432d9b39618387720ab990ed",
|
|
2452
|
-
"lib/vendor/public-suffix-list.dat": "sha256:
|
|
2453
|
-
"lib/vendor/public-suffix-list.data.js": "sha256:
|
|
2455
|
+
"lib/vendor/public-suffix-list.dat": "sha256:0240b3f1e8c90ecd4af5b94597afb5ba89997ad395f292f87318caefb7e10413",
|
|
2456
|
+
"lib/vendor/public-suffix-list.data.js": "sha256:7994e24e1865f197540a988783de567fe20fb29e7f6c3a5c142766bb0cfd9bfd",
|
|
2454
2457
|
"lib/vendor/simplewebauthn-server.cjs": "sha256:e83195dc9f189385da9c856ef38843f4466f93ea8f3d7fc2efcb1e1b18da6f20",
|
|
2455
2458
|
"lib/vendor/vendor-data-pubkey.js": "sha256:a12afa34cd7472e2eaebad2fcd44714102d3edd0601e45769404124a513926d0",
|
|
2456
2459
|
"lib/vex.js": "sha256:1ebb091835a3e0e172d5a33e09cc4a522d5b6d3a60a20baf012e3e31a8272a44",
|
|
@@ -2470,8 +2473,8 @@
|
|
|
2470
2473
|
"oss-fuzz/projects/blamejs/README.md": "sha256:ae13b7bb79ed8d69b1b3276e5562807a0349fb6e6b7d11cf1f683aad1eafdb4b",
|
|
2471
2474
|
"oss-fuzz/projects/blamejs/build.sh": "sha256:0ced1cf21782c97be7f8d74faf5e27a308b60b2f858836fb5ca3b8c4e939a8f7",
|
|
2472
2475
|
"oss-fuzz/projects/blamejs/project.yaml": "sha256:59f2cb83aa622325a175b77416fe155be15b70a9c798bd1a78bba05763b1b03d",
|
|
2473
|
-
"package-lock.json": "sha256:
|
|
2474
|
-
"package.json": "sha256:
|
|
2476
|
+
"package-lock.json": "sha256:8902e9c2877d3560dd76d423beb17a3b871b0875937fa66e3d009a4b167a4a79",
|
|
2477
|
+
"package.json": "sha256:2040935760013dfb1e9e50d944fdcf9d296898485a08b7822363c07ad8c849d5",
|
|
2475
2478
|
"release-notes/v0.0.x.json": "sha256:7a49819f30068ee119000cad7010194882bb8bfaa12acbdab4dfc066efb7982f",
|
|
2476
2479
|
"release-notes/v0.1.x.json": "sha256:6742a8c17f947c5cb76f69dead7eea86b942d80621d914b774ba5488e09937e5",
|
|
2477
2480
|
"release-notes/v0.10.x.json": "sha256:fe498045daf88337bd3d987e5964aa42c99a50e1685b6f09e51f698b8687726f",
|
|
@@ -2497,6 +2500,7 @@
|
|
|
2497
2500
|
"release-notes/v0.17.20.json": "sha256:2ae86917f932b03c28832b8413c3bbb293c9346ac7894f3b9f6dcb436e856b5e",
|
|
2498
2501
|
"release-notes/v0.17.21.json": "sha256:3da6385d9053b4cea942a4c03abf2ee13e614c3b087740d76b63c84e0900f2b5",
|
|
2499
2502
|
"release-notes/v0.17.22.json": "sha256:03f056494887538e235ea38450b2079541f31c3faf8b14d4c4e238d9c301cb6c",
|
|
2503
|
+
"release-notes/v0.17.23.json": "sha256:83e01122687f5720ddbb7035b256fc1359a92741824dc61fa98efea07663d624",
|
|
2500
2504
|
"release-notes/v0.17.3.json": "sha256:cb3b271bda3df3890973bd96dfbe7738453abf21db0e7561a9861720107f4ee5",
|
|
2501
2505
|
"release-notes/v0.17.4.json": "sha256:f4a303fd0523e2ea4cb159a6aeda08dd87afc7c8057bf86929bc0ae2e31eea23",
|
|
2502
2506
|
"release-notes/v0.17.5.json": "sha256:41f07079be6ad83810a8c57d369e9251867fa7c14eed7b5e5e7e9272f5a0454c",
|
|
@@ -2685,6 +2689,7 @@
|
|
|
2685
2689
|
"test/layer-0-primitives/audit-segregation.test.js": "sha256:ab46a0b4c8cb3fa9496e1a5abd88a1db8ca1cb4782a4e3ff54c905c1ac5d2333",
|
|
2686
2690
|
"test/layer-0-primitives/audit-sign-anchor.test.js": "sha256:a972cfd24364628ba603eda505b40832718ddb611774a245daf38381bc4c75d9",
|
|
2687
2691
|
"test/layer-0-primitives/audit-sign-ml-dsa-65.test.js": "sha256:85ab5ae6637304ab7b0c5893b86005fcd7b1f8c98d76548a39e6c97464fed9e9",
|
|
2692
|
+
"test/layer-0-primitives/audit-sign.test.js": "sha256:b22457ae041f31c4ccb3cc40a57c0db519398cf4f9a113c703b70260fc32222b",
|
|
2688
2693
|
"test/layer-0-primitives/audit-signing-key-rotation.test.js": "sha256:47bca36f5b8c1cbec89fd937a88550543f565d0ecd34d5a0b72630d4fcfc79cf",
|
|
2689
2694
|
"test/layer-0-primitives/audit-tools-dual-control.test.js": "sha256:3400879b89f75d00e1ff24fbcc627b621411a4d3767ce086ae98fa7790e784ca",
|
|
2690
2695
|
"test/layer-0-primitives/audit-tools-return-bytes.test.js": "sha256:3ee5d89912b31f451323249d8d5ceaf633789c7c66b577c0b853273a39091dc9",
|
|
@@ -3211,6 +3216,7 @@
|
|
|
3211
3216
|
"test/layer-0-primitives/session-extensions.test.js": "sha256:e3d9b7d09ffbb45e1f41fb952fe64ec7ba25545bf0e19643da4bd1f4a4122479",
|
|
3212
3217
|
"test/layer-0-primitives/session-strict-binding-missing-failclosed.test.js": "sha256:4bdc34b81e08c07d5007083edafa6f40f52a05b38b185222765a13a66e3f0dbb",
|
|
3213
3218
|
"test/layer-0-primitives/session-valid-from.test.js": "sha256:e2588f9f3b3f643f1bc0d54182740c533f9e3d783bc30b71728ba00c6705a209",
|
|
3219
|
+
"test/layer-0-primitives/session.test.js": "sha256:b775c81b372163897f6c843c2bd5dc07bfae505c311e9f9ef6649ead0c44b6c5",
|
|
3214
3220
|
"test/layer-0-primitives/shape-match.test.js": "sha256:dced175c6081bf41d89da69c916a60b7b3dd9739753527aa1cae4bf9ed2079a0",
|
|
3215
3221
|
"test/layer-0-primitives/sigv4-bucket-ops.test.js": "sha256:6598cf90b439ba37531d539a696e3dc48364731ddbfef910c909ac3e08b804a8",
|
|
3216
3222
|
"test/layer-0-primitives/sigv4-multipart-sse.test.js": "sha256:21c454d33046367f70b763ff3c886d9bb136b77360d3a650d950f26b7004cd75",
|
|
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.17.x
|
|
10
10
|
|
|
11
|
+
- v0.17.23 (2026-07-25) — **`b.session` refresh paths enforce the idle/absolute timeout floor, and a strict fingerprint policy without a request fails closed.** Two session-security fixes and one audit-signing robustness fix. b.session.rotate and b.session.updateData reset a session's activity clock but did not re-check the idle/absolute timeout floor that verify() and touch() enforce, so a session already past its idle timeout (default 30 min) -- which verify() would reject -- could be resurrected with a fresh idle window by rotating it or writing session data, defeating the idle timeout (OWASP ASVS 5.0 §3.3 / NIST SP 800-63B-4). Both now enforce the same floor and fail closed. Separately, verify() gated its strict fingerprint controls (requireFingerprintMatch / maxAnomalyScore) on a request being supplied, so passing the strict flag WITHOUT a req silently skipped the check and admitted the session from any device; a strict policy with no way to compute the device fingerprint now fails closed. Finally, b.auditSign.verify honored its documented never-throw contract only for well-formed keys -- a malformed publicKeyPem made it throw; it now returns false. **Changed:** *Vendored Public Suffix List refreshed to the current snapshot* — The bundled Public Suffix List that backs domain classification (registrable-domain and public-suffix checks across URL, cookie, and SSRF handling) is updated to the latest upstream publicsuffix.org snapshot, so newly delegated public suffixes and updated private-domain entries are recognized. **Fixed:** *b.auditSign.verify returns false on a malformed public key instead of throwing* — verify(payload, signature, publicKeyPem) documents that it never throws -- callers branch on the boolean, and a verifier-only consumer (b.backupManifest.verifyBytes) may pass an untrusted publicKeyPem taken from a signature block. A malformed / non-PEM key made node:crypto throw (ERR_OSSL_UNSUPPORTED) while building the key object, crashing such a caller instead of cleanly rejecting. verify() now catches that and returns false, honoring the contract. As a result, b.auditSign.reSignAll now treats an entry whose old key is unverifiable as skipped (unverifiable) rather than as an error. **Security:** *b.session.rotate and b.session.updateData enforce the idle/absolute timeout floor* — A session read path enforces two floors independent of the operator's ttl: an idle timeout (default 30 min) and an absolute timeout (default 12 h). verify() and touch() enforced them, but rotate() and updateData() only checked that the operator ttl had not elapsed while resetting lastActivity -- so a session past its idle floor (which verify() would expire and delete) could be revived with a fresh idle window by rotating it or writing to it. Both now route through the same floor check and fail closed (return null / false and best-effort delete) on breach, so no refresh path can resurrect a session verify() would reject. touch() also now emits the idle/absolute-expiry audit signal on breach, matching verify(). · *A strict fingerprint policy without a request now fails closed* — verify()'s strict device-binding controls -- requireFingerprintMatch: true and maxAnomalyScore -- were only applied when the caller also passed a req to compute the current device fingerprint. Setting a strict flag WITHOUT a req therefore skipped the binding check entirely and admitted the session unchecked from any device -- a fail-open of a control the operator asked to be strict. A strict policy with no req to compare against now fails closed (returns null), the same discipline already applied when the stored binding is unreadable or absent.
|
|
12
|
+
|
|
11
13
|
- v0.17.22 (2026-07-25) — **`b.network.tls.checkServerIdentity9525` parses subjectAltName quote-aware, closing a hostname-verification bypass.** The strict RFC 9525 identity verifier parsed Node's subjectAltName string with a naive comma split, ignoring the JSON-quoted encoding Node emits for a SAN value that itself contains separators (the CVE-2021-44531/44532 remediation). A certificate whose single dNSName is a quoted blob such as `DNS:"x, DNS:victim.com, y"` was broken into pieces by the split, smuggling a clean `victim.com` token the certificate never asserts -- so checkServerIdentity9525 ACCEPTED a host that Node's own tls.checkServerIdentity REJECTS (ERR_TLS_CERT_ALTNAME_INVALID). The verifier now splits SAN entries on commas outside quotes and JSON-decodes quoted spans, matching Node's splitEscapedAltNames, so it asserts only the certificate's actual dNSName / IP values and is at least as strict as the function it advertises replacing. This path gates peer identity in b.crypto.spkiPinVerifier. **Security:** *checkServerIdentity9525 no longer accepts a hostname smuggled through a quoted subjectAltName* — Node renders a subjectAltName value that contains commas or control characters as a JSON-quoted string, so a single dNSName whose value is `x, DNS:victim.com, y` appears in cert.subjectaltname as `DNS:"x, DNS:victim.com, y"`. The verifier's SAN parser split that raw string on every comma, turning one entry into several and extracting a `DNS:victim.com` token the certificate does not actually present -- a hostname-verification bypass, since a strict RFC 9525 drop-in must be no weaker than the Node function it replaces (which rejects this cert). The parser now mirrors Node's splitEscapedAltNames: it splits only on commas outside quotes and JSON-decodes quoted spans, so each parsed dNSName is exactly one GeneralName and a smuggled name can no longer match. Malformed SAN quoting fails closed (asserts no identifiers). Operators using b.network.tls.checkServerIdentity9525 (directly or via b.crypto.spkiPinVerifier) should upgrade.
|
|
12
14
|
|
|
13
15
|
- v0.17.21 (2026-07-25) — **`blamejs erase` treats only `--confirm true` as confirmation, so `--confirm false` no longer triggers the irreversible erase.** Three fixes to the blamejs command line. The `erase` subcommand (a cryptographic single-row erase) gated its irreversible action on a bare-truthiness check, so `--confirm false` -- a string -- counted as confirmation and the erase proceeded; the gate now accepts only `true` / "true", matching `audit purge`, and refuses anything else. A stray `-v` / `--version` token alongside a subcommand no longer short-circuits to the version print: `blamejs migrate up --db x -v` previously returned 0 after printing the version without running the migration, handing automation a false pass; the version flag is now honored only when it is the whole invocation. And `blamejs dev` now survives a crash of the watched child -- it stays up to hot-restart on the next file change instead of draining the event loop and exiting 0 -- and awaits the child's full termination on SIGINT/SIGTERM so a shutdown cannot orphan it. **Fixed:** *blamejs erase refuses --confirm false instead of performing the irreversible erase* — The erase subcommand confirmed its irreversible action with a bare-truthiness check, so the string `--confirm false` was treated as confirmation and the erase ran. Confirmation is now satisfied only by `--confirm true` (or the bare `--confirm`), matching the check `audit purge` already used; any other value -- including `false` -- is refused with a non-zero exit and no erase. A shared confirmation helper backs every destructive subcommand so the acknowledgement contract cannot drift between them. · *A stray -v / --version no longer turns a subcommand into a silent no-op* — The top-level version flag was evaluated before subcommand dispatch and returned 0 whenever `-v` / `--version` appeared anywhere in the arguments, so a consequential command such as `migrate up`, `seed run`, or `erase` alongside a stray `-v` printed the version and exited 0 without doing anything -- a false success for any script checking the exit code. The version flag is now honored only when the invocation carries no subcommand; otherwise the subcommand runs (or fails) as written. · *blamejs dev survives a watched-child crash and shuts the child down cleanly* — The dev supervisor held the event loop open with an unref'd timer, so when the watched child process crashed the supervisor drained the loop and exited 0 -- defeating crash-resilience (it should stay up and hot-restart on the next file change) and reporting success even though the app had crashed. It now holds a referenced heartbeat so a child crash leaves the supervisor running, and on SIGINT/SIGTERM it awaits the child's full termination (graceful signal, then escalation) before exiting, so a shutdown can no longer race ahead and orphan the child.
|
|
@@ -90,7 +90,7 @@ Used for: Top-10000 most-common (breach-derived) passwords. Loaded by
|
|
|
90
90
|
baseline.
|
|
91
91
|
--------------------------------------------------------------------------------
|
|
92
92
|
Component: publicsuffix-list (Mozilla Public Suffix List)
|
|
93
|
-
Version: master snapshot (bundled 2026-07-
|
|
93
|
+
Version: master snapshot (bundled 2026-07-25)
|
|
94
94
|
Source: https://publicsuffix.org/list/public_suffix_list.dat
|
|
95
95
|
License: MPL-2.0
|
|
96
96
|
Copyright: Copyright (c) Mozilla Foundation and Public Suffix List contributors
|
|
@@ -528,7 +528,16 @@ function verify(payload, signature, publicKeyPem) {
|
|
|
528
528
|
var buf = Buffer.isBuffer(payload) ? payload : Buffer.from(String(payload), "utf8");
|
|
529
529
|
var sigBuf = Buffer.isBuffer(signature) ? signature : Buffer.from(signature);
|
|
530
530
|
var pub = publicKeyPem || keys.publicKey;
|
|
531
|
-
|
|
531
|
+
// Contract: never throw — the caller branches on the boolean. A malformed /
|
|
532
|
+
// unsupported publicKeyPem (node:crypto raises ERR_OSSL_UNSUPPORTED building
|
|
533
|
+
// the KeyObject) is a verification FAILURE, not an exception: a verifier-only
|
|
534
|
+
// consumer (e.g. b.backupManifest.verifyBytes) may feed an untrusted PEM from
|
|
535
|
+
// a signature block and must get `false`, not a crash.
|
|
536
|
+
try {
|
|
537
|
+
return nodeCrypto.verify(null, buf, pub, sigBuf);
|
|
538
|
+
} catch (_e) {
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
532
541
|
}
|
|
533
542
|
|
|
534
543
|
/**
|
|
@@ -541,6 +541,24 @@ function _timeoutFloorBreach(row, nowMs, opts) {
|
|
|
541
541
|
return null;
|
|
542
542
|
}
|
|
543
543
|
|
|
544
|
+
// Enforce the idle/absolute floor on a read row and, on breach, emit the
|
|
545
|
+
// audit signal and best-effort delete (leader-only) — returning the breach so
|
|
546
|
+
// the caller fails closed. EVERY session read/refresh path (verify, touch,
|
|
547
|
+
// rotate, updateData) routes through this: the row's TTL being live
|
|
548
|
+
// (expiresAt >= now) is NOT enough, and a refresh must never resurrect a
|
|
549
|
+
// session verify() would expire. The row must carry createdAt + lastActivity.
|
|
550
|
+
async function _floorBreachAndCleanup(row, sidHash, nowMs, opts) {
|
|
551
|
+
var breach = _timeoutFloorBreach(row, nowMs, opts);
|
|
552
|
+
if (!breach) return null;
|
|
553
|
+
try {
|
|
554
|
+
audit.safeEmit({ action: breach.action, outcome: "success", metadata: breach.metadata });
|
|
555
|
+
} catch (_ignored) { /* audit best-effort */ }
|
|
556
|
+
if (cluster.isLeader()) {
|
|
557
|
+
try { await _deleteBySidHash(sidHash); } catch (_e) { /* best-effort */ }
|
|
558
|
+
}
|
|
559
|
+
return breach;
|
|
560
|
+
}
|
|
561
|
+
|
|
544
562
|
async function verify(token, verifyOpts) {
|
|
545
563
|
if (typeof token !== "string" || token.length === 0) return null;
|
|
546
564
|
verifyOpts = verifyOpts || {};
|
|
@@ -572,16 +590,7 @@ async function verify(token, verifyOpts) {
|
|
|
572
590
|
// SP 800-63B-4). These shorten the effective lifetime even when the
|
|
573
591
|
// operator picked a long ttlMs. Defaults: idle 30m, absolute 12h.
|
|
574
592
|
// Operator opt-out by passing 0 (disables that timeout).
|
|
575
|
-
|
|
576
|
-
if (floorBreach) {
|
|
577
|
-
try {
|
|
578
|
-
audit.safeEmit({ action: floorBreach.action, outcome: "success", metadata: floorBreach.metadata });
|
|
579
|
-
} catch (_ignored) { /* audit best-effort */ }
|
|
580
|
-
if (cluster.isLeader()) {
|
|
581
|
-
try { await _deleteBySidHash(sidHash); } catch (_e) { /* best-effort */ }
|
|
582
|
-
}
|
|
583
|
-
return null;
|
|
584
|
-
}
|
|
593
|
+
if (await _floorBreachAndCleanup(row, sidHash, nowMs, verifyOpts)) return null;
|
|
585
594
|
// Unseal sealed columns (userId, data) using the cryptoField pipeline
|
|
586
595
|
// so we return cleartext to the caller — same shape as the previous
|
|
587
596
|
// db().from(...).first() path delivered.
|
|
@@ -642,9 +651,24 @@ async function verify(token, verifyOpts) {
|
|
|
642
651
|
// than silently skip the gate. Treating "no binding to compare" as "the
|
|
643
652
|
// binding matches" is the fail-open this refuses: it admitted an unbound (or
|
|
644
653
|
// unreadable-binding) session from ANY device even under a strict policy.
|
|
645
|
-
var
|
|
646
|
-
|
|
647
|
-
|
|
654
|
+
var strictFlagRequested =
|
|
655
|
+
verifyOpts.requireFingerprintMatch === true ||
|
|
656
|
+
typeof verifyOpts.maxAnomalyScore === "number";
|
|
657
|
+
// A strict policy asserted WITHOUT a req cannot compute the current device
|
|
658
|
+
// fingerprint at all — a contradictory configuration that must ALSO fail
|
|
659
|
+
// CLOSED, never silently admit from any device (the same fail-open the
|
|
660
|
+
// no-stored-fingerprint branch below refuses).
|
|
661
|
+
if (strictFlagRequested && !verifyOpts.req) {
|
|
662
|
+
try {
|
|
663
|
+
audit.safeEmit({
|
|
664
|
+
action: "auth.session.binding_no_request",
|
|
665
|
+
outcome: "failure",
|
|
666
|
+
metadata: { hasUserId: !!unsealed.userId },
|
|
667
|
+
});
|
|
668
|
+
} catch (_ig) { /* audit best-effort */ }
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
var strictBindingRequested = strictFlagRequested && !!verifyOpts.req;
|
|
648
672
|
if (strictBindingRequested && !storedFingerprint) {
|
|
649
673
|
try {
|
|
650
674
|
audit.safeEmit({
|
|
@@ -944,10 +968,7 @@ async function touch(token, opts) {
|
|
|
944
968
|
.toSql();
|
|
945
969
|
var floorRow = await _currentStore().executeOne(floorSel.sql, floorSel.params);
|
|
946
970
|
if (!floorRow) return false;
|
|
947
|
-
if (
|
|
948
|
-
try { await _deleteBySidHash(sidHash); } catch (_e) { /* best-effort */ }
|
|
949
|
-
return false;
|
|
950
|
-
}
|
|
971
|
+
if (await _floorBreachAndCleanup(floorRow, sidHash, nowMs, opts)) return false;
|
|
951
972
|
// Two SQL paths so the SET list stays static (no dynamic column
|
|
952
973
|
// assembly) and matches the call shape clusterStorage expects.
|
|
953
974
|
// extendBy resets expiresAt relative to NOW, not relative to the
|
|
@@ -1008,8 +1029,18 @@ async function touch(token, opts) {
|
|
|
1008
1029
|
* reason?: string, // audit metadata ("login", "mfa", "role-change")
|
|
1009
1030
|
* req?: IncomingMessage, // re-key the device fingerprint to the new sid
|
|
1010
1031
|
* fingerprintFields?: Array<string|fn>, // default ["clientIp","userAgent","acceptLanguage"]
|
|
1032
|
+
* idleTimeoutMs?: number, // idle floor (default 30m; 0 disables)
|
|
1033
|
+
* absoluteTimeoutMs?: number, // absolute floor (default 12h; 0 disables)
|
|
1011
1034
|
* }
|
|
1012
1035
|
*
|
|
1036
|
+
* rotate() enforces the SAME idle/absolute timeout floor verify() does and
|
|
1037
|
+
* fails closed (returns null + deletes) on a session past it — a rotate must
|
|
1038
|
+
* never resurrect a session verify() would expire. Pass idleTimeoutMs /
|
|
1039
|
+
* absoluteTimeoutMs consistently with the values used at verify() (the policy
|
|
1040
|
+
* is per-call): a deployment that disables the idle floor via
|
|
1041
|
+
* verify(token, { idleTimeoutMs: 0 }) must pass the same here, or a
|
|
1042
|
+
* long-idle-but-valid session is purged on rotation.
|
|
1043
|
+
*
|
|
1013
1044
|
* @example
|
|
1014
1045
|
* var rotated = await b.session.rotate(req.cookies.sid, {
|
|
1015
1046
|
* ttlMs: b.constants.TIME.hours(8),
|
|
@@ -1050,12 +1081,15 @@ async function rotate(oldToken, opts) {
|
|
|
1050
1081
|
? opts.fingerprintFields : DEFAULT_FINGERPRINT_FIELDS;
|
|
1051
1082
|
var existingData = null;
|
|
1052
1083
|
var rotSelBuilt = sql.select(_sessionSqlTable(), _sessionSqlOpts())
|
|
1053
|
-
.columns(["data"])
|
|
1084
|
+
.columns(["data", "createdAt", "lastActivity"])
|
|
1054
1085
|
.where("sidHash", oldSidHash)
|
|
1055
1086
|
.where("expiresAt", ">=", nowMs)
|
|
1056
1087
|
.toSql();
|
|
1057
1088
|
var existingRow = await _currentStore().executeOne(rotSelBuilt.sql, rotSelBuilt.params);
|
|
1058
1089
|
if (!existingRow) return null; // unknown / expired old session
|
|
1090
|
+
// A rotate must not resurrect a session past its idle/absolute floor — the
|
|
1091
|
+
// TTL being live is not enough; enforce the same floor verify()/touch() do.
|
|
1092
|
+
if (await _floorBreachAndCleanup(existingRow, oldSidHash, nowMs, opts)) return null;
|
|
1059
1093
|
try {
|
|
1060
1094
|
var unsealedExisting = cryptoField.unsealRow(SESSION_TABLE, existingRow);
|
|
1061
1095
|
if (unsealedExisting.data) existingData = safeJson.parse(unsealedExisting.data);
|
|
@@ -1157,8 +1191,17 @@ async function rotate(oldToken, opts) {
|
|
|
1157
1191
|
* {
|
|
1158
1192
|
* merge?: boolean, // default false (full replace)
|
|
1159
1193
|
* touchLastActivity?: boolean, // default true
|
|
1194
|
+
* idleTimeoutMs?: number, // idle floor (default 30m; 0 disables)
|
|
1195
|
+
* absoluteTimeoutMs?: number, // absolute floor (default 12h; 0 disables)
|
|
1160
1196
|
* }
|
|
1161
1197
|
*
|
|
1198
|
+
* updateData() enforces the SAME idle/absolute timeout floor verify() does and
|
|
1199
|
+
* fails closed (returns false + deletes) on a session past it — a write must
|
|
1200
|
+
* not resurrect a session verify() would expire. The floor policy is per-call:
|
|
1201
|
+
* pass idleTimeoutMs / absoluteTimeoutMs consistently with the values used at
|
|
1202
|
+
* verify(), or a long-idle-but-valid session (e.g. one accepted under
|
|
1203
|
+
* verify(token, { idleTimeoutMs: 0 })) is purged on the next write.
|
|
1204
|
+
*
|
|
1162
1205
|
* @example
|
|
1163
1206
|
* // Replace the data payload entirely.
|
|
1164
1207
|
* await b.session.updateData(req.cookies.sid, { roles: ["admin"], theme: "dark" });
|
|
@@ -1194,6 +1237,10 @@ async function updateData(token, data, opts) {
|
|
|
1194
1237
|
.toSql();
|
|
1195
1238
|
var row = await _currentStore().executeOne(selBuilt.sql, selBuilt.params);
|
|
1196
1239
|
if (!row) return false;
|
|
1240
|
+
// A write must not resurrect a session past its idle/absolute floor (the
|
|
1241
|
+
// same floor verify()/touch() enforce) — resetting lastActivity on an
|
|
1242
|
+
// idle-expired-but-TTL-live session would defeat the idle timeout.
|
|
1243
|
+
if (await _floorBreachAndCleanup(row, sidHash, nowMs, opts)) return false;
|
|
1197
1244
|
|
|
1198
1245
|
// Recover the existing data + reserved fingerprint key (vault-
|
|
1199
1246
|
// sealed at rest). Operators that want a fresh fingerprint also
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"hashes": {
|
|
19
19
|
"server": "sha256:2b30a26f728c5349f4c4b47834f862a4f77393b1224fc12b22abe3ce2cfab78f"
|
|
20
20
|
},
|
|
21
|
-
"refreshedAt": "2026-07-
|
|
21
|
+
"refreshedAt": "2026-07-25T17:09:57.034Z"
|
|
22
22
|
},
|
|
23
23
|
"@noble/curves": {
|
|
24
24
|
"version": "2.2.0",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"hashes": {
|
|
41
41
|
"server": "sha256:2880c288b1285ef51d356d057bee6f0c8a00de36638cf47b47617e8c1faf10d5"
|
|
42
42
|
},
|
|
43
|
-
"refreshedAt": "2026-07-
|
|
43
|
+
"refreshedAt": "2026-07-25T17:09:57.034Z"
|
|
44
44
|
},
|
|
45
45
|
"@noble/post-quantum": {
|
|
46
46
|
"version": "0.6.1",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"hashes": {
|
|
72
72
|
"server": "sha256:f9c94094b3c10fe73dac5343289da582454ea6053494fab2bf66099d9103d6c3"
|
|
73
73
|
},
|
|
74
|
-
"refreshedAt": "2026-07-
|
|
74
|
+
"refreshedAt": "2026-07-25T17:09:57.034Z"
|
|
75
75
|
},
|
|
76
76
|
"@simplewebauthn/server": {
|
|
77
77
|
"version": "13.3.2",
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
"hashes": {
|
|
95
95
|
"server": "sha256:e83195dc9f189385da9c856ef38843f4466f93ea8f3d7fc2efcb1e1b18da6f20"
|
|
96
96
|
},
|
|
97
|
-
"refreshedAt": "2026-07-
|
|
97
|
+
"refreshedAt": "2026-07-25T17:09:57.034Z"
|
|
98
98
|
},
|
|
99
99
|
"SecLists-common-passwords-top-10000": {
|
|
100
100
|
"version": "10k-most-common (master)",
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
},
|
|
115
115
|
"runtime_artifact": "lib/vendor/common-passwords-top-10000.data.js",
|
|
116
116
|
"integrity_layers": "sha256 + sha3-512 + SLH-DSA-SHAKE-256f signature + in-payload canary (where applicable)",
|
|
117
|
-
"refreshedAt": "2026-07-
|
|
117
|
+
"refreshedAt": "2026-07-25T17:09:57.034Z"
|
|
118
118
|
},
|
|
119
119
|
"bimi-trust-anchors": {
|
|
120
120
|
"version": "operator-managed",
|
|
@@ -139,7 +139,7 @@
|
|
|
139
139
|
},
|
|
140
140
|
"runtime_artifact": "lib/vendor/bimi-trust-anchors.data.js",
|
|
141
141
|
"integrity_layers": "sha256 + sha3-512 + SLH-DSA-SHAKE-256f signature + in-payload canary (where applicable)",
|
|
142
|
-
"refreshedAt": "2026-07-
|
|
142
|
+
"refreshedAt": "2026-07-25T17:09:57.034Z"
|
|
143
143
|
},
|
|
144
144
|
"publicsuffix-list": {
|
|
145
145
|
"version": "master",
|
|
@@ -152,14 +152,14 @@
|
|
|
152
152
|
"data_js": "lib/vendor/public-suffix-list.data.js"
|
|
153
153
|
},
|
|
154
154
|
"bundler": "curl https://publicsuffix.org/list/public_suffix_list.dat",
|
|
155
|
-
"bundledAt": "2026-07-
|
|
155
|
+
"bundledAt": "2026-07-25T00:00:00Z",
|
|
156
156
|
"hashes": {
|
|
157
|
-
"server": "sha256:
|
|
158
|
-
"data_js": "sha256:
|
|
157
|
+
"server": "sha256:0240b3f1e8c90ecd4af5b94597afb5ba89997ad395f292f87318caefb7e10413",
|
|
158
|
+
"data_js": "sha256:7994e24e1865f197540a988783de567fe20fb29e7f6c3a5c142766bb0cfd9bfd"
|
|
159
159
|
},
|
|
160
160
|
"runtime_artifact": "lib/vendor/public-suffix-list.data.js",
|
|
161
161
|
"integrity_layers": "sha256 + sha3-512 + SLH-DSA-SHAKE-256f signature + in-payload canary (where applicable)",
|
|
162
|
-
"refreshedAt": "2026-07-
|
|
162
|
+
"refreshedAt": "2026-07-25T17:09:57.034Z"
|
|
163
163
|
},
|
|
164
164
|
"peculiar-pki": {
|
|
165
165
|
"version": "2.0.0+pkijs-3.4.0",
|
|
@@ -190,7 +190,7 @@
|
|
|
190
190
|
"hashes": {
|
|
191
191
|
"server": "sha256:2307ef65e070757ffb13442b377e45efb9fa1a10432d9b39618387720ab990ed"
|
|
192
192
|
},
|
|
193
|
-
"refreshedAt": "2026-07-
|
|
193
|
+
"refreshedAt": "2026-07-25T17:09:57.034Z"
|
|
194
194
|
}
|
|
195
195
|
}
|
|
196
196
|
}
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
// Please pull this list from, and only from https://publicsuffix.org/list/public_suffix_list.dat,
|
|
6
6
|
// rather than any other VCS sites. Pulling from any other URL is not guaranteed to be supported.
|
|
7
7
|
|
|
8
|
-
// VERSION: 2026-07-
|
|
9
|
-
// COMMIT:
|
|
8
|
+
// VERSION: 2026-07-25_14-20-03_UTC
|
|
9
|
+
// COMMIT: e1b8015c3b2f0f4f8c18659c2480fc1a22c07b20
|
|
10
10
|
|
|
11
11
|
// Instructions on pulling and using this list can be found at https://publicsuffix.org/list/.
|
|
12
12
|
|
|
@@ -11304,10 +11304,6 @@ ltd.ua
|
|
|
11304
11304
|
a2hosted.com
|
|
11305
11305
|
cpserver.com
|
|
11306
11306
|
|
|
11307
|
-
// Acorn Labs : https://acorn.io
|
|
11308
|
-
// Submitted by Craig Jellick <domains@acorn.io>
|
|
11309
|
-
*.on-acorn.io
|
|
11310
|
-
|
|
11311
11307
|
// ActiveTrail : https://www.activetrail.biz/
|
|
11312
11308
|
// Submitted by Ofer Kalaora <postmaster@activetrail.com>
|
|
11313
11309
|
activetrail.biz
|