@blamejs/blamejs-shop 0.4.85 → 0.4.87
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/cart.js +75 -0
- package/lib/order.js +4 -2
- package/lib/security-middleware.js +1 -0
- package/lib/storefront.js +8 -1
- 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.87 (2026-06-22) — **Adding a bundle now charges the advertised bundle price even when a member item is already in the cart.** Fixes a pricing bug when adding a product bundle to a cart that already contains one of the bundle's items. Adding a bundle allocates the discounted bundle price across its members and writes each member to the cart at its allocated per-unit price. But when a member variant was already in the cart on its own, the add merged into the existing line and kept that line's standalone list price, so the cart subtotal came out above the advertised bundle price — the shopper was quoted the discounted bundle but charged closer to the full sum of parts. Adding a bundle now adds the bundle's units at the bundle price while preserving any units already in the cart: a fresh bundle add realizes exactly the advertised bundle price, and a bundle added on top of a standalone item keeps that item and adds the bundle at its discounted price (rather than dropping the item or extending the discount to it). No configuration changes; upgrade to pick it up. **Fixed:** *Bundle add honors the bundle price without disturbing existing cart units* — Adding a bundle now adds each member's units at the bundle's allocated per-unit price. On a fresh line the realized subtotal is exactly the advertised bundle price. When the variant is already in the cart, its existing units are kept at their value and the bundle's units are added at the bundle price, so the shopper is neither overcharged (the previous behavior charged the standalone list price for the bundle unit) nor has their existing quantity dropped. Adding the same bundle twice adds two bundles' worth of units. Direct add-to-cart of a standalone item is unchanged.
|
|
12
|
+
|
|
13
|
+
- 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.
|
|
14
|
+
|
|
11
15
|
- 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.
|
|
12
16
|
|
|
13
17
|
- 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.
|
package/lib/asset-manifest.json
CHANGED
package/lib/cart.js
CHANGED
|
@@ -233,6 +233,81 @@ function create(opts) {
|
|
|
233
233
|
return { id: id, cart_id: cartId, variant_id: input.variant_id, sku: variantRow.sku, qty: input.qty, unit_amount_minor: unitAmount, unit_currency: unitCurrency, added_at: ts, updated_at: ts };
|
|
234
234
|
},
|
|
235
235
|
|
|
236
|
+
// Add a bundle member's units at the bundle's allocated price. addLine is a
|
|
237
|
+
// qty-bump that KEEPS the existing line's price, so a member already in the
|
|
238
|
+
// cart standalone would charge the standalone list price for the bundle's
|
|
239
|
+
// unit too — pushing the cart subtotal above the advertised bundle price.
|
|
240
|
+
// setBundleLine instead ADDS the bundle's units to the line (preserving any
|
|
241
|
+
// quantity already there) and prices the line so the existing units keep
|
|
242
|
+
// their value and the bundle's units are added at the bundle price: the new
|
|
243
|
+
// line carries the COMBINED value (existing qty x existing price + bundle
|
|
244
|
+
// qty x bundle price) as a single integer per-unit price. The cart stores
|
|
245
|
+
// one price per variant line — UNIQUE(cart_id, variant_id) — so a
|
|
246
|
+
// pre-existing line and the bundle's units cannot carry separate prices;
|
|
247
|
+
// blending the combined value onto one line is the faithful single-line
|
|
248
|
+
// representation. It neither drops the shopper's existing units (charging
|
|
249
|
+
// less) nor leaks the bundle discount onto units they already had at list
|
|
250
|
+
// (charging less). On a fresh line the price is exactly the bundle's
|
|
251
|
+
// allocated price. Re-adding the same bundle adds another bundle's worth of
|
|
252
|
+
// units (it is NOT idempotent — two adds is two bundles). Rounding the
|
|
253
|
+
// blended unit to a whole minor unit can leave a sub-unit residual on a
|
|
254
|
+
// mixed line (bounded to under one unit); it rounds up so the line never
|
|
255
|
+
// charges below the combined value. The price snapshot is REQUIRED — the
|
|
256
|
+
// bundle pricer is authoritative, there is no catalog fallback.
|
|
257
|
+
setBundleLine: async function (cartId, input) {
|
|
258
|
+
_uuid(cartId, "cart_id");
|
|
259
|
+
if (!input || typeof input !== "object") throw new TypeError("cart.setBundleLine: input object required");
|
|
260
|
+
_uuid(input.variant_id, "variant_id");
|
|
261
|
+
_qty(input.qty);
|
|
262
|
+
if (!Number.isInteger(input.unit_amount_minor) || input.unit_amount_minor < 0) {
|
|
263
|
+
throw new TypeError("cart.setBundleLine: unit_amount_minor must be a non-negative integer");
|
|
264
|
+
}
|
|
265
|
+
if (typeof input.unit_currency !== "string" || !CURRENCY_RE.test(input.unit_currency)) {
|
|
266
|
+
throw new TypeError("cart.setBundleLine: unit_currency must be a 3-letter ISO 4217 code");
|
|
267
|
+
}
|
|
268
|
+
var cartRow = (await query("SELECT * FROM carts WHERE id = ?1", [cartId])).rows[0];
|
|
269
|
+
if (!cartRow) throw new TypeError("cart.setBundleLine: cart " + cartId + " not found");
|
|
270
|
+
if (cartRow.status !== "active") throw new TypeError("cart.setBundleLine: cart status is " + cartRow.status + ", cannot modify");
|
|
271
|
+
var variantRow = (await query("SELECT sku FROM variants WHERE id = ?1", [input.variant_id])).rows[0];
|
|
272
|
+
if (!variantRow) throw new TypeError("cart.setBundleLine: variant " + input.variant_id + " not found");
|
|
273
|
+
var existing = (await query(
|
|
274
|
+
"SELECT * FROM cart_lines WHERE cart_id = ?1 AND variant_id = ?2 LIMIT 1",
|
|
275
|
+
[cartId, input.variant_id],
|
|
276
|
+
)).rows[0];
|
|
277
|
+
var ts = _now();
|
|
278
|
+
if (existing) {
|
|
279
|
+
var mergedQty = existing.qty + input.qty;
|
|
280
|
+
_qty(mergedQty);
|
|
281
|
+
// Combined value of the units already on the line plus the bundle's
|
|
282
|
+
// units at the bundle price, expressed as one integer per-unit price.
|
|
283
|
+
// Round up so a non-divisible blend never charges below the combined
|
|
284
|
+
// value (a sub-unit residual, never the standalone list price).
|
|
285
|
+
var mergedTotal = (existing.qty * existing.unit_amount_minor) + (input.qty * input.unit_amount_minor);
|
|
286
|
+
var blendedUnit = Math.ceil(mergedTotal / mergedQty);
|
|
287
|
+
await query(
|
|
288
|
+
"UPDATE cart_lines SET qty = ?1, unit_amount_minor = ?2, unit_currency = ?3, updated_at = ?4 WHERE id = ?5",
|
|
289
|
+
[mergedQty, blendedUnit, input.unit_currency, ts, existing.id],
|
|
290
|
+
);
|
|
291
|
+
await query("UPDATE carts SET updated_at = ?1 WHERE id = ?2", [ts, cartId]);
|
|
292
|
+
return Object.assign({}, existing, {
|
|
293
|
+
qty: mergedQty, unit_amount_minor: blendedUnit,
|
|
294
|
+
unit_currency: input.unit_currency, updated_at: ts,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
var id = b.uuid.v7();
|
|
298
|
+
await query(
|
|
299
|
+
"INSERT INTO cart_lines (id, cart_id, variant_id, sku, qty, unit_amount_minor, unit_currency, added_at, updated_at) " +
|
|
300
|
+
"VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?8)",
|
|
301
|
+
[id, cartId, input.variant_id, variantRow.sku, input.qty, input.unit_amount_minor, input.unit_currency, ts],
|
|
302
|
+
);
|
|
303
|
+
await query("UPDATE carts SET updated_at = ?1 WHERE id = ?2", [ts, cartId]);
|
|
304
|
+
return {
|
|
305
|
+
id: id, cart_id: cartId, variant_id: input.variant_id, sku: variantRow.sku,
|
|
306
|
+
qty: input.qty, unit_amount_minor: input.unit_amount_minor,
|
|
307
|
+
unit_currency: input.unit_currency, added_at: ts, updated_at: ts,
|
|
308
|
+
};
|
|
309
|
+
},
|
|
310
|
+
|
|
236
311
|
// Scope the mutation to (lineId, cartId): a caller who learns
|
|
237
312
|
// another visitor's cart_lines.id (it's rendered in their own cart
|
|
238
313
|
// HTML as the update/remove form action) can't mutate it because
|
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/storefront.js
CHANGED
|
@@ -21236,7 +21236,14 @@ function mount(router, deps) {
|
|
|
21236
21236
|
try {
|
|
21237
21237
|
for (var k = 0; k < members.length; k += 1) {
|
|
21238
21238
|
var mk = members[k];
|
|
21239
|
-
|
|
21239
|
+
// Add each member's units at the bundle's allocated price. addLine
|
|
21240
|
+
// would qty-bump a pre-existing standalone line and keep its list
|
|
21241
|
+
// price, charging list for the bundle's unit and overcharging the
|
|
21242
|
+
// bundle; setBundleLine adds the bundle units while preserving any
|
|
21243
|
+
// units already on the line (see its contract for the mixed-line
|
|
21244
|
+
// pricing). On a fresh line the price is exactly the allocated price,
|
|
21245
|
+
// so a clean bundle add realizes the advertised bundle total.
|
|
21246
|
+
await deps.cart.setBundleLine(resolved.cart.id, {
|
|
21240
21247
|
variant_id: mk.variant_id,
|
|
21241
21248
|
qty: mk.qty,
|
|
21242
21249
|
unit_amount_minor: mk.unit,
|