@blamejs/blamejs-shop 0.3.0 → 0.3.2

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,10 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.3.x
10
10
 
11
+ - v0.3.2 (2026-05-30) — **Creating a product lands you on its detail screen to finish setup.** Creating a product from the admin console now takes you straight to that product's detail screen — where you add a variant, set its price, and add stock — instead of returning you to the product list. A banner on the new product names the remaining steps to make it sellable. This removes the hunt-for-the-product-you-just-made step from the catalog setup flow. **Changed:** *Product create flows straight into editing* — Submitting the new-product form now redirects to the product's detail screen with a banner naming the next steps — add a variant with a SKU, set its price, and add stock — rather than bouncing back to the product list. Configuring a just-created product no longer requires finding it again first.
12
+
13
+ - v0.3.1 (2026-05-30) — **Fix CSRF origin check refusing authenticated forms behind a TLS-terminating proxy.** The token-based CSRF protection added in 0.3.0 runs an Origin/Referer pre-check on every state-changing request. When the app is served behind a proxy that terminates TLS and forwards plain HTTP to the application — a CDN or load balancer, the standard production topology — the application saw the connection as HTTP and rebuilt its own origin as `http://<host>`, while the browser's request carried `Origin: https://<host>`. That bare scheme mismatch made the check refuse every legitimate same-origin POST as cross-origin, blocking sign-in and every account and admin form. The CSRF gate now trusts the forwarded protocol header — the same proxy stance the session cookies already use — so it derives the real public origin, and it matches against an explicit allowlist of the public origin(s), configurable via SHOP_PUBLIC_ORIGINS. The token cookie also regains its `__Host-` prefix over the forwarded HTTPS connection. No operator action is required for the default origin. **Fixed:** *Authenticated forms no longer refused behind a TLS-terminating proxy* — The CSRF origin pre-check derived the application origin from the raw proxied connection scheme, so a same-origin browser POST carrying `Origin: https://<host>` was refused as cross-origin under a CDN that forwards plain HTTP. The gate now trusts the forwarded `x-forwarded-proto` and matches against an explicit public-origin allowlist — `SHOP_PUBLIC_ORIGINS` (comma-separated), defaulting to the canonical origin — so sign-in and the account and admin forms submit correctly. The double-submit token cookie also regains its `__Host-` prefix over the forwarded HTTPS connection.
14
+
11
15
  - v0.3.0 (2026-05-30) — **Token-based CSRF protection on the account and admin forms.** Every state-changing form on the customer account surface — subscriptions, addresses, wishlist, returns, reviews, loyalty, referrals, passkey management, profile, checkout — and the entire admin console now carries a per-request double-submit CSRF token that is validated on submission. This is a defense-in-depth layer on top of the existing SameSite session cookies and the fetch-metadata cross-site gate. It lands with an update of the vendored blamejs runtime to v0.13.46, whose createApp now wires CSRF into the request lifecycle by default (closing the long-standing gap where it was documented but not enforced). The edge-rendered, cached storefront forms (add-to-cart, cookie consent, newsletter, currency switch) keep their SameSite + fetch-metadata defense — a per-session token cannot be embedded in HTML that is cached and identical for every visitor, and forcing one would break no-JS submission. No operator action or configuration change is required. **Security:** *Per-form CSRF tokens on account and admin* — The container-rendered account and admin forms now embed a hidden `_csrf` field carrying a per-request double-submit token; the server validates it (constant-time) on every state-changing POST. The passkey enrolment / sign-in islands send the matching token in the `X-CSRF-Token` header. This is layered on top of — not a replacement for — the SameSite session cookies and the fetch-metadata cross-site refusal already in place. · *Vendored blamejs runtime updated to v0.13.46* — createApp now wires CSRF (double-submit cookie), plus the cookie / CSP-nonce / fetch-metadata / body-parser layers, into the request lifecycle by default. The shop adopts the CSRF default scoped so the edge-cached, dual-rendered storefront endpoints (cart, consent, newsletter, currency) remain on their SameSite + fetch-metadata defense, while the authenticated account and admin surfaces gain the token check. A lint rule now flags any new edge-rendered POST form that would need that exemption.
12
16
 
13
17
  ## v0.2.x
package/lib/admin.js CHANGED
@@ -507,10 +507,13 @@ function mount(router, deps) {
507
507
  return p;
508
508
  }),
509
509
  async function (req, res) {
510
- // Browser form submit — create, then redirect (PRG). Bad input
510
+ // Browser form submit — create, then redirect (PRG) straight to the
511
+ // new product's detail screen so the operator continues setup (variant,
512
+ // price, stock) there instead of hunting for it in the list. Bad input
511
513
  // re-renders the products page with a notice, never a 500.
514
+ var made;
512
515
  try {
513
- await catalog.products.create(req.body || {});
516
+ made = await catalog.products.create(req.body || {});
514
517
  } catch (e) {
515
518
  if (e instanceof TypeError || e.code === "CATALOG_DUPLICATE" || /slug|exists|duplicate/i.test(e.message || "")) {
516
519
  var page = await catalog.products.list({ limit: 100 });
@@ -521,8 +524,8 @@ function mount(router, deps) {
521
524
  }
522
525
  throw e;
523
526
  }
524
- b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".product.create", outcome: "success", metadata: {} });
525
- _redirect(res, "/admin/products?created=1");
527
+ b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".product.create", outcome: "success", metadata: { id: made.id } });
528
+ _redirect(res, "/admin/products/" + encodeURIComponent(made.id) + "?created=1");
526
529
  },
527
530
  ));
528
531
 
