@blamejs/blamejs-shop 0.1.37 → 0.2.0

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.
Files changed (73) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/lib/admin.js +205 -42
  3. package/lib/asset-manifest.json +5 -5
  4. package/lib/storefront.js +428 -51
  5. package/lib/vendor/MANIFEST.json +2 -2
  6. package/lib/vendor/blamejs/CHANGELOG.md +12 -0
  7. package/lib/vendor/blamejs/README.md +2 -2
  8. package/lib/vendor/blamejs/SECURITY.md +1 -0
  9. package/lib/vendor/blamejs/api-snapshot.json +2 -2
  10. package/lib/vendor/blamejs/lib/ai-disclosure.js +2 -3
  11. package/lib/vendor/blamejs/lib/archive-gz.js +5 -3
  12. package/lib/vendor/blamejs/lib/archive-read.js +101 -35
  13. package/lib/vendor/blamejs/lib/archive-tar-read.js +86 -31
  14. package/lib/vendor/blamejs/lib/archive-tar.js +2 -3
  15. package/lib/vendor/blamejs/lib/auth/fal.js +12 -0
  16. package/lib/vendor/blamejs/lib/auth/jwt-external.js +15 -11
  17. package/lib/vendor/blamejs/lib/auth/jwt.js +2 -2
  18. package/lib/vendor/blamejs/lib/auth/oauth.js +7 -6
  19. package/lib/vendor/blamejs/lib/auth/oid4vci.js +3 -3
  20. package/lib/vendor/blamejs/lib/auth/saml.js +15 -12
  21. package/lib/vendor/blamejs/lib/auth/sd-jwt-vc.js +3 -3
  22. package/lib/vendor/blamejs/lib/backup/index.js +2 -3
  23. package/lib/vendor/blamejs/lib/calendar.js +9 -2
  24. package/lib/vendor/blamejs/lib/circuit-breaker.js +5 -4
  25. package/lib/vendor/blamejs/lib/cose.js +4 -3
  26. package/lib/vendor/blamejs/lib/crypto-hpke.js +1 -1
  27. package/lib/vendor/blamejs/lib/crypto-oprf.js +2 -2
  28. package/lib/vendor/blamejs/lib/crypto.js +2 -2
  29. package/lib/vendor/blamejs/lib/framework-error.js +2 -1
  30. package/lib/vendor/blamejs/lib/guard-jwt.js +3 -2
  31. package/lib/vendor/blamejs/lib/guard-smtp-command.js +2 -2
  32. package/lib/vendor/blamejs/lib/mail-auth.js +2 -2
  33. package/lib/vendor/blamejs/lib/mail-crypto-pgp.js +1 -1
  34. package/lib/vendor/blamejs/lib/mail-crypto-smime.js +7 -7
  35. package/lib/vendor/blamejs/lib/mail-crypto.js +1 -1
  36. package/lib/vendor/blamejs/lib/mail-dav.js +5 -4
  37. package/lib/vendor/blamejs/lib/mail-deploy.js +3 -2
  38. package/lib/vendor/blamejs/lib/mail-server-imap.js +1 -1
  39. package/lib/vendor/blamejs/lib/mail-server-jmap.js +10 -10
  40. package/lib/vendor/blamejs/lib/mail-server-managesieve.js +1 -1
  41. package/lib/vendor/blamejs/lib/mail-server-mx.js +142 -47
  42. package/lib/vendor/blamejs/lib/mail-server-submission.js +6 -5
  43. package/lib/vendor/blamejs/lib/mail-store.js +2 -2
  44. package/lib/vendor/blamejs/lib/mail.js +2 -2
  45. package/lib/vendor/blamejs/lib/mdoc.js +14 -14
  46. package/lib/vendor/blamejs/lib/network-dnssec.js +10 -8
  47. package/lib/vendor/blamejs/lib/network-tls.js +10 -7
  48. package/lib/vendor/blamejs/lib/safe-decompress.js +8 -6
  49. package/lib/vendor/blamejs/lib/safe-ical.js +12 -12
  50. package/lib/vendor/blamejs/lib/safe-mime.js +6 -6
  51. package/lib/vendor/blamejs/lib/safe-sieve.js +1 -1
  52. package/lib/vendor/blamejs/lib/safe-smtp.js +1 -1
  53. package/lib/vendor/blamejs/package.json +1 -1
  54. package/lib/vendor/blamejs/release-notes/v0.13.10.json +44 -0
  55. package/lib/vendor/blamejs/release-notes/v0.13.11.json +27 -0
  56. package/lib/vendor/blamejs/release-notes/v0.13.12.json +36 -0
  57. package/lib/vendor/blamejs/release-notes/v0.13.7.json +27 -0
  58. package/lib/vendor/blamejs/release-notes/v0.13.8.json +27 -0
  59. package/lib/vendor/blamejs/release-notes/v0.13.9.json +27 -0
  60. package/lib/vendor/blamejs/test/layer-0-primitives/archive-read.test.js +82 -0
  61. package/lib/vendor/blamejs/test/layer-0-primitives/auth-jwt-defenses.test.js +4 -4
  62. package/lib/vendor/blamejs/test/layer-0-primitives/calendar.test.js +31 -0
  63. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +188 -6
  64. package/lib/vendor/blamejs/test/layer-0-primitives/fal.test.js +26 -0
  65. package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto-smime.test.js +5 -5
  66. package/lib/vendor/blamejs/test/layer-0-primitives/mail-dav.test.js +1 -1
  67. package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-mx.test.js +166 -1
  68. package/lib/vendor/blamejs/test/layer-0-primitives/rate-limit-cluster.test.js +25 -18
  69. package/lib/vendor/blamejs/test/layer-0-primitives/safe-ical.test.js +2 -2
  70. package/lib/vendor/blamejs/test/layer-0-primitives/sandbox.test.js +12 -12
  71. package/lib/vendor/blamejs/test/layer-0-primitives/scheduler-exactly-once.test.js +15 -9
  72. package/lib/vendor/blamejs/test/layer-0-primitives/websocket-channels.test.js +0 -6
  73. package/package.json +1 -1
package/lib/storefront.js CHANGED
@@ -548,12 +548,12 @@ var HOME_HERO =
548
548
  " </div>\n" +
549
549
  " <div class=\"hero__inner\">\n" +
550
550
  " <div class=\"hero__copy\">\n" +
551
- " <p class=\"eyebrow eyebrow--on-dark\"><span class=\"dot dot--accent\" aria-hidden=\"true\"></span> Open-source ecommerce framework · v" + require("../package.json").version + "</p>\n" +
552
- " <h1 class=\"hero__title\">Run a shop that owes <span class=\"accent\">nothing</span> to the dependency graph.</h1>\n" +
553
- " <p class=\"hero__lede\">Server-rendered HTML. Post-quantum crypto on by default. Zero npm runtime dependencies. Every primitive is composed from a single vendored framework no transitive supply chain to audit.</p>\n" +
551
+ " <p class=\"eyebrow eyebrow--on-dark\">~/blamejs.shop secure commerce · v" + require("../package.json").version + "</p>\n" +
552
+ " <h1 class=\"hero__title\">Sell anything.<br>Trust <span class=\"glitch glitch--live\" data-text=\"nothing.\">nothing.</span><span class=\"term-cursor\" aria-hidden=\"true\"></span></h1>\n" +
553
+ " <p class=\"hero__lede\">An open-source, server-rendered ecommerce framework with post-quantum cryptography baked into every session, cart, and checkout. No client-side validation theater. No npm runtime dependencies. Just hardened HTML.</p>\n" +
554
554
  " <div class=\"hero__cta\">\n" +
