@blamejs/blamejs-shop 0.3.58 → 0.3.59
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/email.js +4 -3
- package/lib/security-middleware.js +6 -0
- package/lib/storefront.js +5 -2
- package/lib/wishlist-sharing.js +2 -1
- 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.3.x
|
|
10
10
|
|
|
11
|
+
- v0.3.59 (2026-06-04) — **Back-in-stock subscribe rate-limited, payment-method metadata in privacy exports, and accessible wishlist alert toggles.** The anonymous back-in-stock "notify me" endpoint sends a confirmation email to the address the request supplies; it now sits in the same tight per-IP rate budget as login, checkout, and the newsletter, closing a victim-addressed email-flooding vector. Privacy (GDPR/CCPA) data exports now include saved-payment-method display metadata — card brand, last four, expiry — which is personal data the export was silently omitting. The wishlist alert and digest toggle buttons gain accessible names that include the alert they control, so a screen reader no longer announces a list of identical "Turn on" buttons. Internal housekeeping rides along: module imports hoisted to file tops, a per-test wall-clock-ceiling helper backing an existing test gate, and stale comments corrected. **Fixed:** *Back-in-stock subscribe joins the tight rate limit* — POST /stock-alert/subscribe is anonymous and sends a double-opt-in confirmation email to the request-supplied address. It previously sat only behind the loose global token bucket, so a script could direct a burst of confirmation emails at an arbitrary victim address — an email-flooding and sender-reputation risk. The route now shares the tight per-IP, per-path budget that already covers login, registration, checkout, gift-card balance, and the newsletter; the integration suite pins the throttle. · *Privacy exports include saved-payment-method metadata* — The subject-access (GDPR/CCPA) export composed every reader except saved payment methods, which was wired to an empty handle — an export silently omitted the card brand, last-four digits, and expiry the shop stores alongside the opaque processor token. The reader now receives the live handle, so a customer's export reflects everything held about them. Only display metadata is stored or exported — never card numbers. · *Wishlist alert toggles are screen-reader distinguishable* — The account wishlist-alerts screen rendered visually identical "Turn on" / "Turn off" / "Subscribe" buttons whose accessible names did not say which alert or digest they control. Each toggle now carries an accessible name that includes its alert label (for example "Turn off Price-drop alerts"), satisfying WCAG 2.4.6 for assistive-technology users walking the button list.
|
|
12
|
+
|
|
11
13
|
- v0.3.58 (2026-06-04) — **Browser form submissions no longer refused as cross-origin, server-side shipping-address validation, and a repaired checkout layout.** Every navigational form POST served by the container — checkout, magic-link sign-in, product reviews, returns, the account area, and the admin console — was refused with a 403 in real browsers. Pages shipped Referrer-Policy: no-referrer, which makes browsers send Origin: null on same-origin form submissions, and the CSRF gate's origin pre-check refuses a null Origin before the token is read. Both the edge and the container now send Referrer-Policy: same-origin: referrer information still never leaves the site, while same-origin posts carry the real Origin the gate verifies. Cross-site and sandboxed-iframe submissions are still refused. Checkout additionally gains backend shipping-address validation with accessible per-field errors that preserve everything the shopper typed, and the checkout page's layout is repaired — fields fill the form column instead of collapsing to its center, and the gift, store-pickup, and loyalty options render inside the form above the submit button. **Added:** *Server-side shipping-address validation with per-field errors* — The checkout POST validates the shipping address on the backend and, on rejection, re-renders the form with the one bad field marked (aria-invalid plus an adjacent role="alert" message) and every typed value preserved — replacing the previous dead-end error page. Country must be a real ISO 3166-1 alpha-2 code, validated against the platform's Intl region data (no new dependencies). US and Canadian destinations validate the state or province code and the ZIP (12345 or 12345-6789) / postal (A1A 1A1) format, and require street, city, region, and postal — the figures that feed destination tax. Every other country keeps lenient length-bounded checks, so international addresses are never over-validated. Email gains a shape check alongside the existing content-safety gate, and the customer name is length-bounded. Headless callers of checkout.confirm keep the presence-optional contract for digital-only orders; malformed values are now refused everywhere. **Fixed:** *Form posts from real browsers were refused as cross-origin (403)* — Pages were served with Referrer-Policy: no-referrer, which per the Fetch specification makes browsers serialize the Origin header as the literal string "null" on same-origin navigational POSTs. The CSRF gate's origin pre-check refuses a null Origin before the double-submit token is validated, so every container-rendered form returned {"error":"CSRF cross-origin request refused."} on submit — checkout, magic-link sign-in, reviews, returns, account self-service, and the admin console. JSON fetch() calls (passkeys, saved cards, the cart-count island) carry a real Origin and were unaffected. Both substrates now send Referrer-Policy: same-origin: no referrer information ever leaves the site (matching the previous policy's intent), same-origin posts carry a real Origin verified against the configured public origins, and cross-site or sandboxed-iframe posts still arrive as null and are still refused. The integration suite now drives the full Origin matrix — same-origin accepted, null and cross-origin refused — closing the gap between loopback tests (which send no Origin header) and real browsers. Operators composing their own securityHeaders options should not reinstate no-referrer: it re-breaks every tokened form post. · *Checkout layout: fields fill the column; gift and pickup options join the form* — A global form rule leaked align-items: center into the checkout's stacked form, collapsing every field to its content width in the middle of a wide column; the stacked form now stretches its rows. The gift-options, store-pickup, and loyalty-redeem blocks were spliced in after the submit button, rendering as an unstyled run of fields below the call to action — they now render inside the form above the submit row, framed as cards consistent with the order summary, with labels matching the shipping fields.
|
|
12
14
|
|
|
13
15
|
- v0.3.57 (2026-06-02) — **Vendored blamejs framework refreshed from v0.14.16 to v0.14.19.** The storefront runs on a vendored copy of the blamejs framework. This refreshes it across three upstream patch releases (v0.14.16 to v0.14.19), picking up framework fixes and hardening. The most operator-relevant is a PKCE-downgrade defense in the OAuth/OIDC client, which the storefront composes for Sign in with Google and Apple, so the federated sign-in flow inherits it. The remaining upstream changes are in framework areas the storefront does not expose (archive writing, SCIM, verifiable-credential issuance, SPF/DMARC tooling, OpenAPI), and the storefront's own behavior is unchanged — verified by the full test suite and the vendored-tree integrity gate against the new tree. **Changed:** *Updated the vendored framework to blamejs v0.14.19* — The vendored framework moves from v0.14.16 to v0.14.19 (three upstream patch releases of fixes and additive, opt-in changes). Notably, the OAuth/OIDC client now refuses an identity provider that advertises PKCE methods without S256 — a stripped-S256 downgrade defense — which hardens the storefront's Sign in with Google and Apple. The integrity manifest over the vendored tree was re-stamped as part of the refresh.
|
package/lib/asset-manifest.json
CHANGED
package/lib/email.js
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
29
|
var b = require("./vendor/blamejs");
|
|
30
|
+
var pricing = require("./pricing");
|
|
30
31
|
|
|
31
32
|
function _htmlEscape(s) {
|
|
32
33
|
if (s == null) return "";
|
|
@@ -60,7 +61,7 @@ function _formatMoney(amountMinor, currency) {
|
|
|
60
61
|
// Delegate to pricing.format so locale + zero-decimal currency
|
|
61
62
|
// handling stays consistent across surfaces. pricing is required
|
|
62
63
|
// — the email module ships with it as a peer in lib/index.js.
|
|
63
|
-
return b &&
|
|
64
|
+
return b && pricing.format(amountMinor, currency);
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
// ---- templates ----------------------------------------------------------
|
|
@@ -369,7 +370,7 @@ function create(opts) {
|
|
|
369
370
|
function _orderVars(order, customer) {
|
|
370
371
|
if (!order || typeof order !== "object") throw new TypeError("email: order object required");
|
|
371
372
|
if (!customer || typeof customer !== "object") throw new TypeError("email: customer object required");
|
|
372
|
-
var format =
|
|
373
|
+
var format = pricing.format;
|
|
373
374
|
return {
|
|
374
375
|
order_id: order.id,
|
|
375
376
|
customer_name: customer.name || "there",
|
|
@@ -409,7 +410,7 @@ function create(opts) {
|
|
|
409
410
|
if (!Number.isInteger(input.amount_minor) || input.amount_minor < 0) {
|
|
410
411
|
throw new TypeError("email.refundConfirmation: amount_minor must be a non-negative integer");
|
|
411
412
|
}
|
|
412
|
-
var format =
|
|
413
|
+
var format = pricing.format;
|
|
413
414
|
var vars = {
|
|
414
415
|
order_id: input.order.id,
|
|
415
416
|
customer_name: input.customer.name || "there",
|
|
@@ -85,6 +85,11 @@ var HEALTH_PATH = "/_/health";
|
|
|
85
85
|
// /survey/ -> /survey/:token (GET form + POST submit)
|
|
86
86
|
// /orders/ -> /orders/:id/cancel|rate|reorder (gated to POST below;
|
|
87
87
|
// the /orders/:id confirmation GET a shopper hits freely is NOT throttled)
|
|
88
|
+
// /stock-alert/ -> subscribe + unsubscribe. Subscribe is anonymous,
|
|
89
|
+
// CSRF-exempt (EDGE_POST_PATHS), and emails the
|
|
90
|
+
// request-supplied address — without the tight
|
|
91
|
+
// budget it is a victim-addressed mail cannon on
|
|
92
|
+
// the loose global bucket alone.
|
|
88
93
|
var TIGHT_PREFIXES = [
|
|
89
94
|
"/account/login",
|
|
90
95
|
"/account/register",
|
|
@@ -96,6 +101,7 @@ var TIGHT_PREFIXES = [
|
|
|
96
101
|
"/products/",
|
|
97
102
|
"/survey/",
|
|
98
103
|
"/orders/",
|
|
104
|
+
"/stock-alert/",
|
|
99
105
|
];
|
|
100
106
|
|
|
101
107
|
// Edge-served state-changing POST endpoints. These forms are rendered at
|
package/lib/storefront.js
CHANGED
|
@@ -2965,7 +2965,10 @@ function _wishlistPrefsPanel(opts) {
|
|
|
2965
2965
|
"<input type=\"hidden\" name=\"on\" value=\"" + (on ? "" : "1") + "\">" +
|
|
2966
2966
|
"<span class=\"wishlist-prefs__label\">" + esc(a.label || a.trigger) + "</span>" +
|
|
2967
2967
|
"<span class=\"wishlist-prefs__state\">" + (on ? "On" : "Off") + "</span>" +
|
|
2968
|
-
|
|
2968
|
+
// aria-label carries the trigger name — the visible "Turn off"/"Turn
|
|
2969
|
+
// on" alone is ambiguous to a screen reader walking a list of
|
|
2970
|
+
// identically-named buttons.
|
|
2971
|
+
"<button type=\"submit\" class=\"btn-ghost btn-ghost--sm\" aria-label=\"" + (on ? "Turn off " : "Turn on ") + esc(a.label || a.trigger) + "\">" + (on ? "Turn off" : "Turn on") + "</button>" +
|
|
2969
2972
|
"</form>";
|
|
2970
2973
|
}
|
|
2971
2974
|
var alertsBlock = alertRows
|
|
@@ -2982,7 +2985,7 @@ function _wishlistPrefsPanel(opts) {
|
|
|
2982
2985
|
"<input type=\"hidden\" name=\"on\" value=\"" + (enrolled ? "" : "1") + "\">" +
|
|
2983
2986
|
"<span class=\"wishlist-prefs__label\">" + esc(d.label || d.slug) + "</span>" +
|
|
2984
2987
|
"<span class=\"wishlist-prefs__state\">" + (enrolled ? "Subscribed" : "Not subscribed") + "</span>" +
|
|
2985
|
-
"<button type=\"submit\" class=\"btn-ghost btn-ghost--sm\">" + (enrolled ? "Unsubscribe" : "Subscribe") + "</button>" +
|
|
2988
|
+
"<button type=\"submit\" class=\"btn-ghost btn-ghost--sm\" aria-label=\"" + (enrolled ? "Unsubscribe from " : "Subscribe to ") + esc(d.label || d.slug) + "\">" + (enrolled ? "Unsubscribe" : "Subscribe") + "</button>" +
|
|
2986
2989
|
"</form>";
|
|
2987
2990
|
}
|
|
2988
2991
|
var digestBlock = digestRows
|
package/lib/wishlist-sharing.js
CHANGED
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
*/
|
|
64
64
|
|
|
65
65
|
var b = require("./vendor/blamejs");
|
|
66
|
+
var wishlistModule = require("./wishlist");
|
|
66
67
|
|
|
67
68
|
// ---- constants ----------------------------------------------------------
|
|
68
69
|
|
|
@@ -256,7 +257,7 @@ function create(opts) {
|
|
|
256
257
|
var wishlistInstance = opts.wishlist || null;
|
|
257
258
|
function _wishlist() {
|
|
258
259
|
if (!wishlistInstance) {
|
|
259
|
-
wishlistInstance =
|
|
260
|
+
wishlistInstance = wishlistModule.create({ query: query });
|
|
260
261
|
}
|
|
261
262
|
return wishlistInstance;
|
|
262
263
|
}
|
package/package.json
CHANGED