@@ -629,8 +632,9 @@ function mount(router, deps) {
629
632
  prices_by_variant: model.prices_by_variant, media: model.media,
630
633
  asset_prefix: assetPrefix, upload_available: !!r2,
631
634
  default_currency: await _defaultCurrency(),
632
- saved: url && url.searchParams.get("saved"),
633
- notice: (url && url.searchParams.get("err")) ? "That action couldn't be completed." : null,
635
+ saved: url && url.searchParams.get("saved"),
636
+ created: url && url.searchParams.get("created"),
637
+ notice: (url && url.searchParams.get("err")) ? "That action couldn't be completed." : null,
634
638
  }));
635
639
  },
636
640
  ));
@@ -7943,6 +7947,7 @@ function renderAdminProduct(opts) {
7943
7947
  var pid = _htmlEscape(p.id);
7944
7948
 
7945
7949
  var saved = opts.saved ? "<div class=\"banner banner--ok\">Saved.</div>" : "";
7950
+ var created = opts.created ? "<div class=\"banner banner--ok\">Product created. Add a variant with a SKU, set its price, and add stock to make it sellable.</div>" : "";
7946
7951
  var notice = opts.notice ? "<div class=\"banner banner--err\">" + _htmlEscape(opts.notice) + "</div>" : "";
7947
7952
 
7948
7953
  // ---- product fields edit ---------------------------------------------
@@ -8128,7 +8133,7 @@ function renderAdminProduct(opts) {
8128
8133
  "<span class=\"status-pill " + statusCls + "\">" + _htmlEscape(p.status) + "</span>" +
8129
8134
  " · <a href=\"/products/" + _htmlEscape(encodeURIComponent(p.slug)) + "\" target=\"_blank\" rel=\"noreferrer\">View on storefront &rarr;</a></p>";
8130
8135
 
8131
- var body = "<section><h2>" + _htmlEscape(p.title) + "</h2>" + saved + notice + head +
8136
+ var body = "<section><h2>" + _htmlEscape(p.title) + "</h2>" + created + saved + notice + head +
8132
8137
  "<div class=\"actions-row mt-0\">" + archiveAction + "</div>" +
8133
8138
  editForm + "</section>" + variantsSection + mediaSection;
8134
8139
  return _renderAdminShell(opts.shop_name, "Product " + p.slug, body, "products", opts.nav_available);
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.3.0",
2
+ "version": "0.3.2",
3
3
  "assets": {
4
4
  "css/admin.css": {
5
5
  "integrity": "sha384-1SIn6oAf1DjECbRfKENZasdKHJiywGdXR58wn0hsFGcdVHzUmvfgMkEz5ANIAZJ3",
@@ -108,6 +108,20 @@ var EDGE_POST_PATHS = [
108
108
  "/announcements/",
109
109
  ];
110
110
 
111
+ // The TLS-terminated public origin(s) the storefront is served on. Behind
112
+ // the Cloudflare Worker the container socket is plain http, so the CSRF
113
+ // origin pre-check would otherwise build `http://<host>` and refuse every
114
+ // same-origin browser POST (which carries `Origin: https://<host>`) on a
115
+ // bare scheme mismatch — breaking sign-in and every authenticated form.
116
+ // The csrf gate below trusts the Worker-forwarded `x-forwarded-proto` (so
117
+ // it derives the real https origin) AND carries this explicit allowlist as
118
+ // belt-and-suspenders for the public host. Override with the comma-
119
+ // separated SHOP_PUBLIC_ORIGINS for an alternate deploy; the default is
120
+ // the canonical origin the Worker + storefront already use as their
121
+ // fallback.
122
+ var PUBLIC_ORIGINS = (process.env.SHOP_PUBLIC_ORIGINS || "https://blamejs.shop")
123
+ .split(",").map(function (s) { return s.trim(); }).filter(Boolean);
124
+
111
125
  /**
112
126
  * Resolve the real client IP for rate-limit keying. Reads the
113
127
  * Cloudflare-injected `cf-connecting-ip` first (the canonical single
@@ -184,7 +198,21 @@ function mountRouteGuards(r) {
184
198
  // shells render it into a hidden `_csrf` field. `skipStateless` passes
185
199
  // cookie-less and bearer-token (Authorization-header) requests through —
186
200
  // they cannot be CSRF-ed (no ambient cookie credential to abuse).
187
- var csrfGate = b.middleware.csrfProtect({ cookie: true, skipStateless: true });
201
+ var csrfGate = b.middleware.csrfProtect({
202
+ cookie: true,
203
+ skipStateless: true,
204
+ // Behind the Cloudflare Worker the container connection is plain http
205
+ // while the visitor is on https; trustProxy opts in the Worker-set
206
+ // `x-forwarded-proto` so the origin pre-check derives the real public
207
+ // origin (and the token cookie carries the `__Host-csrf` prefix),
208
+ // matching the session-cookie stance (storefront `_secureForReq`).
209
+ // allowedOrigins is the explicit public-host allowlist so a legitimate
210
+ // same-origin POST is never refused as cross-origin on a proxy
211
+ // scheme/host mismatch — the regression that breaks sign-in + every
212
+ // authenticated form behind the CDN.
213
+ trustProxy: true,
214
+ allowedOrigins: PUBLIC_ORIGINS,
215
+ });
188
216
  r.use(function csrfGuard(req, res, next) {
189
217
  var pathname = req.pathname || req.url || "/";
190
218
  if (_hasPrefix(pathname, WEBHOOK_PATHS) || pathname === HEALTH_PATH) return next();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/blamejs-shop",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
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": {