@blamejs/blamejs-shop 0.4.125 → 0.4.126

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.126 (2026-06-27) — **Storefront error responses no longer echo internal error detail, and order/account pages render images from the configured asset prefix.** Two storefront correctness fixes. First, around fifteen storefront routes (cart line edit/remove, product compare, saved-for-later, account actions and more) returned the raw error message to the client on an unexpected server error — a string that can carry an internal database column, a vault/crypto detail, or other implementation internals. Those routes now distinguish a client-shape validation error (a 400 that still shows its operator-authored message) from an unexpected server error (a 500 that logs the real error server-side, correlated by the request id, and returns only a generic message plus that id) — the same redaction the WebAuthn ceremony routes already used. Second, the order-detail and account-dashboard pages were rendering product thumbnails from the default "/assets/" path instead of the operator's configured asset prefix, so on a deployment that serves assets from a CDN or object-store prefix those two pages showed broken images; both pages now pass the configured prefix like every other page. **Fixed:** *Storefront routes no longer echo the internal error message on a 500* — About fifteen storefront routes — cart line update/remove, product-compare add/clear, saved-for-later move/remove, and several account actions — handled a caught error with `status(TypeError ? 400 : 500)` and then returned the raw error message for both cases, so an unexpected (non-validation) server error sent its internal message string to the client. These routes now return that message only for a client-shape validation error (a 400); any other error is a 500 that records the real error server-side against the request id and returns a generic "Something went wrong" message plus that id for support correlation. This matches the redaction the WebAuthn register/login routes already applied. · *Order-detail and account pages render images from the configured asset prefix* — The order-detail (`/orders/:id`) and account-dashboard (`/account`) renderers were not passed the configured asset prefix, so they fell back to the default `/assets/` path for product thumbnails while every other page used the operator's prefix. On a deployment that serves static assets from a CDN or object-store prefix, those two pages showed broken images. Both now pass the configured prefix, so their images load from the same place as the rest of the storefront.
12
+
11
13
  - v0.4.125 (2026-06-27) — **Vendored framework refreshed to 0.15.34 — RSA OpenPGP signatures with a high zero byte now verify reliably.** Updates the vendored blamejs framework to 0.15.34. The upstream release fixes the framework's OpenPGP verification: an RSA signature whose value began with one or more zero bytes — about 1 in 256 of all signatures, for any key — was rejected as invalid even though it was correct, because the OpenPGP wire format strips those leading zero bytes and the verifier handed the shortened signature straight to RSA verification (which requires the signature to be exactly the key's modulus length). The signature is now left-padded back to the modulus width before verification, so every valid RSA signature verifies reliably, including signatures produced by other OpenPGP implementations. The release also carries internal test-suite tooling with no runtime effect. **Changed:** *Vendored framework refreshed to 0.15.34 (reliable RSA OpenPGP signature verification)* — Refreshes the vendored blamejs framework to 0.15.34. The upstream fix left-pads an RSA OpenPGP signature back to the key's modulus byte length before verifying it, so a valid signature whose value carried one or more leading zero bytes (~1 in 256, for any key) is no longer intermittently rejected — matching the padding the Ed25519 verification path already applied. Any code path that verifies OpenPGP RSA signatures through the framework now accepts every correct signature. The rest of the release is internal test-suite tooling with no runtime, public-API, or wire-format change.
12
14
 
