@blamejs/blamejs-shop 0.2.23 → 0.2.25
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 +4 -0
- package/lib/admin.js +42 -9
- package/lib/asset-manifest.json +1 -1
- package/lib/cart.js +24 -9
- package/lib/storefront.js +190 -38
- package/lib/vendor/MANIFEST.json +3 -3
- package/lib/vendor/blamejs/CHANGELOG.md +4 -0
- package/lib/vendor/blamejs/GOVERNANCE.md +2 -3
- package/lib/vendor/blamejs/LTS-CALENDAR.md +6 -2
- package/lib/vendor/blamejs/SECURITY.md +1 -1
- package/lib/vendor/blamejs/api-snapshot.json +2 -2
- package/lib/vendor/blamejs/lib/a2a.js +11 -11
- package/lib/vendor/blamejs/lib/agent-snapshot.js +1 -1
- package/lib/vendor/blamejs/lib/ai-capability.js +20 -20
- package/lib/vendor/blamejs/lib/ai-content-detect.js +4 -3
- package/lib/vendor/blamejs/lib/ai-dp.js +17 -17
- package/lib/vendor/blamejs/lib/ai-input.js +3 -3
- package/lib/vendor/blamejs/lib/ai-pref.js +9 -9
- package/lib/vendor/blamejs/lib/ai-quota.js +17 -17
- package/lib/vendor/blamejs/lib/archive-read.js +10 -7
- package/lib/vendor/blamejs/lib/arg-parser.js +38 -38
- package/lib/vendor/blamejs/lib/audit-sign.js +4 -4
- package/lib/vendor/blamejs/lib/auth/acr-vocabulary.js +4 -4
- package/lib/vendor/blamejs/lib/auth/auth-time-tracker.js +1 -1
- package/lib/vendor/blamejs/lib/auth/elevation-grant.js +10 -10
- package/lib/vendor/blamejs/lib/auth/step-up-policy.js +12 -12
- package/lib/vendor/blamejs/lib/auth/step-up.js +15 -15
- package/lib/vendor/blamejs/lib/boot-gates.js +6 -6
- package/lib/vendor/blamejs/lib/break-glass.js +1 -1
- package/lib/vendor/blamejs/lib/budr.js +6 -6
- package/lib/vendor/blamejs/lib/cms-codec.js +10 -9
- package/lib/vendor/blamejs/lib/content-credentials.js +13 -13
- package/lib/vendor/blamejs/lib/dark-patterns.js +15 -15
- package/lib/vendor/blamejs/lib/ddl-change-control.js +37 -37
- package/lib/vendor/blamejs/lib/dr-runbook.js +7 -7
- package/lib/vendor/blamejs/lib/fapi2.js +9 -9
- package/lib/vendor/blamejs/lib/fdx.js +7 -7
- package/lib/vendor/blamejs/lib/graphql-federation.js +2 -2
- package/lib/vendor/blamejs/lib/iab-mspa.js +5 -5
- package/lib/vendor/blamejs/lib/iab-tcf.js +18 -18
- package/lib/vendor/blamejs/lib/mail-crypto-smime.js +10 -6
- package/lib/vendor/blamejs/lib/mcp.js +13 -13
- package/lib/vendor/blamejs/lib/middleware/require-step-up.js +3 -3
- package/lib/vendor/blamejs/lib/mtls-ca.js +2 -2
- package/lib/vendor/blamejs/lib/safe-archive.js +8 -7
- package/lib/vendor/blamejs/lib/sec-cyber.js +3 -3
- package/lib/vendor/blamejs/lib/sse.js +14 -14
- package/lib/vendor/blamejs/lib/tcpa-10dlc.js +5 -5
- package/lib/vendor/blamejs/lib/tenant-quota.js +18 -18
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.13.43.json +39 -0
- package/lib/vendor/blamejs/release-notes/v0.13.44.json +31 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/a2a.test.js +1 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/ai-capability.test.js +15 -15
- package/lib/vendor/blamejs/test/layer-0-primitives/ai-dp.test.js +16 -16
- package/lib/vendor/blamejs/test/layer-0-primitives/ai-input.test.js +2 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/ai-pref.test.js +2 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/ai-quota.test.js +19 -19
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-sign-ml-dsa-65.test.js +1 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/boot-gates.test.js +1 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/budr.test.js +3 -3
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +52 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/content-credentials.test.js +3 -3
- package/lib/vendor/blamejs/test/layer-0-primitives/dark-patterns.test.js +2 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/dr-runbook.test.js +1 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/fapi2.test.js +6 -6
- package/lib/vendor/blamejs/test/layer-0-primitives/fdx.test.js +2 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/graphql-federation.test.js +1 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/iab-mspa.test.js +3 -3
- package/lib/vendor/blamejs/test/layer-0-primitives/iab-tcf.test.js +7 -7
- package/lib/vendor/blamejs/test/layer-0-primitives/mcp.test.js +5 -5
- package/lib/vendor/blamejs/test/layer-0-primitives/sse.test.js +1 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/tcpa-10dlc.test.js +3 -3
- package/lib/vendor/blamejs/test/layer-0-primitives/tenant-quota.test.js +3 -3
- package/lib/vendor/blamejs/test/layer-0-primitives/ws-client.test.js +4 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.2.x
|
|
10
10
|
|
|
11
|
+
- v0.2.25 (2026-05-29) — **Harden session and admin cookies with __Host- / __Secure- name prefixes.** The session, login, and admin cookies now carry the browser-enforced `__Host-` / `__Secure-` name prefixes when served over HTTPS. These prefixes bind a cookie to a secure, same-origin context — a browser only accepts a `__Host-` cookie that was set with `Secure`, `Path=/`, and no `Domain` attribute — which closes off cookie-injection and session-fixation paths from a related origin or a non-secure context. The secure-versus-plain decision follows the forwarded request protocol, so a deployment behind a TLS-terminating proxy (the production setup) gets the hardened prefixes while local development and tests over HTTP keep the bare names and keep working. Returning visitors are not signed out by the upgrade: the cookie reader resolves both the prefixed and the legacy name during the transition. **Security:** *`__Host-` on the session and login cookies* — Over HTTPS the storefront session and login cookies are issued as `__Host-shop_sid` and `__Host-shop_auth` with `Secure`, `Path=/`, and no `Domain` — the attribute set a browser requires before it will store a `__Host-` cookie, so the session cookie can no longer be planted or overwritten by a related origin or a non-secure context. · *`__Secure-` on the admin cookie* — The admin session cookie is scoped to `Path=/admin`, so it takes the `__Secure-` prefix (which requires `Secure`) rather than `__Host-` (which mandates `Path=/`). The edge cache-skip check and the audience bucketing were updated to recognize the prefixed names so a signed-in response is never served from the shared edge cache.
|
|
12
|
+
|
|
13
|
+
- v0.2.24 (2026-05-29) — **Scope cart-line edits to your own cart, harden structured data, and redact auth errors.** A set of request-path hardening fixes. Cart line edit and remove are now scoped to your own session cart, so a cart-line id belonging to another shopper can no longer be used to change quantities on or delete their item — a mismatched id is a no-op, not a mutation. The product structured-data (JSON-LD) embedded in page heads now neutralizes every `</script` close-tag variant (with trailing space, slash, or newline), not just the exact `</script>`, closing a markup-injection path from admin-entered product fields. Account sign-in, registration, and passkey error responses now return a generic message plus a request id instead of the raw internal error string, with the full error logged server-side for the operator. And the worker DB/R2 bridge secret is checked at boot: a secret shorter than 32 characters logs a loud warning so a weak secret is visible in the boot log (the documented generator produces 43 characters, so a by-the-docs deploy stays quiet). **Security:** *Cart-line edits are scoped to your cart* — `POST /cart/lines/:id/update` and `/remove` now resolve the requester's session cart and scope the change to it (`WHERE id = ? AND cart_id = ?`), so a cart-line id from another shopper's cart can't be used to alter or delete their line — it resolves to a no-op rather than a cross-cart mutation. Legitimate edits to your own cart are unchanged. · *Structured-data close-tag escaping hardened* — JSON-LD embedded in page heads (edge and origin, byte-identical) now escapes every `</script` sequence the HTML parser treats as a tag-closer — including `</script `, `</script/`, and `</script` followed by a newline — not only the exact `</script>`. This removes a markup-injection vector from admin-entered product fields that flow into structured data. · *Account auth errors no longer leak internals* — The sign-in, registration, and passkey ceremony endpoints now return a generic message and a request id on an unexpected server error instead of the raw error string; the real error is logged server-side, correlated by that request id. Client-shape (400) and other expected responses are unchanged. · *Bridge-secret entropy warning at boot* — The worker DB/R2 bridge secret (`D1_BRIDGE_SECRET`) authorizes the database and object-storage bridge, so a weak value is high-impact. The container now logs a loud boot warning in production when it is shorter than 32 characters. The documented generator produces 43 characters; if you see the warning, regenerate the secret with the command it prints.
|
|
14
|
+
|
|
11
15
|
- v0.2.23 (2026-05-29) — **Rate limiting and cross-site request isolation on the request lifecycle.** Two request-lifecycle defenses are now composed into every storefront and admin request. A per-client-IP rate limit backs the whole site (a generous global budget that a normal browsing and checkout session never trips) with tighter budgets on the abusable POST/auth endpoints — sign-in, passkey enrolment, checkout, gift-card balance lookup, account registration, newsletter, review/question submit, and survey response — so credential and passkey spraying, gift-card brute-forcing, and checkout flooding are shut down. Separately, a fetch-metadata gate refuses cross-site state-changing requests using the browser's Sec-Fetch headers, adding CSRF defense-in-depth on top of the SameSite session cookies. The client IP is read from the edge-forwarded header (the container sits behind the Worker), so limits apply per real visitor; the payment-webhook and health-probe paths are exempt. No change for normal use. **Added:** *Per-client rate limiting* — A generous global token-bucket limit per client IP backs the whole site, with tight fixed-window budgets on the abusable POST/auth endpoints (login, passkey register/add, checkout, gift-card balance, register, newsletter, review/question, survey). Keyed on the real client IP forwarded from the edge; payment-webhook and health-probe paths are exempt. The single-instance container uses an in-memory backend. · *Cross-site request isolation (fetch-metadata)* — Cross-site state-changing requests (the CSRF vector) are refused using the browser's Sec-Fetch-Site/Mode/Dest headers — same-origin requests and direct navigations pass; payment webhooks are exempt. This restores defense-in-depth alongside the SameSite session cookies.
|
|
12
16
|
|
|
13
17
|
- v0.2.22 (2026-05-29) — **See your full order history, track shipments, reorder, and request returns.** The customer order surface is now complete end-to-end. A new order-history list shows every order (cursor-paginated), and each order page gains a status timeline, a carrier tracking panel (tracking number + the carrier's public tracking link), a one-click Reorder that rebuilds a cart from the order, and a Request-a-return button for eligible orders — the same Reorder/Return actions also appear per row on the account dashboard and the history list. On the operator side, the admin order screen gets a shipment panel to attach a carrier + tracking number and record shipment events, and the returns console's Refund now issues the payment-provider refund first (then records the RMA) whenever the order has a captured charge, so a return can't be marked refunded while the customer is left un-refunded; manual/guest orders with no charge fall back to an explicit record-only step. **Added:** *Order history + tracking + reorder + returns (customer)* — `/account/orders` lists all your orders with pagination; each order page shows a status timeline and carrier tracking, plus Reorder (`POST /orders/:id/reorder` rebuilds a cart from the order's lines at current prices) and Request-a-return for eligible orders. The dashboard and history rows carry the same actions. · *Shipment + tracking panel (admin)* — The admin order detail screen can attach a shipment (carrier + tracking number) and record shipment events; marking delivered advances the order's lifecycle. Tracking the order surfaces the carrier's public tracking link to the customer. **Changed:** *Return refunds move the money* — The returns console Refund action now issues the payment-provider refund first and records the RMA only on success when the order has a captured charge, so an approved return is never marked refunded without the customer actually being refunded. Orders with no captured charge use an explicit record-only step that points to the order.
|
package/lib/admin.js
CHANGED
|
@@ -183,6 +183,17 @@ function _authOk(token, expected) {
|
|
|
183
183
|
// read/write) — scoped to /admin, SameSite=Strict, HttpOnly + Secure.
|
|
184
184
|
var ADMIN_COOKIE_NAME = "shop_admin";
|
|
185
185
|
|
|
186
|
+
// Cookie-prefix-hardened name. The admin session cookie is Path=/admin, so
|
|
187
|
+
// it CANNOT carry `__Host-` (which mandates Path=/); the correct prefix is
|
|
188
|
+
// `__Secure-`, which requires only the Secure attribute (RFC 6265bis
|
|
189
|
+
// §4.1.3.1). As with the storefront session cookies, the prefix moves in
|
|
190
|
+
// lockstep with Secure: a `__Secure-` cookie without Secure is invalid
|
|
191
|
+
// (silently dropped), and Secure cookies don't store over plain http. So
|
|
192
|
+
// https admin sessions carry `__Secure-shop_admin`; http dev/e2e sessions
|
|
193
|
+
// carry the bare `shop_admin`. The validity check resolves the prefixed
|
|
194
|
+
// name first and the bare name second.
|
|
195
|
+
var ADMIN_COOKIE_NAME_SECURE = "__Secure-" + ADMIN_COOKIE_NAME;
|
|
196
|
+
|
|
186
197
|
var _adminJarMemo = null;
|
|
187
198
|
function _adminJar() {
|
|
188
199
|
if (!_adminJarMemo) {
|
|
@@ -194,14 +205,33 @@ function _adminJar() {
|
|
|
194
205
|
return _adminJarMemo;
|
|
195
206
|
}
|
|
196
207
|
|
|
197
|
-
|
|
198
|
-
|
|
208
|
+
// Whether THIS request's PUBLIC connection is https — drives both the
|
|
209
|
+
// Secure attribute and the prefix choice for the admin session cookie.
|
|
210
|
+
// Same rule as the storefront: trust the Worker-set `x-forwarded-proto`
|
|
211
|
+
// (the container socket may be plain http behind the TLS-terminating
|
|
212
|
+
// Worker), and treat a direct dev/e2e connection (no forwarded header,
|
|
213
|
+
// non-encrypted socket) as http so the bare-named, non-Secure cookie is
|
|
214
|
+
// emitted and a real browser stores it.
|
|
215
|
+
function _secureForReq(req) {
|
|
216
|
+
return b.requestHelpers.requestProtocol(req, { trustProxy: true }) === "https";
|
|
217
|
+
}
|
|
218
|
+
function _adminCookieName(secure) { return secure ? ADMIN_COOKIE_NAME_SECURE : ADMIN_COOKIE_NAME; }
|
|
219
|
+
|
|
220
|
+
function _setAdminCookie(req, res) {
|
|
221
|
+
var secure = _secureForReq(req);
|
|
222
|
+
_adminJar().writeSealed(res, _adminCookieName(secure), JSON.stringify({
|
|
199
223
|
admin: true,
|
|
200
224
|
exp: Date.now() + b.constants.TIME.hours(12),
|
|
201
|
-
}), { expires: new Date(Date.now() + b.constants.TIME.hours(12)) });
|
|
225
|
+
}), { expires: new Date(Date.now() + b.constants.TIME.hours(12)), secure: secure });
|
|
202
226
|
}
|
|
203
|
-
function _clearAdminCookie(res) {
|
|
204
|
-
|
|
227
|
+
function _clearAdminCookie(req, res) {
|
|
228
|
+
// Expire-now must match the live request's protocol so the cleared
|
|
229
|
+
// cookie's attributes satisfy the `__Secure-` invariant: over https
|
|
230
|
+
// clear the prefixed name with Secure; over http clear the bare name. A
|
|
231
|
+
// request never carries both at once (the write side emits one per
|
|
232
|
+
// protocol), so clearing the protocol-matched name signs the operator out.
|
|
233
|
+
var secure = _secureForReq(req);
|
|
234
|
+
_adminJar().clear(res, _adminCookieName(secure), { secure: secure });
|
|
205
235
|
}
|
|
206
236
|
|
|
207
237
|
// One-time gift-card code reveal. The issue POST stashes the freshly-issued
|
|
@@ -224,7 +254,10 @@ function _takeGiftCardReveal(req, res, id) {
|
|
|
224
254
|
return (env && env.id === id && typeof env.code === "string") ? env.code : null;
|
|
225
255
|
}
|
|
226
256
|
function _adminCookieValid(req) {
|
|
227
|
-
|
|
257
|
+
// Resolve the prefixed name first and the bare name second so an admin
|
|
258
|
+
// session set in either environment (or mid-rollout) is recognised.
|
|
259
|
+
var raw = _adminJar().readSealed(req, ADMIN_COOKIE_NAME_SECURE);
|
|
260
|
+
if (raw === null) raw = _adminJar().readSealed(req, ADMIN_COOKIE_NAME);
|
|
228
261
|
if (raw === null) return false;
|
|
229
262
|
var env;
|
|
230
263
|
try { env = JSON.parse(raw); } catch (_e) { return false; }
|
|
@@ -4048,7 +4081,7 @@ function mount(router, deps) {
|
|
|
4048
4081
|
if (!_authOk(token, expectedToken)) {
|
|
4049
4082
|
return _sendHtml(res, 401, renderAdminLogin({ shop_name: deps.shop_name, error: true }));
|
|
4050
4083
|
}
|
|
4051
|
-
try { _setAdminCookie(res); }
|
|
4084
|
+
try { _setAdminCookie(req, res); }
|
|
4052
4085
|
catch (e) {
|
|
4053
4086
|
// Sealed cookies need an initialized vault — surface 503 rather
|
|
4054
4087
|
// than 500 so the operator knows to configure VAULT_PASSPHRASE.
|
|
@@ -4060,8 +4093,8 @@ function mount(router, deps) {
|
|
|
4060
4093
|
_redirect(res, (await _setupComplete()) ? "/admin" : "/admin/setup");
|
|
4061
4094
|
});
|
|
4062
4095
|
|
|
4063
|
-
router.post("/admin/logout", async function (
|
|
4064
|
-
_clearAdminCookie(res);
|
|
4096
|
+
router.post("/admin/logout", async function (req, res) {
|
|
4097
|
+
_clearAdminCookie(req, res);
|
|
4065
4098
|
_redirect(res, "/admin");
|
|
4066
4099
|
});
|
|
4067
4100
|
|
package/lib/asset-manifest.json
CHANGED
package/lib/cart.js
CHANGED
|
@@ -184,28 +184,43 @@ function create(opts) {
|
|
|
184
184
|
return { id: id, cart_id: cartId, variant_id: input.variant_id, sku: variantRow.sku, qty: input.qty, unit_amount_minor: unitAmount, unit_currency: unitCurrency, added_at: ts, updated_at: ts };
|
|
185
185
|
},
|
|
186
186
|
|
|
187
|
-
|
|
187
|
+
// Scope the mutation to (lineId, cartId): a caller who learns
|
|
188
|
+
// another visitor's cart_lines.id (it's rendered in their own cart
|
|
189
|
+
// HTML as the update/remove form action) can't mutate it because
|
|
190
|
+
// the row only matches when it also belongs to the caller's session
|
|
191
|
+
// cart. A cross-cart id matches zero rows and becomes a no-op
|
|
192
|
+
// (returns null), never a mutation. cartId is required — every
|
|
193
|
+
// caller resolves the session cart first.
|
|
194
|
+
updateLine: async function (lineId, cartId, patch) {
|
|
188
195
|
_uuid(lineId, "line id");
|
|
196
|
+
_uuid(cartId, "cart_id");
|
|
189
197
|
if (!patch || typeof patch !== "object") throw new TypeError("cart.updateLine: patch object required");
|
|
190
198
|
if (patch.qty == null) throw new TypeError("cart.updateLine: qty is the only updatable field");
|
|
191
199
|
_qty(patch.qty);
|
|
192
200
|
var ts = _now();
|
|
193
201
|
var r = await query(
|
|
194
|
-
"UPDATE cart_lines SET qty = ?1, updated_at = ?2 WHERE id = ?3",
|
|
195
|
-
[patch.qty, ts, lineId],
|
|
202
|
+
"UPDATE cart_lines SET qty = ?1, updated_at = ?2 WHERE id = ?3 AND cart_id = ?4",
|
|
203
|
+
[patch.qty, ts, lineId, cartId],
|
|
196
204
|
);
|
|
197
205
|
if (r.rowCount === 0) return null;
|
|
198
|
-
var row = (await query("SELECT * FROM cart_lines WHERE id = ?1", [lineId])).rows[0];
|
|
199
|
-
await query("UPDATE carts SET updated_at = ?1 WHERE id = ?2", [ts,
|
|
206
|
+
var row = (await query("SELECT * FROM cart_lines WHERE id = ?1 AND cart_id = ?2", [lineId, cartId])).rows[0];
|
|
207
|
+
await query("UPDATE carts SET updated_at = ?1 WHERE id = ?2", [ts, cartId]);
|
|
200
208
|
return row;
|
|
201
209
|
},
|
|
202
210
|
|
|
203
|
-
|
|
211
|
+
// Same cart-scoping as updateLine: a row only deletes when it
|
|
212
|
+
// belongs to the caller's session cart, so a cross-cart id is a
|
|
213
|
+
// no-op (returns false) rather than deleting another visitor's line.
|
|
214
|
+
removeLine: async function (lineId, cartId) {
|
|
204
215
|
_uuid(lineId, "line id");
|
|
205
|
-
|
|
216
|
+
_uuid(cartId, "cart_id");
|
|
217
|
+
var row = (await query(
|
|
218
|
+
"SELECT cart_id FROM cart_lines WHERE id = ?1 AND cart_id = ?2",
|
|
219
|
+
[lineId, cartId],
|
|
220
|
+
)).rows[0];
|
|
206
221
|
if (!row) return false;
|
|
207
|
-
await query("DELETE FROM cart_lines WHERE id = ?1", [lineId]);
|
|
208
|
-
await query("UPDATE carts SET updated_at = ?1 WHERE id = ?2", [_now(),
|
|
222
|
+
await query("DELETE FROM cart_lines WHERE id = ?1 AND cart_id = ?2", [lineId, cartId]);
|
|
223
|
+
await query("UPDATE carts SET updated_at = ?1 WHERE id = ?2", [_now(), cartId]);
|
|
209
224
|
return true;
|
|
210
225
|
},
|
|
211
226
|
|
package/lib/storefront.js
CHANGED
|
@@ -44,6 +44,35 @@ var b = require("./vendor/blamejs");
|
|
|
44
44
|
// finds no store and falls back to the English baseline.
|
|
45
45
|
var _localeAls = new AsyncLocalStorage();
|
|
46
46
|
|
|
47
|
+
// Storefront error logger. Routes through the framework's structured
|
|
48
|
+
// log sink (not console) so operators can redirect / quiet / structured-
|
|
49
|
+
// log every emission point, and so the per-request id the createApp
|
|
50
|
+
// requestId middleware allocates is auto-bound to each line. Used by the
|
|
51
|
+
// 5xx auth-route handlers to record the real failure server-side while
|
|
52
|
+
// the client only ever sees a generic message + the correlating id.
|
|
53
|
+
var _log = b.log.create({});
|
|
54
|
+
|
|
55
|
+
// Generic 500 for an auth/ceremony route: log the real error server-side
|
|
56
|
+
// (correlated by the framework request id) and return a fixed message to
|
|
57
|
+
// the client so no internal error string (stack frame, DB column, vault
|
|
58
|
+
// internals) leaks. The request id — set by the createApp requestId
|
|
59
|
+
// middleware on req.requestId and echoed in the X-Request-Id response
|
|
60
|
+
// header — rides in the body so an operator can grep the logs for a
|
|
61
|
+
// customer's failed ceremony. `where` names the route for the log line
|
|
62
|
+
// only; it is not reflected to the client.
|
|
63
|
+
function _authServerError(req, res, e, where) {
|
|
64
|
+
var rid = (req && req.requestId) || null;
|
|
65
|
+
_log.error("storefront auth route failed", {
|
|
66
|
+
route: where,
|
|
67
|
+
request_id: rid,
|
|
68
|
+
err: (e && e.message) || String(e),
|
|
69
|
+
});
|
|
70
|
+
res.status(500);
|
|
71
|
+
var ref = rid ? " (ref " + rid + ")" : "";
|
|
72
|
+
var msg = "Something went wrong. Please try again." + ref;
|
|
73
|
+
return res.end ? res.end(msg) : res.send(msg);
|
|
74
|
+
}
|
|
75
|
+
|
|
47
76
|
// Payment-webhook signatures (Stripe's HMAC, PayPal's, …) are computed over
|
|
48
77
|
// the EXACT raw request bytes, but the global JSON body-parser reparses and
|
|
49
78
|
// discards them. This middleware buffers the raw body for the given POST
|
|
@@ -3561,10 +3590,16 @@ function _buildCompare(productId) {
|
|
|
3561
3590
|
}
|
|
3562
3591
|
|
|
3563
3592
|
// Schema.org JSON-LD block. JSON.stringify covers the standard escapes;
|
|
3564
|
-
// the
|
|
3565
|
-
// value.
|
|
3593
|
+
// the `</script` → `<\/script` rewrite neutralises any literal closing
|
|
3594
|
+
// tag in a value. The HTML tokenizer ends a <script> on `</script`
|
|
3595
|
+
// followed by whitespace, `/`, or `>`, so matching only the exact
|
|
3596
|
+
// `</script>` byte sequence misses `</script `, `</script\n`,
|
|
3597
|
+
// `</script/` — all of which still break out. Matching `</script`
|
|
3598
|
+
// (any trailing byte) closes every variant. Mirrors the edge
|
|
3599
|
+
// renderer's `jsonLdScript` byte-for-byte (this output is dual-rendered
|
|
3600
|
+
// edge + container; the render-parity tests gate on the two agreeing).
|
|
3566
3601
|
function _jsonLdScript(data) {
|
|
3567
|
-
var serialised = JSON.stringify(data).replace(/<\/
|
|
3602
|
+
var serialised = JSON.stringify(data).replace(/<\/script/gi, "<\\/script");
|
|
3568
3603
|
return "<script type=\"application/ld+json\">" + serialised + "</script>";
|
|
3569
3604
|
}
|
|
3570
3605
|
|
|
@@ -4921,6 +4956,27 @@ var SESSION_COOKIE_NAME = "shop_sid";
|
|
|
4921
4956
|
// initiated logout-everywhere).
|
|
4922
4957
|
var AUTH_COOKIE_NAME = "shop_auth";
|
|
4923
4958
|
|
|
4959
|
+
// Cookie-prefix-hardened names for the two Path=/ session cookies. The
|
|
4960
|
+
// `__Host-` prefix is a browser-enforced integrity marker (RFC 6265bis
|
|
4961
|
+
// §4.1.3.2): a `__Host-`-named cookie is only stored when it was set
|
|
4962
|
+
// Secure, Path=/, and with NO Domain — pinning it to the exact host over
|
|
4963
|
+
// HTTPS, immune to subdomain / related-domain cookie injection + session
|
|
4964
|
+
// fixation. Both cookies are already Path=/ with no Domain, so `__Host-`
|
|
4965
|
+
// is the correct (strongest) prefix.
|
|
4966
|
+
//
|
|
4967
|
+
// The prefix moves in lockstep with the Secure attribute: a `__Host-`
|
|
4968
|
+
// cookie is INVALID (silently dropped by the browser) without Secure, and
|
|
4969
|
+
// Secure cookies are silently dropped over plain http. Local dev + the
|
|
4970
|
+
// e2e harness run over http, so the cookie there must carry the BARE name
|
|
4971
|
+
// (Secure off) or it would never store and every session would break. The
|
|
4972
|
+
// public protocol — https in production where the Cloudflare Worker
|
|
4973
|
+
// terminates TLS and forwards `x-forwarded-proto: https` to the container
|
|
4974
|
+
// — drives the choice via `_secureForReq` below. Read sites resolve the
|
|
4975
|
+
// prefixed name first and fall back to the bare name, so a request in
|
|
4976
|
+
// either environment (or mid-rollout) still resolves its session.
|
|
4977
|
+
var SESSION_COOKIE_NAME_SECURE = "__Host-" + SESSION_COOKIE_NAME;
|
|
4978
|
+
var AUTH_COOKIE_NAME_SECURE = "__Host-" + AUTH_COOKIE_NAME;
|
|
4979
|
+
|
|
4924
4980
|
// WebAuthn ceremony state cookie — short-lived envelope holding the
|
|
4925
4981
|
// random challenge + the ceremony-scoped metadata so register-finish /
|
|
4926
4982
|
// login-finish can verify the same challenge the browser was sent.
|
|
@@ -4978,6 +5034,41 @@ function _readCookie(req, name) {
|
|
|
4978
5034
|
return _cookieJar().read(req, name);
|
|
4979
5035
|
}
|
|
4980
5036
|
|
|
5037
|
+
// Whether THIS request's PUBLIC connection is https — the single source
|
|
5038
|
+
// of truth for both the Secure attribute and the cookie-prefix choice.
|
|
5039
|
+
// The container socket behind the Cloudflare Worker may be plain http even
|
|
5040
|
+
// when the visitor's connection is https, so the decision rides on the
|
|
5041
|
+
// forwarded protocol the Worker sets (`x-forwarded-proto`); `trustProxy`
|
|
5042
|
+
// opts that header in (the framework refuses the attacker-forgeable header
|
|
5043
|
+
// otherwise). Direct dev / e2e connections have no forwarded header and a
|
|
5044
|
+
// non-encrypted socket, so this returns false there and the bare-named,
|
|
5045
|
+
// non-Secure cookie is emitted (a real browser drops a Secure cookie over
|
|
5046
|
+
// http, so the bare name is what keeps dev/e2e sessions working).
|
|
5047
|
+
function _secureForReq(req) {
|
|
5048
|
+
return b.requestHelpers.requestProtocol(req, { trustProxy: true }) === "https";
|
|
5049
|
+
}
|
|
5050
|
+
|
|
5051
|
+
// Resolve the on-wire name for one of the prefix-hardened cookies. Secure
|
|
5052
|
+
// requests (https) carry the `__Host-`-prefixed name; non-Secure requests
|
|
5053
|
+
// (http dev/e2e) carry the bare name so the browser actually stores it.
|
|
5054
|
+
function _sidCookieName(secure) { return secure ? SESSION_COOKIE_NAME_SECURE : SESSION_COOKIE_NAME; }
|
|
5055
|
+
function _authCookieName(secure) { return secure ? AUTH_COOKIE_NAME_SECURE : AUTH_COOKIE_NAME; }
|
|
5056
|
+
|
|
5057
|
+
// Read a prefix-hardened cookie value resolving the prefixed name first
|
|
5058
|
+
// and the bare name second, so a request resolves its session regardless
|
|
5059
|
+
// of which environment wrote the cookie (or a mid-rollout request that
|
|
5060
|
+
// still carries the old bare-named cookie). Returns the raw string or null.
|
|
5061
|
+
function _readPrefixedCookie(req, secureName, bareName) {
|
|
5062
|
+
var v = _cookieJar().read(req, secureName);
|
|
5063
|
+
if (v !== null && v !== undefined) return v;
|
|
5064
|
+
return _cookieJar().read(req, bareName);
|
|
5065
|
+
}
|
|
5066
|
+
function _readPrefixedSealed(req, secureName, bareName) {
|
|
5067
|
+
var v = _cookieJar().readSealed(req, secureName);
|
|
5068
|
+
if (v !== null) return v;
|
|
5069
|
+
return _cookieJar().readSealed(req, bareName);
|
|
5070
|
+
}
|
|
5071
|
+
|
|
4981
5072
|
function _readSidCookie(req) {
|
|
4982
5073
|
// A cookie carrying anything but a well-shaped session id (a stale
|
|
4983
5074
|
// value from an old deploy, a tampered cookie, garbage) reads as "no
|
|
@@ -4985,27 +5076,45 @@ function _readSidCookie(req) {
|
|
|
4985
5076
|
// malformed id and would turn every page that renders the cart count
|
|
4986
5077
|
// into a 500. The cookie grants zero authority, so dropping a
|
|
4987
5078
|
// malformed one silently is safe.
|
|
4988
|
-
var v =
|
|
5079
|
+
var v = _readPrefixedCookie(req, SESSION_COOKIE_NAME_SECURE, SESSION_COOKIE_NAME);
|
|
4989
5080
|
return v && SID_SHAPE_RE.test(v) ? v : null;
|
|
4990
5081
|
}
|
|
4991
5082
|
|
|
4992
|
-
function _setSidCookie(res, sid) {
|
|
5083
|
+
function _setSidCookie(req, res, sid) {
|
|
4993
5084
|
var T = b.constants.TIME;
|
|
4994
|
-
|
|
5085
|
+
var secure = _secureForReq(req);
|
|
5086
|
+
// The jar defaults Path=/ and no Domain — exactly the `__Host-`
|
|
5087
|
+
// invariant. The explicit `secure` here moves the Secure attribute and
|
|
5088
|
+
// the prefixed name together: an https request gets `__Host-shop_sid`
|
|
5089
|
+
// with Secure; an http dev/e2e request gets `shop_sid` without Secure.
|
|
5090
|
+
_cookieJar().write(res, _sidCookieName(secure), sid, {
|
|
5091
|
+
expires: new Date(Date.now() + T.days(30)),
|
|
5092
|
+
secure: secure,
|
|
5093
|
+
});
|
|
4995
5094
|
}
|
|
4996
5095
|
|
|
4997
5096
|
// Auth + WebAuthn-challenge cookies carry a vault-sealed JSON envelope.
|
|
4998
5097
|
// writeSealed/readSealed handle the seal + the on-wire prefix; the
|
|
4999
5098
|
// caller works in plain objects.
|
|
5000
|
-
function _setAuthCookie(res, env) {
|
|
5099
|
+
function _setAuthCookie(req, res, env) {
|
|
5001
5100
|
var T = b.constants.TIME;
|
|
5002
|
-
|
|
5101
|
+
var secure = _secureForReq(req);
|
|
5102
|
+
_cookieJar().writeSealed(res, _authCookieName(secure), JSON.stringify(env), {
|
|
5103
|
+
expires: new Date(Date.now() + T.days(14)),
|
|
5104
|
+
secure: secure,
|
|
5105
|
+
});
|
|
5003
5106
|
}
|
|
5004
|
-
function _clearAuthCookie(res) {
|
|
5005
|
-
|
|
5107
|
+
function _clearAuthCookie(req, res) {
|
|
5108
|
+
// Expire-now must match the live request's protocol so the attributes
|
|
5109
|
+
// satisfy the prefix invariant: over https clear the `__Host-` name with
|
|
5110
|
+
// Secure+Path=/; over http clear the bare name. A request never carries
|
|
5111
|
+
// both names at once (the write side emits exactly one per protocol), so
|
|
5112
|
+
// clearing the protocol-matched name signs the visitor out.
|
|
5113
|
+
var secure = _secureForReq(req);
|
|
5114
|
+
_cookieJar().clear(res, _authCookieName(secure), { secure: secure });
|
|
5006
5115
|
}
|
|
5007
5116
|
function _readAuthEnv(req) {
|
|
5008
|
-
var raw =
|
|
5117
|
+
var raw = _readPrefixedSealed(req, AUTH_COOKIE_NAME_SECURE, AUTH_COOKIE_NAME);
|
|
5009
5118
|
if (raw === null) return null;
|
|
5010
5119
|
try { return JSON.parse(raw); } catch (_e) { return null; }
|
|
5011
5120
|
}
|
|
@@ -6209,7 +6318,10 @@ function mount(router, deps) {
|
|
|
6209
6318
|
router.use(function announcementMiddleware(req, _res, next) {
|
|
6210
6319
|
try {
|
|
6211
6320
|
_refreshAnnouncementCache(deps.announcementBar);
|
|
6212
|
-
|
|
6321
|
+
// Coarse logged-in/guest bucket from auth-cookie PRESENCE — resolve
|
|
6322
|
+
// the prefixed (`__Host-`, https/production) and bare (http/dev)
|
|
6323
|
+
// names so an https visitor isn't mis-bucketed as a guest.
|
|
6324
|
+
var viewerKind = _readPrefixedCookie(req, AUTH_COOKIE_NAME_SECURE, AUTH_COOKIE_NAME) ? "logged_in" : "guest";
|
|
6213
6325
|
var row = _resolveActiveAnnouncement(viewerKind);
|
|
6214
6326
|
var cur = _localeAls.getStore() || {};
|
|
6215
6327
|
_localeAls.enterWith(Object.assign({}, cur, { announcement: row }));
|
|
@@ -6514,7 +6626,7 @@ function mount(router, deps) {
|
|
|
6514
6626
|
var sid = _readSidCookie(req);
|
|
6515
6627
|
if (!sid) {
|
|
6516
6628
|
sid = b.uuid.v7();
|
|
6517
|
-
_setSidCookie(res, sid);
|
|
6629
|
+
_setSidCookie(req, res, sid);
|
|
6518
6630
|
}
|
|
6519
6631
|
var existing = await deps.cart.bySession(sid);
|
|
6520
6632
|
if (existing) return { sid: sid, cart: existing };
|
|
@@ -7063,7 +7175,7 @@ function mount(router, deps) {
|
|
|
7063
7175
|
if (sid) return sid;
|
|
7064
7176
|
if (!mint) return null;
|
|
7065
7177
|
sid = b.uuid.v7();
|
|
7066
|
-
_setSidCookie(res, sid);
|
|
7178
|
+
_setSidCookie(req, res, sid);
|
|
7067
7179
|
return sid;
|
|
7068
7180
|
}
|
|
7069
7181
|
|
|
@@ -8067,8 +8179,11 @@ function mount(router, deps) {
|
|
|
8067
8179
|
return res.end ? res.end(JSON.stringify(startOpts)) : res.send(JSON.stringify(startOpts));
|
|
8068
8180
|
} catch (e) {
|
|
8069
8181
|
if (e && e.code === "vault/not-initialized") return _serviceUnavailable(res, "auth not configured");
|
|
8070
|
-
|
|
8071
|
-
|
|
8182
|
+
if (e instanceof TypeError) {
|
|
8183
|
+
res.status(400);
|
|
8184
|
+
return res.end ? res.end((e && e.message) || "register-begin failed") : res.send((e && e.message) || "register-begin failed");
|
|
8185
|
+
}
|
|
8186
|
+
return _authServerError(req, res, e, "register-begin");
|
|
8072
8187
|
}
|
|
8073
8188
|
});
|
|
8074
8189
|
|
|
@@ -8105,7 +8220,7 @@ function mount(router, deps) {
|
|
|
8105
8220
|
transports: transports,
|
|
8106
8221
|
});
|
|
8107
8222
|
_clearChallengeCookie(res);
|
|
8108
|
-
_setAuthCookie(res, {
|
|
8223
|
+
_setAuthCookie(req, res, {
|
|
8109
8224
|
customer_id: env.customer_id,
|
|
8110
8225
|
exp: Date.now() + b.constants.TIME.days(14),
|
|
8111
8226
|
});
|
|
@@ -8125,8 +8240,11 @@ function mount(router, deps) {
|
|
|
8125
8240
|
return res.end ? res.end("ok") : res.send("ok");
|
|
8126
8241
|
} catch (e) {
|
|
8127
8242
|
if (e && e.code === "vault/not-initialized") return _serviceUnavailable(res, "auth not configured");
|
|
8128
|
-
|
|
8129
|
-
|
|
8243
|
+
if (e instanceof TypeError) {
|
|
8244
|
+
res.status(400);
|
|
8245
|
+
return res.end ? res.end((e && e.message) || "register-finish failed") : res.send((e && e.message) || "register-finish failed");
|
|
8246
|
+
}
|
|
8247
|
+
return _authServerError(req, res, e, "register-finish");
|
|
8130
8248
|
}
|
|
8131
8249
|
});
|
|
8132
8250
|
|
|
@@ -8166,8 +8284,11 @@ function mount(router, deps) {
|
|
|
8166
8284
|
return res.end ? res.end(JSON.stringify(startOpts)) : res.send(JSON.stringify(startOpts));
|
|
8167
8285
|
} catch (e) {
|
|
8168
8286
|
if (e && e.code === "vault/not-initialized") return _serviceUnavailable(res, "auth not configured");
|
|
8169
|
-
|
|
8170
|
-
|
|
8287
|
+
if (e instanceof TypeError) {
|
|
8288
|
+
res.status(400);
|
|
8289
|
+
return res.end ? res.end((e && e.message) || "login-begin failed") : res.send((e && e.message) || "login-begin failed");
|
|
8290
|
+
}
|
|
8291
|
+
return _authServerError(req, res, e, "login-begin");
|
|
8171
8292
|
}
|
|
8172
8293
|
});
|
|
8173
8294
|
|
|
@@ -8222,7 +8343,7 @@ function mount(router, deps) {
|
|
|
8222
8343
|
} catch (_e) { /* best-effort merge; sign-in itself succeeds */ }
|
|
8223
8344
|
}
|
|
8224
8345
|
_clearChallengeCookie(res);
|
|
8225
|
-
_setAuthCookie(res, {
|
|
8346
|
+
_setAuthCookie(req, res, {
|
|
8226
8347
|
customer_id: customer.id,
|
|
8227
8348
|
exp: Date.now() + b.constants.TIME.days(14),
|
|
8228
8349
|
});
|
|
@@ -8230,8 +8351,11 @@ function mount(router, deps) {
|
|
|
8230
8351
|
return res.end ? res.end("ok") : res.send("ok");
|
|
8231
8352
|
} catch (e) {
|
|
8232
8353
|
if (e && e.code === "vault/not-initialized") return _serviceUnavailable(res, "auth not configured");
|
|
8233
|
-
|
|
8234
|
-
|
|
8354
|
+
if (e instanceof TypeError) {
|
|
8355
|
+
res.status(400);
|
|
8356
|
+
return res.end ? res.end((e && e.message) || "login-finish failed") : res.send((e && e.message) || "login-finish failed");
|
|
8357
|
+
}
|
|
8358
|
+
return _authServerError(req, res, e, "login-finish");
|
|
8235
8359
|
}
|
|
8236
8360
|
});
|
|
8237
8361
|
|
|
@@ -8248,7 +8372,7 @@ function mount(router, deps) {
|
|
|
8248
8372
|
}
|
|
8249
8373
|
var customer = await deps.customers.get(auth.customer_id);
|
|
8250
8374
|
if (!customer) {
|
|
8251
|
-
_clearAuthCookie(res);
|
|
8375
|
+
_clearAuthCookie(req, res);
|
|
8252
8376
|
res.status(303); res.setHeader && res.setHeader("location", "/account/login");
|
|
8253
8377
|
return res.end ? res.end() : res.send("");
|
|
8254
8378
|
}
|
|
@@ -8348,8 +8472,8 @@ function mount(router, deps) {
|
|
|
8348
8472
|
});
|
|
8349
8473
|
}
|
|
8350
8474
|
|
|
8351
|
-
router.post("/account/logout", function (
|
|
8352
|
-
_clearAuthCookie(res);
|
|
8475
|
+
router.post("/account/logout", function (req, res) {
|
|
8476
|
+
_clearAuthCookie(req, res);
|
|
8353
8477
|
res.status(303); res.setHeader && res.setHeader("location", "/");
|
|
8354
8478
|
return res.end ? res.end() : res.send("");
|
|
8355
8479
|
});
|
|
@@ -8507,8 +8631,11 @@ function mount(router, deps) {
|
|
|
8507
8631
|
return res.end ? res.end(JSON.stringify(startOpts)) : res.send(JSON.stringify(startOpts));
|
|
8508
8632
|
} catch (e) {
|
|
8509
8633
|
if (e && e.code === "vault/not-initialized") return _serviceUnavailable(res, "auth not configured");
|
|
8510
|
-
|
|
8511
|
-
|
|
8634
|
+
if (e instanceof TypeError) {
|
|
8635
|
+
res.status(400);
|
|
8636
|
+
return res.end ? res.end((e && e.message) || "add-begin failed") : res.send((e && e.message) || "add-begin failed");
|
|
8637
|
+
}
|
|
8638
|
+
return _authServerError(req, res, e, "add-begin");
|
|
8512
8639
|
}
|
|
8513
8640
|
});
|
|
8514
8641
|
|
|
@@ -8560,8 +8687,11 @@ function mount(router, deps) {
|
|
|
8560
8687
|
return res.end ? res.end("ok") : res.send("ok");
|
|
8561
8688
|
} catch (e) {
|
|
8562
8689
|
if (e && e.code === "vault/not-initialized") return _serviceUnavailable(res, "auth not configured");
|
|
8563
|
-
|
|
8564
|
-
|
|
8690
|
+
if (e instanceof TypeError) {
|
|
8691
|
+
res.status(400);
|
|
8692
|
+
return res.end ? res.end((e && e.message) || "add-finish failed") : res.send((e && e.message) || "add-finish failed");
|
|
8693
|
+
}
|
|
8694
|
+
return _authServerError(req, res, e, "add-finish");
|
|
8565
8695
|
}
|
|
8566
8696
|
});
|
|
8567
8697
|
|
|
@@ -8586,7 +8716,7 @@ function mount(router, deps) {
|
|
|
8586
8716
|
var auth = _accountAuth(req, res); if (!auth) return;
|
|
8587
8717
|
var customer = await deps.customers.get(auth.customer_id);
|
|
8588
8718
|
if (!customer) {
|
|
8589
|
-
_clearAuthCookie(res);
|
|
8719
|
+
_clearAuthCookie(req, res);
|
|
8590
8720
|
res.status(303); res.setHeader && res.setHeader("location", "/account/login");
|
|
8591
8721
|
return res.end ? res.end() : res.send("");
|
|
8592
8722
|
}
|
|
@@ -8597,7 +8727,7 @@ function mount(router, deps) {
|
|
|
8597
8727
|
var auth = _accountAuth(req, res); if (!auth) return;
|
|
8598
8728
|
var customer = await deps.customers.get(auth.customer_id);
|
|
8599
8729
|
if (!customer) {
|
|
8600
|
-
_clearAuthCookie(res);
|
|
8730
|
+
_clearAuthCookie(req, res);
|
|
8601
8731
|
res.status(303); res.setHeader && res.setHeader("location", "/account/login");
|
|
8602
8732
|
return res.end ? res.end() : res.send("");
|
|
8603
8733
|
}
|
|
@@ -8707,7 +8837,7 @@ function mount(router, deps) {
|
|
|
8707
8837
|
} else {
|
|
8708
8838
|
try { _clearReferralCookie(res); } catch (_e) { /* best-effort */ }
|
|
8709
8839
|
}
|
|
8710
|
-
_setAuthCookie(res, { customer_id: rv.customer.id, exp: Date.now() + b.constants.TIME.days(14) });
|
|
8840
|
+
_setAuthCookie(req, res, { customer_id: rv.customer.id, exp: Date.now() + b.constants.TIME.days(14) });
|
|
8711
8841
|
res.status(303); res.setHeader && res.setHeader("location", "/account");
|
|
8712
8842
|
return res.end ? res.end() : res.send("");
|
|
8713
8843
|
});
|
|
@@ -8820,7 +8950,7 @@ function mount(router, deps) {
|
|
|
8820
8950
|
} else {
|
|
8821
8951
|
try { _clearReferralCookie(res); } catch (_e) { /* best-effort */ }
|
|
8822
8952
|
}
|
|
8823
|
-
_setAuthCookie(res, { customer_id: rv.customer.id, exp: Date.now() + b.constants.TIME.days(14) });
|
|
8953
|
+
_setAuthCookie(req, res, { customer_id: rv.customer.id, exp: Date.now() + b.constants.TIME.days(14) });
|
|
8824
8954
|
res.status(303); res.setHeader && res.setHeader("location", "/account");
|
|
8825
8955
|
return res.end ? res.end() : res.send("");
|
|
8826
8956
|
});
|
|
@@ -10346,8 +10476,19 @@ function mount(router, deps) {
|
|
|
10346
10476
|
res.status(400);
|
|
10347
10477
|
return res.end ? res.end("Invalid request") : res.send("Invalid request");
|
|
10348
10478
|
}
|
|
10479
|
+
// Resolve the requester's own session cart and scope the mutation
|
|
10480
|
+
// to it (same pattern as POST /cart/lines/:line_id/save). The line
|
|
10481
|
+
// id is rendered in every visitor's own cart HTML, so without this
|
|
10482
|
+
// a caller who learns another visitor's line id could change its
|
|
10483
|
+
// qty. A line that isn't in the caller's cart returns null → 404.
|
|
10484
|
+
var sid = _readSidCookie(req);
|
|
10485
|
+
var cart = sid ? await deps.cart.bySession(sid) : null;
|
|
10486
|
+
if (!cart) {
|
|
10487
|
+
res.status(404);
|
|
10488
|
+
return res.end ? res.end("Line not found") : res.send("Line not found");
|
|
10489
|
+
}
|
|
10349
10490
|
try {
|
|
10350
|
-
var updated = await deps.cart.updateLine(lineId, { qty: qty });
|
|
10491
|
+
var updated = await deps.cart.updateLine(lineId, cart.id, { qty: qty });
|
|
10351
10492
|
if (!updated) {
|
|
10352
10493
|
res.status(404);
|
|
10353
10494
|
return res.end ? res.end("Line not found") : res.send("Line not found");
|
|
@@ -10368,8 +10509,19 @@ function mount(router, deps) {
|
|
|
10368
10509
|
res.status(400);
|
|
10369
10510
|
return res.end ? res.end("Invalid request") : res.send("Invalid request");
|
|
10370
10511
|
}
|
|
10512
|
+
// Scope the delete to the requester's own session cart (same as the
|
|
10513
|
+
// update route) so a known-but-foreign line id can't delete another
|
|
10514
|
+
// visitor's line. No session cart → nothing of the caller's to
|
|
10515
|
+
// remove; redirect to /cart as the no-op success path.
|
|
10516
|
+
var sid = _readSidCookie(req);
|
|
10517
|
+
var cart = sid ? await deps.cart.bySession(sid) : null;
|
|
10518
|
+
if (!cart) {
|
|
10519
|
+
res.status(303);
|
|
10520
|
+
res.setHeader && res.setHeader("location", "/cart");
|
|
10521
|
+
return res.end ? res.end() : res.send("");
|
|
10522
|
+
}
|
|
10371
10523
|
try {
|
|
10372
|
-
await deps.cart.removeLine(lineId);
|
|
10524
|
+
await deps.cart.removeLine(lineId, cart.id);
|
|
10373
10525
|
} catch (e) {
|
|
10374
10526
|
res.status(e instanceof TypeError ? 400 : 500);
|
|
10375
10527
|
return res.end ? res.end((e && e.message) || "Error") : res.send((e && e.message) || "Error");
|
|
@@ -10471,7 +10623,7 @@ function mount(router, deps) {
|
|
|
10471
10623
|
var sid = _readSidCookie(req);
|
|
10472
10624
|
if (!sid) {
|
|
10473
10625
|
sid = b.uuid.v7();
|
|
10474
|
-
_setSidCookie(res, sid);
|
|
10626
|
+
_setSidCookie(req, res, sid);
|
|
10475
10627
|
}
|
|
10476
10628
|
return sid;
|
|
10477
10629
|
}
|
package/lib/vendor/MANIFEST.json
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
"_about": "blamejs.shop vendors a single framework — blamejs — which itself bundles every server-side crypto/identity dependency. The transitive packages blamejs ships are surfaced in its own MANIFEST.json at lib/vendor/blamejs/lib/vendor/MANIFEST.json — Trivy / Grype rely on that nested data for CVE attribution.",
|
|
4
4
|
"packages": {
|
|
5
5
|
"blamejs": {
|
|
6
|
-
"version": "0.13.
|
|
7
|
-
"tag": "v0.13.
|
|
6
|
+
"version": "0.13.44",
|
|
7
|
+
"tag": "v0.13.44",
|
|
8
8
|
"license": "Apache-2.0",
|
|
9
9
|
"author": "blamejs contributors",
|
|
10
10
|
"source": "https://github.com/blamejs/blamejs",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"server": "lib/vendor/blamejs/"
|
|
14
14
|
},
|
|
15
15
|
"bundler": "shallow git clone of release tag from github.com/blamejs/blamejs",
|
|
16
|
-
"bundledAt": "2026-05-
|
|
16
|
+
"bundledAt": "2026-05-30"
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
}
|
|
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.13.x
|
|
10
10
|
|
|
11
|
+
- v0.13.44 (2026-05-29) — **Error codes on the consent, compliance, and protocol namespaces now follow the namespace/kebab-case contract.** The framework's error contract is `err.code = "namespace/kebab-case"`, and the vast majority of namespaces already followed it. This release normalizes the holdouts: fifteen namespaces that threw bare UPPER_SNAKE codes with no namespace, and nine that used a camelCase namespace prefix. After this release every error these namespaces throw carries a `namespace/kebab-case` code, so an operator switching on `err.code` no longer has to special-case them. This is a breaking change for code that matches the old strings — pre-1.0, there is no compatibility shim, so update any `err.code` comparisons against the listed namespaces. A codebase check now enforces the convention so it cannot regress. A small set of older codes (the cluster, scheduler, circuit-breaker, object-store, and upload subsystems) is intentionally left for the 1.0 release, where it will carry a deprecation cycle. **Changed:** *Bare UPPER_SNAKE error codes are now namespaced (breaking)* — Fifteen namespaces threw bare UPPER_SNAKE error codes with no namespace prefix (for example `mcp` threw `BAD_JSON`, `BAD_ENVELOPE`, `BAD_METHOD`). Their `err.code` values are now `namespace/kebab-case` — `mcp/bad-json`, `mcp/bad-envelope`, and so on. The affected namespaces are `b.a2a`, `b.aiInput`, `b.aiPref`, `b.budr`, `b.contentCredentials`, `b.darkPatterns`, `b.fapi2`, `b.fdx`, `b.graphqlFederation`, `b.iabTcf`, `b.iabMspa`, `b.mcp`, `b.secCyber`, `b.sse`, and `b.tcpa10dlc`. Operators matching the old bare codes on `err.code` must update those comparisons; the error message text is unchanged. · *camelCase error-code namespaces are now kebab-case (breaking)* — Nine namespaces emitted error codes whose namespace segment was camelCase (for example `aiDp/bad-bound`, `argParser/flag-duplicate`). The namespace segment is now kebab-case to match every other code: `ai-dp/`, `ai-capability/`, `ai-quota/`, `arg-parser/`, `audit-sign/`, `auth-step-up/`, `ddl-change-control/`, `dr-runbook/`, `tenant-quota/`, and `boot-gates/`. The `b.*` API namespace keys themselves are unchanged (those remain camelCase, e.g. `b.argParser`); only the `err.code` string changed. Operators matching these `err.code` strings must update them. **Detectors:** *Error-code shape is enforced* — A codebase check now flags any error code constructed via `new XError(...)` or the per-class `factory(...)` whose value is a bare UPPER_SNAKE string or carries a camelCase namespace segment, so the `namespace/kebab-case` contract cannot silently regress. It correctly ignores native error constructors (whose first argument is the message, not a code).
|
|
12
|
+
|
|
13
|
+
- v0.13.43 (2026-05-29) — **LTS window stated consistently as 24 months, experimental primitives declared semver-exempt, and stale version references cleaned up.** Documentation and operator-facing string hygiene ahead of the 1.0 stability contract. The LTS support window is now stated as 24 months everywhere (GOVERNANCE.md and the LTS calendar previously disagreed — 24 vs 18). The LTS calendar gains an explicit clause that primitives marked experimental are exempt from the stability/LTS contract, so operators can tell at a glance which surfaces may change between minors. Several error messages and doc blocks that pinned to long-past version numbers ("lands in v0.10.9", "not supported in v0.12.7", "ships in v0.6.45+") are restated version-agnostically with their escape hatch, and the S/MIME module now points operators at the live PGP encrypt/decrypt path for confidentiality today. No API or behavior changes. **Changed:** *LTS support window is consistently 24 months* — `GOVERNANCE.md` promised a 24-month LTS window while `LTS-CALENDAR.md` and `SECURITY.md` stated 18 — a six-month contradiction in the single most load-bearing number of the support contract. All three now state 24 months of security-only patches per major. The calendar table and the supported-versions prose are aligned. · *Experimental primitives are declared exempt from the stability contract* — `LTS-CALENDAR.md` now states explicitly that primitives documented as experimental (shown as "experimental" on their wiki page, and via the `experimental` segment in namespaces like `b.jose.jwe.experimental`) are not covered by the stability contract or the LTS window — they may change signature, behavior, or wire format, or be removed, in any minor without a deprecation cycle. This lets the framework ship primitives that track in-flight standards without freezing an unsettled format, and tells operators precisely which surfaces are not yet frozen. **Fixed:** *Stale version references removed from operator-facing errors and docs* — Error messages and documentation that pinned to long-past versions are restated version-agnostically with the relevant escape hatch: ZIP64 and unsupported-compression errors in archive reading, the CMS AuthEnvelopedData / fielded-decoder notes, the mTLS CRL-engine error, the safe-archive format-detection summary (which also now correctly lists the supported zip / tar / tar.gz set rather than claiming only zip), and the AI-content IPTC-reader note. None changed behavior; they no longer read as broken promises against the published version history. · *S/MIME confidentiality deferral points to the working PGP path* — `b.mail.crypto.smime` ships sign + verify; encrypt/decrypt is deferred. The deferral note previously cited an open-ended internal condition; it now names the escape hatch directly — use `b.mail.crypto.pgp.encrypt` / `decrypt` for mail confidentiality today — and states the concrete trigger that would re-open S/MIME-specific (X.509-recipient) encryption. · *Governance doc no longer references an internal file operators cannot see* — `GOVERNANCE.md` cited a rule number in a contributor-only file that does not ship in the repository. The deprecation-policy statement is now self-contained.
|
|
14
|
+
|
|
11
15
|
- v0.13.42 (2026-05-29) — **S/MIME trust-chain validation binds the leaf to the key that verified the signature.** When b.mail.crypto.smime.verify is given trust anchors, it validates the supplied certificate chain — but it picked the chain leaf unconditionally (the first cert) and never tied it to signerPublicKey, the key that actually verified the signature. A SignedData blob could therefore carry a validly-chained certificate for one identity while the signature was verified under an unrelated key, and the chain-validated result would imply a cert↔signer binding the code never made. Chain validation now selects the leaf as the certificate whose public key matches signerPublicKey, and refuses (signer-not-in-chain) when no certificate in the chain carries that key — so a chain-validated signature is bound to the cert it claims. **Security:** *Trust-chain leaf is bound to the verified signer key* — `smime.verify({ trustAnchorCertsPem })` validated the supplied chain starting from `chain[0]` without checking that the leaf's public key was the one that verified the signature (the operator-supplied `signerPublicKey`). A crafted `SignedData` could pair a validly-chained certificate for identity A with a signature verified under an unrelated key, and the chain-valid result would assert a binding that didn't hold. The chain walk now selects the leaf as the certificate whose public key equals `signerPublicKey` (matched against the certificate's SPKI, raw key or full-encoding form), and throws `mail-crypto/smime/signer-not-in-chain` when no certificate in `SignedData.certificates` carries that key. A certificate whose key cannot be extracted is treated as a non-match, so validation fails closed rather than trusting an unverifiable binding.
|
|
12
16
|
|
|
13
17
|
- v0.13.41 (2026-05-29) — **Agent registry reads can be tenant-scoped; compliance-erasure docs clarify actor is an audit field.** The agent orchestrator's registry reads (list and lookup) gated only on the flat agent-registry:read scope, so any holder could enumerate every tenant's agents and resolve a handle to one — even though the event bus already scopes subscribe and delivery by tenant. The orchestrator now mirrors that: with the new tenantScope option enabled, list returns only the actor's own tenant's agents and lookup refuses a cross-tenant name, unless the actor holds the framework cross-tenant-admin scope. Off by default, so single-tenant deployments are unaffected. Separately, the subject-erasure docs now state explicitly that the recorded actor is an audit field, not authentication — the caller must be authorized upstream. **Added:** *Tenant-scoped agent registry reads (opts.tenantScope)* — `b.agent.orchestrator.create({ tenantScope: true })` now scopes `list` and `lookup` to the calling actor's tenant: `list` filters out agents in other tenants and `lookup` returns null for a cross-tenant name, unless the actor holds the cross-tenant-admin scope (`b.agent.tenant.CROSS_TENANT_ADMIN_SCOPE`). This closes a cross-tenant metadata-enumeration and handle-acquisition path — `agent-registry:read` alone no longer exposes other tenants' agents — and mirrors the tenant scoping the event bus enforces on subscribe and delivery. The option defaults off; existing single-tenant orchestrators behave exactly as before. The `tenantId` argument to `list` remains a convenience filter, distinct from this authorization boundary. **Changed:** *Subject-erasure docs clarify the actor is an audit field, not authentication* — `b.subject.erase` and `b.subject.eraseHard` gate the deletion on operator acknowledgements and the legal-hold registry, not on caller identity. Their documentation now states explicitly that the recorded `actor` is an audit-record field, not authentication — the caller MUST be authenticated and authorized by the route before invoking. No behavior change; this removes an implicit assumption that could otherwise be read as the primitive authorizing the call.
|