@blamejs/blamejs-shop 0.4.48 → 0.4.49
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/inventory-audits.js +5 -0
- package/lib/inventory-locations.js +66 -11
- package/lib/inventory-writeoffs.js +4 -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.49 (2026-06-14) — **A stock write-off or audit adjustment can no longer strand a paid hold by dropping on-hand below what's reserved.** An operator stock write-off and a cycle-count audit adjustment both debit on-hand stock through the location adjustment, which guarded only against the shelf going below zero — not below the quantity already reserved by outstanding held allocations. So a write-off (or an audit-applied negative variance) could drop a location's on-hand below its outstanding holds, and when one of those holds later committed at fulfillment its debit would fail, stranding a paid order that could no longer be picked. Write-offs and audit adjustments now refuse a debit that would push on-hand below the held quantity for that SKU at that location (un-pinned holds count against every location), enforced inside the write so concurrent debits can't slip past it. A hold's own commit debit is unaffected — that stock is already reserved to it. No migration to apply. **Fixed:** *Stock write-offs and audit adjustments respect reserved holds* — The location stock adjustment now takes a hold-respecting mode used by write-offs and audit variance application: a debit is refused when it would drop on-hand below the outstanding held quantity for the SKU at that location, evaluated atomically as part of the write (un-pinned holds count against every location, matching the availability rule). Previously these operator adjustments only prevented on-hand from going negative, so they could shrink the shelf below what paid/committed holds had reserved — and the hold's later commit would then fail at fulfillment. Committing a hold still debits normally, since that stock is already reserved to it.
|
|
12
|
+
|
|
11
13
|
- v0.4.48 (2026-06-14) — **Hardening: pickup scheduling integrity, the save-for-later CSRF token, a store-credit expiry race, and gift-card ledger verification.** A batch of correctness and security hardening. A click-and-collect pickup already marked ready could be silently un-readied by re-scheduling it, which the pickup state machine has no transition for; re-scheduling in place is now restricted to a pending pickup. The pickup capacity gate counted bookings and then inserted in two steps, so two concurrent checkouts could over-book a full time slot; the cap is now enforced inside the write atomically. The authenticated Save-for-later cart action (POST /cart/lines/:line_id/save) inherited the edge cart forms' CSRF exemption by a path-prefix accident and shipped without requiring a token; it now demands the double-submit CSRF token like any other authenticated mutation, while the genuinely token-less edge cart forms stay exempt. Two concurrent store-credit expiry sweeps could over-burn still-valid credit; the sweep is now atomic and idempotent. And the gift-card ledger's chain verification accepted a populated ledger whose hash columns had all been cleared — a full-ledger rewrite read as verified; an unanchored populated chain now fails verification, while a genuinely empty ledger still passes. No migration to apply. **Fixed:** *A ready pickup can't be un-readied by re-scheduling* — Re-scheduling a click-and-collect pickup in place is now allowed only while it is still scheduled. A pickup already marked ready (its goods on the hold shelf) no longer regresses to scheduled and loses its ready timestamp when re-scheduled — the pickup state machine has no ready-to-scheduled transition, so the operator completes or escalates a ready pickup instead. · *Pickup time slots can't be over-booked under concurrency* — The pickup capacity limit is now enforced inside the booking write as a single conditional insert gated on the live count for the time slot, so two checkouts booking the same nearly-full slot at once can't both slip past the limit. Previously the count and the insert were separate steps, leaving a window where concurrent bookings over-filled a slot. · *Concurrent store-credit expiry sweeps can't over-burn valid credit* — The store-credit expiry sweep now burns expired credit atomically: the amount to expire is computed inside the write and conditioned on the credit still being unexpired, so a second sweep running at the same time finds nothing left to burn and can't dip into still-valid balance. Previously the sweep read the expirable total and then wrote in separate steps, so two concurrent sweeps could double-burn. **Security:** *The save-for-later cart action now requires its CSRF token* — POST /cart/lines/:line_id/save is a login-required cart mutation rendered only on the session-bound cart page, but it sat under the /cart/lines path prefix that exempts the cookie-less, token-less edge cart forms from CSRF — so it inherited that exemption and accepted state changes without a double-submit token. The exemption now carves this authenticated path back into CSRF protection from a single source shared by the request guard and the form renderer (so the set the guard enforces and the set the renderer tokenizes can't drift), while the legitimate edge cart forms remain exempt. · *A hash-cleared gift-card ledger no longer verifies as valid* — The gift-card ledger's chain verification reported a populated ledger whose hash-chain columns had all been nulled as verified — so an attacker who rewrote balances and cleared the chain hashes could pass verification undetected. A populated ledger with no chain anchor now fails verification; a genuinely empty ledger (no rows) still passes.
|
|
12
14
|
|
|
13
15
|
- v0.4.47 (2026-06-14) — **Signing out wipes the browser's client-side state via Clear-Site-Data.** Both the storefront account sign-out and the admin sign-out now emit an RFC 9527 Clear-Site-Data response header, instructing the browser to clear its cookies, storage, cache, and execution contexts for the origin. Previously sign-out tore down the server-side session and expired the auth cookie but left the rest of the browser's client-side state in place — on a shared or public browser, the next person could find a cached authenticated page or leftover local/session storage. This is defense-in-depth on top of the existing server-side session teardown. No migration to apply. **Security:** *Sign-out clears client-side browser state* — The /account/logout and /admin/logout routes now send Clear-Site-Data with the cookies, storage, cache, and execution-context directives, so a browser drops the origin's client-side state on sign-out rather than only expiring the auth cookie. This closes the window on a shared or public machine where a cached authenticated page or leftover storage could outlive the session.
|
package/lib/asset-manifest.json
CHANGED
package/lib/inventory-audits.js
CHANGED
|
@@ -624,6 +624,11 @@ function create(opts) {
|
|
|
624
624
|
location_code: line.location_code,
|
|
625
625
|
delta: variance,
|
|
626
626
|
reason: "inventory-audit:" + header.slug,
|
|
627
|
+
// A negative variance shrinks the shelf — guard it from
|
|
628
|
+
// dropping below the outstanding paid holds for the SKU
|
|
629
|
+
// so a later commitHold can't be stranded. Positive
|
|
630
|
+
// variances (credits) ignore the flag.
|
|
631
|
+
respect_holds: true,
|
|
627
632
|
});
|
|
628
633
|
adjustmentsWritten += 1;
|
|
629
634
|
}
|
|
@@ -566,6 +566,17 @@ function create(opts) {
|
|
|
566
566
|
// insert never collide. Both paths re-read the post-write quantity
|
|
567
567
|
// for the audit row + return value rather than trusting a stale
|
|
568
568
|
// pre-read.
|
|
569
|
+
//
|
|
570
|
+
// `respect_holds` (debit only): when set, the conditional UPDATE
|
|
571
|
+
// adds a second floor — the post-debit shelf must still cover the
|
|
572
|
+
// outstanding held sum for the SKU at this location (held rows
|
|
573
|
+
// pinned here PLUS un-pinned holds, which can be satisfied from any
|
|
574
|
+
// location). Operator shrinkage paths (write-offs, audit
|
|
575
|
+
// adjustments) pass this so an over-debit can't drop the shelf
|
|
576
|
+
// below already-paid holds and strand a later commitHold. The
|
|
577
|
+
// committed-stock debit from commitHold itself never sets it: that
|
|
578
|
+
// path flips the row held -> committed BEFORE debiting, so its own
|
|
579
|
+
// quantity is already excluded from the held sum.
|
|
569
580
|
adjustStock: async function (input) {
|
|
570
581
|
if (!input || typeof input !== "object") {
|
|
571
582
|
throw new TypeError("inventory-locations.adjustStock: input object required");
|
|
@@ -576,6 +587,7 @@ function create(opts) {
|
|
|
576
587
|
if (input.delta === 0) {
|
|
577
588
|
throw new TypeError("inventory-locations.adjustStock: delta must be non-zero");
|
|
578
589
|
}
|
|
590
|
+
var respectHolds = input.respect_holds === true;
|
|
579
591
|
var loc = await _getLocationRow(input.location_code);
|
|
580
592
|
if (!loc) {
|
|
581
593
|
throw new TypeError("inventory-locations.adjustStock: location_code " +
|
|
@@ -600,17 +612,60 @@ function create(opts) {
|
|
|
600
612
|
// row at this location can't be debited (no negative
|
|
601
613
|
// first-touch); zero rows there is the insufficient case too.
|
|
602
614
|
var need = -input.delta;
|
|
603
|
-
var r
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
"
|
|
613
|
-
"
|
|
615
|
+
var r;
|
|
616
|
+
if (respectHolds) {
|
|
617
|
+
// Second floor: the post-debit quantity must also cover the
|
|
618
|
+
// outstanding held sum for this SKU at this location. The
|
|
619
|
+
// subquery only references inventory_holds on this opt-in
|
|
620
|
+
// path, so callers that never set respect_holds don't depend
|
|
621
|
+
// on the holds table existing. Un-pinned holds (NULL
|
|
622
|
+
// location_code) count against every location.
|
|
623
|
+
r = await query(
|
|
624
|
+
"UPDATE inventory_stock SET quantity = quantity + ?1, updated_at = ?2 " +
|
|
625
|
+
"WHERE sku = ?3 AND location_code = ?4 AND quantity >= ?5 " +
|
|
626
|
+
"AND quantity + ?1 >= COALESCE((SELECT SUM(quantity) FROM inventory_holds " +
|
|
627
|
+
"WHERE sku = ?3 AND status = 'held' " +
|
|
628
|
+
"AND (location_code = ?4 OR location_code IS NULL)), 0)",
|
|
629
|
+
[input.delta, ts, input.sku, input.location_code, need],
|
|
630
|
+
);
|
|
631
|
+
if (r.rowCount === 0) {
|
|
632
|
+
var curH = await _getStockRow(input.sku, input.location_code);
|
|
633
|
+
var haveH = curH ? curH.quantity : 0;
|
|
634
|
+
var heldRes = await query(
|
|
635
|
+
"SELECT COALESCE(SUM(quantity), 0) AS held FROM inventory_holds " +
|
|
636
|
+
"WHERE sku = ?1 AND status = 'held' " +
|
|
637
|
+
"AND (location_code = ?2 OR location_code IS NULL)",
|
|
638
|
+
[input.sku, input.location_code],
|
|
639
|
+
);
|
|
640
|
+
var heldQty = heldRes.rows[0] ? Number(heldRes.rows[0].held) : 0;
|
|
641
|
+
// Distinguish the two refusal reasons so the operator knows
|
|
642
|
+
// whether it's a plain oversell or a hold-floor breach. The
|
|
643
|
+
// hold-floor message only applies when the debit clears zero
|
|
644
|
+
// but breaches the held sum; otherwise it's the same
|
|
645
|
+
// below-zero refusal the non-respect_holds path raises.
|
|
646
|
+
if (haveH + input.delta >= 0 && haveH + input.delta < heldQty) {
|
|
647
|
+
throw new TypeError("inventory-locations.adjustStock: delta " + input.delta +
|
|
648
|
+
" would strand " + heldQty + " held units (current=" + haveH +
|
|
649
|
+
", held=" + heldQty + ", sku=" + input.sku +
|
|
650
|
+
", location=" + input.location_code + ")");
|
|
651
|
+
}
|
|
652
|
+
throw new TypeError("inventory-locations.adjustStock: delta " + input.delta +
|
|
653
|
+
" would drive stock below zero (current=" + haveH + ", sku=" + input.sku +
|
|
654
|
+
", location=" + input.location_code + ")");
|
|
655
|
+
}
|
|
656
|
+
} else {
|
|
657
|
+
r = await query(
|
|
658
|
+
"UPDATE inventory_stock SET quantity = quantity + ?1, updated_at = ?2 " +
|
|
659
|
+
"WHERE sku = ?3 AND location_code = ?4 AND quantity >= ?5",
|
|
660
|
+
[input.delta, ts, input.sku, input.location_code, need],
|
|
661
|
+
);
|
|
662
|
+
if (r.rowCount === 0) {
|
|
663
|
+
var cur = await _getStockRow(input.sku, input.location_code);
|
|
664
|
+
var have = cur ? cur.quantity : 0;
|
|
665
|
+
throw new TypeError("inventory-locations.adjustStock: delta " + input.delta +
|
|
666
|
+
" would drive stock below zero (current=" + have + ", sku=" + input.sku +
|
|
667
|
+
", location=" + input.location_code + ")");
|
|
668
|
+
}
|
|
614
669
|
}
|
|
615
670
|
}
|
|
616
671
|
var after = await _getStockRow(input.sku, input.location_code);
|
|
@@ -295,6 +295,10 @@ function create(opts) {
|
|
|
295
295
|
location_code: locCode,
|
|
296
296
|
delta: -quantity,
|
|
297
297
|
reason: "writeoff:" + reason + ":" + id,
|
|
298
|
+
// Operator shrinkage must not over-debit the shelf below the
|
|
299
|
+
// outstanding paid holds for this SKU — a later commitHold
|
|
300
|
+
// would otherwise fail with the hold stranded.
|
|
301
|
+
respect_holds: true,
|
|
298
302
|
});
|
|
299
303
|
}
|
|
300
304
|
|
package/package.json
CHANGED