555
- " <a href=\"#catalog\" class=\"btn-primary\">Browse the shop <span aria-hidden=\"true\">→</span></a>\n" +
556
- " <a href=\"https://github.com/blamejs/blamejs.shop\" class=\"btn-ghost btn-ghost--on-dark\" rel=\"noopener\">View on GitHub</a>\n" +
555
+ " <a href=\"#catalog\" class=\"btn-primary\">$ npx create-shop</a>\n" +
556
+ " <a href=\"https://github.com/blamejs/blamejs.shop\" class=\"btn-ghost btn-ghost--on-dark\" rel=\"noopener\">Read the threat model</a>\n" +
557
557
  " </div>\n" +
558
558
  " <dl class=\"hero__stats\">\n" +
559
559
  " <div><dt>Products live</dt><dd>{{product_count}}</dd></div>\n" +
@@ -865,8 +865,8 @@ var FACET_GROUP_HEAD =
865
865
 
866
866
  var FACET_OPTION =
867
867
  "<li class=\"facet-option\">\n" +
868
- " <a class=\"facet-option__link{{selected_class}}\" href=\"{{href}}\" rel=\"nofollow\"{{aria_pressed}}>\n" +
869
- " <span class=\"facet-option__box\" aria-hidden=\"true\">{{box}}</span>\n" +
868
+ " <a class=\"facet-option__link{{selected_class}}\" href=\"{{href}}\" rel=\"nofollow\"{{aria_current}}>\n" +
869
+ " <span class=\"facet-option__box\" aria-hidden=\"true\">{{box}}</span>{{selected_cue}}\n" +
870
870
  " <span class=\"facet-option__label\">{{label}}</span>\n" +
871
871
  " <span class=\"facet-option__count\">{{count}}</span>\n" +
872
872
  " </a>\n" +
@@ -921,11 +921,13 @@ function _renderSearchFacets(facets, filters, q) {
921
921
  optionsHtml += _render(FACET_OPTION, {
922
922
  href: _searchUrl(q, _toggleFilter(filters, facet.key, opt.value)),
923
923
  selected_class: opt.selected ? " is-selected" : "",
924
- aria_pressed: "RAW_ARIA",
924
+ aria_current: "RAW_ARIA",
925
925
  box: opt.selected ? "✓" : "",
926
+ selected_cue: "RAW_CUE",
926
927
  label: opt.label,
927
928
  count: String(opt.count),
928
- }).replace("RAW_ARIA", opt.selected ? " aria-pressed=\"true\"" : " aria-pressed=\"false\"");
929
+ }).replace("RAW_ARIA", opt.selected ? " aria-current=\"true\"" : "")
930
+ .replace("RAW_CUE", opt.selected ? "<span class=\"sr-only\">Selected: </span>" : "");
929
931
  rendered += 1;
930
932
  }
931
933
  if (rendered === 0) continue;
@@ -1102,6 +1104,91 @@ var VARIANT_ROW =
1102
1104
  " </td>\n" +
1103
1105
  "</tr>\n";
1104
1106
 
1107
+ // PDP buy-box. A single cart-add form posting `variant_id` + `qty` to
1108
+ // /cart/lines (unchanged endpoint + field names). Multi-variant
1109
+ // selection is server-rendered radio chips sharing `name="variant_id"`
1110
+ // — the checked radio is what POSTs, so variant choice works with zero
1111
+ // client JS. The lead price renders large + mono + violet; each chip
1112
+ // carries its own price so a shopper sees per-variant pricing before
1113
+ // they pick. Above twelve variants the chip wall gets unwieldy, so the
1114
+ // existing compact variant table (VARIANT_ROW) is the fallback — it
1115
+ // keeps a per-row add form, so the same endpoint contract holds.
1116
+ // `variants` is the pre-formatted array [{ id, sku, title, price }]
1117
+ // the renderers already build; `escAttr` is the path's HTML escaper.
1118
+ // Mirrored byte-for-byte by worker/render/product.js#_buildBuyBox.
1119
+ var BUYBOX_CHIP_LIMIT = 12;
1120
+
1121
+ function _buildBuyBox(variants, escAttr) {
1122
+ if (!variants || variants.length === 0) {
1123
+ return "<div class=\"pdp__variants\">\n" +
1124
+ " <h2 class=\"pdp__variants-title\">Choose a variant</h2>\n" +
1125
+ " <div class=\"table-scroll\">\n" +
1126
+ " <table class=\"variant-table\">\n" +
1127
+ " <thead><tr><th>Variant</th><th>SKU</th><th>Price</th><th class=\"variant-table__action-h\">Action</th></tr></thead>\n" +
1128
+ " <tbody><tr><td colspan=\"4\" class=\"empty\">No variants available.</td></tr></tbody>\n" +
1129
+ " </table>\n" +
1130
+ " </div>\n" +
1131
+ " </div>";
1132
+ }
1133
+
1134
+ var trustLine =
1135
+ "<div class=\"pdp__meta\">\n" +
1136
+ " <span class=\"pdp__badge\"><span class=\"shield\" aria-hidden=\"true\"><span class=\"shield__glyph\">&gt;_</span></span> Post-quantum secured checkout · ML-KEM-1024 key agreement · ML-DSA-65 receipt signature.</span>\n" +
1137
+ " </div>";
1138
+
1139
+ // Many variants → keep the compact table (still a per-row add form).
1140
+ if (variants.length > BUYBOX_CHIP_LIMIT) {
1141
+ var rows = variants.map(function (v) {
1142
+ return _render(VARIANT_ROW, { title: v.title, sku: v.sku, price: v.price, variant_id: v.id });
1143
+ }).join("");
1144
+ return "<div class=\"pdp__variants\">\n" +
1145
+ " <h2 class=\"pdp__variants-title\">Choose a variant</h2>\n" +
1146
+ " <div class=\"table-scroll\">\n" +
1147
+ " <table class=\"variant-table\">\n" +
1148
+ " <thead><tr><th>Variant</th><th>SKU</th><th>Price</th><th class=\"variant-table__action-h\">Action</th></tr></thead>\n" +
1149
+ " <tbody>" + rows + "</tbody>\n" +
1150
+ " </table>\n" +
1151
+ " </div>\n" +
1152
+ " </div>\n" +
1153
+ " " + trustLine;
1154
+ }
1155
+
1156
+ var lead = variants[0];
1157
+ var single = variants.length === 1;
1158
+ var chips = "";
1159
+ if (!single) {
1160
+ for (var i = 0; i < variants.length; i += 1) {
1161
+ var v = variants[i];
1162
+ chips +=
1163
+ "<label class=\"pdp__badge\">" +
1164
+ "<input type=\"radio\" name=\"variant_id\" value=\"" + escAttr(v.id) + "\"" + (i === 0 ? " checked" : "") + ">" +
1165
+ " <span class=\"variant-row__title\">" + escAttr(v.title) + "</span>" +
1166
+ " <span class=\"variant-row__sku\"><code>" + escAttr(v.sku) + "</code></span>" +
1167
+ " <span class=\"variant-row__price price\">" + escAttr(v.price) + "</span>" +
1168
+ "</label>";
1169
+ }
1170
+ }
1171
+
1172
+ var variantBlock = single
1173
+ ? "<p class=\"variant-row__sku\"><code>" + escAttr(lead.sku) + "</code></p>" +
1174
+ "<input type=\"hidden\" name=\"variant_id\" value=\"" + escAttr(lead.id) + "\">"
1175
+ : "<fieldset class=\"pdp__variants\">\n" +
1176
+ " <legend class=\"pdp__variants-title\">Choose a variant</legend>\n" +
1177
+ " <div class=\"pdp__meta\">" + chips + "</div>\n" +
1178
+ " </fieldset>";
1179
+
1180
+ return "<div class=\"pdp__buybox\">\n" +
1181
+ " <p class=\"featured-product__price\">" + escAttr(lead.price) + "</p>\n" +
1182
+ " <form method=\"post\" action=\"/cart/lines\">\n" +
1183
+ " " + variantBlock + "\n" +
1184
+ " <label class=\"pdp__variants-title\" for=\"buybox-qty\">Quantity</label>\n" +
1185
+ " <input id=\"buybox-qty\" type=\"number\" name=\"qty\" value=\"1\" min=\"1\" max=\"99\" class=\"variant-row__qty\" aria-label=\"Quantity\">\n" +
1186
+ " <button type=\"submit\" class=\"btn-primary cart-page__checkout\">$ add to cart</button>\n" +
1187
+ " </form>\n" +
1188
+ " </div>\n" +
1189
+ " " + trustLine;
1190
+ }
1191
+
1105
1192
  var PRODUCT_PAGE =
