@blamejs/blamejs-shop 0.4.2 → 0.4.3
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/security-middleware.js +81 -30
- package/lib/storefront.js +19 -0
- 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.4.x
|
|
10
10
|
|
|
11
|
+
- v0.4.3 (2026-06-05) — **Passkey sign-in works again: WebAuthn is permitted on the pages that host it.** The framework's deny-all Permissions-Policy disabled the browser's WebAuthn API everywhere — including the sign-in page's own top-level document — so attempting a passkey sign-in failed with the browser reporting that publickey-credentials-get is not enabled. The policy now permits exactly the WebAuthn capability each ceremony page needs: credential assertion on the sign-in page, credential creation on the registration and passkey-management pages, both scoped to the page's own origin. Every other page keeps the strict deny-all policy, and every other feature (camera, microphone, geolocation, payment outside the payment page) remains denied on the ceremony pages too. **Fixed:** *Passkey ceremonies are no longer blocked by Permissions-Policy* — Sign-in carries publickey-credentials-get=(self); registration and passkey management carry publickey-credentials-create=(self). The allowance is scoped per route following the same pattern the payment page uses, grants apply only to the page's own origin (no cross-origin delegation), and unrecognized feature requests relax nothing. Tests assert the exact header tokens per route and that unrelated pages still deny both WebAuthn features.
|
|
12
|
+
|
|
11
13
|
- v0.4.2 (2026-06-05) — **Abandoned checkouts release their stock holds, and five more inventory and admin hardening fixes.** The inventory enforcement introduced in 0.4.0 placed a stock hold at checkout but had no path to free it when a buyer abandoned without paying or cancelling — each abandoned checkout permanently subtracted from sellable stock until an operator intervened. A scheduled reaper now cancels pending orders older than a configurable age (default two hours), cancelling the payment intent first so a late payment can never complete against a reaped order, and releasing the held stock through the existing cancellation path. Around it, five more fixes harden the same surface: settlement failures during payment confirmation no longer strand holds silently (each item settles independently and failures land in the operator error log with the exact item and quantity), a rollback path no longer releases holds belonging to an order that was successfully created, pre-order campaigns whose launch date has passed now enforce real stock limits instead of remaining exempt, the admin activity timeline rejects protocol-relative link targets, and activity reads are bounded instead of scanning a customer's full history per page view. **Fixed:** *Stock holds from abandoned checkouts are reclaimed* — A pending order that never completes payment now has its stock hold released automatically. The scheduled reaper cancels pending orders older than CHECKOUT_PENDING_TTL_MINUTES (default 120, minimum 5; invalid values refuse to boot). For card payments the payment intent is cancelled at the processor before the order is touched — if the processor reports the payment already succeeded, the order is left alone for the webhook to settle. Each sweep reports counts of reaped, skipped, and errored orders. · *Payment settlement is crash-safe per item* — When an order is marked paid, each line's stock decrement now settles independently: one item's database failure no longer blocks the others, no longer fails the payment webhook, and is captured to the operator error log naming the item, quantity, and order so the operator can reconcile stock from the existing adjustment screen. · *Checkout rollback no longer releases holds it does not own* — If checkout fails after the order record was created, the error path previously released all of the attempt's stock holds — which could free units belonging to the order itself or, on a shared item, a concurrent shopper's reservation. Holds are now released on failure only when no order was created; once an order exists it owns its holds, and cancellation or the reaper frees them. The same correction applies to the PayPal order-creation path. · *Pre-order campaigns enforce stock after their launch date* — An active pre-order campaign exempts its product from stock holds by design — pre-orders sell beyond the shelf. That exemption now ends when the campaign's launch date passes: a launched product sells from real inventory even if the campaign has not yet been moved out of its pre-order state in the console. · *Admin activity links and reads hardened* — The customer activity timeline's internal-link guard now rejects protocol-relative targets, and each activity source is read with a bound matching the requested page instead of scanning the customer's entire history. Pagination, ordering, and the summary counts are unchanged.
|
|
12
14
|
|
|
13
15
|
- v0.4.1 (2026-06-05) — **Order notes and a customer activity timeline in the admin console.** Two operator surfaces land in the admin console. Every order detail screen gains a customer-service notes thread: operators record internal notes or customer-visible ones, pin the important thread to the top, and mark issues resolved or reopen them — with note bodies length-bounded, control-character-rejected, and escaped at render. Every customer detail screen gains a read-only activity timeline aggregating what that customer has done across the store — orders placed and their lifecycle transitions, loyalty points earned, wishlist additions, reviews submitted, and support tickets opened — read directly from the tables those features already populate, newest first. Both panels appear only when their backing modules are wired, and every note mutation is ownership-scoped to its order and audited. **Added:** *Customer-service notes on order detail* — The admin order screen shows a notes thread with pinned notes floated first and the rest newest-first. Operators add notes as internal (the default) or customer-visible, pin and unpin them, and resolve or reopen them with a short resolution summary; resolving a customer-visible note is refused so customer-facing context is never silently closed out. Bodies are validated server-side (8000-character cap, control characters rejected) and HTML-escaped at render. Each mutation verifies the note belongs to the order in the URL before acting, returns clean errors for unknown or malformed identifiers, and emits an audit event. The same surface is available as JSON under the admin bearer token. · *Customer activity timeline on customer detail* — The admin customer screen shows an aggregated, newest-first activity feed: order placements and status transitions, loyalty point movements, wishlist additions, review submissions, and support tickets. The timeline is a read-only view over the tables those features already write — no new tracking or recording was added anywhere in the request path. The panel shows the most recent fifty events, links each event to its admin screen where one exists, and renders an explicit empty state for customers with no history. The same feed is available as JSON under the admin bearer token.
|
package/lib/asset-manifest.json
CHANGED
|
@@ -380,31 +380,42 @@ function scopedCsp(keys) {
|
|
|
380
380
|
}).join("; ") + ";";
|
|
381
381
|
}
|
|
382
382
|
|
|
383
|
-
// ---- route-scoped Permissions-Policy (payment
|
|
383
|
+
// ---- route-scoped Permissions-Policy (payment + passkey surfaces) --------
|
|
384
384
|
//
|
|
385
385
|
// The app-level Permissions-Policy is the vendored strict denylist
|
|
386
|
-
// (DEFAULT_PERMISSIONS) — every powerful API
|
|
387
|
-
//
|
|
388
|
-
//
|
|
389
|
-
//
|
|
390
|
-
// drive the Payment Request API. Under `payment=()` the browser refuses
|
|
391
|
-
// the API inside the cross-origin pay.google.com / Stripe wallet frames
|
|
392
|
-
// ("Permissions policy violation: payment is not allowed in this
|
|
393
|
-
// document"), so the wallet express buttons are degraded on the payment
|
|
394
|
-
// page. The card form is unaffected (it doesn't use the Payment Request
|
|
395
|
-
// API), which is why card captures complete while wallets don't.
|
|
386
|
+
// (DEFAULT_PERMISSIONS) — every powerful API disabled in every document,
|
|
387
|
+
// including `payment=()`, `publickey-credentials-get=()`, and
|
|
388
|
+
// `publickey-credentials-create=()`. That is correct everywhere EXCEPT the
|
|
389
|
+
// handful of pages whose whole job needs one of those features:
|
|
396
390
|
//
|
|
397
|
-
//
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
//
|
|
403
|
-
//
|
|
404
|
-
//
|
|
405
|
-
//
|
|
406
|
-
//
|
|
407
|
-
//
|
|
391
|
+
// - GET /pay/:order_id mounts Stripe's Express Checkout Element, whose
|
|
392
|
+
// Google Pay / Apple Pay buttons drive the Payment Request API. Under
|
|
393
|
+
// `payment=()` the browser refuses the API inside the cross-origin
|
|
394
|
+
// pay.google.com / Stripe wallet frames, degrading the wallet express
|
|
395
|
+
// buttons (the card form is unaffected, which is why card captures
|
|
396
|
+
// complete while wallets don't).
|
|
397
|
+
//
|
|
398
|
+
// - The passkey (WebAuthn) ceremonies. `navigator.credentials.get()`
|
|
399
|
+
// (assertion / sign-in) is gated by `publickey-credentials-get`;
|
|
400
|
+
// `navigator.credentials.create()` (registration / enrollment) by
|
|
401
|
+
// `publickey-credentials-create`. Under the deny-all default the browser
|
|
402
|
+
// refuses the API in the TOP-LEVEL document with "The
|
|
403
|
+
// 'publickey-credentials-get' feature is not enabled in this document"
|
|
404
|
+
// (resp. -create), so sign-in / enrollment fail outright. These run on
|
|
405
|
+
// container-served routes:
|
|
406
|
+
// publickey-credentials-get — GET /account/login (passkey-login.js)
|
|
407
|
+
// publickey-credentials-create — GET /account/register (passkey-register.js)
|
|
408
|
+
// GET /account/passkeys (passkey-add.js)
|
|
409
|
+
//
|
|
410
|
+
// `scopedPermissionsPolicy(opts)` returns a Permissions-Policy string for
|
|
411
|
+
// `res.setHeader("permissions-policy", ...)` on the route's response:
|
|
412
|
+
// byte-identical to the vendored default EXCEPT the named feature(s), each
|
|
413
|
+
// re-enabled for ONLY the allowlist that feature needs, ONLY on that one
|
|
414
|
+
// response. setHeader OVERWRITES, so the app-level strict header still
|
|
415
|
+
// governs every OTHER route. Every other feature in the denylist stays `()`.
|
|
416
|
+
// Derived from the vendored DEFAULT_PERMISSIONS array (the single source the
|
|
417
|
+
// app-level header is also built from in `securityHeadersOpts` → the vendored
|
|
418
|
+
// default), never a hand-forked copy of the list.
|
|
408
419
|
|
|
409
420
|
// The allowlist that replaces `payment=()` on the pay surface: same origin
|
|
410
421
|
// (the pay page's own form), the Stripe SDK frame, and the Google Pay frame
|
|
@@ -415,21 +426,61 @@ function scopedCsp(keys) {
|
|
|
415
426
|
// it here.
|
|
416
427
|
var _PAYMENT_ALLOWLIST = 'payment=(self "https://js.stripe.com" "https://pay.google.com")';
|
|
417
428
|
|
|
429
|
+
// WebAuthn assertion / attestation run in the page's OWN top-level document
|
|
430
|
+
// (the islands call navigator.credentials.get / .create directly, never from
|
|
431
|
+
// a cross-origin child frame), so `self` is the entire allowlist — no third-
|
|
432
|
+
// party origin is delegated. Keeping the grant to `self` is the tightest
|
|
433
|
+
// value that unblocks the ceremony: a cross-origin iframe on the same page
|
|
434
|
+
// still cannot drive WebAuthn. Single source — every passkey page names the
|
|
435
|
+
// feature → allowlist mapping here.
|
|
436
|
+
var _PASSKEY_FEATURE_ALLOWLIST = {
|
|
437
|
+
"publickey-credentials-get": "publickey-credentials-get=(self)",
|
|
438
|
+
"publickey-credentials-create": "publickey-credentials-create=(self)",
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
// Feature name → its scoped allowlist token. The pay surface relaxes
|
|
442
|
+
// `payment`; the passkey surfaces relax the two WebAuthn features. A caller
|
|
443
|
+
// names which feature(s) to relax; every feature NOT named stays at the
|
|
444
|
+
// vendored `()` deny. This is the single registry of "which feature may be
|
|
445
|
+
// re-enabled, and to exactly what" — nothing outside it can be loosened.
|
|
446
|
+
var _SCOPED_FEATURE_OVERRIDES = Object.assign(
|
|
447
|
+
{ payment: _PAYMENT_ALLOWLIST },
|
|
448
|
+
_PASSKEY_FEATURE_ALLOWLIST
|
|
449
|
+
);
|
|
450
|
+
|
|
418
451
|
/**
|
|
419
452
|
* Build a route-scoped Permissions-Policy string from the vendored strict
|
|
420
|
-
* denylist with
|
|
421
|
-
*
|
|
422
|
-
*
|
|
453
|
+
* denylist with one or more features re-enabled to their scoped allowlist.
|
|
454
|
+
* `opts.features` is an array of feature names to relax (each must appear in
|
|
455
|
+
* `_SCOPED_FEATURE_OVERRIDES`); every other feature is carried through
|
|
456
|
+
* verbatim as `feature=()`. With no argument it defaults to relaxing
|
|
457
|
+
* `payment` — the established pay-surface behavior — so existing callers are
|
|
458
|
+
* unchanged. Returns the string for the route's
|
|
423
459
|
* `res.setHeader("permissions-policy", ...)`; the app-level strict header is
|
|
424
460
|
* unchanged on every other route (setHeader overwrites this one response).
|
|
425
461
|
*
|
|
426
|
-
* Called per-response on
|
|
427
|
-
*
|
|
428
|
-
*
|
|
462
|
+
* Called per-response on a render path, so it never throws — it maps over the
|
|
463
|
+
* vendored default array and swaps only the named entries, failing safe (an
|
|
464
|
+
* unknown feature name simply leaves that feature at the strict deny, and a
|
|
465
|
+
* feature the default does not list is never invented).
|
|
429
466
|
*/
|
|
430
|
-
function scopedPermissionsPolicy() {
|
|
467
|
+
function scopedPermissionsPolicy(opts) {
|
|
468
|
+
var features = (opts && Array.isArray(opts.features) && opts.features.length)
|
|
469
|
+
? opts.features
|
|
470
|
+
: ["payment"];
|
|
471
|
+
// Map requested feature names → their override token, ignoring any name not
|
|
472
|
+
// in the registry (fail-safe: an unknown key relaxes nothing).
|
|
473
|
+
var overrides = {};
|
|
474
|
+
features.forEach(function (name) {
|
|
475
|
+
if (Object.prototype.hasOwnProperty.call(_SCOPED_FEATURE_OVERRIDES, name)) {
|
|
476
|
+
overrides[name] = _SCOPED_FEATURE_OVERRIDES[name];
|
|
477
|
+
}
|
|
478
|
+
});
|
|
431
479
|
return _vendoredSecurityHeaders.DEFAULT_PERMISSIONS.map(function (entry) {
|
|
432
|
-
|
|
480
|
+
var feature = entry.split("=")[0];
|
|
481
|
+
return Object.prototype.hasOwnProperty.call(overrides, feature)
|
|
482
|
+
? overrides[feature]
|
|
483
|
+
: entry;
|
|
433
484
|
}).join(", ");
|
|
434
485
|
}
|
|
435
486
|
|
package/lib/storefront.js
CHANGED
|
@@ -13447,6 +13447,13 @@ function mount(router, deps) {
|
|
|
13447
13447
|
// Login captcha is opt-in (CAPTCHA_GATE_LOGIN). The widget + the scoped
|
|
13448
13448
|
// CSP that admits the provider host render only when login is opted in.
|
|
13449
13449
|
_setAuthCaptchaCsp(res, "login");
|
|
13450
|
+
// Passkey sign-in (passkey-login.js) calls navigator.credentials.get(),
|
|
13451
|
+
// which the app-level Permissions-Policy denies via
|
|
13452
|
+
// publickey-credentials-get=(). Re-enable it for self on THIS response
|
|
13453
|
+
// only so the assertion runs in the top-level document; every other
|
|
13454
|
+
// feature stays denied and every other route keeps the strict default.
|
|
13455
|
+
res.setHeader && res.setHeader("permissions-policy",
|
|
13456
|
+
securityMiddleware.scopedPermissionsPolicy({ features: ["publickey-credentials-get"] }));
|
|
13450
13457
|
_send(res, 200, renderAccountLogin({
|
|
13451
13458
|
shop_name: shopName,
|
|
13452
13459
|
cart_count: cartCount,
|
|
@@ -13464,6 +13471,12 @@ function mount(router, deps) {
|
|
|
13464
13471
|
// Signup captcha renders whenever a provider is active; the scoped CSP
|
|
13465
13472
|
// admits the provider host only then (no setHeader otherwise).
|
|
13466
13473
|
_setAuthCaptchaCsp(res, "signup");
|
|
13474
|
+
// Passkey enrollment (passkey-register.js) calls
|
|
13475
|
+
// navigator.credentials.create(), denied by the app-level
|
|
13476
|
+
// publickey-credentials-create=(). Re-enable it for self on THIS
|
|
13477
|
+
// response only so registration runs in the top-level document.
|
|
13478
|
+
res.setHeader && res.setHeader("permissions-policy",
|
|
13479
|
+
securityMiddleware.scopedPermissionsPolicy({ features: ["publickey-credentials-create"] }));
|
|
13467
13480
|
_send(res, 200, renderAccountRegister({
|
|
13468
13481
|
shop_name: shopName,
|
|
13469
13482
|
cart_count: cartCount,
|
|
@@ -13982,6 +13995,12 @@ function mount(router, deps) {
|
|
|
13982
13995
|
var cartCount = await _cartCountForReq(req);
|
|
13983
13996
|
var url = req.url ? new URL(req.url, "http://localhost") : null;
|
|
13984
13997
|
var okKind = url ? url.searchParams.get("ok") : null;
|
|
13998
|
+
// The "Add a passkey" island (passkey-add.js) calls
|
|
13999
|
+
// navigator.credentials.create(), denied by the app-level
|
|
14000
|
+
// publickey-credentials-create=(). Re-enable it for self on THIS
|
|
14001
|
+
// response only so enrollment runs in the top-level document.
|
|
14002
|
+
res.setHeader && res.setHeader("permissions-policy",
|
|
14003
|
+
securityMiddleware.scopedPermissionsPolicy({ features: ["publickey-credentials-create"] }));
|
|
13985
14004
|
_send(res, code || 200, renderPasskeys({
|
|
13986
14005
|
passkeys: pks,
|
|
13987
14006
|
has_oauth: hasOAuth,
|
package/package.json
CHANGED