13
15
  - v0.4.124 (2026-06-27) — **Adding the same item to a cart from two requests at once always sums the quantity, and the manager role can configure delivery cutoffs and postal zones.** Two correctness fixes. First, adding the same product to a cart from two requests at once — a double-click, or two open tabs — could drop one of the quantity increases: the cart line's quantity was read and then written back as an absolute value, so the second write overwrote the first instead of adding to it. Adding to the cart now sums the quantity in a single atomic database statement, so simultaneous adds of the same item always total correctly; the same atomic write covers applying a bundle's units onto an existing line and merging a guest cart into a signed-in cart at login. Second, an operator with the manager role was wrongly denied the delivery-estimate cutoff-time and postal-zone screens — those two actions were not mapped to the catalog-write permission and fell through to the owner-only fallback, so only the owner could reach them; managers can now manage them alongside the other delivery-estimate settings. **Fixed:** *Simultaneous add-to-cart requests for the same item sum the quantity instead of losing one* — Adding a line to a cart read the existing quantity and then wrote back the absolute sum, so two requests adding the same item to the same cart at the same time (a rapid double-click, or two tabs) could each read the same starting quantity and overwrite each other — one increase was lost. The add is now a single atomic upsert that increments the stored quantity in the database (`qty = qty + added`), so concurrent adds of the same item always total correctly and a concurrent first-add no longer fails on the cart's unique-line constraint. A plain re-add still keeps the line's existing price. The same atomic write applies to placing a bundle's units onto an existing line (the per-unit price re-blends to the combined value, rounded up so it never charges below it) and to merging a guest cart into a signed-in cart at login. · *The manager role can configure delivery cutoff times and postal zones* — The delivery-estimate cutoff-time and postal-zone actions were not mapped to the catalog-write permission, so they fell through to the owner-only fallback and an operator with the manager role was denied access to those two screens while being allowed the adjacent transit-time and holiday screens. Both actions now require catalog-write, matching the rest of the delivery-estimate settings, so managers can configure them.
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.4.125",
2
+ "version": "0.4.126",
3
3
  "assets": {
4
4
  "css/admin.css": {
5
5
  "integrity": "sha384-imfe0otYErcB8rr2h6KLSGTtStirysptpXETSPY4zLv3bZoIT75Lo1dOvkOav+xL",
package/lib/storefront.js CHANGED
@@ -80,6 +80,22 @@ function _authServerError(req, res, e, where) {
80
80
  return res.end ? res.end(msg) : res.send(msg);
81
81
  }
82
82
 
83
+ // Uniform terminal error for a text/HTML route: a malformed-input TypeError is
84
+ // a 400 whose message is operator-authored validation copy (safe to show), but
85
+ // any OTHER error is a 500 whose message could carry an internal string (a DB
86
+ // column, a vault/crypto internal) — so it routes through _authServerError,
87
+ // which logs the real error server-side and returns only a generic message +
88
+ // the correlating request id. Replaces the `res.status(e instanceof TypeError ?
89
+ // 400 : 500); res.end(e.message)` shape, which leaked the raw error on the 500.
90
+ function _routeError(req, res, e) {
91
+ if (e instanceof TypeError) {
92
+ res.status(400);
93
+ var m = (e && e.message) || "Invalid request";
94
+ return res.end ? res.end(m) : res.send(m);
95
+ }
96
+ return _authServerError(req, res, e, (req && req.method && req.url) ? (req.method + " " + req.url) : "route");
97
+ }
98
+
83
99
  // Payment-webhook signatures (Stripe's HMAC, PayPal's, …) are computed over
84
100
  // the EXACT raw request bytes, but the global JSON body-parser reparses and
85
101
  // discards them. This middleware buffers the raw body for the given POST
@@ -14248,8 +14264,7 @@ function mount(router, deps) {
14248
14264
  return _compareRedirect(res, dest, "full");
14249
14265
  }
14250
14266
  // A malformed product id (or session id) is a client error.
14251
- res.status(e instanceof TypeError ? 400 : 500);
14252
- return res.end ? res.end((e && e.message) || "Error") : res.send((e && e.message) || "Error");
14267
+ return _routeError(req, res, e);
14253
14268
  }
14254
14269
 
14255
14270
  if (!dest) {
@@ -14269,8 +14284,7 @@ function mount(router, deps) {
14269
14284
  try { await deps.productCompare.clearCompareList({ session_id: sid }); }
14270
14285
  catch (e) {
14271
14286
  if (!(e instanceof TypeError)) {
14272
- res.status(500);
14273
- return res.end ? res.end((e && e.message) || "Error") : res.send((e && e.message) || "Error");
14287
+ return _authServerError(req, res, e, "POST /compare/clear");
14274
14288
  }
14275
14289
  // A malformed session cookie can't address a basket — treat
14276
14290
  // the clear as a no-op rather than a 400 (the cookie grants
@@ -15592,6 +15606,7 @@ function mount(router, deps) {
15592
15606
  }
15593
15607
  }
15594
15608
  _send(res, 200, renderOrder({
15609
+ asset_prefix: deps.asset_prefix || "/assets/",
15595
15610
  order: o,
15596
15611
  product_lookup: productLookup,
15597
15612
  recommendations: recommendations,
@@ -16596,6 +16611,7 @@ function mount(router, deps) {
16596
16611
 
16597
16612
  var cartCount = await _cartCountForReq(req);
16598
16613
  _send(res, 200, renderAccount({
16614
+ asset_prefix: deps.asset_prefix || "/assets/",
16599
16615
  customer: customer,
16600
16616
  orders: orders,
16601
16617
  order_product_lookup: orderProductLookup,
@@ -17556,8 +17572,7 @@ function mount(router, deps) {
17556
17572
  if (already) { await deps.wishlist.remove({ customer_id: auth.customer_id, product_id: productId }); removed = true; }
17557
17573
  else await deps.wishlist.add({ customer_id: auth.customer_id, product_id: productId });
17558
17574
  } catch (e) {
17559
- res.status(e instanceof TypeError ? 400 : 500);
17560
- return res.end ? res.end((e && e.message) || "Error") : res.send((e && e.message) || "Error");
17575
+ return _routeError(req, res, e);
17561
17576
  }
17562
17577
  var okKind = removed ? "removed" : "added";
17563
17578
  var rt = (req.body || {}).return_to;
@@ -17715,8 +17730,7 @@ function mount(router, deps) {
17715
17730
  await deps.wishlistAlerts.unsubscribeFromAlertKind({ customer_id: auth.customer_id, trigger: trigger });
17716
17731
  }
17717
17732
  } catch (e) {
17718
- res.status(e instanceof TypeError ? 400 : 500);
17719
- return res.end ? res.end((e && e.message) || "Error") : res.send((e && e.message) || "Error");
17733
+ return _routeError(req, res, e);
17720
17734
  }
17721
17735
  res.status(303); res.setHeader && res.setHeader("location", "/account/wishlist?prefs=alerts");
17722
17736
  return res.end ? res.end() : res.send("");
@@ -17759,8 +17773,7 @@ function mount(router, deps) {
17759
17773
  }
17760
17774
  }
17761
17775
  } catch (e) {
17762
- res.status(e instanceof TypeError ? 400 : 500);
17763
- return res.end ? res.end((e && e.message) || "Error") : res.send((e && e.message) || "Error");
17776
+ return _routeError(req, res, e);
17764
17777
  }
17765
17778
  res.status(303); res.setHeader && res.setHeader("location", "/account/wishlist?prefs=digest");
17766
17779
  return res.end ? res.end() : res.send("");
@@ -17856,8 +17869,7 @@ function mount(router, deps) {
17856
17869
  privacy: "unlisted",
17857
17870
  });
17858
17871
  } catch (e) {
17859
- res.status(e instanceof TypeError ? 400 : 500);
17860
- return res.end ? res.end((e && e.message) || "Error") : res.send((e && e.message) || "Error");
17872
+ return _routeError(req, res, e);
17861
17873
  }
17862
17874
  // The full public URL the giver opens. Built from this request's
17863
17875
  // ORIGIN (scheme + host) so the one-time link is a correct absolute
@@ -17905,8 +17917,7 @@ function mount(router, deps) {
17905
17917
  try {
17906
17918
  await deps.wishlistSharing.revokeShareLink({ link_id: shareId, reason: "owner revoked from account" });
17907
17919
  } catch (e) {
17908
- res.status(e instanceof TypeError ? 400 : 500);
17909
- return res.end ? res.end((e && e.message) || "Error") : res.send((e && e.message) || "Error");
17920
+ return _routeError(req, res, e);
17910
17921
  }
17911
17922
  res.status(303); res.setHeader && res.setHeader("location", "/account/wishlist?share=revoked");
17912
17923
  return res.end ? res.end() : res.send("");
@@ -18521,8 +18532,7 @@ function mount(router, deps) {
18521
18532
  line_id: req.params && req.params.line_id,
18522
18533
  });
18523
18534
  } catch (e) {
18524
- res.status(e instanceof TypeError ? 400 : 500);
18525
- return res.end ? res.end((e && e.message) || "Error") : res.send((e && e.message) || "Error");
18535
+ return _routeError(req, res, e);
18526
18536
  }
18527
18537
  res.status(303); res.setHeader && res.setHeader("location", "/cart");
18528
18538
  return res.end ? res.end() : res.send("");
@@ -18606,8 +18616,7 @@ function mount(router, deps) {
18606
18616
  try {
18607
18617
  await deps.saveForLater.remove({ customer_id: auth.customer_id, save_id: req.params && req.params.save_id });
18608
18618
  } catch (e) {
18609
- res.status(e instanceof TypeError ? 400 : 500);
18610
- return res.end ? res.end((e && e.message) || "Error") : res.send((e && e.message) || "Error");
18619
+ return _routeError(req, res, e);
18611
18620
  }
18612
18621
  res.status(303); res.setHeader && res.setHeader("location", "/account/saved?ok=removed");
18613
18622
  return res.end ? res.end() : res.send("");
@@ -18758,8 +18767,7 @@ function mount(router, deps) {
18758
18767
  var addr = await _ownedAddress(req, res, auth); if (!addr) return;
18759
18768
  try { await fn(addr.id); }
18760
18769
  catch (e) {
18761
- res.status(e instanceof TypeError ? 400 : 500);
18762
- return res.end ? res.end((e && e.message) || "Error") : res.send((e && e.message) || "Error");
18770
+ return _routeError(req, res, e);
18763
18771
  }
18764
18772
  res.status(303); res.setHeader && res.setHeader("location", "/account/addresses?ok=" + okKind);
18765
18773
  return res.end ? res.end() : res.send("");
@@ -18788,8 +18796,7 @@ function mount(router, deps) {
18788
18796
  var addr = await _ownedAddress(req, res, auth); if (!addr) return;
18789
18797
  try { await deps.addresses.archive(addr.id); }
18790
18798
  catch (e) {
18791
- res.status(e instanceof TypeError ? 400 : 500);
18792
- return res.end ? res.end((e && e.message) || "Error") : res.send((e && e.message) || "Error");
18799
+ return _routeError(req, res, e);
18793
18800
  }
18794
18801
  res.status(303);
18795
18802
  res.setHeader && res.setHeader("location", "/account/addresses?ok=removed&undo=" + encodeURIComponent(addr.id));
@@ -18813,8 +18820,7 @@ function mount(router, deps) {
18813
18820
  }
18814
18821
  try { await deps.addresses.unarchive(addr.id); }
18815
18822
  catch (e) {
18816
- res.status(e instanceof TypeError ? 400 : 500);
18817
- return res.end ? res.end((e && e.message) || "Error") : res.send((e && e.message) || "Error");
18823
+ return _routeError(req, res, e);
18818
18824
  }
18819
18825
  res.status(303); res.setHeader && res.setHeader("location", "/account/addresses?ok=restored");
18820
18826
  return res.end ? res.end() : res.send("");
@@ -21282,8 +21288,7 @@ function mount(router, deps) {
21282
21288
  });
21283
21289
  }
21284
21290
  } catch (e) {
21285
- res.status(e instanceof TypeError ? 400 : 500);
21286
- return res.end ? res.end((e && e.message) || "Error") : res.send((e && e.message) || "Error");
21291
+ return _routeError(req, res, e);
21287
21292
  }
21288
21293
  res.status(303);
21289
21294
  res.setHeader && res.setHeader("location", "/cart");
@@ -21319,8 +21324,7 @@ function mount(router, deps) {
21319
21324
  return res.end ? res.end("Line not found") : res.send("Line not found");
21320
21325
  }
21321
21326
  } catch (e) {
21322
- res.status(e instanceof TypeError ? 400 : 500);
21323
- return res.end ? res.end((e && e.message) || "Error") : res.send((e && e.message) || "Error");
21327
+ return _routeError(req, res, e);
21324
21328
  }
21325
21329
  res.status(303);
21326
21330
  res.setHeader && res.setHeader("location", "/cart");
@@ -21348,8 +21352,7 @@ function mount(router, deps) {
21348
21352
  try {
21349
21353
  await deps.cart.removeLine(lineId, cart.id);
21350
21354
  } catch (e) {
21351
- res.status(e instanceof TypeError ? 400 : 500);
21352
- return res.end ? res.end((e && e.message) || "Error") : res.send((e && e.message) || "Error");
21355
+ return _routeError(req, res, e);
21353
21356
  }
21354
21357
  res.status(303);
21355
21358
  res.setHeader && res.setHeader("location", "/cart");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/blamejs-shop",
3
- "version": "0.4.125",
3
+ "version": "0.4.126",
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": {