1106
1193
  "<section class=\"pdp\">\n" +
1107
1194
  " <nav class=\"breadcrumb\" aria-label=\"Breadcrumb\">\n" +
@@ -1118,18 +1205,10 @@ var PRODUCT_PAGE =
1118
1205
  " <p class=\"pdp__description\">{{description}}</p>\n" +
1119
1206
  " <div class=\"pdp__meta\">\n" +
1120
1207
  " <span class=\"pdp__badge pdp__badge--ok\"><span class=\"dot dot--live\" aria-hidden=\"true\"></span> In stock</span>\n" +
1121
- " <span class=\"pdp__badge\">Ships from origin</span>\n" +
1208
+ " <span class=\"pdp__badge\">Ships in 1–2 business days</span>\n" +
1122
1209
  " <span class=\"pdp__badge\">Stripe-secured checkout</span>\n" +
1123
1210
  " </div>\n" +
1124
- " <div class=\"pdp__variants\">\n" +
1125
- " <h2 class=\"pdp__variants-title\">Choose a variant</h2>\n" +
1126
- " <div class=\"table-scroll\">\n" +
1127
- " <table class=\"variant-table\">\n" +
1128
- " <thead><tr><th>Variant</th><th>SKU</th><th>Price</th><th class=\"variant-table__action-h\">Action</th></tr></thead>\n" +
1129
- " <tbody>{{variant_rows}}</tbody>\n" +
1130
- " </table>\n" +
1131
- " </div>\n" +
1132
- " </div>\n" +
1211
+ " RAW_BUYBOX_PLACEHOLDER\n" +
1133
1212
  " RAW_QTYBREAK_PLACEHOLDER\n" +
1134
1213
  " RAW_WISHLIST_PLACEHOLDER\n" +
1135
1214
  " RAW_COMPARE_PLACEHOLDER\n" +
@@ -1635,7 +1714,19 @@ function renderWishlist(opts) {
1635
1714
  }
1636
1715
  var inner = rowsHtml
1637
1716
  ? "<ul class=\"wishlist-list\">" + rowsHtml + "</ul>"
1638
- : "<p class=\"wishlist-empty\">You haven't saved anything yet. Browse the shop and tap <strong>Save to wishlist</strong> on products you want to keep an eye on.</p>";
1717
+ : "<div class=\"account-empty\">" +
1718
+ "<p class=\"account-empty__icon\" aria-hidden=\"true\">♡</p>" +
1719
+ "<p class=\"account-empty__lede\">You haven't saved anything yet. Browse the shop and tap <strong>Save to wishlist</strong> on products you want to keep an eye on.</p>" +
1720
+ "<a class=\"btn-secondary\" href=\"/\">Browse the shop →</a>" +
1721
+ "</div>";
1722
+ // Success confirmation after a wishlist toggle, surfaced via
1723
+ // role="status". Driven by the ?ok=<kind> redirect marker; unknown
1724
+ // keys render nothing so a forged query can't inject copy.
1725
+ var WISHLIST_OK = { added: "Saved to your wishlist.", removed: "Removed from your wishlist." };
1726
+ var okMsg = WISHLIST_OK[opts.notice];
1727
+ var notice = okMsg
1728
+ ? "<p class=\"form-notice form-notice--ok\" role=\"status\">" + esc(okMsg) + "</p>"
1729
+ : "";
1639
1730
  var body =
1640
1731
  "<section class=\"account-wishlist\">" +
1641
1732
  "<nav class=\"breadcrumb\" aria-label=\"Breadcrumb\"><ol>" +
