@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 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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.4.73",
2
+ "version": "0.4.74",
3
3
  "assets": {
4
4
  "css/admin.css": {
5
5
  "integrity": "sha384-imfe0otYErcB8rr2h6KLSGTtStirysptpXETSPY4zLv3bZoIT75Lo1dOvkOav+xL",
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. Used by
639
- // erasure (above), passkey-revoke, and explicit sign-out.
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
- var origin = "";
15791
- try { origin = new URL(_requestUrls(req).canonical_url).origin; }
15792
- catch (_eOrigin) { origin = ""; }
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
- var origin = "";
16088
- try { origin = new URL(_requestUrls(req).canonical_url).origin; }
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/blamejs-shop",
3
- "version": "0.4.73",
3
+ "version": "0.4.74",
4
4
  "description": "Open-source framework built on blamejs. Vendored stack, zero npm runtime deps, PQC-first crypto, security-on by default.",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {