@blamejs/blamejs-shop 0.3.51 → 0.3.53
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/README.md +10 -1
- package/SECURITY.md +58 -0
- package/lib/admin.js +779 -1
- package/lib/asset-manifest.json +1 -1
- package/lib/email.js +94 -0
- package/lib/security-middleware.js +9 -0
- package/lib/storefront.js +450 -5
- package/lib/vendor/MANIFEST.json +2665 -2
- package/package.json +2 -1
package/lib/storefront.js
CHANGED
|
@@ -1894,16 +1894,35 @@ function _buildBuyBox(variants, escAttr, availability, headlinePrice) {
|
|
|
1894
1894
|
var soldOutRowBtn =
|
|
1895
1895
|
"<button type=\"submit\" class=\"btn-primary btn-primary--sm\" disabled aria-disabled=\"true\">Out of stock</button>";
|
|
1896
1896
|
|
|
1897
|
+
// "Notify me when back in stock" — a cookie-less, edge-cache-safe form
|
|
1898
|
+
// posting to the DISTINCT CSRF-exempt action `/stock-alert/subscribe`
|
|
1899
|
+
// (NOT /products/:slug/notify — that would over-exempt the token-required
|
|
1900
|
+
// review + question POSTs). The action is in EDGE_POST_PATHS, so the
|
|
1901
|
+
// container's `_injectCsrfFields` leaves it un-tokened, keeping this markup
|
|
1902
|
+
// byte-identical to the worker twin. sku/variant are catalog data; escape
|
|
1903
|
+
// anyway (escape-by-default). DUAL-RENDER: this helper is byte-identical to
|
|
1904
|
+
// worker/render/product.js#_buildBuyBox._notifyForm.
|
|
1905
|
+
function _notifyForm(sku, variantId) {
|
|
1906
|
+
return "<form class=\"pdp__notify\" method=\"post\" action=\"/stock-alert/subscribe\">\n" +
|
|
1907
|
+
" <input type=\"hidden\" name=\"sku\" value=\"" + escAttr(sku) + "\">\n" +
|
|
1908
|
+
(variantId ? " <input type=\"hidden\" name=\"variant_id\" value=\"" + escAttr(variantId) + "\">\n" : "") +
|
|
1909
|
+
" <label class=\"pdp__notify-label\" for=\"notify-email-" + escAttr(sku) + "\">Email me when this is back in stock</label>\n" +
|
|
1910
|
+
" <input id=\"notify-email-" + escAttr(sku) + "\" type=\"email\" name=\"email\" required placeholder=\"you@example.com\" autocomplete=\"email\">\n" +
|
|
1911
|
+
" <button type=\"submit\" class=\"btn-secondary btn-primary--sm\">Notify me</button>\n" +
|
|
1912
|
+
" </form>";
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1897
1915
|
// Many variants → keep the compact table (still a per-row add form).
|
|
1898
1916
|
if (variants.length > BUYBOX_CHIP_LIMIT) {
|
|
1899
1917
|
var rows = variants.map(function (v) {
|
|
1900
1918
|
var row = _render(VARIANT_ROW, { title: v.title, sku: v.sku, price: v.price, variant_id: v.id });
|
|
1901
1919
|
// When the product is out of stock, swap each per-row add button for
|
|
1902
|
-
// the disabled control so no row offers an
|
|
1920
|
+
// the disabled control + a per-row notify form so no row offers an
|
|
1921
|
+
// active purchase but every sold-out SKU offers the alert.
|
|
1903
1922
|
if (!inStock) {
|
|
1904
1923
|
row = row.replace(
|
|
1905
1924
|
"<button type=\"submit\" class=\"btn-primary btn-primary--sm\">Add to cart</button>",
|
|
1906
|
-
soldOutRowBtn);
|
|
1925
|
+
soldOutRowBtn + _notifyForm(v.sku, v.id));
|
|
1907
1926
|
}
|
|
1908
1927
|
return row;
|
|
1909
1928
|
}).join("");
|
|
@@ -1947,6 +1966,13 @@ function _buildBuyBox(variants, escAttr, availability, headlinePrice) {
|
|
|
1947
1966
|
? "<button type=\"submit\" class=\"btn-primary cart-page__checkout\">$ add to cart</button>"
|
|
1948
1967
|
: soldOutBtn;
|
|
1949
1968
|
|
|
1969
|
+
// Out-of-stock chip/single buy box → the notify form sits OUTSIDE the
|
|
1970
|
+
// add-to-cart form (its own form element, distinct action), after the
|
|
1971
|
+
// sold-out control. Keyed to the LEAD variant's SKU — the chip radio does
|
|
1972
|
+
// not JS-swap the hidden field (Trusted Types / no innerHTML island), so a
|
|
1973
|
+
// shopper subscribes against the lead SKU. Documented limitation.
|
|
1974
|
+
var notifyBlock = inStock ? "" : ("\n " + _notifyForm(lead.sku, lead.id));
|
|
1975
|
+
|
|
1950
1976
|
var headline = (headlinePrice != null && headlinePrice !== "") ? headlinePrice : lead.price;
|
|
1951
1977
|
return "<div class=\"pdp__buybox\">\n" +
|
|
1952
1978
|
" <p class=\"featured-product__price\">" + escAttr(headline) + "</p>\n" +
|
|
@@ -1955,7 +1981,7 @@ function _buildBuyBox(variants, escAttr, availability, headlinePrice) {
|
|
|
1955
1981
|
" <label class=\"pdp__variants-title\" for=\"buybox-qty\">Quantity</label>\n" +
|
|
1956
1982
|
" <input id=\"buybox-qty\" type=\"number\" name=\"qty\" value=\"1\" min=\"1\" max=\"99\" class=\"variant-row__qty\" aria-label=\"Quantity\">\n" +
|
|
1957
1983
|
" " + addControl + "\n" +
|
|
1958
|
-
" </form
|
|
1984
|
+
" </form>" + notifyBlock + "\n" +
|
|
1959
1985
|
" </div>\n" +
|
|
1960
1986
|
" " + trustLine;
|
|
1961
1987
|
}
|
|
@@ -6552,12 +6578,16 @@ function renderPayPage(opts) {
|
|
|
6552
6578
|
pk: opts.publishable_key,
|
|
6553
6579
|
client_secret: opts.client_secret,
|
|
6554
6580
|
}).replace("RAW_PAY_SCRIPT", _islandScript("pay.js"));
|
|
6581
|
+
// Operator trust badges at the checkout placement (container-only — the pay
|
|
6582
|
+
// page isn't edge-cached). Pre-resolved + sanitized by the route; appended
|
|
6583
|
+
// after the pay card. Empty string when no badges / no dep.
|
|
6584
|
+
var payTrustBadges = typeof opts.trust_badges_html === "string" ? opts.trust_badges_html : "";
|
|
6555
6585
|
return _wrap({
|
|
6556
6586
|
title: "Pay",
|
|
6557
6587
|
shop_name: shopName,
|
|
6558
6588
|
cart_count: cartCount,
|
|
6559
6589
|
theme_css: opts.theme_css,
|
|
6560
|
-
body: body,
|
|
6590
|
+
body: body + payTrustBadges,
|
|
6561
6591
|
});
|
|
6562
6592
|
}
|
|
6563
6593
|
|
|
@@ -7046,12 +7076,18 @@ function renderOrder(opts) {
|
|
|
7046
7076
|
"<div class=\"grid\">" + railCards + "</div>" +
|
|
7047
7077
|
"</section>";
|
|
7048
7078
|
}
|
|
7079
|
+
// Operator-authored trust badges at the order_confirmation placement
|
|
7080
|
+
// (container-only — this page isn't edge-cached). Pre-resolved + escaped by
|
|
7081
|
+
// the route (via _trustBadgesHtml); spliced raw so a `$` in a sanitized SVG
|
|
7082
|
+
// can't trip String.replace dollar substitution. Empty string when no badges
|
|
7083
|
+
// / no dep.
|
|
7084
|
+
var trustBadgesHtml = typeof opts.trust_badges_html === "string" ? opts.trust_badges_html : "";
|
|
7049
7085
|
return _wrap({
|
|
7050
7086
|
title: "Order " + o.id,
|
|
7051
7087
|
shop_name: shopName,
|
|
7052
7088
|
cart_count: cartCount,
|
|
7053
7089
|
theme_css: opts.theme_css,
|
|
7054
|
-
body: body + railHtml,
|
|
7090
|
+
body: body + railHtml + trustBadgesHtml,
|
|
7055
7091
|
});
|
|
7056
7092
|
}
|
|
7057
7093
|
|
|
@@ -7562,6 +7598,171 @@ function renderUnsubscribeResult(opts) {
|
|
|
7562
7598
|
});
|
|
7563
7599
|
}
|
|
7564
7600
|
|
|
7601
|
+
// ---- back-in-stock "Notify me" pages -----------------------------------
|
|
7602
|
+
//
|
|
7603
|
+
// Container-only (no edge twin) — these aren't edge-cached. Every page is
|
|
7604
|
+
// server-rendered, no auth, escape-by-default. The thank-you page renders
|
|
7605
|
+
// the SAME copy for new / already-pending / already-confirmed so the page
|
|
7606
|
+
// never reveals whether the address was already subscribed beyond friendly
|
|
7607
|
+
// copy. The confirm/unsubscribe pages render a clean outcome on a null/
|
|
7608
|
+
// invalid result rather than a leak or a 500.
|
|
7609
|
+
|
|
7610
|
+
function renderStockAlertThanks(opts) {
|
|
7611
|
+
opts = opts || {};
|
|
7612
|
+
var body =
|
|
7613
|
+
"<section class=\"newsletter-thanks\">" +
|
|
7614
|
+
"<div class=\"newsletter-thanks__card\">" +
|
|
7615
|
+
"<p class=\"eyebrow\">Back-in-stock alert</p>" +
|
|
7616
|
+
"<h1 class=\"newsletter-thanks__title\">Check your email.</h1>" +
|
|
7617
|
+
"<p class=\"newsletter-thanks__lede\">If that address can receive mail, we've sent a confirmation link. Click it and we'll email you once — the moment this item is back in stock. Already confirmed? You're all set.</p>" +
|
|
7618
|
+
"<div class=\"newsletter-thanks__cta\">" +
|
|
7619
|
+
"<a href=\"/\" class=\"btn-primary\">Back to the shop <span aria-hidden=\"true\">→</span></a>" +
|
|
7620
|
+
"</div>" +
|
|
7621
|
+
"</div>" +
|
|
7622
|
+
"</section>";
|
|
7623
|
+
return _wrap({
|
|
7624
|
+
title: "Back-in-stock alert",
|
|
7625
|
+
shop_name: opts.shop_name || "blamejs.shop",
|
|
7626
|
+
cart_count: opts.cart_count,
|
|
7627
|
+
theme_css: opts.theme_css,
|
|
7628
|
+
robots: "noindex",
|
|
7629
|
+
body: body,
|
|
7630
|
+
});
|
|
7631
|
+
}
|
|
7632
|
+
|
|
7633
|
+
function renderStockAlertError(opts) {
|
|
7634
|
+
opts = opts || {};
|
|
7635
|
+
var body =
|
|
7636
|
+
"<section class=\"newsletter-thanks\">" +
|
|
7637
|
+
"<div class=\"newsletter-thanks__card\">" +
|
|
7638
|
+
"<p class=\"eyebrow\">Back-in-stock alert</p>" +
|
|
7639
|
+
"<h1 class=\"newsletter-thanks__title\">Couldn't set that alert.</h1>" +
|
|
7640
|
+
"<p class=\"newsletter-thanks__lede\">" + b.template.escapeHtml(String(opts.message || "Check the email address and try again — only RFC-shape email addresses are accepted.")) + "</p>" +
|
|
7641
|
+
"<div class=\"newsletter-thanks__cta\">" +
|
|
7642
|
+
"<a href=\"/\" class=\"btn-primary\">Back to the shop <span aria-hidden=\"true\">→</span></a>" +
|
|
7643
|
+
"</div>" +
|
|
7644
|
+
"</div>" +
|
|
7645
|
+
"</section>";
|
|
7646
|
+
return _wrap({
|
|
7647
|
+
title: "Back-in-stock alert",
|
|
7648
|
+
shop_name: opts.shop_name || "blamejs.shop",
|
|
7649
|
+
cart_count: opts.cart_count,
|
|
7650
|
+
theme_css: opts.theme_css,
|
|
7651
|
+
robots: "noindex",
|
|
7652
|
+
body: body,
|
|
7653
|
+
});
|
|
7654
|
+
}
|
|
7655
|
+
|
|
7656
|
+
// The confirm-link landing page. `opts.outcome` is one of: "confirmed"
|
|
7657
|
+
// (we just stamped confirmed_at), "already-notified" (the alert already
|
|
7658
|
+
// fired — terminal), "invalid" (null / expired / unknown token). No token
|
|
7659
|
+
// is echoed back; the outcome page reveals nothing beyond friendly copy.
|
|
7660
|
+
function renderStockAlertConfirm(opts) {
|
|
7661
|
+
opts = opts || {};
|
|
7662
|
+
var heading, lede;
|
|
7663
|
+
if (opts.outcome === "confirmed") {
|
|
7664
|
+
heading = "You're all set.";
|
|
7665
|
+
lede = "Your back-in-stock alert is confirmed. We'll email you once — the moment this item returns. Nothing else.";
|
|
7666
|
+
} else if (opts.outcome === "already-notified") {
|
|
7667
|
+
heading = "You've already been notified.";
|
|
7668
|
+
lede = "This item was already back in stock and we emailed you about it. There's nothing more to do.";
|
|
7669
|
+
} else {
|
|
7670
|
+
heading = "This link is no longer valid.";
|
|
7671
|
+
lede = "We couldn't match this confirmation link to an alert. It may have expired, or the link may be incomplete. You can set a fresh alert from the product page.";
|
|
7672
|
+
}
|
|
7673
|
+
var body =
|
|
7674
|
+
"<section class=\"newsletter-thanks\">" +
|
|
7675
|
+
"<div class=\"newsletter-thanks__card\">" +
|
|
7676
|
+
"<p class=\"eyebrow\">Back-in-stock alert</p>" +
|
|
7677
|
+
"<h1 class=\"newsletter-thanks__title\">" + heading + "</h1>" +
|
|
7678
|
+
"<p class=\"newsletter-thanks__lede\">" + lede + "</p>" +
|
|
7679
|
+
"<div class=\"newsletter-thanks__cta\">" +
|
|
7680
|
+
"<a href=\"/\" class=\"btn-primary\">Back to the shop <span aria-hidden=\"true\">→</span></a>" +
|
|
7681
|
+
"</div>" +
|
|
7682
|
+
"</div>" +
|
|
7683
|
+
"</section>";
|
|
7684
|
+
return _wrap({
|
|
7685
|
+
title: "Back-in-stock alert",
|
|
7686
|
+
shop_name: opts.shop_name || "blamejs.shop",
|
|
7687
|
+
cart_count: opts.cart_count,
|
|
7688
|
+
theme_css: opts.theme_css,
|
|
7689
|
+
robots: "noindex",
|
|
7690
|
+
body: body,
|
|
7691
|
+
});
|
|
7692
|
+
}
|
|
7693
|
+
|
|
7694
|
+
// The unsubscribe confirm page (GET). A no-JS form POSTs the (email, sku,
|
|
7695
|
+
// variant) tuple back to /stock-alert/unsubscribe — a deliberate "yes,
|
|
7696
|
+
// stop these alerts" step (no link-prefetcher unsubscribe). The tuple
|
|
7697
|
+
// rides in hidden fields (HTML-escaped). This page is container-rendered
|
|
7698
|
+
// from a GET, so it CAN carry the `_csrf` token — the action is NOT in
|
|
7699
|
+
// EDGE_POST_PATHS, so `_injectCsrfFields` tokens it automatically.
|
|
7700
|
+
function renderStockAlertUnsubscribeConfirm(opts) {
|
|
7701
|
+
opts = opts || {};
|
|
7702
|
+
var email = typeof opts.email === "string" ? opts.email : "";
|
|
7703
|
+
var sku = typeof opts.sku === "string" ? opts.sku : "";
|
|
7704
|
+
var variantId = typeof opts.variant_id === "string" ? opts.variant_id : "";
|
|
7705
|
+
var body =
|
|
7706
|
+
"<section class=\"newsletter-thanks\">" +
|
|
7707
|
+
"<div class=\"newsletter-thanks__card\">" +
|
|
7708
|
+
"<p class=\"eyebrow\">Back-in-stock alert</p>" +
|
|
7709
|
+
"<h1 class=\"newsletter-thanks__title\">Stop this alert?</h1>" +
|
|
7710
|
+
"<p class=\"newsletter-thanks__lede\">Confirm below and we'll cancel your back-in-stock alert for this item. You can set it again any time from the product page.</p>" +
|
|
7711
|
+
"<form class=\"newsletter-unsub__form\" method=\"post\" action=\"/stock-alert/unsubscribe\">" +
|
|
7712
|
+
"<input type=\"hidden\" name=\"email\" value=\"" + _escAttr(email) + "\">" +
|
|
7713
|
+
"<input type=\"hidden\" name=\"sku\" value=\"" + _escAttr(sku) + "\">" +
|
|
7714
|
+
(variantId ? "<input type=\"hidden\" name=\"variant_id\" value=\"" + _escAttr(variantId) + "\">" : "") +
|
|
7715
|
+
"<div class=\"newsletter-thanks__cta\">" +
|
|
7716
|
+
"<button type=\"submit\" class=\"btn-primary\">Stop this alert</button>" +
|
|
7717
|
+
"<a href=\"/\" class=\"btn-ghost\">Keep me subscribed</a>" +
|
|
7718
|
+
"</div>" +
|
|
7719
|
+
"</form>" +
|
|
7720
|
+
"</div>" +
|
|
7721
|
+
"</section>";
|
|
7722
|
+
return _wrap({
|
|
7723
|
+
title: "Unsubscribe",
|
|
7724
|
+
shop_name: opts.shop_name || "blamejs.shop",
|
|
7725
|
+
cart_count: opts.cart_count,
|
|
7726
|
+
theme_css: opts.theme_css,
|
|
7727
|
+
robots: "noindex",
|
|
7728
|
+
body: body,
|
|
7729
|
+
});
|
|
7730
|
+
}
|
|
7731
|
+
|
|
7732
|
+
// The unsubscribe outcome page (POST). Idempotent — a missing row reads as
|
|
7733
|
+
// "you're unsubscribed" (re-clicking the link twice is not an error). A bad
|
|
7734
|
+
// shape reads as the generic non-leaking copy.
|
|
7735
|
+
function renderStockAlertUnsubscribeResult(opts) {
|
|
7736
|
+
opts = opts || {};
|
|
7737
|
+
var heading, lede;
|
|
7738
|
+
if (opts.outcome === "invalid") {
|
|
7739
|
+
heading = "This link isn't valid.";
|
|
7740
|
+
lede = "We couldn't match this unsubscribe link to an alert. It may already have been used, or the link may be incomplete.";
|
|
7741
|
+
} else {
|
|
7742
|
+
heading = "You're unsubscribed.";
|
|
7743
|
+
lede = "We won't email you about this item coming back in stock. Changed your mind? Set a fresh alert from the product page.";
|
|
7744
|
+
}
|
|
7745
|
+
var body =
|
|
7746
|
+
"<section class=\"newsletter-thanks\">" +
|
|
7747
|
+
"<div class=\"newsletter-thanks__card\">" +
|
|
7748
|
+
"<p class=\"eyebrow\">Back-in-stock alert</p>" +
|
|
7749
|
+
"<h1 class=\"newsletter-thanks__title\">" + heading + "</h1>" +
|
|
7750
|
+
"<p class=\"newsletter-thanks__lede\">" + lede + "</p>" +
|
|
7751
|
+
"<div class=\"newsletter-thanks__cta\">" +
|
|
7752
|
+
"<a href=\"/\" class=\"btn-primary\">Back to the shop <span aria-hidden=\"true\">→</span></a>" +
|
|
7753
|
+
"</div>" +
|
|
7754
|
+
"</div>" +
|
|
7755
|
+
"</section>";
|
|
7756
|
+
return _wrap({
|
|
7757
|
+
title: "Unsubscribe",
|
|
7758
|
+
shop_name: opts.shop_name || "blamejs.shop",
|
|
7759
|
+
cart_count: opts.cart_count,
|
|
7760
|
+
theme_css: opts.theme_css,
|
|
7761
|
+
robots: "noindex",
|
|
7762
|
+
body: body,
|
|
7763
|
+
});
|
|
7764
|
+
}
|
|
7765
|
+
|
|
7565
7766
|
// ---- cookie preference center ------------------------------------------
|
|
7566
7767
|
|
|
7567
7768
|
// The four toggleable categories + their operator-facing copy, mirroring
|
|
@@ -9065,6 +9266,42 @@ function mount(router, deps) {
|
|
|
9065
9266
|
return lines.length;
|
|
9066
9267
|
}
|
|
9067
9268
|
|
|
9269
|
+
// Resolve the active trust badges for a container-only placement and
|
|
9270
|
+
// concatenate each one's sanitized renderHtml. Fires an impression per
|
|
9271
|
+
// rendered badge (fire-and-forget — the counter is drop-silent on the hot
|
|
9272
|
+
// path; never await it into the response). Drop-silent on ANY read failure
|
|
9273
|
+
// (returns "") — badges are supplementary; an absent dep / unmigrated table
|
|
9274
|
+
// / read error must never 500 a checkout or order page. Placements wired
|
|
9275
|
+
// today: "checkout", "order_confirmation". The svg_payload was sanitized at
|
|
9276
|
+
// define time via b.guardSvg, so renderHtml's inline emit is safe.
|
|
9277
|
+
async function _trustBadgesHtml(placement, req) {
|
|
9278
|
+
if (!deps.trustBadges) return "";
|
|
9279
|
+
try {
|
|
9280
|
+
var active = await deps.trustBadges.activeForPlacement({ placement: placement });
|
|
9281
|
+
if (!Array.isArray(active) || active.length === 0) return "";
|
|
9282
|
+
var sid = null;
|
|
9283
|
+
try { sid = _readSidCookie(req); } catch (_e) { sid = null; }
|
|
9284
|
+
var parts = [];
|
|
9285
|
+
for (var i = 0; i < active.length; i += 1) {
|
|
9286
|
+
var slug = active[i].slug;
|
|
9287
|
+
var html;
|
|
9288
|
+
try { html = await deps.trustBadges.renderHtml({ slug: slug }); }
|
|
9289
|
+
catch (_e) { continue; /* drop-silent — skip a badge that fails to render */ }
|
|
9290
|
+
parts.push(html);
|
|
9291
|
+
// Fire-and-forget impression — the method is already drop-silent.
|
|
9292
|
+
try {
|
|
9293
|
+
var imp = deps.trustBadges.recordImpression({ slug: slug, placement: placement, session_id: sid || undefined });
|
|
9294
|
+
if (imp && typeof imp.then === "function") imp.then(function () {}, function () {});
|
|
9295
|
+
} catch (_e) { /* drop-silent — impression bump must not affect render */ }
|
|
9296
|
+
}
|
|
9297
|
+
if (!parts.length) return "";
|
|
9298
|
+
return "<section class=\"trust-badges trust-badges--" + b.template.escapeHtml(placement) +
|
|
9299
|
+
"\" aria-label=\"Trust badges\">" + parts.join("") + "</section>";
|
|
9300
|
+
} catch (_e) {
|
|
9301
|
+
return ""; // drop-silent — supplementary; never 500 the page
|
|
9302
|
+
}
|
|
9303
|
+
}
|
|
9304
|
+
|
|
9068
9305
|
// ---- multi-currency display -------------------------------------------
|
|
9069
9306
|
//
|
|
9070
9307
|
// Opt-in: wired only when the operator supplies `deps.currencyDisplay`
|
|
@@ -10381,6 +10618,54 @@ function mount(router, deps) {
|
|
|
10381
10618
|
return (await deps.catalog.batch.searchDecorate({ terms: terms, currency: "USD" })).rows;
|
|
10382
10619
|
}
|
|
10383
10620
|
|
|
10621
|
+
// Operator-tunable rerank of the matched search universe through
|
|
10622
|
+
// searchRanking.applyToResults (pins first, then weighted score DESC).
|
|
10623
|
+
// The decorated rows carry `id` + `in_stock` (bool) + `price_minor` but NOT
|
|
10624
|
+
// `product_id`, which applyToResults requires — project each row with
|
|
10625
|
+
// `product_id: row.id` + a `signals` bag built from the decorated fields.
|
|
10626
|
+
// applyToResults returns rows that preserve every original field plus
|
|
10627
|
+
// product_id/_score/_pinned, so the returned array IS the reranked universe
|
|
10628
|
+
// (the renderer reads its existing fields; the extra keys are inert).
|
|
10629
|
+
//
|
|
10630
|
+
// NEVER-500: ranking is supplementary to search. A missing dep, no active
|
|
10631
|
+
// weight set, a bad/archived weight slug, or any throw → return the
|
|
10632
|
+
// original universe unchanged (drop-silent defensive read). applyToResults
|
|
10633
|
+
// with no active set + no slug is a safe no-op that preserves input order,
|
|
10634
|
+
// so the common "operator hasn't configured ranking" path is inert.
|
|
10635
|
+
//
|
|
10636
|
+
// NOT dual-render — the edge /search (worker/render/search.js) does NOT
|
|
10637
|
+
// rerank. Edge-cached search serves the default order; the container path
|
|
10638
|
+
// serves the ranked order. This deliberate non-parity matches the
|
|
10639
|
+
// synonyms/facets precedent (both container-only at the edge) — search order
|
|
10640
|
+
// is not a price/legal contract, so order differences are acceptable.
|
|
10641
|
+
async function _rerankUniverse(universe, query) {
|
|
10642
|
+
if (!deps.searchRanking || !Array.isArray(universe) || universe.length === 0) {
|
|
10643
|
+
return universe;
|
|
10644
|
+
}
|
|
10645
|
+
try {
|
|
10646
|
+
var projected = universe.map(function (r) {
|
|
10647
|
+
return Object.assign({}, r, {
|
|
10648
|
+
product_id: r.id,
|
|
10649
|
+
signals: {
|
|
10650
|
+
in_stock: r.in_stock === true,
|
|
10651
|
+
// price_minor is a pass-through integer signal (never divided —
|
|
10652
|
+
// no money arithmetic); a null price contributes nothing.
|
|
10653
|
+
price_minor: (typeof r.price_minor === "number" && isFinite(r.price_minor)) ? r.price_minor : 0,
|
|
10654
|
+
},
|
|
10655
|
+
});
|
|
10656
|
+
});
|
|
10657
|
+
var ranked = await deps.searchRanking.applyToResults({
|
|
10658
|
+
query: (typeof query === "string" && query.trim().length) ? query : null,
|
|
10659
|
+
results: projected,
|
|
10660
|
+
});
|
|
10661
|
+
return Array.isArray(ranked) && ranked.length === universe.length ? ranked : universe;
|
|
10662
|
+
} catch (_e) {
|
|
10663
|
+
// Bad weight slug, archived set, or any ranking failure → un-ranked
|
|
10664
|
+
// fallback. Never 500 the search page.
|
|
10665
|
+
return universe;
|
|
10666
|
+
}
|
|
10667
|
+
}
|
|
10668
|
+
|
|
10384
10669
|
router.get("/search", async function (req, res) {
|
|
10385
10670
|
var url = req.url ? new URL(req.url, "http://localhost") : null;
|
|
10386
10671
|
var qRaw = url && url.searchParams.get("q");
|
|
@@ -10425,6 +10710,12 @@ function mount(router, deps) {
|
|
|
10425
10710
|
// in server.js (the searchFacets primitive's `create`, with
|
|
10426
10711
|
// the DB query handle pre-bound).
|
|
10427
10712
|
var universe = await _facetableUniverse(terms);
|
|
10713
|
+
// Operator-tunable rerank — applied to the FULL matched universe
|
|
10714
|
+
// BEFORE the facet adapter windows it, so pins + weights reorder
|
|
10715
|
+
// across ALL pages (searchFacets.previewQuery filters in input order
|
|
10716
|
+
// and slices, preserving the reranked order). Container-only; never
|
|
10717
|
+
// 500s the search page (see _rerankUniverse).
|
|
10718
|
+
universe = await _rerankUniverse(universe, q);
|
|
10428
10719
|
var facetCatalog = {
|
|
10429
10720
|
// The primitive narrows in-memory and drops the focal facet
|
|
10430
10721
|
// per option, so the adapter ignores applied_filters and
|
|
@@ -10454,6 +10745,8 @@ function mount(router, deps) {
|
|
|
10454
10745
|
// matched universe gives the honest total; the page is the
|
|
10455
10746
|
// windowed slice (the same page size the faceted path uses).
|
|
10456
10747
|
var flatUniverse = await _facetableUniverse(terms);
|
|
10748
|
+
// Operator-tunable rerank before windowing — full control of order.
|
|
10749
|
+
flatUniverse = await _rerankUniverse(flatUniverse, q);
|
|
10457
10750
|
totalCount = flatUniverse.length;
|
|
10458
10751
|
page = _clampPage(page, totalCount, SEARCH_PAGE_SIZE);
|
|
10459
10752
|
products = flatUniverse.slice((page - 1) * SEARCH_PAGE_SIZE, page * SEARCH_PAGE_SIZE);
|
|
@@ -11453,12 +11746,16 @@ function mount(router, deps) {
|
|
|
11453
11746
|
// governs every OTHER route. setHeader OVERWRITES the app-level header
|
|
11454
11747
|
// for this response only.
|
|
11455
11748
|
res.setHeader && res.setHeader("content-security-policy", securityMiddleware.scopedCsp(["stripe"]));
|
|
11749
|
+
// Operator trust badges at the checkout placement (container-only;
|
|
11750
|
+
// drop-silent → "" on any failure).
|
|
11751
|
+
var payTrustBadges = await _trustBadgesHtml("checkout", req);
|
|
11456
11752
|
_send(res, 200, renderPayPage({
|
|
11457
11753
|
order: o,
|
|
11458
11754
|
client_secret: clientSecret,
|
|
11459
11755
|
publishable_key: pk,
|
|
11460
11756
|
shop_name: shopName,
|
|
11461
11757
|
theme: theme,
|
|
11758
|
+
trust_badges_html: payTrustBadges,
|
|
11462
11759
|
}));
|
|
11463
11760
|
});
|
|
11464
11761
|
|
|
@@ -11570,11 +11867,15 @@ function mount(router, deps) {
|
|
|
11570
11867
|
} catch (_e) { ratingRow = null; ratingEligible = false; }
|
|
11571
11868
|
}
|
|
11572
11869
|
var ordUrl = req.url ? new URL(req.url, "http://localhost") : null;
|
|
11870
|
+
// Operator trust badges at the order_confirmation placement (container-
|
|
11871
|
+
// only; drop-silent → "" on any failure).
|
|
11872
|
+
var ordTrustBadges = await _trustBadgesHtml("order_confirmation", req);
|
|
11573
11873
|
_send(res, 200, renderOrder({
|
|
11574
11874
|
order: o,
|
|
11575
11875
|
product_lookup: productLookup,
|
|
11576
11876
|
recommendations: recommendations,
|
|
11577
11877
|
shipments: shipments,
|
|
11878
|
+
trust_badges_html: ordTrustBadges,
|
|
11578
11879
|
rating: ratingRow,
|
|
11579
11880
|
rating_eligible: ratingEligible,
|
|
11580
11881
|
rating_notice: ordUrl ? _ratingNoticeFor(ordUrl.searchParams.get("rate_err")) : null,
|
|
@@ -16081,6 +16382,150 @@ function mount(router, deps) {
|
|
|
16081
16382
|
});
|
|
16082
16383
|
}
|
|
16083
16384
|
|
|
16385
|
+
// ---- back-in-stock "Notify me" ------------------------------------------
|
|
16386
|
+
//
|
|
16387
|
+
// The PDP buy box shows a "Notify me when back in stock" form on every
|
|
16388
|
+
// sold-out SKU (dual-rendered, edge-cache-safe). It posts to the distinct
|
|
16389
|
+
// CSRF-exempt action /stock-alert/subscribe (in EDGE_POST_PATHS). Double
|
|
16390
|
+
// opt-in: subscribe writes a pending row + returns a one-time plaintext
|
|
16391
|
+
// token, the confirmation email carries /stock-alert/confirm/<token>, and
|
|
16392
|
+
// the Worker cron sweep emails once when stock returns. Mount only when the
|
|
16393
|
+
// stockAlerts primitive is wired.
|
|
16394
|
+
if (deps.stockAlerts) {
|
|
16395
|
+
// Confirm-link base — SHOP_ORIGIN when present (set into sfDeps as
|
|
16396
|
+
// shop_origin in server.js), else the request Host (the email is best-
|
|
16397
|
+
// effort anyway; a relative path still resolves in a browser).
|
|
16398
|
+
function _stockAlertOrigin(req) {
|
|
16399
|
+
if (typeof deps.shop_origin === "string" && deps.shop_origin) {
|
|
16400
|
+
return deps.shop_origin.replace(/\/$/, "");
|
|
16401
|
+
}
|
|
16402
|
+
var host = req && req.headers && req.headers.host;
|
|
16403
|
+
return host ? "https://" + host : "";
|
|
16404
|
+
}
|
|
16405
|
+
|
|
16406
|
+
router.post("/stock-alert/subscribe", async function (req, res) {
|
|
16407
|
+
var body = req.body || {};
|
|
16408
|
+
var cartCount = 0;
|
|
16409
|
+
try { cartCount = await _cartCountForReq(req); } catch (_e) { /* drop-silent — empty cart fallback */ }
|
|
16410
|
+
try {
|
|
16411
|
+
var result = await deps.stockAlerts.subscribe({
|
|
16412
|
+
email: body.email,
|
|
16413
|
+
sku: body.sku,
|
|
16414
|
+
variant_id: (body.variant_id != null && body.variant_id !== "") ? body.variant_id : null,
|
|
16415
|
+
});
|
|
16416
|
+
// Only a brand-new subscription carries a plaintext token. Send the
|
|
16417
|
+
// confirmation email best-effort; a mailer hiccup must not 500 the
|
|
16418
|
+
// form (the row is already written). already-pending / already-
|
|
16419
|
+
// confirmed render the SAME thank-you copy — never reveal prior state.
|
|
16420
|
+
if (result && result.status === "subscribed" && result.confirmation_token && deps.email) {
|
|
16421
|
+
var origin = _stockAlertOrigin(req);
|
|
16422
|
+
var confirmUrl = origin + "/stock-alert/confirm/" + encodeURIComponent(result.confirmation_token);
|
|
16423
|
+
// Resolve the product title for the SKU (cheap indexed read; drop-
|
|
16424
|
+
// silent → fall back to the SKU as the title).
|
|
16425
|
+
var titleForSku = body.sku;
|
|
16426
|
+
try {
|
|
16427
|
+
if (deps.catalog && deps.catalog.products && typeof deps.catalog.products.bySku === "function") {
|
|
16428
|
+
var prodForSku = await deps.catalog.products.bySku(body.sku);
|
|
16429
|
+
if (prodForSku && prodForSku.title) titleForSku = prodForSku.title;
|
|
16430
|
+
}
|
|
16431
|
+
} catch (_e) { /* drop-silent — fall back to the SKU as the title */ }
|
|
16432
|
+
try {
|
|
16433
|
+
await deps.email.sendStockAlertConfirmation({
|
|
16434
|
+
to: body.email,
|
|
16435
|
+
product_title: titleForSku,
|
|
16436
|
+
sku: body.sku,
|
|
16437
|
+
confirm_url: confirmUrl,
|
|
16438
|
+
});
|
|
16439
|
+
} catch (_mailErr) { /* drop-silent — the row is written; the cron still fires on confirm */ }
|
|
16440
|
+
}
|
|
16441
|
+
return _send(res, 200, renderStockAlertThanks({ shop_name: shopName, cart_count: cartCount }));
|
|
16442
|
+
} catch (e) {
|
|
16443
|
+
// TypeError == operator/customer-fault input refusal (bad email / sku
|
|
16444
|
+
// shape) → 400 friendly page; everything else (D1 unreachable) → 500
|
|
16445
|
+
// non-leaking page.
|
|
16446
|
+
var isInputError = e instanceof TypeError;
|
|
16447
|
+
return _send(res, isInputError ? 400 : 500, renderStockAlertError({
|
|
16448
|
+
shop_name: shopName,
|
|
16449
|
+
cart_count: cartCount,
|
|
16450
|
+
message: isInputError ? "That doesn't look like a valid email address. Check the format and try again." : null,
|
|
16451
|
+
}));
|
|
16452
|
+
}
|
|
16453
|
+
});
|
|
16454
|
+
|
|
16455
|
+
router.get("/stock-alert/confirm/:token", async function (req, res) {
|
|
16456
|
+
var token = req.params && req.params.token;
|
|
16457
|
+
var cartCount = 0;
|
|
16458
|
+
try { cartCount = await _cartCountForReq(req); } catch (_e) { /* drop-silent — empty cart fallback */ }
|
|
16459
|
+
var outcome;
|
|
16460
|
+
try {
|
|
16461
|
+
var row = await deps.stockAlerts.confirm({ token: token });
|
|
16462
|
+
if (!row) outcome = "invalid";
|
|
16463
|
+
else if (row.notified_at != null) outcome = "already-notified";
|
|
16464
|
+
else outcome = "confirmed";
|
|
16465
|
+
} catch (e) {
|
|
16466
|
+
// A bad-shape token throws TypeError — render the generic non-leaking
|
|
16467
|
+
// "invalid" page rather than a 500 on a public, unauthenticated route.
|
|
16468
|
+
if (e instanceof TypeError) outcome = "invalid";
|
|
16469
|
+
else {
|
|
16470
|
+
_log.error("storefront stock-alert confirm failed", {
|
|
16471
|
+
route: "/stock-alert/confirm", request_id: (req && req.requestId) || null,
|
|
16472
|
+
err: (e && e.message) || String(e),
|
|
16473
|
+
});
|
|
16474
|
+
outcome = "invalid";
|
|
16475
|
+
}
|
|
16476
|
+
}
|
|
16477
|
+
return _send(res, 200, renderStockAlertConfirm({
|
|
16478
|
+
shop_name: shopName, cart_count: cartCount, outcome: outcome,
|
|
16479
|
+
}));
|
|
16480
|
+
});
|
|
16481
|
+
|
|
16482
|
+
// Unsubscribe — by (email, sku, variant) tuple carried in the link query
|
|
16483
|
+
// (stockAlerts has no unsubscribe-token API). GET renders a friendly
|
|
16484
|
+
// confirm page (token-carrying container form); POST consumes it.
|
|
16485
|
+
router.get("/stock-alert/unsubscribe", async function (req, res) {
|
|
16486
|
+
var q = (req.query && typeof req.query === "object") ? req.query : {};
|
|
16487
|
+
var cartCount = 0;
|
|
16488
|
+
try { cartCount = await _cartCountForReq(req); } catch (_e) { /* drop-silent — empty cart fallback */ }
|
|
16489
|
+
return _send(res, 200, renderStockAlertUnsubscribeConfirm({
|
|
16490
|
+
shop_name: shopName,
|
|
16491
|
+
cart_count: cartCount,
|
|
16492
|
+
email: typeof q.email === "string" ? q.email : "",
|
|
16493
|
+
sku: typeof q.sku === "string" ? q.sku : "",
|
|
16494
|
+
variant_id: typeof q.variant_id === "string" ? q.variant_id : "",
|
|
16495
|
+
}));
|
|
16496
|
+
});
|
|
16497
|
+
|
|
16498
|
+
router.post("/stock-alert/unsubscribe", async function (req, res) {
|
|
16499
|
+
var body = req.body || {};
|
|
16500
|
+
var cartCount = 0;
|
|
16501
|
+
try { cartCount = await _cartCountForReq(req); } catch (_e) { /* drop-silent — empty cart fallback */ }
|
|
16502
|
+
var outcome;
|
|
16503
|
+
try {
|
|
16504
|
+
// Idempotent — a missing row returns { removed: false }, which still
|
|
16505
|
+
// reads as "you're unsubscribed" (clicking the link twice is fine).
|
|
16506
|
+
await deps.stockAlerts.unsubscribe({
|
|
16507
|
+
email: body.email,
|
|
16508
|
+
sku: body.sku,
|
|
16509
|
+
variant_id: (body.variant_id != null && body.variant_id !== "") ? body.variant_id : null,
|
|
16510
|
+
});
|
|
16511
|
+
outcome = "unsubscribed";
|
|
16512
|
+
} catch (e) {
|
|
16513
|
+
// A bad-shape tuple throws TypeError → generic non-leaking page; a
|
|
16514
|
+
// real fault degrades the same way rather than 500-ing a public route.
|
|
16515
|
+
outcome = "invalid";
|
|
16516
|
+
if (!(e instanceof TypeError)) {
|
|
16517
|
+
_log.error("storefront stock-alert unsubscribe failed", {
|
|
16518
|
+
route: "/stock-alert/unsubscribe", request_id: (req && req.requestId) || null,
|
|
16519
|
+
err: (e && e.message) || String(e),
|
|
16520
|
+
});
|
|
16521
|
+
}
|
|
16522
|
+
}
|
|
16523
|
+
return _send(res, 200, renderStockAlertUnsubscribeResult({
|
|
16524
|
+
shop_name: shopName, cart_count: cartCount, outcome: outcome,
|
|
16525
|
+
}));
|
|
16526
|
+
});
|
|
16527
|
+
}
|
|
16528
|
+
|
|
16084
16529
|
// ---- cookie consent -----------------------------------------------------
|
|
16085
16530
|
//
|
|
16086
16531
|
// GDPR (EU 2016/679 art. 6 + 7) + ePrivacy (2002/58/EC art. 5(3)) opt-in
|