@@ -1643,6 +1734,7 @@ function renderWishlist(opts) {
1643
1734
  "<li aria-current=\"page\">Saved items</li>" +
1644
1735
  "</ol></nav>" +
1645
1736
  "<h1 class=\"account-wishlist__title\">Saved items</h1>" +
1737
+ notice +
1646
1738
  inner +
1647
1739
  "</section>";
1648
1740
  return _wrap({
@@ -1871,7 +1963,19 @@ function renderSaved(opts) {
1871
1963
  }
1872
1964
  var inner = rowsHtml
1873
1965
  ? "<ul class=\"saved-list\">" + rowsHtml + "</ul>"
1874
- : "<p class=\"saved-empty\">Nothing saved for later. Use <strong>Save for later</strong> on a cart item to move it here without losing it.</p>";
1966
+ : "<div class=\"account-empty\">" +
1967
+ "<p class=\"account-empty__icon\" aria-hidden=\"true\">🔖</p>" +
1968
+ "<p class=\"account-empty__lede\">Nothing saved for later. Use <strong>Save for later</strong> on a cart item to move it here without losing it.</p>" +
1969
+ "<a class=\"btn-secondary\" href=\"/cart\">View your cart →</a>" +
1970
+ "</div>";
1971
+ // Success confirmation after a saved-list mutation (currently the
1972
+ // Remove action), surfaced via role="status". Unknown ?ok=<kind> keys
1973
+ // render nothing so a forged query can't inject copy.
1974
+ var SAVED_OK = { removed: "Removed from your saved items.", moved: "Moved to your cart." };
1975
+ var savedMsg = SAVED_OK[opts.notice];
1976
+ var notice = savedMsg
1977
+ ? "<p class=\"form-notice form-notice--ok\" role=\"status\">" + esc(savedMsg) + "</p>"
1978
+ : "";
1875
1979
  var body =
1876
1980
  "<section class=\"account-saved\">" +
1877
1981
  "<nav class=\"breadcrumb\" aria-label=\"Breadcrumb\"><ol>" +
@@ -1879,6 +1983,7 @@ function renderSaved(opts) {
1879
1983
  "<li aria-current=\"page\">Saved for later</li>" +
1880
1984
  "</ol></nav>" +
1881
1985
  "<h1 class=\"account-saved__title\">Saved for later</h1>" +
1986
+ notice +
1882
1987
  inner +
1883
1988
  "</section>";
1884
1989
  return _wrap({
@@ -1958,16 +2063,32 @@ function renderAddresses(opts) {
1958
2063
  "<a class=\"btn-ghost btn-ghost--sm\" href=\"/account/addresses/" + esc(a.id) + "/edit\">Edit</a>" +
1959
2064
  (Number(a.is_default_shipping) === 1 ? "" : "<form method=\"post\" action=\"/account/addresses/" + esc(a.id) + "/default-shipping\"><button type=\"submit\" class=\"btn-ghost btn-ghost--sm\">Set default shipping</button></form>") +
1960
2065
  (Number(a.is_default_billing) === 1 ? "" : "<form method=\"post\" action=\"/account/addresses/" + esc(a.id) + "/default-billing\"><button type=\"submit\" class=\"btn-ghost btn-ghost--sm\">Set default billing</button></form>") +
1961
- "<form method=\"post\" action=\"/account/addresses/" + esc(a.id) + "/archive\"><button type=\"submit\" class=\"btn-ghost btn-ghost--sm\">Remove</button></form>" +
2066
+ "<a class=\"btn-ghost btn-ghost--sm\" href=\"/account/addresses/" + esc(a.id) + "/remove\">Remove</a>" +
1962
2067
  "</div>" +
1963
2068
  "</li>";
1964
2069
  }
1965
2070
  var listHtml = rowsHtml
1966
2071
  ? "<ul class=\"address-list\">" + rowsHtml + "</ul>"
1967
- : "<p class=\"address-empty\">No saved addresses yet. Add one below to speed up checkout.</p>";
2072
+ : "<div class=\"account-empty\">" +
2073
+ "<p class=\"account-empty__icon\" aria-hidden=\"true\">🏠</p>" +
2074
+ "<p class=\"account-empty__lede\">No saved addresses yet. Add one below to speed up checkout.</p>" +
2075
+ "</div>";
1968
2076
  var notice = opts.notice
1969
2077
  ? "<p class=\"form-notice form-notice--error\" role=\"alert\">" + esc(String(opts.notice)) + "</p>"
1970
2078
  : "";
2079
+ // Success confirmation after an address mutation (add / edit / remove /
2080
+ // set-default), surfaced at the top of the list via role="status" so a
2081
+ // screen reader announces it without stealing focus. Driven by the
2082
+ // ?ok=<kind> redirect the POST handlers set on success. A remove also
2083
+ // threads ?undo=<id> so the notice can offer a one-click un-archive.
2084
+ var undoForm = opts.undo_id
2085
+ ? " <form class=\"form-notice__undo\" method=\"post\" action=\"/account/addresses/" + esc(String(opts.undo_id)) + "/unarchive\">" +
2086
+ "<button type=\"submit\" class=\"btn-link\">Undo</button>" +
2087
+ "</form>"
2088
+ : "";
2089
+ var success = opts.success
2090
+ ? "<div class=\"form-notice form-notice--ok\" role=\"status\"><span>" + esc(String(opts.success)) + "</span>" + undoForm + "</div>"
2091
+ : "";
1971
2092
  var editing = opts.edit || null;
1972
2093
  var formHeading = editing ? "Edit address" : "Add an address";
1973
2094
  var formAction = editing ? ("/account/addresses/" + editing.id) : "/account/addresses";
@@ -1978,6 +2099,7 @@ function renderAddresses(opts) {
1978
2099
  "<li aria-current=\"page\">Addresses</li>" +
1979
2100
  "</ol></nav>" +
1980
2101
  "<h1 class=\"account-addresses__title\">Addresses</h1>" +
2102
+ success +
1981
2103
  listHtml +
1982
2104
  "<h2 class=\"account-addresses__form-title\">" + esc(formHeading) + "</h2>" +
1983
2105
  notice +
@@ -1992,6 +2114,45 @@ function renderAddresses(opts) {
1992
2114
  });
1993
2115
  }
1994
2116
 
2117
+ // Server-rendered confirmation step for removing a saved address. CSP
2118
+ // forbids an inline confirm() dialog, so the destructive action is gated
2119
+ // by a second page: a POST form that actually archives, plus a Cancel
2120
+ // link back to the list. JS-off-native — no client script involved.
2121
+ function renderAddressRemoveConfirm(opts) {
2122
+ var esc = b.template.escapeHtml;
2123
+ var a = opts.address || {};
2124
+ var lines = [a.recipient_name, a.company, a.street_line1, a.street_line2,
2125
+ [a.city, a.region, a.postal_code].filter(Boolean).join(", "), a.country, a.phone]
2126
+ .filter(function (x) { return x != null && String(x).length; })
2127
+ .map(function (x) { return "<span>" + esc(String(x)) + "</span>"; }).join("");
2128
+ var body =
2129
+ "<section class=\"account-confirm\">" +
2130
+ "<nav class=\"breadcrumb\" aria-label=\"Breadcrumb\"><ol>" +
2131
+ "<li><a href=\"/account\">Account</a></li>" +
2132
+ "<li><a href=\"/account/addresses\">Addresses</a></li>" +
2133
+ "<li aria-current=\"page\">Remove address</li>" +
2134
+ "</ol></nav>" +
2135
+ "<h1 class=\"account-confirm__title\">Remove this address?</h1>" +
2136
+ (a.label ? "<p class=\"address-card__label\">" + esc(a.label) + "</p>" : "") +
2137
+ "<address class=\"address-card__body\">" + lines + "</address>" +
2138
+ "<p class=\"account-confirm__lede\">This removes the address from your address book. " +
2139
+ "You can add it again later.</p>" +
2140
+ "<div class=\"account-confirm__actions\">" +
2141
+ "<form method=\"post\" action=\"/account/addresses/" + esc(a.id) + "/archive\">" +
2142
+ "<button type=\"submit\" class=\"btn-primary\">Remove address</button>" +
2143
+ "</form>" +
2144
+ "<a class=\"btn-ghost\" href=\"/account/addresses\">Cancel</a>" +
2145
+ "</div>" +
2146
+ "</section>";
2147
+ return _wrap({
2148
+ title: "Remove address",
2149
+ shop_name: opts.shop_name || "blamejs.shop",
2150
+ cart_count: opts.cart_count == null ? 0 : opts.cart_count,
2151
+ theme_css: opts.theme_css,
2152
+ body: body,
2153
+ });
2154
+ }
2155
+
1995
2156
  // Storefront collection index — operator-curated + smart product lists.
1996
2157
  function renderCollectionList(opts) {
1997
2158
  var esc = b.template.escapeHtml;
@@ -2302,7 +2463,21 @@ function renderReturns(opts) {
2302
2463
  }
2303
2464
  var inner = rowsHtml
2304
2465
  ? "<ul class=\"return-list\">" + rowsHtml + "</ul>"
2305
- : "<p class=\"return-empty\">No returns yet. Start one from an order in your account.</p>";
2466
+ : "<div class=\"account-empty\">" +
2467
+ "<p class=\"account-empty__icon\" aria-hidden=\"true\">↩</p>" +
2468
+ "<p class=\"account-empty__lede\">No returns yet. Start one from an order in your account.</p>" +
2469
+ "<a class=\"btn-secondary\" href=\"/account/orders\">View your orders →</a>" +
2470
+ "</div>";
2471
+ // Success confirmation after submitting a return request. The RMA code
2472
+ // round-trips on the ?ok=<code> redirect so the notice can echo it back
2473
+ // — the operator-readable handle the customer references in support.
2474
+ // Validate the shape (RMA-YYMMDD-AAAAA) so a forged query can't inject
2475
+ // arbitrary copy into the status message.
2476
+ var notice = "";
2477
+ if (opts.rma_code && /^RMA-\d{6}-[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{5}$/.test(String(opts.rma_code))) {
2478
+ notice = "<p class=\"form-notice form-notice--ok\" role=\"status\">Return request received — RMA " +
2479
+ esc(String(opts.rma_code)) + ". We'll email you when it's reviewed.</p>";
2480
+ }
2306
2481
  var body =
2307
2482
  "<section class=\"account-returns\">" +
2308
2483
  "<nav class=\"breadcrumb\" aria-label=\"Breadcrumb\"><ol>" +
@@ -2310,6 +2485,7 @@ function renderReturns(opts) {
2310
2485
  "<li aria-current=\"page\">Returns</li>" +
2311
2486
  "</ol></nav>" +
2312
2487
  "<h1 class=\"account-returns__title\">Returns</h1>" +
2488
+ notice +
2313
2489
  inner +
2314
2490
  "</section>";
2315
2491
  return _wrap({
@@ -2692,13 +2868,12 @@ function renderAccountSubscriptions(opts) {
2692
2868
  }
2693
2869
  var cancelControl = "";
2694
2870
  if (canCancel && _subscriptionIsCancelable(s)) {
2871
+ // The cancel decision (and its "immediate vs. at period end"
2872
+ // consequence) is confirmed on a dedicated page — CSP forbids an
2873
+ // inline confirm() dialog, so the destructive step is a second
2874
+ // server-rendered screen rather than a one-click POST.
2695
2875
  cancelControl =
2696
- "<form class=\"subscription-card__cancel\" method=\"post\" action=\"/account/subscriptions/" + esc(s.id) + "/cancel\">" +
2697
- "<label class=\"subscription-card__when\">" +
2698
- "<input type=\"checkbox\" name=\"immediate\" value=\"1\"> Cancel immediately (skip the rest of the period)" +
2699
- "</label>" +
2700
- "<button type=\"submit\" class=\"btn-ghost btn-ghost--sm\">Cancel subscription</button>" +
2701
- "</form>";
2876
+ "<a class=\"btn-ghost btn-ghost--sm subscription-card__cancel-link\" href=\"/account/subscriptions/" + esc(s.id) + "/cancel\">Cancel subscription</a>";
2702
2877
  }
2703
2878
  rowsHtml +=
2704
2879
  "<li class=\"subscription-card\">" +
@@ -2715,7 +2890,11 @@ function renderAccountSubscriptions(opts) {
2715
2890
  : "<p class=\"subscription-note\">Cancellation isn't available on this store yet. Contact support to make changes.</p>";
2716
2891
  var inner = rowsHtml
2717
2892
  ? note + "<ul class=\"subscription-list\">" + rowsHtml + "</ul>"
2718
- : "<p class=\"subscription-empty\">You have no active subscriptions.</p>";
2893
+ : "<div class=\"account-empty\">" +
2894
+ "<p class=\"account-empty__icon\" aria-hidden=\"true\">↻</p>" +
2895
+ "<p class=\"account-empty__lede\">You have no active subscriptions.</p>" +
2896
+ "<a class=\"btn-secondary\" href=\"/\">Browse the shop →</a>" +
2897
+ "</div>";
2719
2898
  var body =
2720
2899
  "<section class=\"account-subscriptions\">" +
2721
2900
  "<nav class=\"breadcrumb\" aria-label=\"Breadcrumb\"><ol>" +
@@ -2735,6 +2914,79 @@ function renderAccountSubscriptions(opts) {
2735
2914
  });
2736
2915
  }
2737
2916
 
2917
+ // Server-rendered confirmation step for canceling a subscription. CSP
2918
+ // forbids an inline confirm() dialog, so the destructive choice is gated
2919
+ // by this page: it spells out the period-end date and, for the
2920
+ // "cancel immediately" path, the days of access forfeited. Two POST
2921
+ // forms (at-period-end / immediate) plus a Cancel link back to the list.
2922
+ // JS-off-native.
2923
+ function renderSubscriptionCancelConfirm(opts) {
2924
+ var esc = b.template.escapeHtml;
2925
+ var s = opts.subscription || {};
2926
+ var plan = s.plan || null;
2927
+ var planSummary = "this subscription";
2928
+ if (plan) {
2929
+ var ccy = String(plan.currency || "usd").toUpperCase();
2930
+ var every = Number(plan.interval_count) > 1
2931
+ ? "every " + Number(plan.interval_count) + " " + String(plan.interval) + "s"
2932
+ : "per " + String(plan.interval);
2933
+ var priceStr = "";
2934
+ try { priceStr = pricing.format(Number(plan.amount_minor), ccy) + " "; } catch (_e) { priceStr = ""; }
2935
+ planSummary = priceStr + every;
2936
+ }
2937
+ var periodEndMs = Number(s.current_period_end) || 0;
2938
+ var periodEnd = periodEndMs ? new Date(periodEndMs).toISOString().slice(0, 10) : "";
2939
+ // Days of paid access forfeited if canceled immediately rather than at
2940
+ // period end — computed from now to the period boundary, floored at 0.
2941
+ var forfeitDays = 0;
2942
+ if (periodEndMs) {
2943
+ forfeitDays = Math.max(0, Math.ceil((periodEndMs - Date.now()) / b.constants.TIME.days(1)));
2944
+ }
2945
+ var periodLine = periodEnd
2946
+ ? "<p class=\"account-confirm__lede\">Your current period is paid through " +
2947
+ "<time datetime=\"" + esc(periodEnd) + "\">" + esc(periodEnd) + "</time>.</p>"
2948
+ : "";
2949
+ var atPeriodEndForm =
2950
+ "<form method=\"post\" action=\"/account/subscriptions/" + esc(s.id) + "/cancel\">" +
2951
+ "<p class=\"account-confirm__option\">Cancel at period end" +
2952
+ (periodEnd ? " — keep access until <time datetime=\"" + esc(periodEnd) + "\">" + esc(periodEnd) + "</time>, then it won't renew." : ", so it won't renew.") +
2953
+ "</p>" +
2954
+ "<button type=\"submit\" class=\"btn-primary\">Cancel at period end</button>" +
2955
+ "</form>";
2956
+ var immediateForm =
2957
+ "<form method=\"post\" action=\"/account/subscriptions/" + esc(s.id) + "/cancel\">" +
2958
+ "<input type=\"hidden\" name=\"immediate\" value=\"1\">" +
2959
+ "<p class=\"account-confirm__option\">Cancel immediately" +
2960
+ (forfeitDays > 0
2961
+ ? " — access ends now and you forfeit the remaining " + forfeitDays + " day" + (forfeitDays === 1 ? "" : "s") + " of this period."
2962
+ : " — access ends now.") +
2963
+ "</p>" +
2964
+ "<button type=\"submit\" class=\"btn-ghost\">Cancel immediately</button>" +
2965
+ "</form>";
2966
+ var body =
2967
+ "<section class=\"account-confirm\">" +
2968
+ "<nav class=\"breadcrumb\" aria-label=\"Breadcrumb\"><ol>" +
2969
+ "<li><a href=\"/account\">Account</a></li>" +
2970
+ "<li><a href=\"/account/subscriptions\">Subscriptions</a></li>" +
2971
+ "<li aria-current=\"page\">Cancel</li>" +
2972
+ "</ol></nav>" +
2973
+ "<h1 class=\"account-confirm__title\">Cancel " + esc(planSummary) + "?</h1>" +
2974
+ periodLine +
2975
+ "<div class=\"account-confirm__actions\">" +
2976
+ atPeriodEndForm +
2977
+ immediateForm +
2978
+ "<a class=\"btn-ghost\" href=\"/account/subscriptions\">Keep my subscription</a>" +
2979
+ "</div>" +
2980
+ "</section>";
2981
+ return _wrap({
2982
+ title: "Cancel subscription",
2983
+ shop_name: opts.shop_name || "blamejs.shop",
2984
+ cart_count: opts.cart_count == null ? 0 : opts.cart_count,
2985
+ theme_css: opts.theme_css,
2986
+ body: body,
2987
+ });
2988
+ }
2989
+
2738
2990
  // Product-level "Save to wishlist" control + social-proof count.
2739
2991
  // Byte-compatible with the edge renderer (`worker/render/product.js`)
2740
2992
  // so both paths emit identical markup. Action-only label — the toggle
@@ -2819,10 +3071,7 @@ function renderProduct(opts) {
2819
3071
  asset_css_main: opts.theme.assetUrl("css/main.css"),
2820
3072
  });
2821
3073
  }
2822
- var rows = rendered.map(function (v) {
2823
- return _render(VARIANT_ROW, { title: v.title, sku: v.sku, price: v.price, variant_id: v.id });
2824
- }).join("");
2825
- if (!rows) rows = "<tr><td colspan=\"4\" class=\"empty\">No variants available.</td></tr>";
3074
+ var buyboxHtml = _buildBuyBox(rendered, b.template.escapeHtml);
2826
3075
  var galleryHtml = _buildPdpGallery(opts.product, opts.media || [], opts.asset_prefix || "/assets/");
2827
3076
  var reviewsHtml = _buildReviews(opts.review_summary, opts.reviews, opts.review_cta);
2828
3077
  var qaHtml = _buildProductQa(opts.qa_questions, opts.qa_cta);
@@ -2833,10 +3082,9 @@ function renderProduct(opts) {
2833
3082
  var body = _render(PRODUCT_PAGE, {
2834
3083
  title: opts.product.title,
2835
3084
  description: description,
2836
- variant_rows: "RAW_ROWS_PLACEHOLDER",
2837
3085
  })
2838
3086
  .replace("RAW_GALLERY_PLACEHOLDER", galleryHtml)
2839
- .replace("RAW_ROWS_PLACEHOLDER", rows)
3087
+ .replace("RAW_BUYBOX_PLACEHOLDER", buyboxHtml)
2840
3088
  .replace("RAW_QTYBREAK_PLACEHOLDER", qtyBreaksHtml)
2841
3089
  .replace("RAW_WISHLIST_PLACEHOLDER", wishlistHtml)
2842
3090
  .replace("RAW_COMPARE_PLACEHOLDER", compareHtml)
@@ -5801,6 +6049,20 @@ function mount(router, deps) {
5801
6049
  if (!orderId) return _send(res, 404, renderNotFound({ shop_name: shopName, theme: theme }));
5802
6050
  var o = await deps.order.get(orderId);
5803
6051
  if (!o) return _send(res, 404, renderNotFound({ shop_name: shopName, theme: theme }));
6052
+ // Ownership gate against IDOR: an order's confirmation page exposes
6053
+ // the customer's name, address, and line items by UUID alone. An order
6054
+ // that BELONGS to a customer (customer_id set) is viewable only by that
6055
+ // signed-in customer — anyone else (a different customer OR an
6056
+ // unauthenticated request) 404s rather than leaking it. A guest order
6057
+ // carries no customer_id and remains reachable via its unguessable URL
6058
+ // (the capability-URL model), so BOTH the just-placed-as-guest path AND
6059
+ // the signed-in-shopper-with-an-anonymous-cart path (checkout.confirm
6060
+ // derives the order from a cart that has no customer_id) still render
6061
+ // their own confirmation here.
6062
+ var orderAuth = _currentCustomerEnv(req);
6063
+ if (o.customer_id && (!orderAuth || o.customer_id !== orderAuth.customer_id)) {
6064
+ return _send(res, 404, renderNotFound({ shop_name: shopName, theme: theme }));
6065
+ }
5804
6066
  // Same variant_id → {product, hero_media} lookup pattern as the
5805
6067
  // cart route, applied to the order's frozen line items so the
5806
6068
  // post-checkout page shows what the customer bought visually.
@@ -6557,22 +6819,26 @@ function mount(router, deps) {
6557
6819
  return res.end ? res.end() : res.send("");
6558
6820
  }
6559
6821
  var productId = (req.body || {}).product_id;
6822
+ var removed = false;
6560
6823
  try {
6561
6824
  var already = await deps.wishlist.isWishlisted({ customer_id: auth.customer_id, product_id: productId });
6562
- if (already) await deps.wishlist.remove({ customer_id: auth.customer_id, product_id: productId });
6825
+ if (already) { await deps.wishlist.remove({ customer_id: auth.customer_id, product_id: productId }); removed = true; }
6563
6826
  else await deps.wishlist.add({ customer_id: auth.customer_id, product_id: productId });
6564
6827
  } catch (e) {
6565
6828
  res.status(e instanceof TypeError ? 400 : 500);
6566
6829
  return res.end ? res.end((e && e.message) || "Error") : res.send((e && e.message) || "Error");
6567
6830
  }
6831
+ var okKind = removed ? "removed" : "added";
6568
6832
  var rt = (req.body || {}).return_to;
6569
6833
  var dest;
6570
6834
  if (typeof rt === "string" && /^\/[^/]/.test(rt)) {
6571
- dest = rt;
6835
+ // Only thread the success marker when returning to the wishlist
6836
+ // page (the PDP heart toggle is its own visible cue).
6837
+ dest = rt.indexOf("/account/wishlist") === 0 ? "/account/wishlist?ok=" + okKind : rt;
6572
6838
  } else {
6573
6839
  var product = null;
6574
6840
  try { product = await deps.catalog.products.get(productId); } catch (_e) { product = null; }
6575
- dest = product ? ("/products/" + encodeURIComponent(product.slug)) : "/account/wishlist";
6841
+ dest = product ? ("/products/" + encodeURIComponent(product.slug)) : ("/account/wishlist?ok=" + okKind);
6576
6842
  }
6577
6843
  res.status(303); res.setHeader && res.setHeader("location", dest);
6578
6844
  return res.end ? res.end() : res.send("");
@@ -6603,8 +6869,10 @@ function mount(router, deps) {
6603
6869
  items.push({ product: product, hero_media: media.length ? media[0] : null });
6604
6870
  }
6605
6871
  var cartCount = await _cartCountForReq(req);
6872
+ var wlUrl = req.url ? new URL(req.url, "http://localhost") : null;
6606
6873
  _send(res, 200, renderWishlist({
6607
6874
  items: items,
6875
+ notice: wlUrl ? wlUrl.searchParams.get("ok") : null,
6608
6876
  shop_name: shopName,
6609
6877
  cart_count: cartCount,
6610
6878
  asset_prefix: deps.asset_prefix || "/assets/",
@@ -6675,8 +6943,10 @@ function mount(router, deps) {
6675
6943
  items.push({ save: row, product: product, hero_media: media.length ? media[0] : null });
6676
6944
  }
6677
6945
  var cartCount = await _cartCountForReq(req);
6946
+ var savedUrl = req.url ? new URL(req.url, "http://localhost") : null;
6678
6947
  _send(res, 200, renderSaved({
6679
6948
  items: items,
6949
+ notice: savedUrl ? savedUrl.searchParams.get("ok") : null,
6680
6950
  shop_name: shopName,
6681
6951
  cart_count: cartCount,
6682
6952
  asset_prefix: deps.asset_prefix || "/assets/",
@@ -6734,7 +7004,7 @@ function mount(router, deps) {
6734
7004
  res.status(e instanceof TypeError ? 400 : 500);
6735
7005
  return res.end ? res.end((e && e.message) || "Error") : res.send((e && e.message) || "Error");
6736
7006
  }
6737
- res.status(303); res.setHeader && res.setHeader("location", "/account/saved");
7007
+ res.status(303); res.setHeader && res.setHeader("location", "/account/saved?ok=removed");
6738
7008
  return res.end ? res.end() : res.send("");
6739
7009
  });
6740
7010
  }
@@ -6774,13 +7044,33 @@ function mount(router, deps) {
6774
7044
  }
6775
7045
  return addr;
6776
7046
  }
7047
+ // Map the ?ok=<kind> redirect marker the POST handlers set on
7048
+ // success to the human confirmation copy rendered (role="status")
7049
+ // at the top of the list. Unknown markers degrade to no notice.
7050
+ function _addrSuccessCopy(kind) {
7051
+ if (kind === "added") return "Address saved.";
7052
+ if (kind === "updated") return "Address updated.";
7053
+ if (kind === "removed") return "Address removed.";
7054
+ if (kind === "restored") return "Address restored.";
7055
+ if (kind === "default-shipping") return "Default shipping address updated.";
7056
+ if (kind === "default-billing") return "Default billing address updated.";
7057
+ return null;
7058
+ }
6777
7059
  async function _renderAddrPage(req, res, auth, editAddr, notice, code) {
6778
7060
  var rows = await deps.addresses.listForCustomer(auth.customer_id, { limit: 50 });
6779
7061
  var cartCount = await _cartCountForReq(req);
7062
+ var url = req.url ? new URL(req.url, "http://localhost") : null;
7063
+ var okKind = url ? url.searchParams.get("ok") : null;
7064
+ var success = _addrSuccessCopy(okKind);
7065
+ // An Undo control is only offered for a just-removed row, and only
7066
+ // when the ?undo=<id> marker round-trips a real owned address id.
7067
+ var undoId = (okKind === "removed" && url) ? url.searchParams.get("undo") : null;
6780
7068
  _send(res, code || 200, renderAddresses({
6781
7069
  addresses: rows,
6782
7070
  edit: editAddr || null,
6783
7071
  notice: notice || null,
7072
+ success: success,
7073
+ undo_id: undoId || null,
6784
7074
  shop_name: shopName,
6785
7075
  cart_count: cartCount,
6786
7076
  }));
@@ -6824,7 +7114,7 @@ function mount(router, deps) {
6824
7114
  if (e instanceof TypeError) return _renderAddrPage(req, res, auth, null, (e && e.message) || "Please check the address.", 400);
6825
7115
  throw e;
6826
7116
  }
6827
- res.status(303); res.setHeader && res.setHeader("location", "/account/addresses");
7117
+ res.status(303); res.setHeader && res.setHeader("location", "/account/addresses?ok=added");
6828
7118
  return res.end ? res.end() : res.send("");
6829
7119
  });
6830
7120
 
@@ -6840,11 +7130,11 @@ function mount(router, deps) {
6840
7130
  }
6841
7131
  throw e;
6842
7132
  }
6843
- res.status(303); res.setHeader && res.setHeader("location", "/account/addresses");
7133
+ res.status(303); res.setHeader && res.setHeader("location", "/account/addresses?ok=updated");
6844
7134
  return res.end ? res.end() : res.send("");
6845
7135
  });
6846
7136
 
6847
- function _addrAction(verb, fn) {
7137
+ function _addrAction(verb, okKind, fn) {
6848
7138
  router.post("/account/addresses/:id/" + verb, async function (req, res) {
6849
7139
  var auth = _addrAuth(req, res); if (!auth) return;
6850
7140
  var addr = await _ownedAddress(req, res, auth); if (!addr) return;
@@ -6853,13 +7143,64 @@ function mount(router, deps) {
6853
7143
  res.status(e instanceof TypeError ? 400 : 500);
6854
7144
  return res.end ? res.end((e && e.message) || "Error") : res.send((e && e.message) || "Error");
6855
7145
  }
6856
- res.status(303); res.setHeader && res.setHeader("location", "/account/addresses");
7146
+ res.status(303); res.setHeader && res.setHeader("location", "/account/addresses?ok=" + okKind);
6857
7147
  return res.end ? res.end() : res.send("");
6858
7148
  });
6859
7149
  }
6860
- _addrAction("default-shipping", function (id) { return deps.addresses.setDefaultShipping(id); });
6861
- _addrAction("default-billing", function (id) { return deps.addresses.setDefaultBilling(id); });
6862
- _addrAction("archive", function (id) { return deps.addresses.archive(id); });
7150
+ _addrAction("default-shipping", "default-shipping", function (id) { return deps.addresses.setDefaultShipping(id); });
7151
+ _addrAction("default-billing", "default-billing", function (id) { return deps.addresses.setDefaultBilling(id); });
7152
+
7153
+ // Remove is destructive and CSP forbids a confirm() dialog, so it
7154
+ // routes through a server-rendered confirm page first; the POST that
7155
+ // actually archives lives behind that page. The list then surfaces a
7156
+ // success notice with an Undo (unarchive) control.
7157
+ router.get("/account/addresses/:id/remove", async function (req, res) {
7158
+ var auth = _addrAuth(req, res); if (!auth) return;
7159
+ var addr = await _ownedAddress(req, res, auth); if (!addr) return;
7160
+ var cartCount = await _cartCountForReq(req);
7161
+ _send(res, 200, renderAddressRemoveConfirm({
7162
+ address: addr,
7163
+ shop_name: shopName,
7164
+ cart_count: cartCount,
7165
+ }));
7166
+ });
7167
+
7168
+ router.post("/account/addresses/:id/archive", async function (req, res) {
7169
+ var auth = _addrAuth(req, res); if (!auth) return;
7170
+ var addr = await _ownedAddress(req, res, auth); if (!addr) return;
7171
+ try { await deps.addresses.archive(addr.id); }
7172
+ catch (e) {
7173
+ res.status(e instanceof TypeError ? 400 : 500);
7174
+ return res.end ? res.end((e && e.message) || "Error") : res.send((e && e.message) || "Error");
7175
+ }
7176
+ res.status(303);
7177
+ res.setHeader && res.setHeader("location", "/account/addresses?ok=removed&undo=" + encodeURIComponent(addr.id));
7178
+ return res.end ? res.end() : res.send("");
7179
+ });
7180
+
7181
+ // Undo path for a just-removed address. Unlike the by-id routes
7182
+ // above, this one resolves the row WITH archived rows included (the
7183
+ // address is archived by definition here) but still enforces
7184
+ // customer ownership before un-archiving.
7185
+ router.post("/account/addresses/:id/unarchive", async function (req, res) {
7186
+ var auth = _addrAuth(req, res); if (!auth) return;
7187
+ var addr;
7188
+ try { addr = await deps.addresses.get(req.params && req.params.id); }
7189
+ catch (e) {
7190
+ if (e instanceof TypeError) { _send(res, 404, renderNotFound({ shop_name: shopName, theme: theme })); return; }
7191
+ throw e;
7192
+ }
7193
+ if (!addr || addr.customer_id !== auth.customer_id) {
7194
+ return _send(res, 404, renderNotFound({ shop_name: shopName, theme: theme }));
7195
+ }
7196
+ try { await deps.addresses.unarchive(addr.id); }
7197
+ catch (e) {
7198
+ res.status(e instanceof TypeError ? 400 : 500);
7199
+ return res.end ? res.end((e && e.message) || "Error") : res.send((e && e.message) || "Error");
7200
+ }
7201
+ res.status(303); res.setHeader && res.setHeader("location", "/account/addresses?ok=restored");
7202
+ return res.end ? res.end() : res.send("");
7203
+ });
6863
7204
  }
6864
7205
 
6865
7206
  // Subscription self-management — the signed-in customer views their
@@ -6950,6 +7291,31 @@ function mount(router, deps) {
6950
7291
  // Cancel mounts only when payment is wired (cancel composes Stripe).
6951
7292
  // Without payment the list above stays read-only with a note.
6952
7293
  if (deps.payment) {
7294
+ // Confirmation step (GET) ahead of the destructive POST. Renders
7295
+ // the period-end date + the days-forfeited consequence of an
7296
+ // immediate cancel. A subscription that isn't cancelable
7297
+ // (already canceled / winding down) redirects back to the list.
7298
+ router.get("/account/subscriptions/:id/cancel", async function (req, res) {
7299
+ var auth = _subsAuth(req, res); if (!auth) return;
7300
+ var sub = await _ownedSubscription(req, res, auth); if (!sub) return;
7301
+ if (!_subscriptionIsCancelable(sub)) {
7302
+ res.status(303); res.setHeader && res.setHeader("location", "/account/subscriptions");
7303
+ return res.end ? res.end() : res.send("");
7304
+ }
7305
+ // Join the plan for the confirm-page summary (best-effort — a
7306
+ // missing plan degrades the heading to a generic phrase).
7307
+ if (sub.plan_id != null) {
7308
+ try { sub.plan = await subscriptions.plans.get(sub.plan_id); }
7309
+ catch (_e) { sub.plan = null; }
7310
+ }
7311
+ var cartCount = await _cartCountForReq(req);
7312
+ _send(res, 200, renderSubscriptionCancelConfirm({
7313
+ subscription: sub,
7314
+ shop_name: shopName,
7315
+ cart_count: cartCount,
7316
+ }));
7317
+ });
7318
+
6953
7319
  router.post("/account/subscriptions/:id/cancel", async function (req, res) {
6954
7320
  var auth = _subsAuth(req, res); if (!auth) return;
6955
7321
  var sub = await _ownedSubscription(req, res, auth); if (!sub) return;
@@ -7016,7 +7382,13 @@ function mount(router, deps) {
7016
7382
  var auth = _returnsAuth(req, res); if (!auth) return;
7017
7383
  var page = await deps.returns.listForCustomer(auth.customer_id, { limit: 50 });
7018
7384
  var cartCount = await _cartCountForReq(req);
7019
- _send(res, 200, renderReturns({ rmas: page.rows, shop_name: shopName, cart_count: cartCount }));
7385
+ var retUrl = req.url ? new URL(req.url, "http://localhost") : null;
7386
+ _send(res, 200, renderReturns({
7387
+ rmas: page.rows,
7388
+ rma_code: retUrl ? retUrl.searchParams.get("ok") : null,
7389
+ shop_name: shopName,
7390
+ cart_count: cartCount,
7391
+ }));
7020
7392
  });
7021
7393
 
7022
7394
  router.get("/account/orders/:order_id/return", async function (req, res) {
@@ -7049,8 +7421,9 @@ function mount(router, deps) {
7049
7421
  shop_name: shopName, cart_count: cartCount,
7050
7422
  }));
7051
7423
  }
7424
+ var requested;
7052
7425
  try {
7053
- await deps.returns.request({
7426
+ requested = await deps.returns.request({
7054
7427
  order_id: order.id,
7055
7428
  customer_id: auth.customer_id,
7056
7429
  reason: body.reason,
@@ -7066,7 +7439,11 @@ function mount(router, deps) {
7066
7439
  }
7067
7440
  throw e;
7068
7441
  }
7069
- res.status(303); res.setHeader && res.setHeader("location", "/account/returns");
7442
+ // Round-trip the RMA code on the redirect so the list page can
7443
+ // confirm the request and echo the operator-readable handle.
7444
+ var rmaCode = requested && requested.rma_code ? requested.rma_code : "";
7445
+ res.status(303);
7446
+ res.setHeader && res.setHeader("location", "/account/returns?ok=" + encodeURIComponent(rmaCode));
7070
7447
  return res.end ? res.end() : res.send("");
7071
7448
  });
7072
7449
  }