@blamejs/blamejs-shop 0.4.84 → 0.4.86
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/asset-manifest.json +1 -1
- package/lib/inventory-locations.js +86 -0
- package/lib/order.js +4 -2
- package/lib/security-middleware.js +1 -0
- package/lib/stock-transfers.js +21 -14
- package/lib/vendor/MANIFEST.json +71 -39
- package/lib/vendor/blamejs/CHANGELOG.md +2 -0
- package/lib/vendor/blamejs/api-snapshot.json +11 -3
- package/lib/vendor/blamejs/lib/atomic-file.js +34 -0
- package/lib/vendor/blamejs/lib/auth/fido-mds3.js +10 -0
- package/lib/vendor/blamejs/lib/auth/password.js +1 -0
- package/lib/vendor/blamejs/lib/auth/saml.js +11 -3
- package/lib/vendor/blamejs/lib/daemon.js +4 -1
- package/lib/vendor/blamejs/lib/external-db.js +131 -0
- package/lib/vendor/blamejs/lib/graphql-federation.js +25 -15
- package/lib/vendor/blamejs/lib/log-stream-cloudwatch.js +1 -0
- package/lib/vendor/blamejs/lib/log-stream-local.js +14 -1
- package/lib/vendor/blamejs/lib/log-stream-otlp.js +1 -0
- package/lib/vendor/blamejs/lib/log-stream-webhook.js +1 -0
- package/lib/vendor/blamejs/lib/mail-auth.js +69 -14
- package/lib/vendor/blamejs/lib/mail-bimi.js +6 -0
- package/lib/vendor/blamejs/lib/mail-crypto-smime.js +10 -0
- package/lib/vendor/blamejs/lib/mail-dkim.js +68 -15
- package/lib/vendor/blamejs/lib/mail.js +39 -0
- package/lib/vendor/blamejs/lib/middleware/api-encrypt.js +6 -2
- package/lib/vendor/blamejs/lib/network-dns-resolver.js +61 -11
- package/lib/vendor/blamejs/lib/network-dns.js +47 -2
- package/lib/vendor/blamejs/lib/network-nts.js +16 -0
- package/lib/vendor/blamejs/lib/network-proxy.js +55 -2
- package/lib/vendor/blamejs/lib/object-store/azure-blob-bucket-ops.js +1 -0
- package/lib/vendor/blamejs/lib/object-store/gcs-bucket-ops.js +1 -0
- package/lib/vendor/blamejs/lib/object-store/http-request.js +4 -0
- package/lib/vendor/blamejs/lib/outbox.js +29 -0
- package/lib/vendor/blamejs/lib/queue-sqs.js +1 -0
- package/lib/vendor/blamejs/lib/request-helpers.js +25 -6
- package/lib/vendor/blamejs/lib/session-device-binding.js +46 -24
- package/lib/vendor/blamejs/lib/session.js +85 -28
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.15.16.json +94 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/api-encrypt.test.js +51 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-open-append-nofollow.test.js +87 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +48 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/external-db-non-atomic-backend.test.js +200 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/fido-mds3-cert-bad-validity.test.js +159 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/graphql-federation.test.js +49 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth-dmarc-policy-failclosed.test.js +139 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-bimi-cert-validity-unparseable.test.js +137 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto-smime-bad-validity.test.js +134 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-dkim-numericdate-failclosed.test.js +155 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-proxy-framing-bounds.test.js +263 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network-dns-lookup-timeout-default.test.js +116 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network-dns-resolver-timeout.test.js +126 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network-nts-handshake-byte-cap.test.js +127 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/saml-subjectconfirmation-notbefore.test.js +238 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/session-destroy-all-store-backed.test.js +128 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/session-device-binding-ipv6-canonical-and-no-store.test.js +202 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/session-device-binding.test.js +18 -9
- package/lib/vendor/blamejs/test/layer-0-primitives/webhook-verify-nonce-atomic.test.js +169 -0
- package/lib/webhooks.js +38 -9
- 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.4.x
|
|
10
10
|
|
|
11
|
+
- v0.4.86 (2026-06-22) — **Failed outbound webhook deliveries are now retried automatically on the backoff schedule, not only by a manual click.** Restores the automatic retry guarantee for outbound webhooks. When an order-lifecycle or low-stock delivery to a subscriber endpoint fails, the delivery already records its next retry time and walks a backoff schedule toward the dead-letter queue — but nothing was ever driving those retries, so a delivery only re-attempted when an operator hand-clicked retry in the admin console. A briefly-unreachable receiver (a deploy, a load-balancer reload) therefore lost every event in the gap. A scheduled tick now re-attempts every due delivery once a minute, so a failed delivery is retried automatically — at one minute, five minutes, thirty minutes, four hours, then a day — until it succeeds or lands in the dead-letter queue, with no manual intervention. The manual retry remains available as an immediate override. No configuration changes; upgrade to pick it up. **Fixed:** *Outbound webhook retries now run automatically* — A scheduled retry pass re-attempts every delivery whose backoff has come due, advancing it through the retry schedule to success or to the dead-letter queue without an operator click. Each delivery is claimed atomically before it is attempted, so two overlapping passes can never re-send the same delivery twice, and a receiver that recovers within the backoff window now receives the events it missed. An operator can still force an immediate retry from the admin console. **Security:** *Bundled framework refreshed to blamejs 0.15.16* — Picks up a broad fail-closed and resource-exhaustion hardening pass that rides inside the composed primitives: a present-but-unparseable timestamp on a credential or certificate verifier (DKIM, ARC, SAML NotBefore, FIDO/BIMI/S-MIME validity windows) now refuses the credential instead of silently skipping the check; DMARC refuses a record with no usable policy rather than defaulting a failing message to delivery; outbound clients enforce an overall wall-clock timeout (not only an idle timer) so a byte-trickling peer can no longer hold a request open indefinitely; DNS lookups gain a default deadline; and append-only log sinks refuse a symlinked path. No configuration changes.
|
|
12
|
+
|
|
13
|
+
- v0.4.85 (2026-06-22) — **Stock-transfer reconciliation credits the destination exactly once, even when retried after a mid-operation failure.** Fixes an inventory-integrity bug in stock-transfer reconciliation. Reconciling a received transfer credits each line's received quantity to the destination shelf and then records the line's discrepancy. If that discrepancy write failed partway through the transfer, reconciliation rolled its own state back so the operator could retry — but the retry credited the already-credited lines a second time, inflating the destination shelf with stock that was never received and could then be oversold. The destination credit is now keyed and idempotent: a retry recognizes a line it has already credited and skips it, so the shelf is credited exactly once no matter how many times reconciliation runs. The release adds one additive, backward-compatible database migration; apply pending migrations on upgrade. **Fixed:** *Stock-transfer reconciliation no longer double-credits the destination on retry* — Reconciling a received transfer credited each line to the destination shelf before stamping the line's discrepancy. A failure between those two steps rolled the transfer back to 'received' for a clean retry, but the retry re-credited every line whose discrepancy had not yet been stamped — including lines already credited on the failed attempt — minting phantom inventory at the destination. The per-line destination credit is now idempotent and keyed to the transfer and line, so re-running reconciliation after any partial failure credits each line exactly once; the discrepancy stamp is no longer load-bearing for correctness. **Migration:** *inventory_adjustments gains a nullable idempotency key* — Adds a nullable idempotency_key column to inventory_adjustments and a unique index over it, so a keyed stock credit records that it has already been applied and a retry that re-presents the key credits nothing. The column is NULL for every existing row and for plain stock adjustments, which already apply atomically; only keyed credits set it. The change is additive and backward-compatible — apply pending D1 migrations on upgrade.
|
|
14
|
+
|
|
11
15
|
- v0.4.84 (2026-06-22) — **Payment idempotency and analytics cache keys now use the framework's canonical-JSON encoder.** Internal consistency change with no configuration to apply. The Stripe idempotency replay-protection hash and the subscription-analytics cache key each carried their own copy of a sorted-key JSON serializer; both now compose the framework's canonical-JSON encoder, so a single serializer — with one set of key-ordering, string-escaping, and number-format rules — backs every replay and cache hash. Idempotency replays issued across the upgrade keep working: a request hash written by the prior release is still recognized for the row's lifetime, so a payment retried across the upgrade boundary replays its cached response rather than being refused as a mismatch. Upgrade to pick it up. **Changed:** *Replay and cache hashes share one canonical-JSON encoder* — Payment idempotency-key replay protection and the subscription-analytics cache key now build their hashes with the framework's canonical-JSON encoder instead of a locally maintained serializer. The encoder emits object members in one stable key order, so two requests — or two analytics queries — that differ only in member order resolve to the same hash, and the order is now stable even for integer-like keys, which the previous serializer reordered. Idempotency rows written by the prior release are still matched for their lifetime, so a payment retried across the upgrade boundary replays its cached response and is never refused as a false mismatch; the compatibility path expires with those rows.
|
|
12
16
|
|
|
13
17
|
- v0.4.83 (2026-06-22) — **Downloading a subject-access export now requires the customer-write grant, not just read access.** Closes an access-control gap on the data-subject-request surface. Downloading a fulfilled subject-access export streams the customer's full personal-data bundle, but the download route was gated as a plain read, so a read-only operator (a viewer with no customers-write grant) could pull the entire PII bundle. The download now gates on the customers-write capability — the same grant fulfilling, dispatching, and erasing a request already require — so a read-only viewer is refused on the verb and the denial is recorded in the operator audit log, on both the API and browser paths. No configuration changes; upgrade to pick it up. **Security:** *Subject-access export download requires customers-write* — The DSR export-download route streams a customer's full PII bundle, so it is a customers-write-class capability like fulfilling, dispatching, or erasing a request — not a read. It now gates on customers-write on both the bearer-API and cookie/browser paths, so a read-only viewer can no longer download the bundle; the refused attempt is chained through the operator audit log like the other DSR write denials. Viewing the DSR queue (a read) is unchanged.
|
package/lib/asset-manifest.json
CHANGED
|
@@ -79,6 +79,7 @@ var CODE_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
|
79
79
|
var SKU_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
80
80
|
var NAME_RE = /^[\S\s]{1,128}$/;
|
|
81
81
|
var MAX_REASON = 256;
|
|
82
|
+
var MAX_IDEMPOTENCY_KEY = 256;
|
|
82
83
|
var MAX_LINES = 1000;
|
|
83
84
|
var TYPES = Object.freeze(["warehouse", "retail", "dropship"]);
|
|
84
85
|
var STRATEGIES = Object.freeze([
|
|
@@ -674,6 +675,91 @@ function create(opts) {
|
|
|
674
675
|
return { sku: input.sku, location_code: input.location_code, quantity: nextQty, delta: input.delta };
|
|
675
676
|
},
|
|
676
677
|
|
|
678
|
+
// Idempotent keyed credit. For a positive credit that is one step of a
|
|
679
|
+
// larger non-transactional flow (e.g. stock-transfer reconciliation),
|
|
680
|
+
// adjustStock alone is not safe to retry: a crash between the stock upsert
|
|
681
|
+
// and the next step lets a re-run credit the shelf a SECOND time, minting
|
|
682
|
+
// phantom inventory. creditOnce makes a retry safe by making the audit
|
|
683
|
+
// row's idempotency_key the claim: the first call with a given key inserts
|
|
684
|
+
// the row (winning the claim) and applies the credit; any later call with
|
|
685
|
+
// the same key finds it present and is a no-op. A credit never breaches the
|
|
686
|
+
// zero/holds floor, so this path is positive-delta only.
|
|
687
|
+
//
|
|
688
|
+
// Why claim-first rather than a single atomic write: the truly atomic form
|
|
689
|
+
// would be an AFTER INSERT trigger crediting the shelf in the same INSERT,
|
|
690
|
+
// but `wrangler d1 migrations apply` ships each statement to the D1 HTTP
|
|
691
|
+
// API without its trailing `;`, and D1 rejects a trigger body that way
|
|
692
|
+
// ("incomplete input"), so a trigger cannot be applied through the project's
|
|
693
|
+
// migration tool. Claim-first is therefore the design. D1 has no
|
|
694
|
+
// interactive transactions, so a single-statement gap is unavoidable; the
|
|
695
|
+
// claim (audit row) is written BEFORE the stock upsert and the compensating
|
|
696
|
+
// delete below covers the upsert throwing. The only residual is the host
|
|
697
|
+
// process dying (a container crash / deploy roll — this runs in the
|
|
698
|
+
// persistent container, not a per-request isolate) between the two
|
|
699
|
+
// statements: the key is durable but the stock was not credited, so a retry
|
|
700
|
+
// no-ops and the shelf is UNDER-credited until an operator recount, never
|
|
701
|
+
// double-credited. Under-credit is the safe direction (a recoverable
|
|
702
|
+
// shortfall, not oversellable phantom stock) and matches every other
|
|
703
|
+
// claim-first transition in the codebase.
|
|
704
|
+
creditOnce: async function (input) {
|
|
705
|
+
if (!input || typeof input !== "object") {
|
|
706
|
+
throw new TypeError("inventory-locations.creditOnce: input object required");
|
|
707
|
+
}
|
|
708
|
+
_sku(input.sku);
|
|
709
|
+
_code(input.location_code, "location_code");
|
|
710
|
+
_int(input.delta, "delta");
|
|
711
|
+
if (input.delta <= 0) {
|
|
712
|
+
throw new TypeError("inventory-locations.creditOnce: delta must be a positive credit");
|
|
713
|
+
}
|
|
714
|
+
var key = input.idempotency_key;
|
|
715
|
+
if (typeof key !== "string" || !key.length || key.length > MAX_IDEMPOTENCY_KEY) {
|
|
716
|
+
throw new TypeError("inventory-locations.creditOnce: idempotency_key must be a non-empty string ≤ " +
|
|
717
|
+
MAX_IDEMPOTENCY_KEY + " chars");
|
|
718
|
+
}
|
|
719
|
+
var loc = await _getLocationRow(input.location_code);
|
|
720
|
+
if (!loc) {
|
|
721
|
+
throw new TypeError("inventory-locations.creditOnce: location_code " +
|
|
722
|
+
JSON.stringify(input.location_code) + " not found");
|
|
723
|
+
}
|
|
724
|
+
var ts = _now();
|
|
725
|
+
// Claim: insert the keyed audit row. ON CONFLICT DO NOTHING means a
|
|
726
|
+
// replay (same key) lands zero rows and the credit is skipped entirely.
|
|
727
|
+
var claim = await query(
|
|
728
|
+
"INSERT INTO inventory_adjustments " +
|
|
729
|
+
"(id, sku, location_code, delta, reason, occurred_at, idempotency_key) " +
|
|
730
|
+
"VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) ON CONFLICT(idempotency_key) DO NOTHING",
|
|
731
|
+
[b.uuid.v7(), input.sku, input.location_code, input.delta, _reason(input.reason), ts, key],
|
|
732
|
+
);
|
|
733
|
+
var changes = claim && (claim.rowCount != null ? claim.rowCount
|
|
734
|
+
: (claim.meta && claim.meta.changes != null ? claim.meta.changes : claim.changes));
|
|
735
|
+
if (Number(changes || 0) === 0) {
|
|
736
|
+
var existing = await _getStockRow(input.sku, input.location_code);
|
|
737
|
+
return {
|
|
738
|
+
sku: input.sku, location_code: input.location_code,
|
|
739
|
+
quantity: existing ? existing.quantity : 0, delta: 0, applied: false,
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
// Won the claim — apply the credit. If the upsert fails, roll the claim
|
|
743
|
+
// back so a retry re-attempts rather than stranding the credit at zero.
|
|
744
|
+
try {
|
|
745
|
+
await query(
|
|
746
|
+
"INSERT INTO inventory_stock (sku, location_code, quantity, updated_at) " +
|
|
747
|
+
"VALUES (?1, ?2, ?3, ?4) " +
|
|
748
|
+
"ON CONFLICT(sku, location_code) DO UPDATE SET " +
|
|
749
|
+
"quantity = quantity + ?3, updated_at = ?4",
|
|
750
|
+
[input.sku, input.location_code, input.delta, ts],
|
|
751
|
+
);
|
|
752
|
+
} catch (e) {
|
|
753
|
+
await query("DELETE FROM inventory_adjustments WHERE idempotency_key = ?1", [key]);
|
|
754
|
+
throw e;
|
|
755
|
+
}
|
|
756
|
+
var creditedRow = await _getStockRow(input.sku, input.location_code);
|
|
757
|
+
return {
|
|
758
|
+
sku: input.sku, location_code: input.location_code,
|
|
759
|
+
quantity: creditedRow ? creditedRow.quantity : 0, delta: input.delta, applied: true,
|
|
760
|
+
};
|
|
761
|
+
},
|
|
762
|
+
|
|
677
763
|
// Atomic two-row move. Reads the source quantity; if
|
|
678
764
|
// insufficient, refuses the whole transfer before mutating
|
|
679
765
|
// anything (no money created, no row half-committed). Writes
|
package/lib/order.js
CHANGED
|
@@ -802,8 +802,10 @@ function create(opts) {
|
|
|
802
802
|
// outbound HTTP, or a slow / unreachable endpoint would block the
|
|
803
803
|
// transition for seconds (the HTTP client's idle timeout plus
|
|
804
804
|
// retry). send() persists a delivery row per subscriber and attempts
|
|
805
|
-
// delivery in the background; a failure
|
|
806
|
-
//
|
|
805
|
+
// delivery in the background; a failure stamps `next_retry_at` and is
|
|
806
|
+
// re-attempted automatically on the backoff schedule by the
|
|
807
|
+
// /_/webhook-retry-tick cron (an operator can also force an immediate
|
|
808
|
+
// retry from the admin console), walking toward success or the DLQ.
|
|
807
809
|
// The promise is detached with a swallowing .catch so a rejection —
|
|
808
810
|
// or a synchronous throw from send() — never becomes an
|
|
809
811
|
// unhandledRejection and never touches the transition's latency.
|
package/lib/stock-transfers.js
CHANGED
|
@@ -261,15 +261,18 @@ function _now() { return Date.now(); }
|
|
|
261
261
|
function create(opts) {
|
|
262
262
|
opts = opts || {};
|
|
263
263
|
// The inventoryLocations primitive is the sole owner of
|
|
264
|
-
// `inventory_stock` mutations. This primitive composes its
|
|
265
|
-
//
|
|
266
|
-
// to wire one through fails loud
|
|
264
|
+
// `inventory_stock` mutations. This primitive composes its adjustStock
|
|
265
|
+
// verb to debit origin and its creditOnce verb to credit the destination
|
|
266
|
+
// idempotently during reconcile — refusing to wire one through fails loud
|
|
267
|
+
// at boot rather than at first call (a stub missing creditOnce would
|
|
268
|
+
// otherwise boot fine and only break mid-reconcile, after the status claim).
|
|
267
269
|
if (!opts.inventoryLocations ||
|
|
268
270
|
typeof opts.inventoryLocations.adjustStock !== "function" ||
|
|
271
|
+
typeof opts.inventoryLocations.creditOnce !== "function" ||
|
|
269
272
|
typeof opts.inventoryLocations.getLocation !== "function" ||
|
|
270
273
|
typeof opts.inventoryLocations.stockForSku !== "function") {
|
|
271
274
|
throw new TypeError("stock-transfers.create: opts.inventoryLocations with " +
|
|
272
|
-
"adjustStock + getLocation + stockForSku is required");
|
|
275
|
+
"adjustStock + creditOnce + getLocation + stockForSku is required");
|
|
273
276
|
}
|
|
274
277
|
var locations = opts.inventoryLocations;
|
|
275
278
|
var query = opts.query;
|
|
@@ -669,25 +672,29 @@ function create(opts) {
|
|
|
669
672
|
" is no longer received (already reconciled by a concurrent call) — refusing to double-credit");
|
|
670
673
|
}
|
|
671
674
|
var discrepancies = [];
|
|
672
|
-
// Credit the destination one line at a time. The claim above
|
|
673
|
-
//
|
|
674
|
-
//
|
|
675
|
-
//
|
|
676
|
-
//
|
|
677
|
-
//
|
|
678
|
-
//
|
|
679
|
-
//
|
|
675
|
+
// Credit the destination one line at a time. The status claim above
|
|
676
|
+
// serializes concurrent reconciles, but the exactly-once guarantee on the
|
|
677
|
+
// destination credit is the KEYED credit itself: creditOnce stamps a
|
|
678
|
+
// per-line idempotency key (stock-transfer:reconcile:<transfer>:<line>),
|
|
679
|
+
// so even if a line is credited and the loop then throws BEFORE the
|
|
680
|
+
// discrepancy column is stamped — rolling the status claim back to
|
|
681
|
+
// 'received' and re-enabling a full re-run — the re-credit finds the key
|
|
682
|
+
// present and is a no-op. The discrepancy skip-guard is a redundant
|
|
683
|
+
// fast-path, not the safety gate. If any line throws, the terminal claim
|
|
684
|
+
// is rolled back so the transfer stays eligible for another reconcile that
|
|
685
|
+
// finishes the remaining credits rather than stranding it.
|
|
680
686
|
try {
|
|
681
687
|
for (var i = 0; i < transfer.lines.length; i += 1) {
|
|
682
688
|
var line = transfer.lines[i];
|
|
683
|
-
if (line.discrepancy != null) continue; // already credited on a prior attempt
|
|
689
|
+
if (line.discrepancy != null) continue; // already credited + stamped on a prior attempt
|
|
684
690
|
var rx = line.quantity_received == null ? 0 : line.quantity_received;
|
|
685
691
|
if (rx > 0) {
|
|
686
|
-
await locations.
|
|
692
|
+
await locations.creditOnce({
|
|
687
693
|
sku: line.sku,
|
|
688
694
|
location_code: transfer.to_location,
|
|
689
695
|
delta: rx,
|
|
690
696
|
reason: "stock-transfer:reconcile:" + id,
|
|
697
|
+
idempotency_key: "stock-transfer:reconcile:" + id + ":" + line.id,
|
|
691
698
|
});
|
|
692
699
|
}
|
|
693
700
|
var diff = line.quantity_shipped - rx;
|