@blamejs/blamejs-shop 0.4.73 → 0.4.74
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 +2 -0
- package/lib/asset-manifest.json +1 -1
- package/lib/customers.js +5 -2
- package/lib/storefront.js +41 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.4.x
|
|
10
10
|
|
|
11
|
+
- v0.4.74 (2026-06-21) — **Sign-in links are built from the configured origin, and signing out invalidates every device's session.** Two account-security hardenings. The passwordless sign-in (magic) link was assembled from the request's Host header, so a request that reached the app carrying a forged Host could produce a link pointing at another domain while still carrying the customer's single-use sign-in token. The link is now built from the operator-configured trusted origin — the same source the referral links, back-in-stock links, and the OAuth redirect already use — so the link always points at the real storefront regardless of the request Host. Separately, signing out cleared the session cookie only in the responding browser; because the sealed auth cookie is stateless for its full lifetime, a copy captured before sign-out stayed valid until it naturally expired. Explicit sign-out now moves the account's server-side session-revocation boundary forward, so every live session cookie for the account is invalidated at once. **Fixed:** *Magic-link sign-in URLs are built from the configured origin, not the request Host* — The passwordless sign-in link and the post-checkout account-claim link now derive their origin from the operator's configured trusted origin first, falling back to the request origin only when none is configured (single-host dev). A forged Host header can no longer redirect a credential-bearing sign-in link to another domain. Both email-link sends share one origin resolver, matching the referral, back-in-stock, and OAuth-redirect discipline. · *Signing out invalidates every live session, not just the current browser* — POST /account/logout now moves the customer's server-side session-revocation boundary forward before clearing the cookie, so every sealed auth cookie issued for the account — including a copy captured on another device or before sign-out — fails its next request and signs out. Previously the stateless cookie was only cleared in the responding browser and remained valid elsewhere for its full lifetime.
|
|
12
|
+
|
|
11
13
|
- v0.4.73 (2026-06-21) — **Subject-access exports now carry every operator note, question, quote and rating — not the first page.** A data-subject-access (GDPR Art. 15) export read only the first 100 rows of four sections — operator CRM notes, product questions, RFQ quotes, and post-fulfilment ratings — yet the bundle's completeness manifest still reported each of those sections as fully exported. A customer with a long history (most plausibly a long-tenured account carrying many operator notes) received a bundle that silently dropped the remainder while presenting as complete. The export now drains the operator-notes and product-question sections to the last row by following each reader's own cursor, and pulls the quotes and ratings sections up to the primitive's maximum of 500 rows — beyond any realistic per-customer count — so a subject-access bundle carries the customer's complete record. Both the assembled bundle and the streamed download share the same readers, so both are now complete. **Fixed:** *Export sections drain past the first 100 rows* — The operator-notes and product-question export sections now follow their reader's cursor to exhaustion, so every row reaches the bundle; the quotes and ratings sections read up to the 500-row primitive maximum, which a single customer's RFQ or rating history never exceeds in practice. A long-tenured customer's subject-access export no longer omits rows past the first page while reporting the section complete.
|
|
12
14
|
|
|
13
15
|
- v0.4.72 (2026-06-21) — **Cycle counting no longer strands a finalized count when a shelf adjustment fails midway.** When a cycle count was finalized with shelf adjustments, it advanced the count to finalized first and then applied each per-location adjustment in an unguarded loop. If one adjustment failed partway — most plausibly a negative (shrinkage) variance that would drive a location below the units already held for paid orders — the lines before it stayed applied, the lines after it never applied, and the count was left stuck finalized: a retry was refused and the header's aggregate totals were never stamped, so a tool whose whole job is to close shelf-vs-record drift instead baked in an unrecoverable partial drift. finalizeCount now mirrors the inventory-audit finalize it diverged from: it computes every variance first, claims the finalize transition AND stamps the aggregate totals in one statement, then applies each shelf adjustment reconciling against what already landed under the count's own reason — so a retry lands only the remaining delta and never doubles — and passes the hold-aware guard so a shrinkage debit can't drop a shelf below its outstanding holds. On any failure the count re-opens to in_progress, so the operator clears the blocker and re-runs to completion. **Fixed:** *A failed shelf adjustment re-opens the count instead of stranding it finalized* — finalizeCount now computes and persists every line's variance before claiming the finalize transition, stamps the aggregate variance totals in the same statement as the status flip (so a finalized count is never left with empty totals), and applies the per-location adjustments in a guarded pass that reconciles each against the deltas already recorded under the count's reason. If an adjustment throws, the count re-opens to in_progress and the error surfaces; a re-run applies only the remaining delta, so the lines that already moved are never double-applied. · *Shrinkage adjustments respect outstanding holds* — A negative cycle-count variance now debits the shelf with the hold-aware guard, matching the inventory-audit and write-off paths, so a count that records less stock than the record believed can no longer drop a location below the units already held for paid orders and strand a later fulfilment.
|
package/lib/asset-manifest.json
CHANGED
package/lib/customers.js
CHANGED
|
@@ -635,8 +635,11 @@ function create(opts) {
|
|
|
635
635
|
// and every pre-`iat` cookie once a boundary exists — fails the
|
|
636
636
|
// revocation check on its next authenticated request and signs out.
|
|
637
637
|
// Idempotent and monotonic: a later bump only ever moves the boundary
|
|
638
|
-
// forward, so two concurrent revokes can't roll it backwards.
|
|
639
|
-
//
|
|
638
|
+
// forward, so two concurrent revokes can't roll it backwards. Called by
|
|
639
|
+
// the storefront's explicit sign-out (POST /account/logout) so a sealed
|
|
640
|
+
// cookie copied before sign-out can't outlive it. Erasure moves the same
|
|
641
|
+
// boundary forward directly via _bumpSessionRevocation
|
|
642
|
+
// (eraseAuthForCustomer), so an erased account's live cookies die too.
|
|
640
643
|
revokeSessionsForCustomer: async function (id, at) {
|
|
641
644
|
_uuid(id, "customer id");
|
|
642
645
|
var boundary = (at == null) ? _now() : at;
|
package/lib/storefront.js
CHANGED
|
@@ -12922,6 +12922,24 @@ function mount(router, deps) {
|
|
|
12922
12922
|
return origin + "/r/" + encodeURIComponent(code);
|
|
12923
12923
|
}
|
|
12924
12924
|
|
|
12925
|
+
// The absolute origin for a credential-bearing email link (the magic-link /
|
|
12926
|
+
// portal sign-in URL). A single-use sign-in token in that link authenticates
|
|
12927
|
+
// on its own, so the link must point at the REAL site, never a host an
|
|
12928
|
+
// attacker forged into the request `Host`: it prefers the operator's
|
|
12929
|
+
// configured trusted origin (deps.shop_origin / SHOP_ORIGIN) — the same
|
|
12930
|
+
// discipline _referralLink, _stockAlertOrigin, and the OAuth redirectUri
|
|
12931
|
+
// already follow. Falls back to the request's own origin only when
|
|
12932
|
+
// shop_origin isn't configured (dev / single-host deploys, where the Host is
|
|
12933
|
+
// the loopback the operator is already on). Request-shape reader — returns a
|
|
12934
|
+
// string, never throws.
|
|
12935
|
+
function _emailLinkOrigin(req) {
|
|
12936
|
+
if (typeof deps.shop_origin === "string" && deps.shop_origin) {
|
|
12937
|
+
return deps.shop_origin.replace(/\/$/, "");
|
|
12938
|
+
}
|
|
12939
|
+
try { return new URL(_requestUrls(req).canonical_url).origin || ""; }
|
|
12940
|
+
catch (_e) { return ""; }
|
|
12941
|
+
}
|
|
12942
|
+
|
|
12925
12943
|
// The signed-in customer's sealed-cookie envelope, or null. Shared by
|
|
12926
12944
|
// the PDP view recorder (mounted outside the `if (deps.customers)`
|
|
12927
12945
|
// block) and the account routes inside it, so there's one auth-cookie
|
|
@@ -15787,9 +15805,9 @@ function mount(router, deps) {
|
|
|
15787
15805
|
customer_id: customer.id,
|
|
15788
15806
|
scope: "full",
|
|
15789
15807
|
});
|
|
15790
|
-
|
|
15791
|
-
|
|
15792
|
-
|
|
15808
|
+
// Trusted-origin-first so a forged Host can't poison the
|
|
15809
|
+
// single-use portal sign-in link (see _emailLinkOrigin).
|
|
15810
|
+
var origin = _emailLinkOrigin(req);
|
|
15793
15811
|
var linkUrl = origin + "/account/portal/" + encodeURIComponent(minted.plaintext_token);
|
|
15794
15812
|
await deps.customerPortalEmail.sendMagicLink({
|
|
15795
15813
|
customer_email: email,
|
|
@@ -16083,10 +16101,9 @@ function mount(router, deps) {
|
|
|
16083
16101
|
|
|
16084
16102
|
// Resolve the absolute origin while the request is in hand (the
|
|
16085
16103
|
// mint + send below runs after the response, but reads no further
|
|
16086
|
-
// request state).
|
|
16087
|
-
|
|
16088
|
-
|
|
16089
|
-
catch (_e2) { origin = ""; }
|
|
16104
|
+
// request state). Trusted-origin-first so a forged Host can't poison
|
|
16105
|
+
// the single-use sign-in link (see _emailLinkOrigin).
|
|
16106
|
+
var origin = _emailLinkOrigin(req);
|
|
16090
16107
|
|
|
16091
16108
|
// SECURITY (no account-existence oracle): send the generic
|
|
16092
16109
|
// confirmation 303 BEFORE any matched-only work, so a registered
|
|
@@ -16688,7 +16705,23 @@ function mount(router, deps) {
|
|
|
16688
16705
|
});
|
|
16689
16706
|
}
|
|
16690
16707
|
|
|
16691
|
-
router.post("/account/logout", function (req, res) {
|
|
16708
|
+
router.post("/account/logout", async function (req, res) {
|
|
16709
|
+
// Move this customer's server-side session-revocation boundary forward
|
|
16710
|
+
// BEFORE wiping the cookie, so an explicit sign-out invalidates EVERY
|
|
16711
|
+
// live sealed cookie for the account — not just the copy in the
|
|
16712
|
+
// responding browser. The auth cookie is stateless for its full TTL, so
|
|
16713
|
+
// clearing it here only signs out THIS browser; a cookie copied or
|
|
16714
|
+
// exfiltrated before sign-out would otherwise stay valid until it
|
|
16715
|
+
// naturally expires. Drop-silent + best-effort: a signed-out or already-
|
|
16716
|
+
// revoked caller resolves to null and we simply clear the cookie, and a
|
|
16717
|
+
// failed bump never blocks the local sign-out below.
|
|
16718
|
+
try {
|
|
16719
|
+
var auth = await _currentCustomer(req);
|
|
16720
|
+
if (auth && auth.customer_id && deps.customers &&
|
|
16721
|
+
typeof deps.customers.revokeSessionsForCustomer === "function") {
|
|
16722
|
+
await deps.customers.revokeSessionsForCustomer(auth.customer_id);
|
|
16723
|
+
}
|
|
16724
|
+
} catch (_eRev) { /* drop-silent — the cookie clear below still signs out this browser */ }
|
|
16692
16725
|
// Wipe the browser's client-side state for the origin on sign-out
|
|
16693
16726
|
// (cookies, storage, cache, execution contexts) — defense-in-depth so a
|
|
16694
16727
|
// shared or public browser can't carry a cached authenticated page or
|
package/package.json
CHANGED