@blamejs/blamejs-shop 0.4.71 → 0.4.73

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.4.x
10
10
 
11
+ - v0.4.73 (2026-06-21) — **Subject-access exports now carry every operator note, question, quote and rating — not the first page.** A data-subject-access (GDPR Art. 15) export read only the first 100 rows of four sections — operator CRM notes, product questions, RFQ quotes, and post-fulfilment ratings — yet the bundle's completeness manifest still reported each of those sections as fully exported. A customer with a long history (most plausibly a long-tenured account carrying many operator notes) received a bundle that silently dropped the remainder while presenting as complete. The export now drains the operator-notes and product-question sections to the last row by following each reader's own cursor, and pulls the quotes and ratings sections up to the primitive's maximum of 500 rows — beyond any realistic per-customer count — so a subject-access bundle carries the customer's complete record. Both the assembled bundle and the streamed download share the same readers, so both are now complete. **Fixed:** *Export sections drain past the first 100 rows* — The operator-notes and product-question export sections now follow their reader's cursor to exhaustion, so every row reaches the bundle; the quotes and ratings sections read up to the 500-row primitive maximum, which a single customer's RFQ or rating history never exceeds in practice. A long-tenured customer's subject-access export no longer omits rows past the first page while reporting the section complete.
12
+
13
+ - v0.4.72 (2026-06-21) — **Cycle counting no longer strands a finalized count when a shelf adjustment fails midway.** When a cycle count was finalized with shelf adjustments, it advanced the count to finalized first and then applied each per-location adjustment in an unguarded loop. If one adjustment failed partway — most plausibly a negative (shrinkage) variance that would drive a location below the units already held for paid orders — the lines before it stayed applied, the lines after it never applied, and the count was left stuck finalized: a retry was refused and the header's aggregate totals were never stamped, so a tool whose whole job is to close shelf-vs-record drift instead baked in an unrecoverable partial drift. finalizeCount now mirrors the inventory-audit finalize it diverged from: it computes every variance first, claims the finalize transition AND stamps the aggregate totals in one statement, then applies each shelf adjustment reconciling against what already landed under the count's own reason — so a retry lands only the remaining delta and never doubles — and passes the hold-aware guard so a shrinkage debit can't drop a shelf below its outstanding holds. On any failure the count re-opens to in_progress, so the operator clears the blocker and re-runs to completion. **Fixed:** *A failed shelf adjustment re-opens the count instead of stranding it finalized* — finalizeCount now computes and persists every line's variance before claiming the finalize transition, stamps the aggregate variance totals in the same statement as the status flip (so a finalized count is never left with empty totals), and applies the per-location adjustments in a guarded pass that reconciles each against the deltas already recorded under the count's reason. If an adjustment throws, the count re-opens to in_progress and the error surfaces; a re-run applies only the remaining delta, so the lines that already moved are never double-applied. · *Shrinkage adjustments respect outstanding holds* — A negative cycle-count variance now debits the shelf with the hold-aware guard, matching the inventory-audit and write-off paths, so a count that records less stock than the record believed can no longer drop a location below the units already held for paid orders and strand a later fulfilment.
14
+
11
15
  - v0.4.71 (2026-06-21) — **SMS dispatch and shipping-insurance adjudication now claim their state transition atomically.** Two exactly-once state transitions advanced their row with a primary-key UPDATE that was not gated on the expected prior status, so two concurrent callers could both win the transition. The scheduled SMS dispatch tick flipped a queued message to sent without an `AND status = 'queued'` guard, so two overlapping ticks could both hand the same message to the provider send hook — a duplicate text and a duplicate carrier charge. The shipping-insurance claim adjudication flipped a filed claim to approved or denied the same way, so a double-submitted approval (or an approval racing a denial) could record two different payout amounts as THE payout, or let a denial clobber an approval. Both transitions now gate the UPDATE on the prior status and act only on the row the UPDATE actually claimed (rowCount === 1); the loser is skipped or refused, matching the atomic-claim shape already used by the push-notification dispatch tick and the affiliate-commission payout. **Fixed:** *SMS dispatch tick advances each message exactly once under concurrent ticks* — The queued→sent (and the no-provider queued→failed) transition in the SMS dispatch tick now gates its UPDATE on `AND status = 'queued'` and hands a message to the operator's send hook only when the UPDATE actually claimed the row. Two overlapping ticks can no longer both dispatch the same message, so a customer is never double-texted and the carrier is never billed twice for one message. · *Shipping-insurance claim adjudication records exactly one outcome under concurrent callers* — markClaimApproved and markClaimDenied now gate their transition on `AND status = 'filed'` and refuse the caller whose UPDATE claimed no row. A double-submitted approval can no longer record two different payout amounts as the approved payout, and an approval can no longer clobber a denial (or vice-versa) — exactly one adjudication outcome sticks.
