@blamejs/blamejs-shop 0.1.20 → 0.1.23
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 +6 -0
- package/README.md +7 -3
- package/SECURITY.md +12 -0
- package/lib/admin.js +1035 -138
- package/lib/checkout.js +192 -22
- package/lib/collections.js +4 -3
- package/lib/customers.js +102 -0
- package/lib/giftcards.js +40 -0
- package/lib/order.js +22 -0
- package/lib/storefront.js +400 -53
- package/lib/vendor/MANIFEST.json +3 -3
- package/lib/vendor/blamejs/CHANGELOG.md +10 -0
- package/lib/vendor/blamejs/README.md +4 -0
- package/lib/vendor/blamejs/api-snapshot.json +129 -2
- package/lib/vendor/blamejs/index.js +12 -0
- package/lib/vendor/blamejs/lib/json-merge-patch.js +93 -0
- package/lib/vendor/blamejs/lib/json-patch.js +206 -0
- package/lib/vendor/blamejs/lib/json-path.js +638 -0
- package/lib/vendor/blamejs/lib/json-pointer.js +109 -0
- package/lib/vendor/blamejs/lib/jtd.js +234 -0
- package/lib/vendor/blamejs/lib/link-header.js +169 -0
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.12.57.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.12.58.json +22 -0
- package/lib/vendor/blamejs/release-notes/v0.12.60.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.12.61.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.12.62.json +18 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +18 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/json-merge-patch.test.js +81 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/json-patch.test.js +184 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/json-path.test.js +113 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/jtd.test.js +52 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/link-header.test.js +96 -0
- package/package.json +4 -2
package/lib/storefront.js
CHANGED
|
@@ -86,7 +86,7 @@ var LAYOUT =
|
|
|
86
86
|
" <title>{{title}} — {{shop_name}}</title>\n" +
|
|
87
87
|
" <meta name=\"description\" content=\"{{og_description}}\">\n" +
|
|
88
88
|
" <link rel=\"icon\" type=\"image/svg+xml\" href=\"/assets/brand/favicon.svg\">\n" +
|
|
89
|
-
" <link rel=\"stylesheet\" href=\"{{theme_css}}\">\n" +
|
|
89
|
+
" <link rel=\"stylesheet\" href=\"{{theme_css}}\"RAW_CSS_INTEGRITY>\n" +
|
|
90
90
|
" <meta property=\"og:type\" content=\"{{og_type}}\">\n" +
|
|
91
91
|
" <meta property=\"og:site_name\" content=\"{{shop_name}}\">\n" +
|
|
92
92
|
" <meta property=\"og:title\" content=\"{{og_title}}\">\n" +
|
|
@@ -211,10 +211,57 @@ var LAYOUT =
|
|
|
211
211
|
// forces every active session to re-fetch.
|
|
212
212
|
var DEFAULT_THEME_CSS_URL = "/assets/themes/default/css/main.css?v=" + require("../package.json").version;
|
|
213
213
|
|
|
214
|
+
// Footer copyright year — resolved once at module load. It's a near-static
|
|
215
|
+
// value (changes once a year); a `new Date()` allocation on every page
|
|
216
|
+
// render was wasteful. Containers are long-lived and restart often enough
|
|
217
|
+
// that a year-boundary staleness window doesn't matter for a copyright line.
|
|
218
|
+
var _COPYRIGHT_YEAR = String(new Date().getUTCFullYear());
|
|
219
|
+
|
|
220
|
+
// Client "island" scripts are served as external assets (same `?v=` cache-
|
|
221
|
+
// bust as the CSS), never inline — the storefront's strict `script-src
|
|
222
|
+
// 'self'` CSP blocks inline <script>, so an inline island silently fails
|
|
223
|
+
// in production. `'self'` allows these /assets/ files; the R2 asset sync
|
|
224
|
+
// (npm run sync-assets) uploads them alongside the stylesheets.
|
|
225
|
+
var _ASSET_JS_VERSION = require("../package.json").version;
|
|
226
|
+
var _assetFs = require("node:fs"); // allow:non-shop-require — one sync read of a bundled in-repo asset to compute its SRI hash via b.crypto.sri (the primitive needs the bytes); no remote/write, and b.staticServe.integrity is async so can't run at module load
|
|
227
|
+
|
|
228
|
+
// Subresource Integrity for the static assets. `b.crypto.sri` (SHA-384,
|
|
229
|
+
// W3C SRI 1.0) hashes the exact bytes the asset ships with; the browser
|
|
230
|
+
// refuses to run/apply the resource if the served bytes don't match — a
|
|
231
|
+
// defense against an R2/edge compromise or on-path injection. Computed
|
|
232
|
+
// once per asset from the in-repo file (the same bytes the deploy's asset
|
|
233
|
+
// sync uploads to R2), cached, and emitted as the `integrity` attribute.
|
|
234
|
+
// Same-origin, so no `crossorigin` is required for the check to run.
|
|
235
|
+
var _SRI_CACHE = {};
|
|
236
|
+
function _assetSri(relUnderThemeAssets) {
|
|
237
|
+
if (Object.prototype.hasOwnProperty.call(_SRI_CACHE, relUnderThemeAssets)) {
|
|
238
|
+
return _SRI_CACHE[relUnderThemeAssets];
|
|
239
|
+
}
|
|
240
|
+
var sri = null;
|
|
241
|
+
try {
|
|
242
|
+
// Forward slashes resolve cross-platform for fs reads; avoids a
|
|
243
|
+
// node:path require for one join.
|
|
244
|
+
var p = __dirname + "/../themes/default/assets/" + relUnderThemeAssets;
|
|
245
|
+
sri = b.crypto.sri(_assetFs.readFileSync(p), { algorithm: "sha384" });
|
|
246
|
+
} catch (_e) { sri = null; } // asset absent (e.g. unit-test cwd) → omit integrity, don't crash render
|
|
247
|
+
_SRI_CACHE[relUnderThemeAssets] = sri;
|
|
248
|
+
return sri;
|
|
249
|
+
}
|
|
250
|
+
function _islandScript(name) {
|
|
251
|
+
var sri = _assetSri("js/" + name);
|
|
252
|
+
return "<script src=\"/assets/themes/default/js/" + name + "?v=" + _ASSET_JS_VERSION + "\"" +
|
|
253
|
+
(sri ? " integrity=\"" + sri + "\"" : "") + " defer></script>";
|
|
254
|
+
}
|
|
255
|
+
|
|
214
256
|
function _wrap(opts) {
|
|
215
257
|
var themeCss = (opts && typeof opts.theme_css === "string" && opts.theme_css.length)
|
|
216
258
|
? opts.theme_css
|
|
217
259
|
: DEFAULT_THEME_CSS_URL;
|
|
260
|
+
// SRI for the stylesheet — only for the default theme CSS we ship (and
|
|
261
|
+
// can hash); a custom operator-supplied `theme_css` is their asset with
|
|
262
|
+
// an unknown body, so it's left without an integrity attribute.
|
|
263
|
+
var cssSri = (themeCss === DEFAULT_THEME_CSS_URL) ? _assetSri("css/main.css") : null;
|
|
264
|
+
var themeCssIntegrity = cssSri ? " integrity=\"" + cssSri + "\"" : "";
|
|
218
265
|
// OpenGraph / Twitter Card defaults — every page sets reasonable
|
|
219
266
|
// fallbacks; per-page renderers (PDP, etc.) can override via
|
|
220
267
|
// `opts.og_*` for a product-specific share preview.
|
|
@@ -228,16 +275,16 @@ function _wrap(opts) {
|
|
|
228
275
|
title: opts.title,
|
|
229
276
|
shop_name: shopName,
|
|
230
277
|
cart_count: opts.cart_count == null ? 0 : opts.cart_count,
|
|
231
|
-
year:
|
|
278
|
+
year: _COPYRIGHT_YEAR,
|
|
232
279
|
search_q: opts.search_q == null ? "" : opts.search_q,
|
|
233
|
-
theme_css:
|
|
280
|
+
theme_css: themeCss,
|
|
234
281
|
og_type: ogType,
|
|
235
282
|
og_title: ogTitle,
|
|
236
283
|
og_description: ogDescription,
|
|
237
284
|
og_image: ogImage,
|
|
238
285
|
og_url: ogUrl,
|
|
239
286
|
body: "RAW_BODY_PLACEHOLDER",
|
|
240
|
-
}).replace("RAW_BODY_PLACEHOLDER", opts.body);
|
|
287
|
+
}).replace("RAW_CSS_INTEGRITY", themeCssIntegrity).replace("RAW_BODY_PLACEHOLDER", opts.body);
|
|
241
288
|
// The body is RAW HTML (already rendered + escaped at the
|
|
242
289
|
// per-fragment level). The placeholder swap is post-render so the
|
|
243
290
|
// outer renderer's HTML-escape doesn't double-escape the inner
|
|
@@ -1371,6 +1418,107 @@ function renderReturns(opts) {
|
|
|
1371
1418
|
});
|
|
1372
1419
|
}
|
|
1373
1420
|
|
|
1421
|
+
// Subscription status pill — mirrors `_returnStatusBadge`. The status
|
|
1422
|
+
// string is one of Stripe's enum values (active, trialing, past_due,
|
|
1423
|
+
// canceled, …), surfaced as a CSS-classed badge the theme can style.
|
|
1424
|
+
function _subscriptionStatusBadge(status) {
|
|
1425
|
+
return "<span class=\"subscription-status subscription-status--" + b.template.escapeHtml(String(status)) + "\">" +
|
|
1426
|
+
b.template.escapeHtml(String(status)) + "</span>";
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
// A subscription is cancelable from the storefront while it's still
|
|
1430
|
+
// billing — active or trialing, and not already wound down at period
|
|
1431
|
+
// end. A canceled / past_due / incomplete row shows no Cancel control.
|
|
1432
|
+
var CANCELABLE_SUB_STATUSES = ["active", "trialing", "past_due"];
|
|
1433
|
+
function _subscriptionIsCancelable(sub) {
|
|
1434
|
+
return CANCELABLE_SUB_STATUSES.indexOf(sub.status) !== -1 && Number(sub.cancel_at_period_end) !== 1;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
// Customer-facing subscription list. `opts.subscriptions` is an array of
|
|
1438
|
+
// subscription rows, each optionally carrying a resolved `plan` (joined
|
|
1439
|
+
// by the route). `opts.can_cancel` is false when the deploy has no
|
|
1440
|
+
// payment handle wired — the list renders read-only with a note, since
|
|
1441
|
+
// cancel composes Stripe. Empty state points at the catalog (creation is
|
|
1442
|
+
// a separate Stripe-subscription-checkout surface, not built here).
|
|
1443
|
+
function renderAccountSubscriptions(opts) {
|
|
1444
|
+
var esc = b.template.escapeHtml;
|
|
1445
|
+
var subs = opts.subscriptions || [];
|
|
1446
|
+
var canCancel = opts.can_cancel !== false;
|
|
1447
|
+
var rowsHtml = "";
|
|
1448
|
+
for (var i = 0; i < subs.length; i += 1) {
|
|
1449
|
+
var s = subs[i];
|
|
1450
|
+
var plan = s.plan || null;
|
|
1451
|
+
var planSummary = "";
|
|
1452
|
+
if (plan) {
|
|
1453
|
+
var every = Number(plan.interval_count) > 1
|
|
1454
|
+
? "every " + Number(plan.interval_count) + " " + esc(String(plan.interval)) + "s"
|
|
1455
|
+
: "per " + esc(String(plan.interval));
|
|
1456
|
+
// Plans mirror Stripe's lowercase currency; pricing.format wants
|
|
1457
|
+
// the uppercase ISO 4217 code. A malformed currency / amount on a
|
|
1458
|
+
// row falls back to the interval-only summary rather than throwing
|
|
1459
|
+
// out of the renderer.
|
|
1460
|
+
var ccy = String(plan.currency || "usd").toUpperCase();
|
|
1461
|
+
var priceStr;
|
|
1462
|
+
try { priceStr = esc(pricing.format(Number(plan.amount_minor), ccy)) + " "; }
|
|
1463
|
+
catch (_e) { priceStr = ""; }
|
|
1464
|
+
planSummary = priceStr + every;
|
|
1465
|
+
} else {
|
|
1466
|
+
planSummary = "Plan unavailable";
|
|
1467
|
+
}
|
|
1468
|
+
var periodEnd = s.current_period_end
|
|
1469
|
+
? new Date(Number(s.current_period_end)).toISOString().slice(0, 10)
|
|
1470
|
+
: "";
|
|
1471
|
+
var renewalNote = "";
|
|
1472
|
+
if (Number(s.cancel_at_period_end) === 1 && periodEnd) {
|
|
1473
|
+
renewalNote = "Ends <time datetime=\"" + esc(periodEnd) + "\">" + esc(periodEnd) + "</time>";
|
|
1474
|
+
} else if (periodEnd) {
|
|
1475
|
+
renewalNote = "Renews <time datetime=\"" + esc(periodEnd) + "\">" + esc(periodEnd) + "</time>";
|
|
1476
|
+
}
|
|
1477
|
+
var cancelControl = "";
|
|
1478
|
+
if (canCancel && _subscriptionIsCancelable(s)) {
|
|
1479
|
+
cancelControl =
|
|
1480
|
+
"<form class=\"subscription-card__cancel\" method=\"post\" action=\"/account/subscriptions/" + esc(s.id) + "/cancel\">" +
|
|
1481
|
+
"<label class=\"subscription-card__when\">" +
|
|
1482
|
+
"<input type=\"checkbox\" name=\"immediate\" value=\"1\"> Cancel immediately (skip the rest of the period)" +
|
|
1483
|
+
"</label>" +
|
|
1484
|
+
"<button type=\"submit\" class=\"btn-ghost btn-ghost--sm\">Cancel subscription</button>" +
|
|
1485
|
+
"</form>";
|
|
1486
|
+
}
|
|
1487
|
+
rowsHtml +=
|
|
1488
|
+
"<li class=\"subscription-card\">" +
|
|
1489
|
+
"<div class=\"subscription-card__head\">" +
|
|
1490
|
+
"<span class=\"subscription-card__plan\">" + planSummary + "</span>" +
|
|
1491
|
+
_subscriptionStatusBadge(s.status) +
|
|
1492
|
+
"</div>" +
|
|
1493
|
+
(renewalNote ? "<p class=\"subscription-card__meta\">" + renewalNote + "</p>" : "") +
|
|
1494
|
+
cancelControl +
|
|
1495
|
+
"</li>";
|
|
1496
|
+
}
|
|
1497
|
+
var note = canCancel
|
|
1498
|
+
? ""
|
|
1499
|
+
: "<p class=\"subscription-note\">Cancellation isn't available on this store yet. Contact support to make changes.</p>";
|
|
1500
|
+
var inner = rowsHtml
|
|
1501
|
+
? note + "<ul class=\"subscription-list\">" + rowsHtml + "</ul>"
|
|
1502
|
+
: "<p class=\"subscription-empty\">You have no active subscriptions.</p>";
|
|
1503
|
+
var body =
|
|
1504
|
+
"<section class=\"account-subscriptions\">" +
|
|
1505
|
+
"<nav class=\"breadcrumb\" aria-label=\"Breadcrumb\"><ol>" +
|
|
1506
|
+
"<li><a href=\"/account\">Account</a></li>" +
|
|
1507
|
+
"<li aria-current=\"page\">Subscriptions</li>" +
|
|
1508
|
+
"</ol></nav>" +
|
|
1509
|
+
"<h1 class=\"account-subscriptions__title\">Subscriptions</h1>" +
|
|
1510
|
+
(opts.notice ? "<p class=\"form-notice\" role=\"status\">" + esc(String(opts.notice)) + "</p>" : "") +
|
|
1511
|
+
inner +
|
|
1512
|
+
"</section>";
|
|
1513
|
+
return _wrap({
|
|
1514
|
+
title: "Subscriptions",
|
|
1515
|
+
shop_name: opts.shop_name || "blamejs.shop",
|
|
1516
|
+
cart_count: opts.cart_count == null ? 0 : opts.cart_count,
|
|
1517
|
+
theme_css: opts.theme_css,
|
|
1518
|
+
body: body,
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1374
1522
|
// Product-level "Save to wishlist" control + social-proof count.
|
|
1375
1523
|
// Byte-compatible with the edge renderer (`worker/render/product.js`)
|
|
1376
1524
|
// so both paths emit identical markup. Action-only label — the toggle
|
|
@@ -1593,6 +1741,7 @@ var CHECKOUT_PAGE =
|
|
|
1593
1741
|
" <label class=\"form-field\"><span class=\"form-field__label\">State / Region</span><input type=\"text\" name=\"state\" maxlength=\"5\" autocomplete=\"address-level1\" class=\"form-field__input--xs\"></label>\n" +
|
|
1594
1742
|
" <label class=\"form-field\"><span class=\"form-field__label\">Postal code</span><input type=\"text\" name=\"postal\" maxlength=\"16\" autocomplete=\"postal-code\" class=\"form-field__input--sm\"></label>\n" +
|
|
1595
1743
|
" </div>\n" +
|
|
1744
|
+
" <div class=\"form-row\"><label class=\"form-field\"><span class=\"form-field__label\">Gift card code <span class=\"small\">(optional)</span></span><input type=\"text\" name=\"gift_card_code\" autocomplete=\"off\" placeholder=\"XXXX-XXXX-XXXX-XXXX\" maxlength=\"24\"></label></div>\n" +
|
|
1596
1745
|
" <div class=\"checkout-summary\">\n" +
|
|
1597
1746
|
" <h3>Order summary</h3>\n" +
|
|
1598
1747
|
" <dl>\n" +
|
|
@@ -1638,6 +1787,63 @@ function renderCheckoutForm(opts) {
|
|
|
1638
1787
|
});
|
|
1639
1788
|
}
|
|
1640
1789
|
|
|
1790
|
+
// ---- gift-card balance check (customer-facing) -------------------------
|
|
1791
|
+
//
|
|
1792
|
+
// A bearer gift-card code is private — the page never confirms whether a
|
|
1793
|
+
// code "exists". A recognized active card shows its balance; anything else
|
|
1794
|
+
// (unknown, malformed, expired, voided, redeemed) shows the same generic
|
|
1795
|
+
// "couldn't find a balance" message so the page is not a code-probing
|
|
1796
|
+
// oracle. Server-rendered; the result re-renders the same page with the
|
|
1797
|
+
// outcome inline.
|
|
1798
|
+
var GIFT_CARD_PAGE =
|
|
1799
|
+
"<section class=\"checkout-page\" style=\"max-width:32rem;margin:0 auto;\">\n" +
|
|
1800
|
+
" <header class=\"section-head\">\n" +
|
|
1801
|
+
" <p class=\"eyebrow\">Gift cards</p>\n" +
|
|
1802
|
+
" <h1 class=\"section-head__title\">Check a gift card balance</h1>\n" +
|
|
1803
|
+
" <p class=\"section-head__lede\">Enter the code printed on your gift card to see what's left to spend. Apply it at checkout.</p>\n" +
|
|
1804
|
+
" </header>\n" +
|
|
1805
|
+
" RAW_RESULT" +
|
|
1806
|
+
" <form method=\"post\" action=\"/gift-cards/balance\" class=\"form-stack\">\n" +
|
|
1807
|
+
" <div class=\"form-row\"><label class=\"form-field\"><span class=\"form-field__label\">Gift card code</span><input type=\"text\" name=\"code\" autocomplete=\"off\" placeholder=\"XXXX-XXXX-XXXX-XXXX\" maxlength=\"24\" required></label></div>\n" +
|
|
1808
|
+
" <div class=\"form-actions\"><button type=\"submit\" class=\"btn-primary\">Check balance</button></div>\n" +
|
|
1809
|
+
" </form>\n" +
|
|
1810
|
+
"</section>\n";
|
|
1811
|
+
|
|
1812
|
+
function renderGiftCardBalance(opts) {
|
|
1813
|
+
opts = opts || {};
|
|
1814
|
+
var shopName = opts.shop_name || "blamejs.shop";
|
|
1815
|
+
var resultHtml = "";
|
|
1816
|
+
if (opts.balance != null) {
|
|
1817
|
+
var bal = pricing.format(opts.balance.balance_minor, opts.balance.currency);
|
|
1818
|
+
resultHtml =
|
|
1819
|
+
"<div class=\"checkout-summary\"><h3>Balance</h3><dl>" +
|
|
1820
|
+
"<div class=\"checkout-summary__total\"><dt>Available</dt><dd>" + b.template.escapeHtml(bal) + "</dd></div>" +
|
|
1821
|
+
"</dl></div>";
|
|
1822
|
+
} else if (opts.not_found) {
|
|
1823
|
+
resultHtml =
|
|
1824
|
+
"<p class=\"auth-form__message auth-form__message--err\">" +
|
|
1825
|
+
"We couldn't find a balance for that code. Check the characters and try again." +
|
|
1826
|
+
"</p>";
|
|
1827
|
+
}
|
|
1828
|
+
if (opts.theme) {
|
|
1829
|
+
return opts.theme.render("gift-card-balance", {
|
|
1830
|
+
title: "Gift card balance",
|
|
1831
|
+
shop_name: shopName,
|
|
1832
|
+
cart_count: opts.cart_count == null ? 0 : opts.cart_count,
|
|
1833
|
+
result: resultHtml,
|
|
1834
|
+
asset_css_main: opts.theme.assetUrl("css/main.css"),
|
|
1835
|
+
});
|
|
1836
|
+
}
|
|
1837
|
+
var body = GIFT_CARD_PAGE.replace("RAW_RESULT", resultHtml);
|
|
1838
|
+
return _wrap({
|
|
1839
|
+
title: "Gift card balance",
|
|
1840
|
+
shop_name: shopName,
|
|
1841
|
+
cart_count: opts.cart_count == null ? 0 : opts.cart_count,
|
|
1842
|
+
theme_css: opts.theme_css,
|
|
1843
|
+
body: body,
|
|
1844
|
+
});
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1641
1847
|
function _paypalCheckoutBlock(clientId, currency) {
|
|
1642
1848
|
var esc = b.template.escapeHtml;
|
|
1643
1849
|
var cid = esc(String(clientId));
|
|
@@ -2287,28 +2493,7 @@ var ACCOUNT_LOGIN_PAGE =
|
|
|
2287
2493
|
" RAW_LOGIN_OAUTH\n" +
|
|
2288
2494
|
" <p class=\"auth-card__alt\">New here? <a href=\"/account/register\">Create an account →</a></p>\n" +
|
|
2289
2495
|
" </div>\n" +
|
|
2290
|
-
"
|
|
2291
|
-
" (function () {\n" +
|
|
2292
|
-
" function _b64uToBuf(s){ s = s.replace(/-/g,'+').replace(/_/g,'/'); while(s.length%4)s+='='; var raw=atob(s); var arr=new Uint8Array(raw.length); for(var i=0;i<raw.length;i++)arr[i]=raw.charCodeAt(i); return arr.buffer; }\n" +
|
|
2293
|
-
" function _bufToB64u(buf){ var b=new Uint8Array(buf), s=''; for(var i=0;i<b.length;i++)s+=String.fromCharCode(b[i]); return btoa(s).replace(/\\+/g,'-').replace(/\\//g,'_').replace(/=+$/,''); }\n" + // allow:inline-base64url-three-replace — browser-side helper; the page has no `b.crypto` to call, btoa is the runtime-built-in equivalent
|
|
2294
|
-
" var form=document.getElementById('login-form');\n" +
|
|
2295
|
-
" var msg=document.getElementById('login-message');\n" +
|
|
2296
|
-
" form.addEventListener('submit', async function(ev){\n" +
|
|
2297
|
-
" ev.preventDefault(); msg.textContent='Requesting challenge...';\n" +
|
|
2298
|
-
" try {\n" +
|
|
2299
|
-
" var beginR = await fetch('/account/passkey/login-begin', { method:'POST', credentials:'same-origin', headers:{'content-type':'application/json'}, body: JSON.stringify({ email: document.getElementById('email').value }) });\n" +
|
|
2300
|
-
" if (!beginR.ok) { msg.textContent = 'Sign-in unavailable.'; return; }\n" +
|
|
2301
|
-
" var options = await beginR.json();\n" +
|
|
2302
|
-
" options.challenge = _b64uToBuf(options.challenge);\n" +
|
|
2303
|
-
" if (options.allowCredentials) options.allowCredentials = options.allowCredentials.map(function(c){ return Object.assign({}, c, { id: _b64uToBuf(c.id) }); });\n" +
|
|
2304
|
-
" var assertion = await navigator.credentials.get({ publicKey: options });\n" +
|
|
2305
|
-
" var payload = { id: assertion.id, rawId: _bufToB64u(assertion.rawId), type: assertion.type, response: { authenticatorData: _bufToB64u(assertion.response.authenticatorData), clientDataJSON: _bufToB64u(assertion.response.clientDataJSON), signature: _bufToB64u(assertion.response.signature), userHandle: assertion.response.userHandle ? _bufToB64u(assertion.response.userHandle) : null } };\n" +
|
|
2306
|
-
" var finishR = await fetch('/account/passkey/login-finish', { method:'POST', credentials:'same-origin', headers:{'content-type':'application/json'}, body: JSON.stringify(payload) });\n" +
|
|
2307
|
-
" if (finishR.ok) { window.location.href = '/account'; } else { msg.textContent = 'Sign-in failed.'; }\n" +
|
|
2308
|
-
" } catch (e) { msg.textContent = (e && e.message) || 'Sign-in error.'; }\n" +
|
|
2309
|
-
" });\n" +
|
|
2310
|
-
" })();\n" +
|
|
2311
|
-
" </script>\n" +
|
|
2496
|
+
" RAW_LOGIN_SCRIPT\n" +
|
|
2312
2497
|
"</section>\n";
|
|
2313
2498
|
|
|
2314
2499
|
var LOGIN_ERROR_MESSAGES = {
|
|
@@ -2336,7 +2521,8 @@ function renderAccountLogin(opts) {
|
|
|
2336
2521
|
: "";
|
|
2337
2522
|
var body = ACCOUNT_LOGIN_PAGE
|
|
2338
2523
|
.replace("RAW_LOGIN_OAUTH", oauthHtml)
|
|
2339
|
-
.replace("RAW_LOGIN_ERROR", errHtml)
|
|
2524
|
+
.replace("RAW_LOGIN_ERROR", errHtml)
|
|
2525
|
+
.replace("RAW_LOGIN_SCRIPT", _islandScript("passkey-login.js"));
|
|
2340
2526
|
return _wrap({
|
|
2341
2527
|
title: "Sign in",
|
|
2342
2528
|
shop_name: opts.shop_name || "blamejs.shop",
|
|
@@ -2360,29 +2546,7 @@ var ACCOUNT_REGISTER_PAGE =
|
|
|
2360
2546
|
" </form>\n" +
|
|
2361
2547
|
" <p class=\"auth-card__alt\">Already have one? <a href=\"/account/login\">Sign in →</a></p>\n" +
|
|
2362
2548
|
" </div>\n" +
|
|
2363
|
-
"
|
|
2364
|
-
" (function () {\n" +
|
|
2365
|
-
" function _b64uToBuf(s){ s = s.replace(/-/g,'+').replace(/_/g,'/'); while(s.length%4)s+='='; var raw=atob(s); var arr=new Uint8Array(raw.length); for(var i=0;i<raw.length;i++)arr[i]=raw.charCodeAt(i); return arr.buffer; }\n" +
|
|
2366
|
-
" function _bufToB64u(buf){ var b=new Uint8Array(buf), s=''; for(var i=0;i<b.length;i++)s+=String.fromCharCode(b[i]); return btoa(s).replace(/\\+/g,'-').replace(/\\//g,'_').replace(/=+$/,''); }\n" + // allow:inline-base64url-three-replace — browser-side helper; the page has no `b.crypto` to call, btoa is the runtime-built-in equivalent
|
|
2367
|
-
" var form=document.getElementById('reg-form');\n" +
|
|
2368
|
-
" var msg=document.getElementById('reg-message');\n" +
|
|
2369
|
-
" form.addEventListener('submit', async function(ev){\n" +
|
|
2370
|
-
" ev.preventDefault(); msg.textContent='Creating account...';\n" +
|
|
2371
|
-
" try {\n" +
|
|
2372
|
-
" var beginR = await fetch('/account/passkey/register-begin', { method:'POST', credentials:'same-origin', headers:{'content-type':'application/json'}, body: JSON.stringify({ email: document.getElementById('email').value, display_name: document.getElementById('display_name').value }) });\n" +
|
|
2373
|
-
" if (!beginR.ok) { msg.textContent = (await beginR.text()) || 'Registration unavailable.'; return; }\n" +
|
|
2374
|
-
" var options = await beginR.json();\n" +
|
|
2375
|
-
" options.challenge = _b64uToBuf(options.challenge);\n" +
|
|
2376
|
-
" options.user.id = _b64uToBuf(options.user.id);\n" +
|
|
2377
|
-
" if (options.excludeCredentials) options.excludeCredentials = options.excludeCredentials.map(function(c){ return Object.assign({}, c, { id: _b64uToBuf(c.id) }); });\n" +
|
|
2378
|
-
" var att = await navigator.credentials.create({ publicKey: options });\n" +
|
|
2379
|
-
" var payload = { id: att.id, rawId: _bufToB64u(att.rawId), type: att.type, response: { attestationObject: _bufToB64u(att.response.attestationObject), clientDataJSON: _bufToB64u(att.response.clientDataJSON), transports: att.response.getTransports ? att.response.getTransports() : [] } };\n" +
|
|
2380
|
-
" var finishR = await fetch('/account/passkey/register-finish', { method:'POST', credentials:'same-origin', headers:{'content-type':'application/json'}, body: JSON.stringify(payload) });\n" +
|
|
2381
|
-
" if (finishR.ok) { window.location.href = '/account'; } else { msg.textContent = (await finishR.text()) || 'Enrollment failed.'; }\n" +
|
|
2382
|
-
" } catch (e) { msg.textContent = (e && e.message) || 'Registration error.'; }\n" +
|
|
2383
|
-
" });\n" +
|
|
2384
|
-
" })();\n" +
|
|
2385
|
-
" </script>\n" +
|
|
2549
|
+
" RAW_REGISTER_SCRIPT\n" +
|
|
2386
2550
|
"</section>\n";
|
|
2387
2551
|
|
|
2388
2552
|
function renderAccountRegister(opts) {
|
|
@@ -2392,7 +2556,7 @@ function renderAccountRegister(opts) {
|
|
|
2392
2556
|
shop_name: opts.shop_name || "blamejs.shop",
|
|
2393
2557
|
cart_count: opts.cart_count,
|
|
2394
2558
|
theme_css: opts.theme_css,
|
|
2395
|
-
body: ACCOUNT_REGISTER_PAGE,
|
|
2559
|
+
body: ACCOUNT_REGISTER_PAGE.replace("RAW_REGISTER_SCRIPT", _islandScript("passkey-register.js")),
|
|
2396
2560
|
});
|
|
2397
2561
|
}
|
|
2398
2562
|
|
|
@@ -2410,6 +2574,7 @@ var ACCOUNT_DASH_PAGE =
|
|
|
2410
2574
|
" <a class=\"btn-secondary\" href=\"/account/recently-viewed\">Recently viewed</a>\n" +
|
|
2411
2575
|
" <a class=\"btn-secondary\" href=\"/account/addresses\">Addresses</a>\n" +
|
|
2412
2576
|
" <a class=\"btn-secondary\" href=\"/account/returns\">Returns</a>\n" +
|
|
2577
|
+
" <a class=\"btn-secondary\" href=\"/account/subscriptions\">Subscriptions</a>\n" +
|
|
2413
2578
|
" <form method=\"post\" action=\"/account/logout\"><button type=\"submit\" class=\"btn-ghost\">Sign out</button></form>\n" +
|
|
2414
2579
|
" </div>\n" +
|
|
2415
2580
|
" </header>\n" +
|
|
@@ -2523,6 +2688,10 @@ function mount(router, deps) {
|
|
|
2523
2688
|
// the inline-string templates above stay in force (operators on
|
|
2524
2689
|
// older deploys keep their current look without a migration step).
|
|
2525
2690
|
var theme = deps.theme || null;
|
|
2691
|
+
// Subscription self-management — opts in /account/subscriptions. The
|
|
2692
|
+
// cancel route additionally needs the payment handle (cancel composes
|
|
2693
|
+
// Stripe via the primitive); without it the list stays read-only.
|
|
2694
|
+
var subscriptions = deps.subscriptions || null;
|
|
2526
2695
|
|
|
2527
2696
|
function _send(res, status, html) {
|
|
2528
2697
|
res.status(status);
|
|
@@ -2898,8 +3067,17 @@ function mount(router, deps) {
|
|
|
2898
3067
|
ship_to: shipTo,
|
|
2899
3068
|
selected_shipping_id: defaultShipId || "std",
|
|
2900
3069
|
customer: { email: body.email, name: body.name },
|
|
3070
|
+
gift_card_code: body.gift_card_code || undefined,
|
|
2901
3071
|
idempotency_key: "checkout:" + c.id + ":" + b.uuid.v7(),
|
|
2902
3072
|
});
|
|
3073
|
+
// When a gift card fully covered the order there's no Stripe
|
|
3074
|
+
// intent — the order is already paid. Skip the pay-cookie +
|
|
3075
|
+
// pay page and land the customer straight on the confirmation.
|
|
3076
|
+
if (!result.payment_intent) {
|
|
3077
|
+
res.status(303);
|
|
3078
|
+
res.setHeader && res.setHeader("location", "/orders/" + result.order.id);
|
|
3079
|
+
return res.end ? res.end() : res.send("");
|
|
3080
|
+
}
|
|
2903
3081
|
// Set a short-lived pay cookie so /pay/:order_id can serve the
|
|
2904
3082
|
// client_secret without re-running confirm. Scoped to /pay/ +
|
|
2905
3083
|
// SameSite=Strict so it's only ever sent to the pay route.
|
|
@@ -2910,7 +3088,12 @@ function mount(router, deps) {
|
|
|
2910
3088
|
res.setHeader && res.setHeader("location", "/pay/" + result.order.id);
|
|
2911
3089
|
return res.end ? res.end() : res.send("");
|
|
2912
3090
|
} catch (e) {
|
|
2913
|
-
|
|
3091
|
+
// A bad customer input (malformed shape OR a gift-card code the
|
|
3092
|
+
// customer typed that can't be applied) is a 400, not a 500 —
|
|
3093
|
+
// the gift-card errors carry a GIFTCARD_* code so a fat-fingered
|
|
3094
|
+
// code re-prompts rather than 500-ing the checkout.
|
|
3095
|
+
var clientErr = (e instanceof TypeError) || (e && typeof e.code === "string" && e.code.indexOf("GIFTCARD_") === 0);
|
|
3096
|
+
res.status(clientErr ? 400 : 500);
|
|
2914
3097
|
var msg = (e && e.message) || "checkout failed";
|
|
2915
3098
|
return res.end ? res.end(msg) : res.send(msg);
|
|
2916
3099
|
}
|
|
@@ -2948,14 +3131,22 @@ function mount(router, deps) {
|
|
|
2948
3131
|
ship_to: shipTo,
|
|
2949
3132
|
selected_shipping_id: body.selected_shipping_id || defaultShipId || "std",
|
|
2950
3133
|
customer: { email: body.email, name: body.name },
|
|
3134
|
+
gift_card_code: body.gift_card_code || undefined,
|
|
2951
3135
|
idempotency_key: "paypal:" + c.id + ":" + b.uuid.v7(),
|
|
2952
3136
|
return_url: body.return_url || undefined,
|
|
2953
3137
|
cancel_url: body.cancel_url || undefined,
|
|
2954
3138
|
});
|
|
3139
|
+
// Gift card fully covered the order — no PayPal order id to
|
|
3140
|
+
// approve. Tell the button to redirect straight to the
|
|
3141
|
+
// confirmation page instead of opening the PayPal popup.
|
|
3142
|
+
if (!created.paypal_order_id) {
|
|
3143
|
+
return _json(200, { paid_by_gift_card: true, order_id: created.order.id, redirect: "/orders/" + created.order.id });
|
|
3144
|
+
}
|
|
2955
3145
|
// The PayPal JS SDK's createOrder expects `{ id }`.
|
|
2956
3146
|
return _json(200, { id: created.paypal_order_id, order_id: created.order.id });
|
|
2957
3147
|
} catch (e) {
|
|
2958
|
-
|
|
3148
|
+
var gcErr = e && typeof e.code === "string" && e.code.indexOf("GIFTCARD_") === 0;
|
|
3149
|
+
return _json((e instanceof TypeError || gcErr) ? 400 : 502, { error: (e && e.message) || "paypal-create-failed" });
|
|
2959
3150
|
}
|
|
2960
3151
|
});
|
|
2961
3152
|
|
|
@@ -3129,6 +3320,47 @@ function mount(router, deps) {
|
|
|
3129
3320
|
}
|
|
3130
3321
|
}
|
|
3131
3322
|
|
|
3323
|
+
// ---- gift-card balance check (customer-facing) ---------------------
|
|
3324
|
+
//
|
|
3325
|
+
// Mounts when deps.giftcards is wired. GET renders the lookup form;
|
|
3326
|
+
// POST looks the code up and re-renders with the balance (or a
|
|
3327
|
+
// generic not-found). The page is deliberately not a code-existence
|
|
3328
|
+
// oracle: unknown, malformed, expired, voided, and redeemed codes
|
|
3329
|
+
// all produce the same "couldn't find a balance" outcome.
|
|
3330
|
+
if (deps.giftcards) {
|
|
3331
|
+
router.get("/gift-cards", async function (req, res) {
|
|
3332
|
+
_send(res, 200, renderGiftCardBalance({
|
|
3333
|
+
shop_name: shopName, theme: theme, cart_count: await _cartCountForReq(req),
|
|
3334
|
+
}));
|
|
3335
|
+
});
|
|
3336
|
+
|
|
3337
|
+
router.post("/gift-cards/balance", async function (req, res) {
|
|
3338
|
+
var body = req.body || {};
|
|
3339
|
+
var code = typeof body.code === "string" ? body.code : "";
|
|
3340
|
+
var balance = null;
|
|
3341
|
+
var notFound = true;
|
|
3342
|
+
try {
|
|
3343
|
+
var view = await deps.giftcards.balance(code);
|
|
3344
|
+
// Only an active card with a positive balance surfaces a
|
|
3345
|
+
// number — an expired / voided / fully-redeemed card is the
|
|
3346
|
+
// same dead end as an unknown code to the customer.
|
|
3347
|
+
if (view && view.status === "active") {
|
|
3348
|
+
balance = { balance_minor: view.balance_minor, currency: view.currency };
|
|
3349
|
+
notFound = false;
|
|
3350
|
+
}
|
|
3351
|
+
} catch (e) {
|
|
3352
|
+
// A malformed code throws TypeError at the canonicalizer —
|
|
3353
|
+
// swallow it into the same generic not-found. Any other error
|
|
3354
|
+
// (e.g. a DB fault) bubbles so it isn't masked as not-found.
|
|
3355
|
+
if (!(e instanceof TypeError)) throw e;
|
|
3356
|
+
}
|
|
3357
|
+
_send(res, 200, renderGiftCardBalance({
|
|
3358
|
+
shop_name: shopName, theme: theme, cart_count: await _cartCountForReq(req),
|
|
3359
|
+
balance: balance, not_found: notFound,
|
|
3360
|
+
}));
|
|
3361
|
+
});
|
|
3362
|
+
}
|
|
3363
|
+
|
|
3132
3364
|
// ---- customer accounts (passkey-only) ------------------------------
|
|
3133
3365
|
//
|
|
3134
3366
|
// Mount only when deps.customers is supplied (operator opts in by
|
|
@@ -3978,6 +4210,119 @@ function mount(router, deps) {
|
|
|
3978
4210
|
_addrAction("archive", function (id) { return deps.addresses.archive(id); });
|
|
3979
4211
|
}
|
|
3980
4212
|
|
|
4213
|
+
// Subscription self-management — the signed-in customer views their
|
|
4214
|
+
// own recurring subscriptions and cancels them. The list mounts on
|
|
4215
|
+
// the subscriptions primitive alone; the cancel route additionally
|
|
4216
|
+
// needs the payment handle (cancel composes Stripe through
|
|
4217
|
+
// `subscriptions.subscriptions.cancel`, mirroring the admin gate). A
|
|
4218
|
+
// subscription is fetched + ownership-checked against the authed
|
|
4219
|
+
// customer before any cancel — a guessed/forged id never reaches
|
|
4220
|
+
// another customer's row. Creation (a Stripe subscription-checkout
|
|
4221
|
+
// flow) is a separate surface; this is the management view.
|
|
4222
|
+
if (subscriptions) {
|
|
4223
|
+
var subsCanCancel = !!deps.payment;
|
|
4224
|
+
|
|
4225
|
+
function _subsAuth(req, res) {
|
|
4226
|
+
var auth;
|
|
4227
|
+
try { auth = _currentCustomer(req); }
|
|
4228
|
+
catch (e) {
|
|
4229
|
+
if (e && e.code === "vault/not-initialized") { _serviceUnavailable(res, "auth not configured"); return null; }
|
|
4230
|
+
throw e;
|
|
4231
|
+
}
|
|
4232
|
+
if (!auth) {
|
|
4233
|
+
res.status(303); res.setHeader && res.setHeader("location", "/account/login");
|
|
4234
|
+
res.end ? res.end() : res.send("");
|
|
4235
|
+
return null;
|
|
4236
|
+
}
|
|
4237
|
+
return auth;
|
|
4238
|
+
}
|
|
4239
|
+
|
|
4240
|
+
// Resolve the customer's subscriptions and join each to its plan.
|
|
4241
|
+
// Plans are batched: one `plans.get` per distinct plan_id, cached
|
|
4242
|
+
// across rows, so a customer with many subscriptions on the same
|
|
4243
|
+
// plan costs one plan read, not one per subscription (no N+1). A
|
|
4244
|
+
// read failure (table not migrated) degrades to an empty list
|
|
4245
|
+
// rather than 500-ing the account page.
|
|
4246
|
+
async function _subsForCustomer(customerId) {
|
|
4247
|
+
var rows;
|
|
4248
|
+
try { rows = await subscriptions.subscriptions.list({ customer_id: customerId }); }
|
|
4249
|
+
catch (e) {
|
|
4250
|
+
if (e instanceof TypeError) return [];
|
|
4251
|
+
throw e;
|
|
4252
|
+
}
|
|
4253
|
+
var planCache = {};
|
|
4254
|
+
for (var i = 0; i < rows.length; i += 1) {
|
|
4255
|
+
var pid = rows[i].plan_id;
|
|
4256
|
+
if (pid != null && !Object.prototype.hasOwnProperty.call(planCache, pid)) {
|
|
4257
|
+
try { planCache[pid] = await subscriptions.plans.get(pid); }
|
|
4258
|
+
catch (_e) { planCache[pid] = null; }
|
|
4259
|
+
}
|
|
4260
|
+
rows[i].plan = pid != null ? planCache[pid] : null;
|
|
4261
|
+
}
|
|
4262
|
+
return rows;
|
|
4263
|
+
}
|
|
4264
|
+
|
|
4265
|
+
// Load the subscription named in :id and confirm it belongs to the
|
|
4266
|
+
// signed-in customer. A malformed id (guardUuid TypeError), a
|
|
4267
|
+
// missing row, or another customer's subscription all return null
|
|
4268
|
+
// after sending a 404 — never a 500, never a cross-customer cancel.
|
|
4269
|
+
async function _ownedSubscription(req, res, auth) {
|
|
4270
|
+
var sub;
|
|
4271
|
+
try { sub = await subscriptions.subscriptions.get(req.params && req.params.id); }
|
|
4272
|
+
catch (e) {
|
|
4273
|
+
if (e instanceof TypeError) { _send(res, 404, renderNotFound({ shop_name: shopName, theme: theme })); return null; }
|
|
4274
|
+
throw e;
|
|
4275
|
+
}
|
|
4276
|
+
if (!sub || sub.customer_id !== auth.customer_id) {
|
|
4277
|
+
_send(res, 404, renderNotFound({ shop_name: shopName, theme: theme }));
|
|
4278
|
+
return null;
|
|
4279
|
+
}
|
|
4280
|
+
return sub;
|
|
4281
|
+
}
|
|
4282
|
+
|
|
4283
|
+
router.get("/account/subscriptions", async function (req, res) {
|
|
4284
|
+
var auth = _subsAuth(req, res); if (!auth) return;
|
|
4285
|
+
var rows = await _subsForCustomer(auth.customer_id);
|
|
4286
|
+
var cartCount = await _cartCountForReq(req);
|
|
4287
|
+
var url = req.url ? new URL(req.url, "http://localhost") : null;
|
|
4288
|
+
var canceled = !!(url && url.searchParams.get("canceled"));
|
|
4289
|
+
_send(res, 200, renderAccountSubscriptions({
|
|
4290
|
+
subscriptions: rows,
|
|
4291
|
+
can_cancel: subsCanCancel,
|
|
4292
|
+
notice: canceled ? "Your subscription has been canceled." : null,
|
|
4293
|
+
shop_name: shopName,
|
|
4294
|
+
cart_count: cartCount,
|
|
4295
|
+
}));
|
|
4296
|
+
});
|
|
4297
|
+
|
|
4298
|
+
// Cancel mounts only when payment is wired (cancel composes Stripe).
|
|
4299
|
+
// Without payment the list above stays read-only with a note.
|
|
4300
|
+
if (deps.payment) {
|
|
4301
|
+
router.post("/account/subscriptions/:id/cancel", async function (req, res) {
|
|
4302
|
+
var auth = _subsAuth(req, res); if (!auth) return;
|
|
4303
|
+
var sub = await _ownedSubscription(req, res, auth); if (!sub) return;
|
|
4304
|
+
// Default cancel-at-period-end (the customer keeps access through
|
|
4305
|
+
// the period they've paid for); the form opts into immediate via
|
|
4306
|
+
// the `immediate` checkbox.
|
|
4307
|
+
var atPeriodEnd = !((req.body || {}).immediate === "1");
|
|
4308
|
+
try {
|
|
4309
|
+
await subscriptions.subscriptions.cancel(sub.id, { at_period_end: atPeriodEnd });
|
|
4310
|
+
} catch (e) {
|
|
4311
|
+
// A bad id was already screened by _ownedSubscription; a
|
|
4312
|
+
// downstream TypeError (shape) bounces back to the list with a
|
|
4313
|
+
// notice rather than 500-ing.
|
|
4314
|
+
if (e instanceof TypeError) {
|
|
4315
|
+
res.status(303); res.setHeader && res.setHeader("location", "/account/subscriptions");
|
|
4316
|
+
return res.end ? res.end() : res.send("");
|
|
4317
|
+
}
|
|
4318
|
+
throw e;
|
|
4319
|
+
}
|
|
4320
|
+
res.status(303); res.setHeader && res.setHeader("location", "/account/subscriptions?canceled=1");
|
|
4321
|
+
return res.end ? res.end() : res.send("");
|
|
4322
|
+
});
|
|
4323
|
+
}
|
|
4324
|
+
}
|
|
4325
|
+
|
|
3981
4326
|
// Self-serve returns — a customer requests an RMA against one of
|
|
3982
4327
|
// their own orders and tracks its status. Operators action it via
|
|
3983
4328
|
// the admin /admin/returns queue. Needs the returns primitive + an
|
|
@@ -4415,11 +4760,13 @@ module.exports = {
|
|
|
4415
4760
|
renderProduct: renderProduct,
|
|
4416
4761
|
renderCart: renderCart,
|
|
4417
4762
|
renderCheckoutForm: renderCheckoutForm,
|
|
4763
|
+
renderGiftCardBalance: renderGiftCardBalance,
|
|
4418
4764
|
renderPayPage: renderPayPage,
|
|
4419
4765
|
renderOrder: renderOrder,
|
|
4420
4766
|
renderAccountLogin: renderAccountLogin,
|
|
4421
4767
|
renderAccountRegister: renderAccountRegister,
|
|
4422
4768
|
renderAccount: renderAccount,
|
|
4769
|
+
renderAccountSubscriptions: renderAccountSubscriptions,
|
|
4423
4770
|
renderNotFound: renderNotFound,
|
|
4424
4771
|
// Layout exposed so operators forking the framework can override.
|
|
4425
4772
|
_wrap: _wrap,
|
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.12.
|
|
7
|
-
"tag": "v0.12.
|
|
6
|
+
"version": "0.12.62",
|
|
7
|
+
"tag": "v0.12.62",
|
|
8
8
|
"license": "Apache-2.0",
|
|
9
9
|
"author": "blamejs contributors",
|
|
10
10
|
"source": "https://github.com/blamejs/blamejs",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"server": "lib/vendor/blamejs/"
|
|
14
14
|
},
|
|
15
15
|
"bundler": "shallow git clone of release tag from github.com/blamejs/blamejs",
|
|
16
|
-
"bundledAt": "2026-05-
|
|
16
|
+
"bundledAt": "2026-05-26"
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
}
|