@blamejs/blamejs-shop 0.1.37 → 0.1.38
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 +2 -0
- package/lib/admin.js +205 -42
- package/lib/asset-manifest.json +5 -5
- package/lib/storefront.js +334 -30
- package/lib/vendor/MANIFEST.json +2 -2
- package/lib/vendor/blamejs/CHANGELOG.md +4 -0
- package/lib/vendor/blamejs/README.md +1 -1
- package/lib/vendor/blamejs/api-snapshot.json +2 -2
- package/lib/vendor/blamejs/lib/ai-disclosure.js +2 -3
- package/lib/vendor/blamejs/lib/archive-gz.js +5 -3
- package/lib/vendor/blamejs/lib/archive-read.js +84 -35
- package/lib/vendor/blamejs/lib/archive-tar-read.js +86 -31
- package/lib/vendor/blamejs/lib/archive-tar.js +2 -3
- package/lib/vendor/blamejs/lib/backup/index.js +2 -3
- package/lib/vendor/blamejs/lib/cose.js +4 -3
- package/lib/vendor/blamejs/lib/mail-server-jmap.js +9 -9
- package/lib/vendor/blamejs/lib/mail-server-submission.js +3 -2
- package/lib/vendor/blamejs/lib/mdoc.js +14 -14
- package/lib/vendor/blamejs/lib/network-dnssec.js +10 -8
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.13.7.json +27 -0
- package/lib/vendor/blamejs/release-notes/v0.13.8.json +27 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/archive-read.test.js +50 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +89 -0
- package/package.json +1 -1
package/lib/storefront.js
CHANGED
|
@@ -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\"{{
|
|
869
|
-
" <span class=\"facet-option__box\" aria-hidden=\"true\">{{box}}</span
|
|
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
|
-
|
|
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-
|
|
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;
|
|
@@ -1635,7 +1637,19 @@ function renderWishlist(opts) {
|
|
|
1635
1637
|
}
|
|
1636
1638
|
var inner = rowsHtml
|
|
1637
1639
|
? "<ul class=\"wishlist-list\">" + rowsHtml + "</ul>"
|
|
1638
|
-
: "<
|
|
1640
|
+
: "<div class=\"account-empty\">" +
|
|
1641
|
+
"<p class=\"account-empty__icon\" aria-hidden=\"true\">♡</p>" +
|
|
1642
|
+
"<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>" +
|
|
1643
|
+
"<a class=\"btn-secondary\" href=\"/\">Browse the shop →</a>" +
|
|
1644
|
+
"</div>";
|
|
1645
|
+
// Success confirmation after a wishlist toggle, surfaced via
|
|
1646
|
+
// role="status". Driven by the ?ok=<kind> redirect marker; unknown
|
|
1647
|
+
// keys render nothing so a forged query can't inject copy.
|
|
1648
|
+
var WISHLIST_OK = { added: "Saved to your wishlist.", removed: "Removed from your wishlist." };
|
|
1649
|
+
var okMsg = WISHLIST_OK[opts.notice];
|
|
1650
|
+
var notice = okMsg
|
|
1651
|
+
? "<p class=\"form-notice form-notice--ok\" role=\"status\">" + esc(okMsg) + "</p>"
|
|
1652
|
+
: "";
|
|
1639
1653
|
var body =
|
|
1640
1654
|
"<section class=\"account-wishlist\">" +
|
|
1641
1655
|
"<nav class=\"breadcrumb\" aria-label=\"Breadcrumb\"><ol>" +
|
|
@@ -1643,6 +1657,7 @@ function renderWishlist(opts) {
|
|
|
1643
1657
|
"<li aria-current=\"page\">Saved items</li>" +
|
|
1644
1658
|
"</ol></nav>" +
|
|
1645
1659
|
"<h1 class=\"account-wishlist__title\">Saved items</h1>" +
|
|
1660
|
+
notice +
|
|
1646
1661
|
inner +
|
|
1647
1662
|
"</section>";
|
|
1648
1663
|
return _wrap({
|
|
@@ -1871,7 +1886,19 @@ function renderSaved(opts) {
|
|
|
1871
1886
|
}
|
|
1872
1887
|
var inner = rowsHtml
|
|
1873
1888
|
? "<ul class=\"saved-list\">" + rowsHtml + "</ul>"
|
|
1874
|
-
: "<
|
|
1889
|
+
: "<div class=\"account-empty\">" +
|
|
1890
|
+
"<p class=\"account-empty__icon\" aria-hidden=\"true\">🔖</p>" +
|
|
1891
|
+
"<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>" +
|
|
1892
|
+
"<a class=\"btn-secondary\" href=\"/cart\">View your cart →</a>" +
|
|
1893
|
+
"</div>";
|
|
1894
|
+
// Success confirmation after a saved-list mutation (currently the
|
|
1895
|
+
// Remove action), surfaced via role="status". Unknown ?ok=<kind> keys
|
|
1896
|
+
// render nothing so a forged query can't inject copy.
|
|
1897
|
+
var SAVED_OK = { removed: "Removed from your saved items.", moved: "Moved to your cart." };
|
|
1898
|
+
var savedMsg = SAVED_OK[opts.notice];
|
|
1899
|
+
var notice = savedMsg
|
|
1900
|
+
? "<p class=\"form-notice form-notice--ok\" role=\"status\">" + esc(savedMsg) + "</p>"
|
|
1901
|
+
: "";
|
|
1875
1902
|
var body =
|
|
1876
1903
|
"<section class=\"account-saved\">" +
|
|
1877
1904
|
"<nav class=\"breadcrumb\" aria-label=\"Breadcrumb\"><ol>" +
|
|
@@ -1879,6 +1906,7 @@ function renderSaved(opts) {
|
|
|
1879
1906
|
"<li aria-current=\"page\">Saved for later</li>" +
|
|
1880
1907
|
"</ol></nav>" +
|
|
1881
1908
|
"<h1 class=\"account-saved__title\">Saved for later</h1>" +
|
|
1909
|
+
notice +
|
|
1882
1910
|
inner +
|
|
1883
1911
|
"</section>";
|
|
1884
1912
|
return _wrap({
|
|
@@ -1958,16 +1986,32 @@ function renderAddresses(opts) {
|
|
|
1958
1986
|
"<a class=\"btn-ghost btn-ghost--sm\" href=\"/account/addresses/" + esc(a.id) + "/edit\">Edit</a>" +
|
|
1959
1987
|
(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
1988
|
(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
|
-
"<
|
|
1989
|
+
"<a class=\"btn-ghost btn-ghost--sm\" href=\"/account/addresses/" + esc(a.id) + "/remove\">Remove</a>" +
|
|
1962
1990
|
"</div>" +
|
|
1963
1991
|
"</li>";
|
|
1964
1992
|
}
|
|
1965
1993
|
var listHtml = rowsHtml
|
|
1966
1994
|
? "<ul class=\"address-list\">" + rowsHtml + "</ul>"
|
|
1967
|
-
: "<
|
|
1995
|
+
: "<div class=\"account-empty\">" +
|
|
1996
|
+
"<p class=\"account-empty__icon\" aria-hidden=\"true\">🏠</p>" +
|
|
1997
|
+
"<p class=\"account-empty__lede\">No saved addresses yet. Add one below to speed up checkout.</p>" +
|
|
1998
|
+
"</div>";
|
|
1968
1999
|
var notice = opts.notice
|
|
1969
2000
|
? "<p class=\"form-notice form-notice--error\" role=\"alert\">" + esc(String(opts.notice)) + "</p>"
|
|
1970
2001
|
: "";
|
|
2002
|
+
// Success confirmation after an address mutation (add / edit / remove /
|
|
2003
|
+
// set-default), surfaced at the top of the list via role="status" so a
|
|
2004
|
+
// screen reader announces it without stealing focus. Driven by the
|
|
2005
|
+
// ?ok=<kind> redirect the POST handlers set on success. A remove also
|
|
2006
|
+
// threads ?undo=<id> so the notice can offer a one-click un-archive.
|
|
2007
|
+
var undoForm = opts.undo_id
|
|
2008
|
+
? " <form class=\"form-notice__undo\" method=\"post\" action=\"/account/addresses/" + esc(String(opts.undo_id)) + "/unarchive\">" +
|
|
2009
|
+
"<button type=\"submit\" class=\"btn-link\">Undo</button>" +
|
|
2010
|
+
"</form>"
|
|
2011
|
+
: "";
|
|
2012
|
+
var success = opts.success
|
|
2013
|
+
? "<div class=\"form-notice form-notice--ok\" role=\"status\"><span>" + esc(String(opts.success)) + "</span>" + undoForm + "</div>"
|
|
2014
|
+
: "";
|
|
1971
2015
|
var editing = opts.edit || null;
|
|
1972
2016
|
var formHeading = editing ? "Edit address" : "Add an address";
|
|
1973
2017
|
var formAction = editing ? ("/account/addresses/" + editing.id) : "/account/addresses";
|
|
@@ -1978,6 +2022,7 @@ function renderAddresses(opts) {
|
|
|
1978
2022
|
"<li aria-current=\"page\">Addresses</li>" +
|
|
1979
2023
|
"</ol></nav>" +
|
|
1980
2024
|
"<h1 class=\"account-addresses__title\">Addresses</h1>" +
|
|
2025
|
+
success +
|
|
1981
2026
|
listHtml +
|
|
1982
2027
|
"<h2 class=\"account-addresses__form-title\">" + esc(formHeading) + "</h2>" +
|
|
1983
2028
|
notice +
|
|
@@ -1992,6 +2037,45 @@ function renderAddresses(opts) {
|
|
|
1992
2037
|
});
|
|
1993
2038
|
}
|
|
1994
2039
|
|
|
2040
|
+
// Server-rendered confirmation step for removing a saved address. CSP
|
|
2041
|
+
// forbids an inline confirm() dialog, so the destructive action is gated
|
|
2042
|
+
// by a second page: a POST form that actually archives, plus a Cancel
|
|
2043
|
+
// link back to the list. JS-off-native — no client script involved.
|
|
2044
|
+
function renderAddressRemoveConfirm(opts) {
|
|
2045
|
+
var esc = b.template.escapeHtml;
|
|
2046
|
+
var a = opts.address || {};
|
|
2047
|
+
var lines = [a.recipient_name, a.company, a.street_line1, a.street_line2,
|
|
2048
|
+
[a.city, a.region, a.postal_code].filter(Boolean).join(", "), a.country, a.phone]
|
|
2049
|
+
.filter(function (x) { return x != null && String(x).length; })
|
|
2050
|
+
.map(function (x) { return "<span>" + esc(String(x)) + "</span>"; }).join("");
|
|
2051
|
+
var body =
|
|
2052
|
+
"<section class=\"account-confirm\">" +
|
|
2053
|
+
"<nav class=\"breadcrumb\" aria-label=\"Breadcrumb\"><ol>" +
|
|
2054
|
+
"<li><a href=\"/account\">Account</a></li>" +
|
|
2055
|
+
"<li><a href=\"/account/addresses\">Addresses</a></li>" +
|
|
2056
|
+
"<li aria-current=\"page\">Remove address</li>" +
|
|
2057
|
+
"</ol></nav>" +
|
|
2058
|
+
"<h1 class=\"account-confirm__title\">Remove this address?</h1>" +
|
|
2059
|
+
(a.label ? "<p class=\"address-card__label\">" + esc(a.label) + "</p>" : "") +
|
|
2060
|
+
"<address class=\"address-card__body\">" + lines + "</address>" +
|
|
2061
|
+
"<p class=\"account-confirm__lede\">This removes the address from your address book. " +
|
|
2062
|
+
"You can add it again later.</p>" +
|
|
2063
|
+
"<div class=\"account-confirm__actions\">" +
|
|
2064
|
+
"<form method=\"post\" action=\"/account/addresses/" + esc(a.id) + "/archive\">" +
|
|
2065
|
+
"<button type=\"submit\" class=\"btn-primary\">Remove address</button>" +
|
|
2066
|
+
"</form>" +
|
|
2067
|
+
"<a class=\"btn-ghost\" href=\"/account/addresses\">Cancel</a>" +
|
|
2068
|
+
"</div>" +
|
|
2069
|
+
"</section>";
|
|
2070
|
+
return _wrap({
|
|
2071
|
+
title: "Remove address",
|
|
2072
|
+
shop_name: opts.shop_name || "blamejs.shop",
|
|
2073
|
+
cart_count: opts.cart_count == null ? 0 : opts.cart_count,
|
|
2074
|
+
theme_css: opts.theme_css,
|
|
2075
|
+
body: body,
|
|
2076
|
+
});
|
|
2077
|
+
}
|
|
2078
|
+
|
|
1995
2079
|
// Storefront collection index — operator-curated + smart product lists.
|
|
1996
2080
|
function renderCollectionList(opts) {
|
|
1997
2081
|
var esc = b.template.escapeHtml;
|
|
@@ -2302,7 +2386,21 @@ function renderReturns(opts) {
|
|
|
2302
2386
|
}
|
|
2303
2387
|
var inner = rowsHtml
|
|
2304
2388
|
? "<ul class=\"return-list\">" + rowsHtml + "</ul>"
|
|
2305
|
-
: "<
|
|
2389
|
+
: "<div class=\"account-empty\">" +
|
|
2390
|
+
"<p class=\"account-empty__icon\" aria-hidden=\"true\">↩</p>" +
|
|
2391
|
+
"<p class=\"account-empty__lede\">No returns yet. Start one from an order in your account.</p>" +
|
|
2392
|
+
"<a class=\"btn-secondary\" href=\"/account/orders\">View your orders →</a>" +
|
|
2393
|
+
"</div>";
|
|
2394
|
+
// Success confirmation after submitting a return request. The RMA code
|
|
2395
|
+
// round-trips on the ?ok=<code> redirect so the notice can echo it back
|
|
2396
|
+
// — the operator-readable handle the customer references in support.
|
|
2397
|
+
// Validate the shape (RMA-YYMMDD-AAAAA) so a forged query can't inject
|
|
2398
|
+
// arbitrary copy into the status message.
|
|
2399
|
+
var notice = "";
|
|
2400
|
+
if (opts.rma_code && /^RMA-\d{6}-[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{5}$/.test(String(opts.rma_code))) {
|
|
2401
|
+
notice = "<p class=\"form-notice form-notice--ok\" role=\"status\">Return request received — RMA " +
|
|
2402
|
+
esc(String(opts.rma_code)) + ". We'll email you when it's reviewed.</p>";
|
|
2403
|
+
}
|
|
2306
2404
|
var body =
|
|
2307
2405
|
"<section class=\"account-returns\">" +
|
|
2308
2406
|
"<nav class=\"breadcrumb\" aria-label=\"Breadcrumb\"><ol>" +
|
|
@@ -2310,6 +2408,7 @@ function renderReturns(opts) {
|
|
|
2310
2408
|
"<li aria-current=\"page\">Returns</li>" +
|
|
2311
2409
|
"</ol></nav>" +
|
|
2312
2410
|
"<h1 class=\"account-returns__title\">Returns</h1>" +
|
|
2411
|
+
notice +
|
|
2313
2412
|
inner +
|
|
2314
2413
|
"</section>";
|
|
2315
2414
|
return _wrap({
|
|
@@ -2692,13 +2791,12 @@ function renderAccountSubscriptions(opts) {
|
|
|
2692
2791
|
}
|
|
2693
2792
|
var cancelControl = "";
|
|
2694
2793
|
if (canCancel && _subscriptionIsCancelable(s)) {
|
|
2794
|
+
// The cancel decision (and its "immediate vs. at period end"
|
|
2795
|
+
// consequence) is confirmed on a dedicated page — CSP forbids an
|
|
2796
|
+
// inline confirm() dialog, so the destructive step is a second
|
|
2797
|
+
// server-rendered screen rather than a one-click POST.
|
|
2695
2798
|
cancelControl =
|
|
2696
|
-
"<
|
|
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>";
|
|
2799
|
+
"<a class=\"btn-ghost btn-ghost--sm subscription-card__cancel-link\" href=\"/account/subscriptions/" + esc(s.id) + "/cancel\">Cancel subscription</a>";
|
|
2702
2800
|
}
|
|
2703
2801
|
rowsHtml +=
|
|
2704
2802
|
"<li class=\"subscription-card\">" +
|
|
@@ -2715,7 +2813,11 @@ function renderAccountSubscriptions(opts) {
|
|
|
2715
2813
|
: "<p class=\"subscription-note\">Cancellation isn't available on this store yet. Contact support to make changes.</p>";
|
|
2716
2814
|
var inner = rowsHtml
|
|
2717
2815
|
? note + "<ul class=\"subscription-list\">" + rowsHtml + "</ul>"
|
|
2718
|
-
: "<
|
|
2816
|
+
: "<div class=\"account-empty\">" +
|
|
2817
|
+
"<p class=\"account-empty__icon\" aria-hidden=\"true\">↻</p>" +
|
|
2818
|
+
"<p class=\"account-empty__lede\">You have no active subscriptions.</p>" +
|
|
2819
|
+
"<a class=\"btn-secondary\" href=\"/\">Browse the shop →</a>" +
|
|
2820
|
+
"</div>";
|
|
2719
2821
|
var body =
|
|
2720
2822
|
"<section class=\"account-subscriptions\">" +
|
|
2721
2823
|
"<nav class=\"breadcrumb\" aria-label=\"Breadcrumb\"><ol>" +
|
|
@@ -2735,6 +2837,79 @@ function renderAccountSubscriptions(opts) {
|
|
|
2735
2837
|
});
|
|
2736
2838
|
}
|
|
2737
2839
|
|
|
2840
|
+
// Server-rendered confirmation step for canceling a subscription. CSP
|
|
2841
|
+
// forbids an inline confirm() dialog, so the destructive choice is gated
|
|
2842
|
+
// by this page: it spells out the period-end date and, for the
|
|
2843
|
+
// "cancel immediately" path, the days of access forfeited. Two POST
|
|
2844
|
+
// forms (at-period-end / immediate) plus a Cancel link back to the list.
|
|
2845
|
+
// JS-off-native.
|
|
2846
|
+
function renderSubscriptionCancelConfirm(opts) {
|
|
2847
|
+
var esc = b.template.escapeHtml;
|
|
2848
|
+
var s = opts.subscription || {};
|
|
2849
|
+
var plan = s.plan || null;
|
|
2850
|
+
var planSummary = "this subscription";
|
|
2851
|
+
if (plan) {
|
|
2852
|
+
var ccy = String(plan.currency || "usd").toUpperCase();
|
|
2853
|
+
var every = Number(plan.interval_count) > 1
|
|
2854
|
+
? "every " + Number(plan.interval_count) + " " + String(plan.interval) + "s"
|
|
2855
|
+
: "per " + String(plan.interval);
|
|
2856
|
+
var priceStr = "";
|
|
2857
|
+
try { priceStr = pricing.format(Number(plan.amount_minor), ccy) + " "; } catch (_e) { priceStr = ""; }
|
|
2858
|
+
planSummary = priceStr + every;
|
|
2859
|
+
}
|
|
2860
|
+
var periodEndMs = Number(s.current_period_end) || 0;
|
|
2861
|
+
var periodEnd = periodEndMs ? new Date(periodEndMs).toISOString().slice(0, 10) : "";
|
|
2862
|
+
// Days of paid access forfeited if canceled immediately rather than at
|
|
2863
|
+
// period end — computed from now to the period boundary, floored at 0.
|
|
2864
|
+
var forfeitDays = 0;
|
|
2865
|
+
if (periodEndMs) {
|
|
2866
|
+
forfeitDays = Math.max(0, Math.ceil((periodEndMs - Date.now()) / b.constants.TIME.days(1)));
|
|
2867
|
+
}
|
|
2868
|
+
var periodLine = periodEnd
|
|
2869
|
+
? "<p class=\"account-confirm__lede\">Your current period is paid through " +
|
|
2870
|
+
"<time datetime=\"" + esc(periodEnd) + "\">" + esc(periodEnd) + "</time>.</p>"
|
|
2871
|
+
: "";
|
|
2872
|
+
var atPeriodEndForm =
|
|
2873
|
+
"<form method=\"post\" action=\"/account/subscriptions/" + esc(s.id) + "/cancel\">" +
|
|
2874
|
+
"<p class=\"account-confirm__option\">Cancel at period end" +
|
|
2875
|
+
(periodEnd ? " — keep access until <time datetime=\"" + esc(periodEnd) + "\">" + esc(periodEnd) + "</time>, then it won't renew." : ", so it won't renew.") +
|
|
2876
|
+
"</p>" +
|
|
2877
|
+
"<button type=\"submit\" class=\"btn-primary\">Cancel at period end</button>" +
|
|
2878
|
+
"</form>";
|
|
2879
|
+
var immediateForm =
|
|
2880
|
+
"<form method=\"post\" action=\"/account/subscriptions/" + esc(s.id) + "/cancel\">" +
|
|
2881
|
+
"<input type=\"hidden\" name=\"immediate\" value=\"1\">" +
|
|
2882
|
+
"<p class=\"account-confirm__option\">Cancel immediately" +
|
|
2883
|
+
(forfeitDays > 0
|
|
2884
|
+
? " — access ends now and you forfeit the remaining " + forfeitDays + " day" + (forfeitDays === 1 ? "" : "s") + " of this period."
|
|
2885
|
+
: " — access ends now.") +
|
|
2886
|
+
"</p>" +
|
|
2887
|
+
"<button type=\"submit\" class=\"btn-ghost\">Cancel immediately</button>" +
|
|
2888
|
+
"</form>";
|
|
2889
|
+
var body =
|
|
2890
|
+
"<section class=\"account-confirm\">" +
|
|
2891
|
+
"<nav class=\"breadcrumb\" aria-label=\"Breadcrumb\"><ol>" +
|
|
2892
|
+
"<li><a href=\"/account\">Account</a></li>" +
|
|
2893
|
+
"<li><a href=\"/account/subscriptions\">Subscriptions</a></li>" +
|
|
2894
|
+
"<li aria-current=\"page\">Cancel</li>" +
|
|
2895
|
+
"</ol></nav>" +
|
|
2896
|
+
"<h1 class=\"account-confirm__title\">Cancel " + esc(planSummary) + "?</h1>" +
|
|
2897
|
+
periodLine +
|
|
2898
|
+
"<div class=\"account-confirm__actions\">" +
|
|
2899
|
+
atPeriodEndForm +
|
|
2900
|
+
immediateForm +
|
|
2901
|
+
"<a class=\"btn-ghost\" href=\"/account/subscriptions\">Keep my subscription</a>" +
|
|
2902
|
+
"</div>" +
|
|
2903
|
+
"</section>";
|
|
2904
|
+
return _wrap({
|
|
2905
|
+
title: "Cancel subscription",
|
|
2906
|
+
shop_name: opts.shop_name || "blamejs.shop",
|
|
2907
|
+
cart_count: opts.cart_count == null ? 0 : opts.cart_count,
|
|
2908
|
+
theme_css: opts.theme_css,
|
|
2909
|
+
body: body,
|
|
2910
|
+
});
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2738
2913
|
// Product-level "Save to wishlist" control + social-proof count.
|
|
2739
2914
|
// Byte-compatible with the edge renderer (`worker/render/product.js`)
|
|
2740
2915
|
// so both paths emit identical markup. Action-only label — the toggle
|
|
@@ -5801,6 +5976,20 @@ function mount(router, deps) {
|
|
|
5801
5976
|
if (!orderId) return _send(res, 404, renderNotFound({ shop_name: shopName, theme: theme }));
|
|
5802
5977
|
var o = await deps.order.get(orderId);
|
|
5803
5978
|
if (!o) return _send(res, 404, renderNotFound({ shop_name: shopName, theme: theme }));
|
|
5979
|
+
// Ownership gate against IDOR: an order's confirmation page exposes
|
|
5980
|
+
// the customer's name, address, and line items by UUID alone. An order
|
|
5981
|
+
// that BELONGS to a customer (customer_id set) is viewable only by that
|
|
5982
|
+
// signed-in customer — anyone else (a different customer OR an
|
|
5983
|
+
// unauthenticated request) 404s rather than leaking it. A guest order
|
|
5984
|
+
// carries no customer_id and remains reachable via its unguessable URL
|
|
5985
|
+
// (the capability-URL model), so BOTH the just-placed-as-guest path AND
|
|
5986
|
+
// the signed-in-shopper-with-an-anonymous-cart path (checkout.confirm
|
|
5987
|
+
// derives the order from a cart that has no customer_id) still render
|
|
5988
|
+
// their own confirmation here.
|
|
5989
|
+
var orderAuth = _currentCustomerEnv(req);
|
|
5990
|
+
if (o.customer_id && (!orderAuth || o.customer_id !== orderAuth.customer_id)) {
|
|
5991
|
+
return _send(res, 404, renderNotFound({ shop_name: shopName, theme: theme }));
|
|
5992
|
+
}
|
|
5804
5993
|
// Same variant_id → {product, hero_media} lookup pattern as the
|
|
5805
5994
|
// cart route, applied to the order's frozen line items so the
|
|
5806
5995
|
// post-checkout page shows what the customer bought visually.
|
|
@@ -6557,22 +6746,26 @@ function mount(router, deps) {
|
|
|
6557
6746
|
return res.end ? res.end() : res.send("");
|
|
6558
6747
|
}
|
|
6559
6748
|
var productId = (req.body || {}).product_id;
|
|
6749
|
+
var removed = false;
|
|
6560
6750
|
try {
|
|
6561
6751
|
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 });
|
|
6752
|
+
if (already) { await deps.wishlist.remove({ customer_id: auth.customer_id, product_id: productId }); removed = true; }
|
|
6563
6753
|
else await deps.wishlist.add({ customer_id: auth.customer_id, product_id: productId });
|
|
6564
6754
|
} catch (e) {
|
|
6565
6755
|
res.status(e instanceof TypeError ? 400 : 500);
|
|
6566
6756
|
return res.end ? res.end((e && e.message) || "Error") : res.send((e && e.message) || "Error");
|
|
6567
6757
|
}
|
|
6758
|
+
var okKind = removed ? "removed" : "added";
|
|
6568
6759
|
var rt = (req.body || {}).return_to;
|
|
6569
6760
|
var dest;
|
|
6570
6761
|
if (typeof rt === "string" && /^\/[^/]/.test(rt)) {
|
|
6571
|
-
|
|
6762
|
+
// Only thread the success marker when returning to the wishlist
|
|
6763
|
+
// page (the PDP heart toggle is its own visible cue).
|
|
6764
|
+
dest = rt.indexOf("/account/wishlist") === 0 ? "/account/wishlist?ok=" + okKind : rt;
|
|
6572
6765
|
} else {
|
|
6573
6766
|
var product = null;
|
|
6574
6767
|
try { product = await deps.catalog.products.get(productId); } catch (_e) { product = null; }
|
|
6575
|
-
dest = product ? ("/products/" + encodeURIComponent(product.slug)) : "/account/wishlist";
|
|
6768
|
+
dest = product ? ("/products/" + encodeURIComponent(product.slug)) : ("/account/wishlist?ok=" + okKind);
|
|
6576
6769
|
}
|
|
6577
6770
|
res.status(303); res.setHeader && res.setHeader("location", dest);
|
|
6578
6771
|
return res.end ? res.end() : res.send("");
|
|
@@ -6603,8 +6796,10 @@ function mount(router, deps) {
|
|
|
6603
6796
|
items.push({ product: product, hero_media: media.length ? media[0] : null });
|
|
6604
6797
|
}
|
|
6605
6798
|
var cartCount = await _cartCountForReq(req);
|
|
6799
|
+
var wlUrl = req.url ? new URL(req.url, "http://localhost") : null;
|
|
6606
6800
|
_send(res, 200, renderWishlist({
|
|
6607
6801
|
items: items,
|
|
6802
|
+
notice: wlUrl ? wlUrl.searchParams.get("ok") : null,
|
|
6608
6803
|
shop_name: shopName,
|
|
6609
6804
|
cart_count: cartCount,
|
|
6610
6805
|
asset_prefix: deps.asset_prefix || "/assets/",
|
|
@@ -6675,8 +6870,10 @@ function mount(router, deps) {
|
|
|
6675
6870
|
items.push({ save: row, product: product, hero_media: media.length ? media[0] : null });
|
|
6676
6871
|
}
|
|
6677
6872
|
var cartCount = await _cartCountForReq(req);
|
|
6873
|
+
var savedUrl = req.url ? new URL(req.url, "http://localhost") : null;
|
|
6678
6874
|
_send(res, 200, renderSaved({
|
|
6679
6875
|
items: items,
|
|
6876
|
+
notice: savedUrl ? savedUrl.searchParams.get("ok") : null,
|
|
6680
6877
|
shop_name: shopName,
|
|
6681
6878
|
cart_count: cartCount,
|
|
6682
6879
|
asset_prefix: deps.asset_prefix || "/assets/",
|
|
@@ -6734,7 +6931,7 @@ function mount(router, deps) {
|
|
|
6734
6931
|
res.status(e instanceof TypeError ? 400 : 500);
|
|
6735
6932
|
return res.end ? res.end((e && e.message) || "Error") : res.send((e && e.message) || "Error");
|
|
6736
6933
|
}
|
|
6737
|
-
res.status(303); res.setHeader && res.setHeader("location", "/account/saved");
|
|
6934
|
+
res.status(303); res.setHeader && res.setHeader("location", "/account/saved?ok=removed");
|
|
6738
6935
|
return res.end ? res.end() : res.send("");
|
|
6739
6936
|
});
|
|
6740
6937
|
}
|
|
@@ -6774,13 +6971,33 @@ function mount(router, deps) {
|
|
|
6774
6971
|
}
|
|
6775
6972
|
return addr;
|
|
6776
6973
|
}
|
|
6974
|
+
// Map the ?ok=<kind> redirect marker the POST handlers set on
|
|
6975
|
+
// success to the human confirmation copy rendered (role="status")
|
|
6976
|
+
// at the top of the list. Unknown markers degrade to no notice.
|
|
6977
|
+
function _addrSuccessCopy(kind) {
|
|
6978
|
+
if (kind === "added") return "Address saved.";
|
|
6979
|
+
if (kind === "updated") return "Address updated.";
|
|
6980
|
+
if (kind === "removed") return "Address removed.";
|
|
6981
|
+
if (kind === "restored") return "Address restored.";
|
|
6982
|
+
if (kind === "default-shipping") return "Default shipping address updated.";
|
|
6983
|
+
if (kind === "default-billing") return "Default billing address updated.";
|
|
6984
|
+
return null;
|
|
6985
|
+
}
|
|
6777
6986
|
async function _renderAddrPage(req, res, auth, editAddr, notice, code) {
|
|
6778
6987
|
var rows = await deps.addresses.listForCustomer(auth.customer_id, { limit: 50 });
|
|
6779
6988
|
var cartCount = await _cartCountForReq(req);
|
|
6989
|
+
var url = req.url ? new URL(req.url, "http://localhost") : null;
|
|
6990
|
+
var okKind = url ? url.searchParams.get("ok") : null;
|
|
6991
|
+
var success = _addrSuccessCopy(okKind);
|
|
6992
|
+
// An Undo control is only offered for a just-removed row, and only
|
|
6993
|
+
// when the ?undo=<id> marker round-trips a real owned address id.
|
|
6994
|
+
var undoId = (okKind === "removed" && url) ? url.searchParams.get("undo") : null;
|
|
6780
6995
|
_send(res, code || 200, renderAddresses({
|
|
6781
6996
|
addresses: rows,
|
|
6782
6997
|
edit: editAddr || null,
|
|
6783
6998
|
notice: notice || null,
|
|
6999
|
+
success: success,
|
|
7000
|
+
undo_id: undoId || null,
|
|
6784
7001
|
shop_name: shopName,
|
|
6785
7002
|
cart_count: cartCount,
|
|
6786
7003
|
}));
|
|
@@ -6824,7 +7041,7 @@ function mount(router, deps) {
|
|
|
6824
7041
|
if (e instanceof TypeError) return _renderAddrPage(req, res, auth, null, (e && e.message) || "Please check the address.", 400);
|
|
6825
7042
|
throw e;
|
|
6826
7043
|
}
|
|
6827
|
-
res.status(303); res.setHeader && res.setHeader("location", "/account/addresses");
|
|
7044
|
+
res.status(303); res.setHeader && res.setHeader("location", "/account/addresses?ok=added");
|
|
6828
7045
|
return res.end ? res.end() : res.send("");
|
|
6829
7046
|
});
|
|
6830
7047
|
|
|
@@ -6840,11 +7057,11 @@ function mount(router, deps) {
|
|
|
6840
7057
|
}
|
|
6841
7058
|
throw e;
|
|
6842
7059
|
}
|
|
6843
|
-
res.status(303); res.setHeader && res.setHeader("location", "/account/addresses");
|
|
7060
|
+
res.status(303); res.setHeader && res.setHeader("location", "/account/addresses?ok=updated");
|
|
6844
7061
|
return res.end ? res.end() : res.send("");
|
|
6845
7062
|
});
|
|
6846
7063
|
|
|
6847
|
-
function _addrAction(verb, fn) {
|
|
7064
|
+
function _addrAction(verb, okKind, fn) {
|
|
6848
7065
|
router.post("/account/addresses/:id/" + verb, async function (req, res) {
|
|
6849
7066
|
var auth = _addrAuth(req, res); if (!auth) return;
|
|
6850
7067
|
var addr = await _ownedAddress(req, res, auth); if (!addr) return;
|
|
@@ -6853,13 +7070,64 @@ function mount(router, deps) {
|
|
|
6853
7070
|
res.status(e instanceof TypeError ? 400 : 500);
|
|
6854
7071
|
return res.end ? res.end((e && e.message) || "Error") : res.send((e && e.message) || "Error");
|
|
6855
7072
|
}
|
|
6856
|
-
res.status(303); res.setHeader && res.setHeader("location", "/account/addresses");
|
|
7073
|
+
res.status(303); res.setHeader && res.setHeader("location", "/account/addresses?ok=" + okKind);
|
|
6857
7074
|
return res.end ? res.end() : res.send("");
|
|
6858
7075
|
});
|
|
6859
7076
|
}
|
|
6860
|
-
_addrAction("default-shipping", function (id) { return deps.addresses.setDefaultShipping(id); });
|
|
6861
|
-
_addrAction("default-billing", function (id) { return deps.addresses.setDefaultBilling(id); });
|
|
6862
|
-
|
|
7077
|
+
_addrAction("default-shipping", "default-shipping", function (id) { return deps.addresses.setDefaultShipping(id); });
|
|
7078
|
+
_addrAction("default-billing", "default-billing", function (id) { return deps.addresses.setDefaultBilling(id); });
|
|
7079
|
+
|
|
7080
|
+
// Remove is destructive and CSP forbids a confirm() dialog, so it
|
|
7081
|
+
// routes through a server-rendered confirm page first; the POST that
|
|
7082
|
+
// actually archives lives behind that page. The list then surfaces a
|
|
7083
|
+
// success notice with an Undo (unarchive) control.
|
|
7084
|
+
router.get("/account/addresses/:id/remove", async function (req, res) {
|
|
7085
|
+
var auth = _addrAuth(req, res); if (!auth) return;
|
|
7086
|
+
var addr = await _ownedAddress(req, res, auth); if (!addr) return;
|
|
7087
|
+
var cartCount = await _cartCountForReq(req);
|
|
7088
|
+
_send(res, 200, renderAddressRemoveConfirm({
|
|
7089
|
+
address: addr,
|
|
7090
|
+
shop_name: shopName,
|
|
7091
|
+
cart_count: cartCount,
|
|
7092
|
+
}));
|
|
7093
|
+
});
|
|
7094
|
+
|
|
7095
|
+
router.post("/account/addresses/:id/archive", async function (req, res) {
|
|
7096
|
+
var auth = _addrAuth(req, res); if (!auth) return;
|
|
7097
|
+
var addr = await _ownedAddress(req, res, auth); if (!addr) return;
|
|
7098
|
+
try { await deps.addresses.archive(addr.id); }
|
|
7099
|
+
catch (e) {
|
|
7100
|
+
res.status(e instanceof TypeError ? 400 : 500);
|
|
7101
|
+
return res.end ? res.end((e && e.message) || "Error") : res.send((e && e.message) || "Error");
|
|
7102
|
+
}
|
|
7103
|
+
res.status(303);
|
|
7104
|
+
res.setHeader && res.setHeader("location", "/account/addresses?ok=removed&undo=" + encodeURIComponent(addr.id));
|
|
7105
|
+
return res.end ? res.end() : res.send("");
|
|
7106
|
+
});
|
|
7107
|
+
|
|
7108
|
+
// Undo path for a just-removed address. Unlike the by-id routes
|
|
7109
|
+
// above, this one resolves the row WITH archived rows included (the
|
|
7110
|
+
// address is archived by definition here) but still enforces
|
|
7111
|
+
// customer ownership before un-archiving.
|
|
7112
|
+
router.post("/account/addresses/:id/unarchive", async function (req, res) {
|
|
7113
|
+
var auth = _addrAuth(req, res); if (!auth) return;
|
|
7114
|
+
var addr;
|
|
7115
|
+
try { addr = await deps.addresses.get(req.params && req.params.id); }
|
|
7116
|
+
catch (e) {
|
|
7117
|
+
if (e instanceof TypeError) { _send(res, 404, renderNotFound({ shop_name: shopName, theme: theme })); return; }
|
|
7118
|
+
throw e;
|
|
7119
|
+
}
|
|
7120
|
+
if (!addr || addr.customer_id !== auth.customer_id) {
|
|
7121
|
+
return _send(res, 404, renderNotFound({ shop_name: shopName, theme: theme }));
|
|
7122
|
+
}
|
|
7123
|
+
try { await deps.addresses.unarchive(addr.id); }
|
|
7124
|
+
catch (e) {
|
|
7125
|
+
res.status(e instanceof TypeError ? 400 : 500);
|
|
7126
|
+
return res.end ? res.end((e && e.message) || "Error") : res.send((e && e.message) || "Error");
|
|
7127
|
+
}
|
|
7128
|
+
res.status(303); res.setHeader && res.setHeader("location", "/account/addresses?ok=restored");
|
|
7129
|
+
return res.end ? res.end() : res.send("");
|
|
7130
|
+
});
|
|
6863
7131
|
}
|
|
6864
7132
|
|
|
6865
7133
|
// Subscription self-management — the signed-in customer views their
|
|
@@ -6950,6 +7218,31 @@ function mount(router, deps) {
|
|
|
6950
7218
|
// Cancel mounts only when payment is wired (cancel composes Stripe).
|
|
6951
7219
|
// Without payment the list above stays read-only with a note.
|
|
6952
7220
|
if (deps.payment) {
|
|
7221
|
+
// Confirmation step (GET) ahead of the destructive POST. Renders
|
|
7222
|
+
// the period-end date + the days-forfeited consequence of an
|
|
7223
|
+
// immediate cancel. A subscription that isn't cancelable
|
|
7224
|
+
// (already canceled / winding down) redirects back to the list.
|
|
7225
|
+
router.get("/account/subscriptions/:id/cancel", async function (req, res) {
|
|
7226
|
+
var auth = _subsAuth(req, res); if (!auth) return;
|
|
7227
|
+
var sub = await _ownedSubscription(req, res, auth); if (!sub) return;
|
|
7228
|
+
if (!_subscriptionIsCancelable(sub)) {
|
|
7229
|
+
res.status(303); res.setHeader && res.setHeader("location", "/account/subscriptions");
|
|
7230
|
+
return res.end ? res.end() : res.send("");
|
|
7231
|
+
}
|
|
7232
|
+
// Join the plan for the confirm-page summary (best-effort — a
|
|
7233
|
+
// missing plan degrades the heading to a generic phrase).
|
|
7234
|
+
if (sub.plan_id != null) {
|
|
7235
|
+
try { sub.plan = await subscriptions.plans.get(sub.plan_id); }
|
|
7236
|
+
catch (_e) { sub.plan = null; }
|
|
7237
|
+
}
|
|
7238
|
+
var cartCount = await _cartCountForReq(req);
|
|
7239
|
+
_send(res, 200, renderSubscriptionCancelConfirm({
|
|
7240
|
+
subscription: sub,
|
|
7241
|
+
shop_name: shopName,
|
|
7242
|
+
cart_count: cartCount,
|
|
7243
|
+
}));
|
|
7244
|
+
});
|
|
7245
|
+
|
|
6953
7246
|
router.post("/account/subscriptions/:id/cancel", async function (req, res) {
|
|
6954
7247
|
var auth = _subsAuth(req, res); if (!auth) return;
|
|
6955
7248
|
var sub = await _ownedSubscription(req, res, auth); if (!sub) return;
|
|
@@ -7016,7 +7309,13 @@ function mount(router, deps) {
|
|
|
7016
7309
|
var auth = _returnsAuth(req, res); if (!auth) return;
|
|
7017
7310
|
var page = await deps.returns.listForCustomer(auth.customer_id, { limit: 50 });
|
|
7018
7311
|
var cartCount = await _cartCountForReq(req);
|
|
7019
|
-
|
|
7312
|
+
var retUrl = req.url ? new URL(req.url, "http://localhost") : null;
|
|
7313
|
+
_send(res, 200, renderReturns({
|
|
7314
|
+
rmas: page.rows,
|
|
7315
|
+
rma_code: retUrl ? retUrl.searchParams.get("ok") : null,
|
|
7316
|
+
shop_name: shopName,
|
|
7317
|
+
cart_count: cartCount,
|
|
7318
|
+
}));
|
|
7020
7319
|
});
|
|
7021
7320
|
|
|
7022
7321
|
router.get("/account/orders/:order_id/return", async function (req, res) {
|
|
@@ -7049,8 +7348,9 @@ function mount(router, deps) {
|
|
|
7049
7348
|
shop_name: shopName, cart_count: cartCount,
|
|
7050
7349
|
}));
|
|
7051
7350
|
}
|
|
7351
|
+
var requested;
|
|
7052
7352
|
try {
|
|
7053
|
-
await deps.returns.request({
|
|
7353
|
+
requested = await deps.returns.request({
|
|
7054
7354
|
order_id: order.id,
|
|
7055
7355
|
customer_id: auth.customer_id,
|
|
7056
7356
|
reason: body.reason,
|
|
@@ -7066,7 +7366,11 @@ function mount(router, deps) {
|
|
|
7066
7366
|
}
|
|
7067
7367
|
throw e;
|
|
7068
7368
|
}
|
|
7069
|
-
|
|
7369
|
+
// Round-trip the RMA code on the redirect so the list page can
|
|
7370
|
+
// confirm the request and echo the operator-readable handle.
|
|
7371
|
+
var rmaCode = requested && requested.rma_code ? requested.rma_code : "";
|
|
7372
|
+
res.status(303);
|
|
7373
|
+
res.setHeader && res.setHeader("location", "/account/returns?ok=" + encodeURIComponent(rmaCode));
|
|
7070
7374
|
return res.end ? res.end() : res.send("");
|
|
7071
7375
|
});
|
|
7072
7376
|
}
|
package/lib/vendor/MANIFEST.json
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
"_about": "blamejs.shop vendors a single framework — blamejs — which itself bundles every server-side crypto/identity dependency. The transitive packages blamejs ships are surfaced in its own MANIFEST.json at lib/vendor/blamejs/lib/vendor/MANIFEST.json — Trivy / Grype rely on that nested data for CVE attribution.",
|
|
4
4
|
"packages": {
|
|
5
5
|
"blamejs": {
|
|
6
|
-
"version": "0.13.
|
|
7
|
-
"tag": "v0.13.
|
|
6
|
+
"version": "0.13.8",
|
|
7
|
+
"tag": "v0.13.8",
|
|
8
8
|
"license": "Apache-2.0",
|
|
9
9
|
"author": "blamejs contributors",
|
|
10
10
|
"source": "https://github.com/blamejs/blamejs",
|
|
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.13.x
|
|
10
10
|
|
|
11
|
+
- v0.13.8 (2026-05-26) — **In-memory archive extraction for read-only / serverless filesystems.** Archive readers gain an in-memory extraction path so an uploaded archive can be opened and its contents read without writing anything to disk — the case a read-only or ephemeral serverless filesystem requires. b.archive.read.zip(...).extractEntries() and b.archive.read.tar(...).extractEntries() are async generators that yield each regular file entry as { name, bytes, size }, applying the same bomb-policy caps, b.guardArchive metadata cascade (which refuses a Zip-Slip / traversal archive wholesale), and entry-type-policy refusals as the disk extract() — only the disk realpath agreement check is omitted, since nothing is written and the caller owns where the returned bytes land. Directory and link entries carry no content and are not yielded. The guard cascade is factored into one shared path so disk and in-memory extraction refuse identically. Also a documentation fix: b.archive.gz no longer claims a b.archive.zip().toGzip() convenience exists — a ZIP is already DEFLATE-compressed per entry, so gzip-wrapping it gains nothing; gzip the uncompressed tar stream (the canonical .tar.gz) instead. **Added:** *`extractEntries()` — in-memory archive extraction (ZIP + tar)* — `b.archive.read.zip(source).extractEntries(opts?)` and `b.archive.read.tar(source).extractEntries(opts?)` are async generators yielding `{ name, bytes, size }` per regular file entry, never touching disk — for serverless / read-only filesystems where the disk `extract({ destination })` path cannot run. Same bomb-policy, guard-archive cascade, and entry-type-policy refusals as disk extraction; the bytes are byte-identical to what `extract()` writes. **Fixed:** *Removed the inaccurate `b.archive.zip().toGzip()` doc claim* — The `b.archive.gz` documentation described a `b.archive.zip().toGzip()` convenience method that does not (and should not) exist: a ZIP is already DEFLATE-compressed per entry, so gzip-wrapping it would compress already-compressed data for no benefit. `b.archive.tar().toGzip()` (the real `.tar.gz`) is unchanged.
|
|
12
|
+
|
|
13
|
+
- v0.13.7 (2026-05-26) — **Documentation accuracy — several primitives described shipped features as deferred.** A documentation sweep corrected primitive descriptions that still called features deferred after they shipped, so the wiki and inline docs now match the code. b.mdoc documents that device authentication (the ISO 18013-5 §9.1.3 signature variant, verifyDeviceAuth) is verified, not deferred — only the COSE_Mac0 device-auth variant remains refused. b.network.dns.dnssec documents that the root-to-zone chain walk against the IANA trust anchors (verifyChain) and NSEC / NSEC3 denial of existence (verifyDenial / nsec3Hash) ship, where the card previously said they were deferred. b.cose lists COSE_Mac0 and COSE_Encrypt0 among what it ships. The JMAP server documents its push channel, blob upload/download, and EmailSubmission handlers as present, and the submission server documents CHUNKING / BDAT as supported. A new test detector keeps this class of drift from recurring: it fails the build when a comment promises a feature lands in a version that has already shipped. **Fixed:** *Corrected `deferred`/`does-not-ship` docs for features that have shipped* — `b.mdoc` (device authentication, §9.1.3), `b.network.dns.dnssec` (chain walk + NSEC/NSEC3), `b.cose` (COSE_Mac0 + COSE_Encrypt0), the JMAP server (push, blob, EmailSubmission), and the submission server (BDAT/CHUNKING) all carried `@card`/`@intro` text describing shipped capabilities as deferred or not-shipped. The descriptions now match the implemented surface; genuinely-deferred items (the mdoc MAC variant, DNSSEC in-RDATA name canonicalization, COSE multi-signer/multi-recipient) remain documented as such. **Detectors:** *Overdue-defer detector in the codebase-pattern gate* — A new check fails the build when a comment promises a feature "lands in" / is "deferred to" / is "not supported in" a version that the package has already reached — catching stale deferral notes (a feature that shipped but whose comment still says otherwise, or a missed deadline) before they reach a release. An allowlist records the deliberate defer-with-condition exceptions.
|
|
14
|
+
|
|
11
15
|
- v0.13.6 (2026-05-26) — **`b.ai.frontierModelProtocol` — California SB 53 frontier-AI obligations.** b.ai.frontierModelProtocol assesses a developer's obligations under California's Transparency in Frontier Artificial Intelligence Act — SB 53, Cal. Bus. & Prof. Code §22757.10, effective 2026-01-01 — from a model's training compute and the developer's revenue. It reports whether the model crosses the frontier threshold (more than 10^26 training FLOPs), whether the developer is a large frontier developer (prior-year revenue, with affiliates, above $500M), and the resulting obligations: every frontier developer must report critical safety incidents and publish a transparency report, and a large frontier developer must additionally publish an annual safety framework and disclose its catastrophic-risk assessment. Passing a candidate safety framework reports which required elements (risk identification, mitigation, governance, cybersecurity, standards alignment) are missing. b.ai.frontierModelProtocol.incidentReport validates a critical-incident type against the Act's four categories and computes the notification deadline to the California Office of Emergency Services — 15 days from discovery, or 24 hours when there is an imminent risk of death or serious physical injury. The ca-tfaia compliance posture was already in the catalog. **Added:** *`b.ai.frontierModelProtocol` — SB 53 threshold classification, obligations, and incident reporting* — `b.ai.frontierModelProtocol({ trainingFlops, annualRevenueUsd, framework? })` returns `isFrontierModel`, `isLargeFrontierDeveloper`, the `obligations` list, and (when a framework is supplied) its `frameworkGaps`. `b.ai.frontierModelProtocol.incidentReport({ type, discoveredAt, imminentRiskToLife? })` builds a critical-safety-incident report with the California OES recipient and a `dueAt` / `deadlineHours` computed from the 15-day (or 24-hour imminent-risk) statutory window; `type` must be one of the four categories in `INCIDENT_TYPES`. Throws `FrontierProtocolError` on malformed input.
|
|
12
16
|
|
|
13
17
|
- v0.13.5 (2026-05-26) — **`b.ai.aedtBiasAudit` — NYC Local Law 144 bias audit.** b.ai.aedtBiasAudit computes the bias-audit figures New York City Local Law 144 requires before an Automated Employment Decision Tool may screen candidates (NYC Admin. Code §20-870 et seq.; DCWP rules 6 RCNY §5-300). Given the per-category counts an independent auditor collected — selected/total for a pass-fail tool, or scored-above-the-overall-median/total for a continuous-score tool — it returns the selection (or scoring) rate, the impact ratio (each group's rate divided by the most-selected group's rate), and an adverse-impact flag (impact ratio below the EEOC four-fifths threshold of 0.8) for every group, across the sex, race/ethnicity, and intersectional dimensions, plus the most-selected group per dimension and an overall flag. Categories under 2% of the audited data are marked excluded per DCWP discretion. It is a pure calculation that produces exactly the figures the annual published summary must contain — the law mandates the calculation, not any particular remediation. The relevant compliance postures (nyc-ll144, and ca-tfaia for California SB 53) were already in the catalog. **Added:** *`b.ai.aedtBiasAudit` — Local Law 144 selection/scoring rates and four-fifths impact ratios* — `b.ai.aedtBiasAudit({ type, metadata, categories, minCategoryShare? })` where `type` is `"selection"` (group entries `{ selected, total }`) or `"scoring"` (`{ scoredAboveMedian, total }`). Returns per-group rate, impact ratio, and `adverseImpact` flag across the `sex`, `raceEthnicity`, and `intersectional` dimensions, plus the most-selected group per dimension and an `anyAdverseImpact` summary. Categories below `minCategoryShare` (2% default) are excluded from the impact-ratio basis. Throws `AedtBiasAuditError` on malformed input.
|
|
@@ -228,7 +228,7 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
|
|
|
228
228
|
- **i18n** — CLDR plural rules, Accept-Language negotiation, Intl formatters, RTL (`b.i18n`)
|
|
229
229
|
- **CSV** — RFC 4180 with Excel formula-injection prevention (`b.csv`)
|
|
230
230
|
- **IDs + slugs** — RFC 9562 UUID v4 + v7 (`b.uuid`); URL-safe slugs (`b.slug`)
|
|
231
|
-
- **Time + archive** — TZ-aware datetime (`b.time`); ZIP creation + adversarial-safe read with bomb caps + path-traversal + LFH/CD-skew defense (`b.archive` + `b.archive.read.zip`); one-liner quarantine extraction (`b.safeArchive.extract`); fs / objectStore / http / buffer / trusted-stream adapter contract (`b.archive.adapters`)
|
|
231
|
+
- **Time + archive** — TZ-aware datetime (`b.time`); ZIP creation + adversarial-safe read with bomb caps + path-traversal + LFH/CD-skew defense (`b.archive` + `b.archive.read.zip`); one-liner quarantine extraction (`b.safeArchive.extract`); in-memory extraction with no disk write for read-only / serverless filesystems (`b.archive.read.zip(...).extractEntries()` / `.tar`); fs / objectStore / http / buffer / trusted-stream adapter contract (`b.archive.adapters`)
|
|
232
232
|
- **Pagination + forms** — HMAC-signed cursor pagination (`b.pagination`); HTML form rendering + validation + CSRF (`b.forms`)
|
|
233
233
|
|
|
234
234
|
### Production
|