12
16
 
13
17
  - v0.4.70 (2026-06-21) — **Bundled framework refreshed to blamejs 0.15.14, with the proxy-trust hardening wired through.** Refreshes the vendored blamejs to 0.15.14 — a broad security-hardening release that peer-gates every request-IP and protocol trust decision against the connecting socket, fails closed on a set of authentication and signature verifiers, bounds pre-authentication parsers against algorithmic-complexity denial of service, makes the framework's own file writes atomic and symlink-refusing, and adds a JSON serializer safe to embed in an inline script. The one behavior change that touches a deployment behind a TLS-terminating edge is how the client protocol is trusted: a bare trust-the-forwarded-header flag is no longer accepted for the HSTS and CSRF decisions, because a caller able to reach the origin socket could forge it. The storefront resolves the forwarded protocol through one owned model that the HSTS header, the CSRF origin check, and both session-cookie Secure decisions now share — so HSTS on container responses and authenticated forms behind the Cloudflare edge keep working with no operator action. **Changed:** *Bundled blamejs refreshed to 0.15.14* — Picks up the upstream security-hardening release: client-IP and protocol trust resolved by a peer-gated walk anchored at the connecting socket; SAML, electronic-signature, SD-JWT, DPoP, CIBA, mdoc, and COSE verifiers that could accept a malformed or unsigned credential now refuse it; pre-authentication parsers (CBOR, JSONPath, tar, gzip, YAML, ASN.1 DER) bounded against quadratic-time and frame-desync denial of service; allowlist and posture lookups keyed by untrusted input can no longer reach the prototype chain; the framework's predictable-temp and symlink-following writes go through atomic, symlink-refusing primitives; and several lost-update races (login lockout, bot-challenge, inline webhook delivery) are serialized. · *HSTS and CSRF protocol trust resolved through one owned model behind the edge* — The framework retired the bare forwarded-header trust flag for the decisions that depend on the client protocol (whether to emit HSTS, whether to set the Secure cookie attribute, and the CSRF origin pre-check), since a caller that can reach the origin socket could forge the header. The storefront now resolves the protocol through a single function shared by the security-headers middleware, the CSRF gate, and both the storefront and admin session-cookie paths. Behind the Cloudflare edge the resolver honours the worker-forwarded scheme; a direct connection with no forwarded header resolves to the socket scheme. HSTS on container-served responses and authenticated forms behind the CDN are unchanged; no configuration is required.
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.4.71",
2
+ "version": "0.4.73",
3
3
  "assets": {
4
4
  "css/admin.css": {
5
5
  "integrity": "sha384-imfe0otYErcB8rr2h6KLSGTtStirysptpXETSPY4zLv3bZoIT75Lo1dOvkOav+xL",
@@ -576,29 +576,13 @@ function create(opts) {
576
576
  throw new TypeError("cycle-counting.finalizeCount: count is " + header.status +
577
577
  ", only scheduled or in_progress counts can be finalized");
578
578
  }
579
- // Claim the (scheduled|in_progress) -> finalized transition atomically
580
- // BEFORE computing variances or applying adjustments. Two concurrent
581
- // finalizes both pass the read above, but only one UPDATE matches the
582
- // expected status; the loser refuses, so the per-shelf adjustments are
583
- // applied exactly once. (Without this guard both calls would write the
584
- // variance adjustments through inventoryLocations.adjustStock, double-
585
- // applying.) The final aggregate counts are stamped below once the loop
586
- // has run.
587
- var ts = _now();
588
- var claim = await query(
589
- "UPDATE cycle_counts SET status = 'finalized', finalized_at = ?1 " +
590
- "WHERE slug = ?2 AND status IN ('scheduled', 'in_progress')",
591
- [ts, input.slug],
592
- );
593
- if (Number(claim.rowCount || 0) !== 1) {
594
- throw new TypeError("cycle-counting.finalizeCount: count " +
595
- JSON.stringify(input.slug) + " is no longer scheduled or in_progress " +
596
- "(finalized by a concurrent call)");
597
- }
579
+ // PASS 1 compute each line's variance, persist it (idempotent),
580
+ // and collect the shelf adjustments to apply. No shelf is touched
581
+ // yet: the atomic claim below decides who, if anyone, applies them.
598
582
  var lines = await _getLines(input.slug);
599
583
  var varianceCount = 0;
600
584
  var varianceValueMinor = 0;
601
- var adjustmentsWritten = 0;
585
+ var adjustable = [];
602
586
  for (var i = 0; i < lines.length; i += 1) {
603
587
  var line = lines[i];
604
588
  // NULL actual_quantity = operator didn't count this line.
@@ -615,33 +599,94 @@ function create(opts) {
615
599
  varianceCount += 1;
616
600
  var unitValue = await _unitValueMinor(line.sku);
617
601
  varianceValueMinor += Math.abs(variance) * unitValue;
618
- if (applyAdjustments) {
619
- // Adjustments only land on lines that carry a location_code.
620
- // A global count (no location_code on any line) computes
621
- // variance for the operator's reconciliation report but
622
- // does NOT write to inventory_locations — the operator's
623
- // catalog-side correction (catalog.inventory.restock /
624
- // release / setStock) is the right verb for the single-
625
- // bucket inventory.
626
- if (line.location_code) {
627
- await inventoryLocations.adjustStock({
628
- sku: line.sku,
629
- location_code: line.location_code,
630
- delta: variance,
631
- reason: "cycle-count:" + input.slug,
632
- });
633
- adjustmentsWritten += 1;
634
- }
602
+ // Adjustments only land on lines that carry a location_code.
603
+ // A global count (no location_code on any line) computes
604
+ // variance for the operator's reconciliation report but does
605
+ // NOT write to inventory_locations — the operator's catalog-
606
+ // side correction (catalog.inventory.restock / release /
607
+ // setStock) is the right verb for the single-bucket inventory.
608
+ if (applyAdjustments && line.location_code) {
609
+ adjustable.push({ sku: line.sku, location_code: line.location_code, variance: variance });
635
610
  }
636
611
  }
637
612
  }
638
- // Status + finalized_at were already claimed above; stamp the computed
639
- // aggregate variance totals onto the (now-finalized) header.
640
- await query(
641
- "UPDATE cycle_counts SET variance_count = ?1, variance_value_minor = ?2 " +
642
- "WHERE slug = ?3",
643
- [varianceCount, varianceValueMinor, input.slug],
613
+
614
+ var ts = _now();
615
+ // ATOMIC CLAIM — advance the count out of scheduled/in_progress AND
616
+ // stamp the computed aggregate totals in a SINGLE statement BEFORE any
617
+ // shelf moves. On the D1 bridge there are no interactive transactions,
618
+ // so this UPDATE is the serialization point: two concurrent finalizes
619
+ // both reach here, but only one matches a row. The loser changes zero
620
+ // rows, applies NOTHING, and returns the deterministic totals — so a
621
+ // shelf can never be double-adjusted by overlapping finalizes. Stamping
622
+ // the aggregates in the SAME statement means a finalized count is never
623
+ // left with a NULL variance_count if PASS 2 below aborts midway.
624
+ var claim = await query(
625
+ "UPDATE cycle_counts SET status = 'finalized', variance_count = ?1, " +
626
+ "variance_value_minor = ?2, finalized_at = ?3 " +
627
+ "WHERE slug = ?4 AND status IN ('scheduled', 'in_progress')",
628
+ [varianceCount, varianceValueMinor, ts, input.slug],
644
629
  );
630
+ if (Number(claim.rowCount || 0) !== 1) {
631
+ // Lost the claim — a concurrent (or prior) finalize already advanced
632
+ // this count and owns the adjustment writes. Touch no shelves; return
633
+ // the totals computed from the now-frozen lines.
634
+ return {
635
+ variance_count: varianceCount,
636
+ variance_value_minor: varianceValueMinor,
637
+ adjustments_written: adjustable.length,
638
+ };
639
+ }
640
+
641
+ // PASS 2 (winner only) — apply each shelf adjustment, reconciling
642
+ // against what already landed under this count's reason. Applying only
643
+ // `variance - applied` makes a retry land exactly the remaining delta
644
+ // and never a double, and self-corrects if a recount changed the
645
+ // variance before the retry. The common abort is a respect_holds floor
646
+ // refusal on a negative variance; the catch re-opens the count so the
647
+ // operator can clear the blocker and retry — and because PASS 2 is
648
+ // reconciling, the retry never double-applies the lines that already
649
+ // landed.
650
+ var adjustmentsWritten = 0;
651
+ var adjReason = "cycle-count:" + input.slug;
652
+ try {
653
+ for (var j = 0; j < adjustable.length; j += 1) {
654
+ var adj = adjustable[j];
655
+ var appliedRow = await query(
656
+ "SELECT COALESCE(SUM(delta), 0) AS applied FROM inventory_adjustments " +
657
+ "WHERE sku = ?1 AND location_code = ?2 AND reason = ?3",
658
+ [adj.sku, adj.location_code, adjReason],
659
+ );
660
+ var alreadyApplied = Number((appliedRow.rows[0] || {}).applied || 0);
661
+ var needed = adj.variance - alreadyApplied;
662
+ if (needed !== 0) {
663
+ await inventoryLocations.adjustStock({
664
+ sku: adj.sku,
665
+ location_code: adj.location_code,
666
+ delta: needed,
667
+ reason: adjReason,
668
+ // A negative variance shrinks the shelf — guard it from
669
+ // dropping below the outstanding paid holds for the SKU so a
670
+ // later commitHold can't be stranded. Positive variances
671
+ // (credits) ignore the flag. Mirrors inventory-audits and
672
+ // inventory-writeoffs, the other operator-shrinkage writers.
673
+ respect_holds: true,
674
+ });
675
+ }
676
+ adjustmentsWritten += 1;
677
+ }
678
+ } catch (applyErr) {
679
+ // Re-open the count so the operator can clear the blocker and retry.
680
+ // The reconciling apply above guarantees the retry lands only the
681
+ // remaining delta, so nothing double-counts.
682
+ await query(
683
+ "UPDATE cycle_counts SET status = 'in_progress', finalized_at = NULL " +
684
+ "WHERE slug = ?1 AND status = 'finalized'",
685
+ [input.slug],
686
+ );
687
+ throw applyErr;
688
+ }
689
+
645
690
  return {
646
691
  variance_count: varianceCount,
647
692
  variance_value_minor: varianceValueMinor,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/blamejs-shop",
3
- "version": "0.4.71",
3
+ "version": "0.4.73",
4
4
  "description": "Open-source framework built on blamejs. Vendored stack, zero npm runtime deps, PQC-first crypto, security-on by default.",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {