@blamejs/blamejs-shop 0.4.7 → 0.4.8
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/order.js +25 -3
- 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.8 (2026-06-05) — **Concurrent order status changes are race-safe: a cancelled order can't be revived and a freed stock hold can't be settled twice.** An order's status change read the current state, decided the next one, then wrote it back. Under two writers racing the same order — a payment confirmation arriving as the stale-checkout sweeper cancels it, or an operator marking it paid while a provider webhook lands — both could act on the same starting state, so the later write silently overwrote the earlier one. The write now carries the state it was decided from, so it only lands while the order is still in that state; the writer that loses the race applies nothing and reports the order as the winner left it. This closes a window where the sweeper could free an order's stock hold and the payment could then still confirm — overselling the last unit — and where a settled hold could be released or debited a second time. The common card path was already shielded by a pre-cancel check against the payment provider; this protects the paths that aren't (express wallets and PayPal, a store running without the card provider configured, an operator marking an order paid by hand, and a shopper cancelling as a webhook arrives). **Fixed:** *Order status transitions are serialized by a conditional claim* — A status transition now writes with the condition that the order is still in the state the transition was computed from, so concurrent transitions on one order resolve to exactly one winner. The loser matches no row and becomes a no-op: it writes no status change, records no transition entry, and runs no inventory settlement — preventing a stock hold from being released or debited twice and preventing a terminal order from being moved back to an active state. An uncontended transition behaves exactly as before. This mirrors the existing claim used when attaching a payment intent to a pending order. · *Stock holds can no longer be double-settled across the cancel and capture paths* — When the abandoned-checkout sweeper cancels a pending order at the same moment its payment confirms, only one edge now settles the hold: the cancel frees it or the capture debits it, never both. Previously the two could interleave so a hold was released and the same order still reached paid, overselling the unit. The fix covers the paths the provider-side pre-cancel check does not — express-wallet and PayPal orders, deployments without the card provider configured, an operator marking an order paid from the console, and a shopper cancelling while a webhook is in flight.
|
|
12
|
+
|
|
11
13
|
- v0.4.7 (2026-06-05) — **Payment page: Trusted Types now permits Stripe's loader through a real allowlist, and the Amazon Pay button loads.** Two payment-page fixes. The site enforces Trusted Types, and Stripe's browser library assigns script URLs without registering a policy — the console logged a TrustedScriptURL violation on every payment page, the failure class that can break a 3-D Secure challenge mid-payment. Rather than relaxing enforcement, the page now registers the default Trusted Types policy before Stripe loads: it admits script URLs from Stripe's own origins and throws for anything else, so enforcement stays on with a genuine gate. Second, the express-wallet row offered an Amazon Pay button whose frame the payment page's policy refused to load; the page's frame allowance now includes the Stripe-operated CDN that serves wallet buttons, scoped to the payment page only. Every other page keeps the strict policy unchanged. **Fixed:** *Trusted Types policy for the Stripe loader* — The payment page registers the default Trusted Types policy before the Stripe script tag, synchronously, integrity-pinned like every other script. The policy admits script URLs whose origin is js.stripe.com or a subdomain of it and throws for any other host — lookalike domains, plain http, and non-Stripe CDNs are refused. Browsers without Trusted Types are unaffected. Pages outside the payment route keep require-trusted-types-for enforcement with no registered loader policy, exactly as before. · *Express wallet buttons can load their frames* — The payment page's frame allowance adds Stripe's wallet-button CDN (b.stripecdn.com) and Stripe's dynamic subdomains (*.js.stripe.com) alongside the existing js.stripe.com and hooks.stripe.com entries, restoring the Amazon Pay express button. The allowance is scoped to the payment page; the storefront-wide policy still permits no third-party frames at all.
|
|
12
14
|
|
|
13
15
|
- v0.4.6 (2026-06-05) — **Guest order pages require proof of purchase, and guests can save their order to an account in one tap.** Two changes to what happens after a guest checks out. First, the order confirmation page — which shows the buyer's name, address, and items — is no longer reachable by anyone who learns the order id. A guest order now admits exactly three readers: the browser that placed it (a sealed device cookie set at checkout, surviving the payment redirect), anyone opening the signed link in the order-receipt email (an order-scoped, expiring signed token that works on any device), and the signed-in account that owns it. Everything else sees a 404 indistinguishable from a missing order, and the reorder action is gated the same way. Second, the confirmation page now offers a guest one tap to save the order to an account: it emails a sign-in link to the address they checked out with — shown masked on screen, held only in a short-lived encrypted cookie, never written to the database — and redeeming the link creates the account if needed and attaches their guest orders by a hashed-email match. The response is identical whether or not an account already exists, the trigger is rate-limited, and a second click sends nothing. **Added:** *One-tap account setup after checkout* — A guest buyer's confirmation page offers to save the order to an account: one click emails a magic sign-in link to the checkout address (masked on screen, e.g. r***@e***.com), creating the account on redemption if none exists and linking the buyer's guest orders by hashed-email match at the moment they prove control of the inbox. Signed-in buyers never see the offer; buyers whose address already has an account get sign-in wording with the identical send path. The plaintext address lives only in an encrypted cookie for fifteen minutes — the database keeps only the one-way hash it already kept. **Changed:** *Guest order confirmation pages are access-gated* — A guest order's page requires the placing browser's sealed device cookie (set at checkout, capped to recent orders, 30-day refresh), the signed token in the order-receipt email's view-order link (order-scoped, roughly 90-day expiry, constant-time verified), or the signed-in owner. Unauthorized requests — including a signed-in customer who does not own the order — receive a 404 identical to a missing order. The reorder action carries the same gate; cancel, rating, and return actions were already owner-only. The payment-provider return flow and order analytics are unaffected: the device cookie is set before the redirect to the payment page, so the buyer lands back on their confirmation with access intact.
|
package/lib/asset-manifest.json
CHANGED
package/lib/order.js
CHANGED
|
@@ -398,10 +398,32 @@ function create(opts) {
|
|
|
398
398
|
throw err;
|
|
399
399
|
}
|
|
400
400
|
var ts = _now();
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
401
|
+
// Conditional claim guard — the state write only lands if the row is
|
|
402
|
+
// STILL in the snapshot state we validated the edge against. `transition`
|
|
403
|
+
// is a read-then-write (SELECT at the top, FSM replay, then this UPDATE);
|
|
404
|
+
// without the `AND status = ?` predicate a concurrent writer that moved
|
|
405
|
+
// the order between our SELECT and UPDATE would be silently overwritten.
|
|
406
|
+
// The live race: the stale-pending-order reaper firing cancel
|
|
407
|
+
// (pending → cancelled, releasing the hold) at the same instant a payment
|
|
408
|
+
// webhook fires mark_paid (pending → paid, decrementing the shelf). On D1
|
|
409
|
+
// a single UPDATE is atomic, so binding the snapshot status serializes
|
|
410
|
+
// them: exactly one edge wins, the loser matches zero rows and bails
|
|
411
|
+
// before it can double-release / double-decrement the hold or resurrect a
|
|
412
|
+
// terminal order. Mirrors setPaymentIntent's own `WHERE id = ? AND
|
|
413
|
+
// status = 'pending'` claim guard elsewhere in this file.
|
|
414
|
+
var upd = await query(
|
|
415
|
+
"UPDATE orders SET status = ?1, updated_at = ?2 WHERE id = ?3 AND status = ?4",
|
|
416
|
+
[result.to, ts, orderId, current.status],
|
|
404
417
|
);
|
|
418
|
+
if (Number(upd.rowCount || 0) === 0) {
|
|
419
|
+
// Lost the race: another writer already transitioned this order out of
|
|
420
|
+
// `current.status`. Our edge was computed from a now-stale snapshot, so
|
|
421
|
+
// we apply nothing — no order_transitions row, no inventory settlement
|
|
422
|
+
// (the winning edge settled its own holds), no webhook / loyalty /
|
|
423
|
+
// referral fan-out. Collapse to a no-op and return the order as the
|
|
424
|
+
// winner left it, so the caller observes the authoritative state.
|
|
425
|
+
return await this.get(orderId);
|
|
426
|
+
}
|
|
405
427
|
await query(
|
|
406
428
|
"INSERT INTO order_transitions (id, order_id, from_state, to_state, on_event, reason, metadata_json, occurred_at) " +
|
|
407
429
|
"VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
|
package/package.json
CHANGED