@blamejs/blamejs-shop 0.3.72 → 0.3.74
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/admin.js +44 -6
- package/lib/asset-manifest.json +3 -3
- package/lib/security-middleware.js +9 -0
- package/lib/storefront.js +18 -4
- 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.3.x
|
|
10
10
|
|
|
11
|
+
- v0.3.74 (2026-06-05) — **The payment form matches the shop's dark theme.** The Stripe Payment Element and the express wallet buttons on the pay page previously rendered in Stripe's default light style — a white card floating on the shop's near-black page. The elements now use Stripe's night appearance recolored with the shop's own design tokens: violet accent on focus and selected tabs, the shop's charcoal input surfaces, soft ink text, and matching corner radii. The Apple Pay, Google Pay, and PayPal express buttons switch to their white and outline variants, which keep each brand's contrast rules legible on the dark card. No payment behavior changes; this is purely the appearance configuration on the existing elements. **Changed:** *Dark-themed payment elements* — The pay page's Stripe elements adopt the night appearance with the shop's design tokens — violet primary, charcoal surfaces, soft ink, ten-pixel radii, violet focus rings — and the express wallet buttons render in their white and outline variants sized to the page's controls. Inside Stripe's cross-origin frame the typeface falls back to the system stack; everything else mirrors the storefront's stylesheet tokens.
|
|
12
|
+
|
|
13
|
+
- v0.3.73 (2026-06-05) — **Unlock codes are manageable from the Discounts screen, and coupon guessing is rate-capped.** Code-unlocked discount rules shipped with an API-only gap: the Discounts console had no field for the unlock code, so creating a code-gated rule required a raw API call. The create and edit forms now carry an optional Unlock code input — clearing it on edit reverts the rule to purely automatic — and the rule list and detail show which rules are code-gated; the screen's description covers both kinds. The cart's code-apply endpoint joins the tight per-address rate budget that already guards gift-card balance lookups, capping coupon-namespace guessing at the same rate. Also fixed: a failed confirmation-resend in the browser console now lands in the error log with a clean notice instead of an unrecorded failure, and the signed-in cart page resolves the shopper's destination once instead of twice per view. **Fixed:** *Unlock codes editable in the Discounts console* — The create and edit forms gain an optional Unlock code field, threaded through the same validation as the API. On edit, submitting the field blank explicitly clears the code (the rule becomes purely automatic again), while the inline quick-edit leaves it untouched. The rule list and the detail view display each rule's code, escaped, so code-gated rules are visible at a glance, and the screen copy now describes both automatic and code-unlocked rules. · *Coupon-code guessing joins the tight rate budget* — POST /cart/coupon now sits in the per-address, per-path rate budget alongside gift-card balance lookups — both accept guessable secrets and answer uniformly, so both deserve the same throttle. The pinned integration test sprays the endpoint and asserts the cap engages. · *Failed confirmation resends are captured and surfaced* — A mailer fault during a browser-initiated confirmation resend previously escaped both the error log and the screen. It now records to the error log and redirects back to the order with an honest failure notice; the API path captures identically. The signed-in cart view also drops a duplicated destination lookup per render.
|
|
14
|
+
|
|
11
15
|
- v0.3.72 (2026-06-05) — **Resend an order confirmation from the console, and export a segment's members as CSV.** Two operator actions land in the admin console. The order detail screen gains a Resend-confirmation action: buyer emails are stored only as one-way hashes, so the original address cannot be recovered — the operator types the recipient (from the customer's own request), and a fresh receipt rendered from the stored order is sent, rate-bounded to three per order per hour, with every send audited and the recipient kept out of the audit trail. The action appears only when a mailer is configured. Customer segments gain a members CSV export that streams id, display name, segment join date, and order count — deliberately no email column, since addresses are not stored in readable form; the file says so in a leading comment, cell values are quoted and spreadsheet-formula-neutralized, and the stream is written batch by batch so a large segment never buffers in memory. **Added:** *Resend order confirmation* — POST /admin/orders/:id/resend-confirmation sends a fresh receipt for the stored order to an operator-supplied address, since the buyer's email exists only as a hash and cannot be recovered — the recipient comes from the customer's own "I didn't get it" request. Rate-bounded to three sends per order per hour, audited without recording the recipient, gated on a configured mailer (the panel renders an honest disabled note otherwise), and validated for address shape. · *Segment members CSV export* — GET /admin/segments/:slug/members.csv streams a segment's members — customer id, display name, join date, order count — in keyset-paginated batches with RFC 4180 quoting and spreadsheet-formula-injection neutralization. There is no email column: addresses are stored hashed, and both the screen copy and a leading CSV comment state it. Unknown segments return 404 before any bytes stream; an archived segment yields a well-formed header-only file; each export is audited.
|
|
12
16
|
|
|
13
17
|
- v0.3.71 (2026-06-05) — **Vendored blamejs framework refreshed from v0.14.19 to v0.14.21.** The storefront runs on a vendored copy of the blamejs framework; this refreshes it across two upstream patch releases. The most operator-relevant upstream changes for this shop: the OAuth client — which Sign in with Google and Apple compose — gains RFC 9396 Rich Authorization Request validation and attestation-based client authentication primitives, and refuses token grants an identity provider broadened beyond what was requested; HEAD requests now conform by carrying no response body; the sealed-field store gains an unseal rate cap; and a framework-wide sweep ensures every accepted option is actually read, surfacing configuration typos that were previously silent. The remaining upstream changes are in framework areas the storefront does not expose (SCIM bulk operations, OID4VCI credential proofs, DMARC forensic-report parsing). The storefront's own behavior is unchanged, verified by the full test suite — including the regression guards that pin the CSRF origin posture, the referrer policy, and the payment-processor TLS agent against vendor drift — and the vendored-tree integrity manifest was re-stamped as part of the refresh. **Changed:** *Updated the vendored framework to blamejs v0.14.21* — The vendored framework moves from v0.14.19 to v0.14.21 (two upstream patch releases of fixes and additive, opt-in changes). Notable for this shop: hardened OAuth client validation behind federated sign-in, HTTP HEAD conformance, a sealed-field unseal rate cap, and boot-time validation of previously-unread options. The integrity manifest over the vendored tree was re-stamped as part of the refresh.
|
package/lib/admin.js
CHANGED
|
@@ -2890,9 +2890,19 @@ function mount(router, deps) {
|
|
|
2890
2890
|
var body = req.body || {};
|
|
2891
2891
|
try { await _resendReceipt(o, body.email || body.to); }
|
|
2892
2892
|
catch (e) {
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2893
|
+
// Validation / rate-limit faults are TypeErrors → a contextual
|
|
2894
|
+
// banner. A mailer fault (the send itself failing) is NOT a
|
|
2895
|
+
// TypeError: route it through _safeNotice so it lands in the
|
|
2896
|
+
// operator error log (the htmlHandler isn't wrapped by _wrap, so
|
|
2897
|
+
// a re-throw here would bypass the capture the JSON path gets via
|
|
2898
|
+
// the wrapper's catch), then redirect to a generic send-failed
|
|
2899
|
+
// banner rather than 500-ing the page.
|
|
2900
|
+
if (e instanceof TypeError) {
|
|
2901
|
+
var reason = e.code === "RESEND_RATE_LIMITED" ? "rate" : "email";
|
|
2902
|
+
return _redirect(res, "/admin/orders/" + enc + "?resend_err=" + reason);
|
|
2903
|
+
}
|
|
2904
|
+
_safeNotice(e, "order.receipt.resend");
|
|
2905
|
+
return _redirect(res, "/admin/orders/" + enc + "?resend_err=send");
|
|
2896
2906
|
}
|
|
2897
2907
|
b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".order.receipt.resend", outcome: "success", metadata: { id: id } });
|
|
2898
2908
|
_redirect(res, "/admin/orders/" + enc + "?resent=1");
|
|
@@ -10768,6 +10778,13 @@ function mount(router, deps) {
|
|
|
10768
10778
|
if (body.priority != null && body.priority !== "") {
|
|
10769
10779
|
input.priority = _strictMinorInt(body.priority, "autoDiscount", "priority");
|
|
10770
10780
|
}
|
|
10781
|
+
// Optional shopper-typed unlock code. A non-empty value makes the rule
|
|
10782
|
+
// code-gated (dormant until presented at /cart/coupon); the lib
|
|
10783
|
+
// validates the code shape and rejects a clash with another active
|
|
10784
|
+
// rule's code. Blank leaves it a pure automatic.
|
|
10785
|
+
if (typeof body.unlock_code === "string" && body.unlock_code.trim() !== "") {
|
|
10786
|
+
input.unlock_code = body.unlock_code.trim();
|
|
10787
|
+
}
|
|
10771
10788
|
return input;
|
|
10772
10789
|
}
|
|
10773
10790
|
|
|
@@ -10835,6 +10852,14 @@ function mount(router, deps) {
|
|
|
10835
10852
|
if (body.active_present === "1") patch.active = (body.active === "on" || body.active === "1");
|
|
10836
10853
|
if (body.trigger_kind != null && body.trigger_kind !== "") patch.trigger = _discountTrigger(body);
|
|
10837
10854
|
if (body.value_kind != null && body.value_kind !== "") patch.value = _discountValue(body);
|
|
10855
|
+
// The edit form always renders the unlock-code field with a hidden
|
|
10856
|
+
// `unlock_code_present` marker, so a blank submission is an explicit
|
|
10857
|
+
// CLEAR (the lib maps "" → null, reverting the rule to a pure
|
|
10858
|
+
// automatic) rather than an omission. The lib validates the shape and
|
|
10859
|
+
// rejects a code already claimed by another active rule.
|
|
10860
|
+
if (body.unlock_code_present === "1") {
|
|
10861
|
+
patch.unlock_code = typeof body.unlock_code === "string" ? body.unlock_code.trim() : "";
|
|
10862
|
+
}
|
|
10838
10863
|
return patch;
|
|
10839
10864
|
}
|
|
10840
10865
|
|
|
@@ -12165,7 +12190,9 @@ function renderAdminOrder(opts) {
|
|
|
12165
12190
|
? "<div class=\"banner banner--err\">Resend limit reached for this order. Try again later.</div>"
|
|
12166
12191
|
: (opts.resend_error === "email"
|
|
12167
12192
|
? "<div class=\"banner banner--err\">Enter a valid recipient email address to resend.</div>"
|
|
12168
|
-
: ""
|
|
12193
|
+
: (opts.resend_error === "send"
|
|
12194
|
+
? "<div class=\"banner banner--err\">The confirmation email couldn't be sent. The failure was logged — try again, or check the error log.</div>"
|
|
12195
|
+
: ""));
|
|
12169
12196
|
var notice = opts.notice ? "<div class=\"banner banner--err\">" + _htmlEscape(opts.notice) + "</div>" : "";
|
|
12170
12197
|
|
|
12171
12198
|
var lineRows = (o.lines || []).map(function (l) {
|
|
@@ -15077,6 +15104,8 @@ function renderAdminDiscount(opts) {
|
|
|
15077
15104
|
_setupField("· BOGO buy qty", "value_buy_qty", v.kind === "bogo" && v.buy_qty != null ? String(v.buy_qty) : "", "number", "For \"Buy X get Y\".", " min=\"1\"") +
|
|
15078
15105
|
_setupField("· BOGO get qty", "value_get_qty", v.kind === "bogo" && v.get_qty != null ? String(v.get_qty) : "", "number", "For \"Buy X get Y\".", " min=\"1\"") +
|
|
15079
15106
|
_setupField("Priority", "priority", String(r.priority), "number", "Higher wins ties.", " min=\"0\"") +
|
|
15107
|
+
"<input type=\"hidden\" name=\"unlock_code_present\" value=\"1\">" +
|
|
15108
|
+
_setupField("Unlock code (optional)", "unlock_code", r.unlock_code || "", "text", "Code that gates this rule — shoppers type it at the cart to unlock it. Clear the field to make the rule a pure automatic again. Letters, digits, . _ - only.", " maxlength=\"64\"") +
|
|
15080
15109
|
"<label class=\"kv\"><input type=\"checkbox\" name=\"active\"" + (r.active ? " checked" : "") + "> Active</label>" +
|
|
15081
15110
|
"<div class=\"actions-row\"><button class=\"btn\" type=\"submit\">Save changes</button></div>" +
|
|
15082
15111
|
"</form>" +
|
|
@@ -15087,6 +15116,7 @@ function renderAdminDiscount(opts) {
|
|
|
15087
15116
|
"<div><dt>Rule</dt><dd><strong>" + _htmlEscape(r.title) + "</strong><br><code class=\"order-id\">" + _htmlEscape(r.slug) + "</code></dd></div>" +
|
|
15088
15117
|
"<div><dt>Trigger</dt><dd>" + _htmlEscape(_fmtTrigger(r.trigger)) + "</dd></div>" +
|
|
15089
15118
|
"<div><dt>Value</dt><dd>" + _htmlEscape(_fmtValue(r.value)) + "</dd></div>" +
|
|
15119
|
+
"<div><dt>Unlock code</dt><dd>" + (r.unlock_code ? "<code class=\"order-id\">" + _htmlEscape(r.unlock_code) + "</code> <span class=\"meta\">(shopper types this at the cart)</span>" : "<span class=\"meta\">none — pure automatic</span>") + "</dd></div>" +
|
|
15090
15120
|
"<div><dt>Priority</dt><dd>" + _htmlEscape(String(r.priority)) + "</dd></div>" +
|
|
15091
15121
|
"<div><dt>Status</dt><dd><span class=\"status-pill " + (isArchived ? "cancelled" : (r.active ? "paid" : "pending")) + "\">" + (isArchived ? "archived" : (r.active ? "active" : "paused")) + "</span></dd></div>" +
|
|
15092
15122
|
"</dl></div>" +
|
|
@@ -15121,8 +15151,15 @@ function renderAdminDiscounts(opts) {
|
|
|
15121
15151
|
"<a class=\"btn btn--ghost\" href=\"/admin/discounts/" + _htmlEscape(encodeURIComponent(r.slug)) + "\">Edit terms</a> " +
|
|
15122
15152
|
"<form method=\"post\" action=\"/admin/discounts/" + _htmlEscape(encodeURIComponent(r.slug)) + "/archive\" class=\"form-inline\">" +
|
|
15123
15153
|
"<button class=\"btn btn--danger\" type=\"submit\">Archive</button></form>";
|
|
15154
|
+
// A code-gated rule is dormant until a shopper types its unlock code at
|
|
15155
|
+
// the cart; surface the code so an operator can see at a glance which
|
|
15156
|
+
// rules are gated vs pure-automatic. The code is operator-authored free
|
|
15157
|
+
// text → escaped at the sink.
|
|
15158
|
+
var gate = r.unlock_code
|
|
15159
|
+
? "<br><span class=\"meta\">code: <code class=\"order-id\">" + _htmlEscape(r.unlock_code) + "</code></span>"
|
|
15160
|
+
: "";
|
|
15124
15161
|
return "<tr>" +
|
|
15125
|
-
"<td><strong>" + _htmlEscape(r.title) + "</strong><br><code class=\"order-id\">" + _htmlEscape(r.slug) + "</code
|
|
15162
|
+
"<td><strong>" + _htmlEscape(r.title) + "</strong><br><code class=\"order-id\">" + _htmlEscape(r.slug) + "</code>" + gate + "</td>" +
|
|
15126
15163
|
"<td>" + _htmlEscape(_fmtTrigger(r.trigger)) + "</td>" +
|
|
15127
15164
|
"<td>" + _htmlEscape(_fmtValue(r.value)) + "</td>" +
|
|
15128
15165
|
"<td class=\"num\">" + _htmlEscape(String(r.priority)) + "</td>" +
|
|
@@ -15168,6 +15205,7 @@ function renderAdminDiscounts(opts) {
|
|
|
15168
15205
|
_setupField("· BOGO buy qty", "value_buy_qty", "", "number", "For \"Buy X get Y\".", " min=\"1\"") +
|
|
15169
15206
|
_setupField("· BOGO get qty", "value_get_qty", "", "number", "For \"Buy X get Y\".", " min=\"1\"") +
|
|
15170
15207
|
_setupField("Priority", "priority", "", "number", "Higher wins ties. Default 0.", " min=\"0\"") +
|
|
15208
|
+
_setupField("Unlock code (optional)", "unlock_code", "", "text", "Leave blank for a pure automatic. Set a code to gate the rule — it stays dormant until a shopper types this code at the cart. Letters, digits, . _ - only.", " maxlength=\"64\"") +
|
|
15171
15209
|
"<div class=\"actions-row\"><button class=\"btn\" type=\"submit\">Add rule</button></div>" +
|
|
15172
15210
|
"</form>" +
|
|
15173
15211
|
"</div>";
|
|
@@ -15215,7 +15253,7 @@ function renderAdminDiscounts(opts) {
|
|
|
15215
15253
|
}
|
|
15216
15254
|
|
|
15217
15255
|
var body = "<section><h2>Discounts</h2>" + created + updated + archived + notice +
|
|
15218
|
-
"<p class=\"meta\">
|
|
15256
|
+
"<p class=\"meta\">Cart-level discount rules. By default a rule applies automatically when the cart matches its trigger; set an unlock code on a rule to keep it dormant until a shopper types that code at the cart.</p>" +
|
|
15219
15257
|
ruleTable + createForm + policySection + "</section>";
|
|
15220
15258
|
return _renderAdminShell(opts.shop_name, "Discounts", body, "discounts", opts.nav_available);
|
|
15221
15259
|
}
|
package/lib/asset-manifest.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.3.
|
|
2
|
+
"version": "0.3.74",
|
|
3
3
|
"assets": {
|
|
4
4
|
"css/admin.css": {
|
|
5
5
|
"integrity": "sha384-6k53cvkRrxMgmeStLIoLjVXZQHqIJgTmv1Izd8TYhh1HOC4POgE6GCvx1bsalyEP",
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"fingerprinted": "js/passkey-register.02b0e196fb9608d8.js"
|
|
39
39
|
},
|
|
40
40
|
"js/pay.js": {
|
|
41
|
-
"integrity": "sha384-
|
|
42
|
-
"fingerprinted": "js/pay.
|
|
41
|
+
"integrity": "sha384-W11JVQhv1RZq4WhsAOglu56gTZTDz1ByLd+b1HFQBCi8xGoEhJ0PZ2yI3FqbYzt6",
|
|
42
|
+
"fingerprinted": "js/pay.683a905563e54a47.js"
|
|
43
43
|
},
|
|
44
44
|
"js/paypal-checkout.js": {
|
|
45
45
|
"integrity": "sha384-LI6y/1z0Y9F8Kx8RhW4EwY2WqJPXLwJozCXqnhDT+dTckLHyvhly0SsRpH0bsdui",
|
|
@@ -90,6 +90,14 @@ var HEALTH_PATH = "/_/health";
|
|
|
90
90
|
// request-supplied address — without the tight
|
|
91
91
|
// budget it is a victim-addressed mail cannon on
|
|
92
92
|
// the loose global bucket alone.
|
|
93
|
+
// /cart/coupon -> apply + remove a typed discount code. POST /cart/coupon
|
|
94
|
+
// validates the code against the discount engine; on a
|
|
95
|
+
// miss it returns a UNIFORM error (no existence oracle),
|
|
96
|
+
// which makes the loose global bucket alone a code-guessing
|
|
97
|
+
// engine — a sprayer can grind the coupon namespace for a
|
|
98
|
+
// live code. The tight per-(IP+path) budget caps the
|
|
99
|
+
// guess rate (same guessable-secret rationale as the
|
|
100
|
+
// /gift-cards/balance lookup). Container-only.
|
|
93
101
|
var TIGHT_PREFIXES = [
|
|
94
102
|
"/account/login",
|
|
95
103
|
"/account/register",
|
|
@@ -102,6 +110,7 @@ var TIGHT_PREFIXES = [
|
|
|
102
110
|
"/survey/",
|
|
103
111
|
"/orders/",
|
|
104
112
|
"/stock-alert/",
|
|
113
|
+
"/cart/coupon",
|
|
105
114
|
];
|
|
106
115
|
|
|
107
116
|
// Edge-served state-changing POST endpoints. These forms are rendered at
|
package/lib/storefront.js
CHANGED
|
@@ -11306,7 +11306,12 @@ function mount(router, deps) {
|
|
|
11306
11306
|
// address with no usable postal falls through to null here.
|
|
11307
11307
|
var coAuth = _currentCustomerEnv(req);
|
|
11308
11308
|
if (!coAuth) return null;
|
|
11309
|
-
|
|
11309
|
+
// Reuse a destination the caller already resolved (the cart GET
|
|
11310
|
+
// resolves it once and threads it into both this and
|
|
11311
|
+
// _estimateCartTotals) so a signed-in cart render doesn't run the
|
|
11312
|
+
// address lookup twice. Falls back to resolving it here for callers
|
|
11313
|
+
// that don't pass one (the PDP).
|
|
11314
|
+
var dest = opts.dest || await _estimateDestination(req);
|
|
11310
11315
|
if (!dest || !dest.from_saved || !dest.ship_to || !dest.ship_to.postal) return null;
|
|
11311
11316
|
// Operator-configured origin — the primitive won't guess one. Resolved
|
|
11312
11317
|
// at boot into `deps.delivery_estimate_origin` (a plain slug string) from
|
|
@@ -11395,9 +11400,13 @@ function mount(router, deps) {
|
|
|
11395
11400
|
destination: null,
|
|
11396
11401
|
};
|
|
11397
11402
|
if (!deps.checkout || typeof deps.checkout.quote !== "function") return result;
|
|
11403
|
+
// `opts.ship_to` (a shopper-confirmed address from the checkout POST) wins;
|
|
11404
|
+
// else reuse a destination the caller already resolved (`opts.dest`, so a
|
|
11405
|
+
// signed-in cart render doesn't run the address lookup twice — once here
|
|
11406
|
+
// and once in _resolveDeliveryEstimate); else resolve it now.
|
|
11398
11407
|
var dest = opts.ship_to
|
|
11399
11408
|
? { ship_to: opts.ship_to, from_saved: false }
|
|
11400
|
-
: await _estimateDestination(req);
|
|
11409
|
+
: (opts.dest || await _estimateDestination(req));
|
|
11401
11410
|
result.destination = dest;
|
|
11402
11411
|
try {
|
|
11403
11412
|
// quote() without a selected_shipping_id returns the tax row + ALL
|
|
@@ -12288,12 +12297,17 @@ function mount(router, deps) {
|
|
|
12288
12297
|
catch (_e) { appliedCodes = []; }
|
|
12289
12298
|
}
|
|
12290
12299
|
var appliedCodeStrings = appliedCodes.map(function (r) { return r.code; });
|
|
12300
|
+
// Resolve the estimate destination ONCE for the render: both the totals
|
|
12301
|
+
// estimate and the "Get it by <date>" delivery estimate need it, and the
|
|
12302
|
+
// lookup (a saved-address read for a signed-in customer) is otherwise run
|
|
12303
|
+
// twice per cart GET. Thread the single result into both.
|
|
12304
|
+
var estimateDest = await _estimateDestination(req);
|
|
12291
12305
|
// Real total before pay: compose the same tax + shipping primitives the
|
|
12292
12306
|
// charge runs through (estimated against the shopper's saved/default
|
|
12293
12307
|
// destination until they confirm an address at checkout). Falls back to
|
|
12294
12308
|
// a subtotal-only breakdown — with tax/shipping labelled "calculated at
|
|
12295
12309
|
// checkout" — when checkout isn't wired or no zone matches.
|
|
12296
|
-
var totalsDetail = await _estimateCartTotals(req, c, lines, { codes: appliedCodeStrings });
|
|
12310
|
+
var totalsDetail = await _estimateCartTotals(req, c, lines, { codes: appliedCodeStrings, dest: estimateDest });
|
|
12297
12311
|
var totals = totalsDetail.totals;
|
|
12298
12312
|
// Truthful per-line stock state (out / low / ok) so the cart never
|
|
12299
12313
|
// implies a sold-out line is buyable.
|
|
@@ -12340,7 +12354,7 @@ function mount(router, deps) {
|
|
|
12340
12354
|
// cart estimate is the destination window, not a per-parcel weight quote);
|
|
12341
12355
|
// the primitive falls back to its weight-agnostic transit rows. Drop-
|
|
12342
12356
|
// silent → null, and the summary renders no estimate.
|
|
12343
|
-
var cartEstimate = await _resolveDeliveryEstimate(req, {});
|
|
12357
|
+
var cartEstimate = await _resolveDeliveryEstimate(req, { dest: estimateDest });
|
|
12344
12358
|
_send(res, 200, renderCart(Object.assign({
|
|
12345
12359
|
lines: lines,
|
|
12346
12360
|
totals: totals,
|
package/package.json
CHANGED