@blamejs/blamejs-shop 0.0.129 → 0.1.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.
- package/CHANGELOG.md +4 -0
- package/lib/admin.js +1 -2
- package/lib/affiliates.js +4 -3
- package/lib/analytics.js +3 -2
- package/lib/api-keys.js +1 -1
- package/lib/assembly-instructions.js +2 -1
- package/lib/auto-replenish.js +4 -3
- package/lib/backorder.js +2 -1
- package/lib/business-hours.js +8 -1
- package/lib/carrier-accounts.js +1 -1
- package/lib/carrier-rates.js +1 -1
- package/lib/cart-abandonment.js +3 -2
- package/lib/cart-bulk-ops.js +2 -1
- package/lib/cart-recovery.js +5 -4
- package/lib/cart.js +6 -2
- package/lib/catalog-drafts.js +1 -1
- package/lib/click-and-collect.js +3 -2
- package/lib/clickstream.js +4 -3
- package/lib/config.js +2 -1
- package/lib/cookie-consent.js +2 -1
- package/lib/credit-limits.js +2 -1
- package/lib/currency-display.js +2 -1
- package/lib/customer-activity.js +3 -2
- package/lib/customer-impersonation.js +3 -3
- package/lib/customer-merge.js +4 -3
- package/lib/customer-portal.js +4 -4
- package/lib/customer-risk-profile.js +2 -1
- package/lib/customer-segments.js +2 -1
- package/lib/customer-surveys.js +6 -3
- package/lib/delivery-estimate.js +2 -2
- package/lib/demand-forecast.js +2 -1
- package/lib/discount-analytics.js +2 -2
- package/lib/dunning.js +4 -1
- package/lib/email-warmup.js +6 -1
- package/lib/email.js +1 -8
- package/lib/error-log.js +3 -2
- package/lib/event-log.js +3 -2
- package/lib/fraud-screen.js +3 -1
- package/lib/fulfillment-sla.js +3 -1
- package/lib/index.js +11 -3
- package/lib/inventory-allocations.js +3 -0
- package/lib/inventory-snapshots.js +2 -1
- package/lib/invoice-renderer.js +2 -1
- package/lib/line-gift-wrap.js +6 -1
- package/lib/live-chat.js +2 -1
- package/lib/loyalty-redemption.js +2 -1
- package/lib/newsletter.js +6 -1
- package/lib/operator-activity-feed.js +4 -3
- package/lib/operator-sessions.js +7 -7
- package/lib/order-exchanges.js +1 -0
- package/lib/order-timeline.js +2 -1
- package/lib/payment-retries.js +2 -1
- package/lib/payment.js +5 -4
- package/lib/pixel-events.js +6 -5
- package/lib/preorder.js +2 -1
- package/lib/print-queue.js +2 -1
- package/lib/product-compare.js +2 -1
- package/lib/product-qa.js +2 -1
- package/lib/push-notifications.js +6 -5
- package/lib/recently-viewed.js +7 -2
- package/lib/recommendations.js +7 -2
- package/lib/referral-leaderboard.js +2 -1
- package/lib/refund-automation.js +1 -1
- package/lib/refund-policy.js +1 -1
- package/lib/reorder-reminders.js +2 -1
- package/lib/reorder-thresholds.js +2 -1
- package/lib/robots-config.js +1 -0
- package/lib/sales-reports.js +17 -14
- package/lib/sales-tax-filings.js +2 -1
- package/lib/save-for-later.js +2 -1
- package/lib/search-suggestions.js +1 -1
- package/lib/shipping-insurance.js +2 -1
- package/lib/shipping-labels.js +3 -2
- package/lib/shipping-zones.js +1 -0
- package/lib/shrinkage-report.js +9 -8
- package/lib/sms-dispatcher.js +6 -5
- package/lib/stock-alerts.js +1 -1
- package/lib/stock-receipts.js +2 -1
- package/lib/store-credit.js +2 -1
- package/lib/storefront-forms.js +1 -1
- package/lib/storefront.js +93 -112
- package/lib/subscription-analytics.js +7 -2
- package/lib/subscription-controls.js +9 -8
- package/lib/subscription-gifts.js +2 -1
- package/lib/subscriptions.js +2 -0
- package/lib/support-tickets.js +4 -4
- package/lib/tax-cert-renewals.js +2 -1
- package/lib/tax-remittance.js +2 -1
- package/lib/theme-assets.js +1 -1
- package/lib/vendor/MANIFEST.json +2 -2
- package/lib/vendor/blamejs/CHANGELOG.md +2 -0
- package/lib/vendor/blamejs/README.md +1 -0
- package/lib/vendor/blamejs/api-snapshot.json +92 -2
- package/lib/vendor/blamejs/index.js +1 -0
- package/lib/vendor/blamejs/lib/did.js +367 -0
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.12.41.json +18 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +9 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/did.test.js +147 -0
- package/lib/vendor-invoices.js +1 -1
- package/lib/webhook-receiver.js +8 -2
- package/lib/webhook-subscriptions.js +1 -1
- package/lib/webhooks.js +6 -5
- package/lib/winback-campaigns.js +2 -1
- package/lib/wishlist-alerts.js +2 -1
- package/lib/wishlist-digest.js +2 -1
- package/package.json +1 -1
package/lib/storefront.js
CHANGED
|
@@ -1510,9 +1510,9 @@ var CART_LINE =
|
|
|
1510
1510
|
" </span>\n" +
|
|
1511
1511
|
" </a>\n" +
|
|
1512
1512
|
" </td>\n" +
|
|
1513
|
-
" <td>{{qty}}</td>\n" +
|
|
1514
|
-
" <td class=\"price\">{{unit}}</td>\n" +
|
|
1515
|
-
" <td class=\"price\">{{total}}</td>\n" +
|
|
1513
|
+
" <td data-label=\"Qty\">{{qty}}</td>\n" +
|
|
1514
|
+
" <td class=\"price\" data-label=\"Unit\">{{unit}}</td>\n" +
|
|
1515
|
+
" <td class=\"price\" data-label=\"Total\">{{total}}</td>\n" +
|
|
1516
1516
|
"</tr>\n";
|
|
1517
1517
|
|
|
1518
1518
|
// Editable cart line — shown on the /cart page. Includes an inline
|
|
@@ -1536,14 +1536,14 @@ var CART_LINE_EDITABLE =
|
|
|
1536
1536
|
" </span>\n" +
|
|
1537
1537
|
" </a>\n" +
|
|
1538
1538
|
" </td>\n" +
|
|
1539
|
-
" <td class=\"cart-line__qty\">\n" +
|
|
1539
|
+
" <td class=\"cart-line__qty\" data-label=\"Qty\">\n" +
|
|
1540
1540
|
" <form method=\"post\" action=\"/cart/lines/{{line_id}}/update\" class=\"cart-line__update\">\n" +
|
|
1541
|
-
" <input type=\"number\" name=\"qty\" value=\"{{qty}}\" min=\"1\" max=\"
|
|
1542
|
-
" <button type=\"submit\" class=\"cart-line__btn\">Update</button>\n" +
|
|
1541
|
+
" <input type=\"number\" name=\"qty\" value=\"{{qty}}\" min=\"1\" max=\"99999\" class=\"cart-line__qty-input\" aria-label=\"Quantity\">\n" +
|
|
1542
|
+
" <button type=\"submit\" class=\"cart-line__btn cart-line__btn--update\">Update</button>\n" +
|
|
1543
1543
|
" </form>\n" +
|
|
1544
1544
|
" </td>\n" +
|
|
1545
|
-
" <td class=\"price\">{{unit}}</td>\n" +
|
|
1546
|
-
" <td class=\"price\">{{total}}</td>\n" +
|
|
1545
|
+
" <td class=\"price\" data-label=\"Price\">{{unit}}</td>\n" +
|
|
1546
|
+
" <td class=\"price\" data-label=\"Total\">{{total}}</td>\n" +
|
|
1547
1547
|
" <td class=\"cart-line__remove-cell\">\n" +
|
|
1548
1548
|
" RAW_CART_LINE_SAVE" +
|
|
1549
1549
|
" <form method=\"post\" action=\"/cart/lines/{{line_id}}/remove\">\n" +
|
|
@@ -1691,7 +1691,7 @@ var ORDER_PAGE =
|
|
|
1691
1691
|
" <div class=\"order-page__items\">\n" +
|
|
1692
1692
|
" <h2 class=\"pdp__variants-title\">Items</h2>\n" +
|
|
1693
1693
|
" <div class=\"table-scroll\">\n" +
|
|
1694
|
-
" <table>\n" +
|
|
1694
|
+
" <table class=\"cart-table\">\n" +
|
|
1695
1695
|
" <thead><tr><th>Product</th><th>Qty</th><th>Unit</th><th>Total</th></tr></thead>\n" +
|
|
1696
1696
|
" <tbody>{{line_rows}}</tbody>\n" +
|
|
1697
1697
|
" </table>\n" +
|
|
@@ -2082,7 +2082,6 @@ function renderNotFound(opts) {
|
|
|
2082
2082
|
// — it's a routing key, not an authentication token. The cart itself
|
|
2083
2083
|
// transitions to `customer_id` on login via cart.setCustomer.
|
|
2084
2084
|
var SESSION_COOKIE_NAME = "shop_sid";
|
|
2085
|
-
var SESSION_COOKIE_MAX = 60 * 60 * 24 * 30; // 30 days
|
|
2086
2085
|
|
|
2087
2086
|
// Authenticated-customer cookie — carries an opaque sealed envelope
|
|
2088
2087
|
// `{ customer_id, exp }`, AEAD-encrypted via b.vault.seal so the
|
|
@@ -2092,79 +2091,85 @@ var SESSION_COOKIE_MAX = 60 * 60 * 24 * 30; // 30 days
|
|
|
2092
2091
|
// rotated vault invalidates every outstanding auth cookie (operator-
|
|
2093
2092
|
// initiated logout-everywhere).
|
|
2094
2093
|
var AUTH_COOKIE_NAME = "shop_auth";
|
|
2095
|
-
var AUTH_COOKIE_MAX = 60 * 60 * 24 * 14; // 14 days
|
|
2096
|
-
var AUTH_TTL_MS = 14 * 24 * 60 * 60 * 1000;
|
|
2097
2094
|
|
|
2098
2095
|
// WebAuthn ceremony state cookie — short-lived envelope holding the
|
|
2099
2096
|
// random challenge + the ceremony-scoped metadata so register-finish /
|
|
2100
2097
|
// login-finish can verify the same challenge the browser was sent.
|
|
2101
2098
|
// Path-scoped to /account so it never leaks to other routes.
|
|
2102
2099
|
var CHALLENGE_COOKIE_NAME = "shop_auth_chal";
|
|
2103
|
-
var CHALLENGE_COOKIE_MAX = 5 * 60; // 5 minutes
|
|
2104
2100
|
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2101
|
+
// Short-lived cookie carrying the Stripe PaymentIntent client_secret
|
|
2102
|
+
// from POST /checkout to GET /pay/:order_id. Path-scoped to /pay/ +
|
|
2103
|
+
// SameSite=Strict so it's only ever sent to the pay route.
|
|
2104
|
+
var PAY_COOKIE_NAME = "shop_pay";
|
|
2105
|
+
|
|
2106
|
+
// Shape of a valid session id — mirrors cart.js's SESSION_ID_RE.
|
|
2107
|
+
var SID_SHAPE_RE = /^[A-Za-z0-9_-]{16,64}$/;
|
|
2108
|
+
|
|
2109
|
+
// All cookie transport composes the framework's cookie primitive
|
|
2110
|
+
// (`b.cookies`) — RFC 6265 parse/serialize, prefix invariants, and the
|
|
2111
|
+
// vault-sealed read/write helpers — rather than hand-built Set-Cookie
|
|
2112
|
+
// strings and manual header splitting. The jar is memoized; it only
|
|
2113
|
+
// captures the vault reference (seal/unseal run lazily per call), so
|
|
2114
|
+
// building it before vault.init() has completed is safe.
|
|
2115
|
+
var _jar = null;
|
|
2116
|
+
function _cookieJar() {
|
|
2117
|
+
if (!_jar) {
|
|
2118
|
+
_jar = _b().cookies.create({
|
|
2119
|
+
vault: _b().vault,
|
|
2120
|
+
defaults: { httpOnly: true, secure: true, sameSite: "Lax", path: "/" },
|
|
2121
|
+
});
|
|
2118
2122
|
}
|
|
2119
|
-
return
|
|
2120
|
-
}
|
|
2121
|
-
|
|
2122
|
-
function _setSidCookie(res, sid) {
|
|
2123
|
-
var attrs = "Max-Age=" + SESSION_COOKIE_MAX + "; Path=/; HttpOnly; Secure; SameSite=Lax";
|
|
2124
|
-
var header = SESSION_COOKIE_NAME + "=" + encodeURIComponent(sid) + "; " + attrs;
|
|
2125
|
-
if (typeof res.appendHeader === "function") res.appendHeader("Set-Cookie", header);
|
|
2126
|
-
else if (typeof res.setHeader === "function") res.setHeader("Set-Cookie", header);
|
|
2123
|
+
return _jar;
|
|
2127
2124
|
}
|
|
2128
2125
|
|
|
2129
2126
|
function _readCookie(req, name) {
|
|
2130
|
-
|
|
2131
|
-
if (!raw) return null;
|
|
2132
|
-
var parts = raw.split(";");
|
|
2133
|
-
for (var i = 0; i < parts.length; i += 1) {
|
|
2134
|
-
var p = parts[i].trim();
|
|
2135
|
-
var eq = p.indexOf("=");
|
|
2136
|
-
if (eq <= 0) continue;
|
|
2137
|
-
if (p.slice(0, eq) === name) {
|
|
2138
|
-
try { return decodeURIComponent(p.slice(eq + 1)); }
|
|
2139
|
-
catch (_e) { return null; }
|
|
2140
|
-
}
|
|
2141
|
-
}
|
|
2142
|
-
return null;
|
|
2127
|
+
return _cookieJar().read(req, name);
|
|
2143
2128
|
}
|
|
2144
2129
|
|
|
2145
|
-
function
|
|
2146
|
-
|
|
2147
|
-
|
|
2130
|
+
function _readSidCookie(req) {
|
|
2131
|
+
// A cookie carrying anything but a well-shaped session id (a stale
|
|
2132
|
+
// value from an old deploy, a tampered cookie, garbage) reads as "no
|
|
2133
|
+
// session" rather than reaching cart.bySession — which throws on a
|
|
2134
|
+
// malformed id and would turn every page that renders the cart count
|
|
2135
|
+
// into a 500. The cookie grants zero authority, so dropping a
|
|
2136
|
+
// malformed one silently is safe.
|
|
2137
|
+
var v = _cookieJar().read(req, SESSION_COOKIE_NAME);
|
|
2138
|
+
return v && SID_SHAPE_RE.test(v) ? v : null;
|
|
2148
2139
|
}
|
|
2149
2140
|
|
|
2150
|
-
function
|
|
2151
|
-
var
|
|
2152
|
-
|
|
2141
|
+
function _setSidCookie(res, sid) {
|
|
2142
|
+
var T = _b().constants.TIME;
|
|
2143
|
+
_cookieJar().write(res, SESSION_COOKIE_NAME, sid, { expires: new Date(Date.now() + T.days(30)) });
|
|
2153
2144
|
}
|
|
2154
2145
|
|
|
2146
|
+
// Auth + WebAuthn-challenge cookies carry a vault-sealed JSON envelope.
|
|
2147
|
+
// writeSealed/readSealed handle the seal + the on-wire prefix; the
|
|
2148
|
+
// caller works in plain objects.
|
|
2149
|
+
function _setAuthCookie(res, env) {
|
|
2150
|
+
var T = _b().constants.TIME;
|
|
2151
|
+
_cookieJar().writeSealed(res, AUTH_COOKIE_NAME, JSON.stringify(env), { expires: new Date(Date.now() + T.days(14)) });
|
|
2152
|
+
}
|
|
2155
2153
|
function _clearAuthCookie(res) {
|
|
2156
|
-
|
|
2157
|
-
AUTH_COOKIE_NAME + "=; Max-Age=0; Path=/; HttpOnly; Secure; SameSite=Lax");
|
|
2154
|
+
_cookieJar().clear(res, AUTH_COOKIE_NAME);
|
|
2158
2155
|
}
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2156
|
+
function _readAuthEnv(req) {
|
|
2157
|
+
var raw = _cookieJar().readSealed(req, AUTH_COOKIE_NAME);
|
|
2158
|
+
if (raw === null) return null;
|
|
2159
|
+
try { return JSON.parse(raw); } catch (_e) { return null; }
|
|
2163
2160
|
}
|
|
2164
2161
|
|
|
2162
|
+
function _setChallengeCookie(res, env) {
|
|
2163
|
+
var T = _b().constants.TIME;
|
|
2164
|
+
_cookieJar().writeSealed(res, CHALLENGE_COOKIE_NAME, JSON.stringify(env), { expires: new Date(Date.now() + T.minutes(5)), path: "/account" });
|
|
2165
|
+
}
|
|
2165
2166
|
function _clearChallengeCookie(res) {
|
|
2166
|
-
|
|
2167
|
-
|
|
2167
|
+
_cookieJar().clear(res, CHALLENGE_COOKIE_NAME, { path: "/account" });
|
|
2168
|
+
}
|
|
2169
|
+
function _readChallengeEnv(req) {
|
|
2170
|
+
var raw = _cookieJar().readSealed(req, CHALLENGE_COOKIE_NAME);
|
|
2171
|
+
if (raw === null) return null;
|
|
2172
|
+
try { return JSON.parse(raw); } catch (_e) { return null; }
|
|
2168
2173
|
}
|
|
2169
2174
|
|
|
2170
2175
|
// ---- account-page renderers --------------------------------------------
|
|
@@ -2303,10 +2308,10 @@ var ACCOUNT_DASH_PAGE =
|
|
|
2303
2308
|
|
|
2304
2309
|
var ACCOUNT_DASH_ORDER_ROW =
|
|
2305
2310
|
"<tr>\n" +
|
|
2306
|
-
" <td><a href=\"/orders/{{order_id}}\" class=\"account-order__id\"><code>{{order_id_short}}</code></a></td>\n" +
|
|
2307
|
-
" <td class=\"account-order__items\">RAW_ACCOUNT_ORDER_THUMBS</td>\n" +
|
|
2308
|
-
" <td><span class=\"pdp__badge {{status_class}}\">{{status}}</span></td>\n" +
|
|
2309
|
-
" <td class=\"price\">{{total}}</td>\n" +
|
|
2311
|
+
" <td data-label=\"Order\"><a href=\"/orders/{{order_id}}\" class=\"account-order__id\"><code>{{order_id_short}}</code></a></td>\n" +
|
|
2312
|
+
" <td class=\"account-order__items\" data-label=\"Items\">RAW_ACCOUNT_ORDER_THUMBS</td>\n" +
|
|
2313
|
+
" <td data-label=\"Status\"><span class=\"pdp__badge {{status_class}}\">{{status}}</span></td>\n" +
|
|
2314
|
+
" <td class=\"price\" data-label=\"Total\">{{total}}</td>\n" +
|
|
2310
2315
|
"</tr>\n";
|
|
2311
2316
|
|
|
2312
2317
|
function renderAccount(opts) {
|
|
@@ -2407,7 +2412,7 @@ function mount(router, deps) {
|
|
|
2407
2412
|
// so the /admin landing + the onNotFound 404 handler (mounted
|
|
2408
2413
|
// outside the `if (deps.customers)` block below) can reach it.
|
|
2409
2414
|
async function _cartCountForReq(req) {
|
|
2410
|
-
var sid =
|
|
2415
|
+
var sid = _readSidCookie(req);
|
|
2411
2416
|
if (!sid) return 0;
|
|
2412
2417
|
var c = await deps.cart.bySession(sid);
|
|
2413
2418
|
if (!c) return 0;
|
|
@@ -2421,10 +2426,7 @@ function mount(router, deps) {
|
|
|
2421
2426
|
// reader rather than a copy per call site. A missing / malformed /
|
|
2422
2427
|
// expired cookie returns null — never throws.
|
|
2423
2428
|
function _currentCustomerEnv(req) {
|
|
2424
|
-
var
|
|
2425
|
-
if (!raw) return null;
|
|
2426
|
-
var env;
|
|
2427
|
-
try { env = JSON.parse(_b().vault.unseal(raw)); } catch (_e) { return null; }
|
|
2429
|
+
var env = _readAuthEnv(req);
|
|
2428
2430
|
if (!env || !env.customer_id || !env.exp || env.exp < Date.now()) return null;
|
|
2429
2431
|
return env;
|
|
2430
2432
|
}
|
|
@@ -2775,11 +2777,11 @@ function mount(router, deps) {
|
|
|
2775
2777
|
idempotency_key: "checkout:" + c.id + ":" + _b().uuid.v7(),
|
|
2776
2778
|
});
|
|
2777
2779
|
// Set a short-lived pay cookie so /pay/:order_id can serve the
|
|
2778
|
-
// client_secret without re-running confirm.
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2780
|
+
// client_secret without re-running confirm. Scoped to /pay/ +
|
|
2781
|
+
// SameSite=Strict so it's only ever sent to the pay route.
|
|
2782
|
+
_cookieJar().write(res, PAY_COOKIE_NAME, result.payment_intent.client_secret, {
|
|
2783
|
+
expires: new Date(Date.now() + _b().constants.TIME.minutes(15)), path: "/pay/", sameSite: "Strict",
|
|
2784
|
+
});
|
|
2783
2785
|
res.status(303);
|
|
2784
2786
|
res.setHeader && res.setHeader("location", "/pay/" + result.order.id);
|
|
2785
2787
|
return res.end ? res.end() : res.send("");
|
|
@@ -2798,14 +2800,7 @@ function mount(router, deps) {
|
|
|
2798
2800
|
// Read the client_secret from the shop_pay cookie set on POST
|
|
2799
2801
|
// /checkout. The cookie is scoped Path=/pay/ + SameSite=Strict
|
|
2800
2802
|
// so it's only sent to the pay route and never cross-origin.
|
|
2801
|
-
var
|
|
2802
|
-
var clientSecret = null;
|
|
2803
|
-
rawCookies.split(";").forEach(function (p) {
|
|
2804
|
-
var t = p.trim();
|
|
2805
|
-
if (t.indexOf("shop_pay=") === 0) {
|
|
2806
|
-
try { clientSecret = decodeURIComponent(t.slice("shop_pay=".length)); } catch (_e) { /* drop */ }
|
|
2807
|
-
}
|
|
2808
|
-
});
|
|
2803
|
+
var clientSecret = _readCookie(req, PAY_COOKIE_NAME);
|
|
2809
2804
|
if (!clientSecret) {
|
|
2810
2805
|
res.status(303); res.setHeader && res.setHeader("location", "/cart");
|
|
2811
2806
|
return res.end ? res.end() : res.send("");
|
|
@@ -2871,16 +2866,6 @@ function mount(router, deps) {
|
|
|
2871
2866
|
return _b().crypto.toBase64Url(buf);
|
|
2872
2867
|
}
|
|
2873
2868
|
|
|
2874
|
-
function _sealEnvelope(obj) {
|
|
2875
|
-
return _b().vault.seal(JSON.stringify(obj));
|
|
2876
|
-
}
|
|
2877
|
-
function _unsealEnvelope(s) {
|
|
2878
|
-
try {
|
|
2879
|
-
var raw = _b().vault.unseal(s);
|
|
2880
|
-
return JSON.parse(raw);
|
|
2881
|
-
} catch (_e) { return null; }
|
|
2882
|
-
}
|
|
2883
|
-
|
|
2884
2869
|
function _currentCustomer(req) {
|
|
2885
2870
|
return _currentCustomerEnv(req);
|
|
2886
2871
|
}
|
|
@@ -2940,13 +2925,12 @@ function mount(router, deps) {
|
|
|
2940
2925
|
// Seal the ceremony state (challenge + customer_id) into the
|
|
2941
2926
|
// shop_auth_chal cookie so register-finish verifies against
|
|
2942
2927
|
// the same challenge without server-side state.
|
|
2943
|
-
|
|
2928
|
+
_setChallengeCookie(res, {
|
|
2944
2929
|
kind: "register",
|
|
2945
2930
|
customer_id: customer.id,
|
|
2946
2931
|
challenge: startOpts.challenge,
|
|
2947
2932
|
created_at: Date.now(),
|
|
2948
2933
|
});
|
|
2949
|
-
_setChallengeCookie(res, sealed);
|
|
2950
2934
|
res.status(200);
|
|
2951
2935
|
res.setHeader && res.setHeader("content-type", "application/json");
|
|
2952
2936
|
return res.end ? res.end(JSON.stringify(startOpts)) : res.send(JSON.stringify(startOpts));
|
|
@@ -2959,10 +2943,9 @@ function mount(router, deps) {
|
|
|
2959
2943
|
|
|
2960
2944
|
router.post("/account/passkey/register-finish", async function (req, res) {
|
|
2961
2945
|
try {
|
|
2962
|
-
var
|
|
2963
|
-
if (!
|
|
2964
|
-
|
|
2965
|
-
if (!env || env.kind !== "register") {
|
|
2946
|
+
var env = _readChallengeEnv(req);
|
|
2947
|
+
if (!env) { res.status(400); return res.end ? res.end("missing challenge") : res.send("missing challenge"); }
|
|
2948
|
+
if (env.kind !== "register") {
|
|
2966
2949
|
res.status(400); return res.end ? res.end("bad challenge") : res.send("bad challenge");
|
|
2967
2950
|
}
|
|
2968
2951
|
var att = _readJsonBody(req);
|
|
@@ -2991,10 +2974,10 @@ function mount(router, deps) {
|
|
|
2991
2974
|
transports: transports,
|
|
2992
2975
|
});
|
|
2993
2976
|
_clearChallengeCookie(res);
|
|
2994
|
-
_setAuthCookie(res,
|
|
2977
|
+
_setAuthCookie(res, {
|
|
2995
2978
|
customer_id: env.customer_id,
|
|
2996
|
-
exp: Date.now() +
|
|
2997
|
-
})
|
|
2979
|
+
exp: Date.now() + _b().constants.TIME.days(14),
|
|
2980
|
+
});
|
|
2998
2981
|
res.status(200);
|
|
2999
2982
|
return res.end ? res.end("ok") : res.send("ok");
|
|
3000
2983
|
} catch (e) {
|
|
@@ -3029,13 +3012,12 @@ function mount(router, deps) {
|
|
|
3029
3012
|
allowCredentials: allow,
|
|
3030
3013
|
userVerification: "preferred",
|
|
3031
3014
|
});
|
|
3032
|
-
|
|
3015
|
+
_setChallengeCookie(res, {
|
|
3033
3016
|
kind: "login",
|
|
3034
3017
|
email_hash: hash,
|
|
3035
3018
|
challenge: startOpts.challenge,
|
|
3036
3019
|
created_at: Date.now(),
|
|
3037
3020
|
});
|
|
3038
|
-
_setChallengeCookie(res, sealed);
|
|
3039
3021
|
res.status(200);
|
|
3040
3022
|
res.setHeader && res.setHeader("content-type", "application/json");
|
|
3041
3023
|
return res.end ? res.end(JSON.stringify(startOpts)) : res.send(JSON.stringify(startOpts));
|
|
@@ -3048,10 +3030,9 @@ function mount(router, deps) {
|
|
|
3048
3030
|
|
|
3049
3031
|
router.post("/account/passkey/login-finish", async function (req, res) {
|
|
3050
3032
|
try {
|
|
3051
|
-
var
|
|
3052
|
-
if (!
|
|
3053
|
-
|
|
3054
|
-
if (!env || env.kind !== "login") { res.status(400); return res.end ? res.end("bad challenge") : res.send("bad challenge"); }
|
|
3033
|
+
var env = _readChallengeEnv(req);
|
|
3034
|
+
if (!env) { res.status(400); return res.end ? res.end("missing challenge") : res.send("missing challenge"); }
|
|
3035
|
+
if (env.kind !== "login") { res.status(400); return res.end ? res.end("bad challenge") : res.send("bad challenge"); }
|
|
3055
3036
|
var assertion = _readJsonBody(req);
|
|
3056
3037
|
var credentialId = assertion.id || assertion.rawId;
|
|
3057
3038
|
if (!credentialId) { res.status(400); return res.end ? res.end("missing credential id") : res.send("missing credential id"); }
|
|
@@ -3090,7 +3071,7 @@ function mount(router, deps) {
|
|
|
3090
3071
|
}
|
|
3091
3072
|
// Merge the anonymous cart into a customer-owned cart so
|
|
3092
3073
|
// the shopper doesn't lose items on sign-in.
|
|
3093
|
-
var sid =
|
|
3074
|
+
var sid = _readSidCookie(req);
|
|
3094
3075
|
if (sid) {
|
|
3095
3076
|
try {
|
|
3096
3077
|
var anonCart = await deps.cart.bySession(sid);
|
|
@@ -3098,10 +3079,10 @@ function mount(router, deps) {
|
|
|
3098
3079
|
} catch (_e) { /* best-effort merge; sign-in itself succeeds */ }
|
|
3099
3080
|
}
|
|
3100
3081
|
_clearChallengeCookie(res);
|
|
3101
|
-
_setAuthCookie(res,
|
|
3082
|
+
_setAuthCookie(res, {
|
|
3102
3083
|
customer_id: customer.id,
|
|
3103
|
-
exp: Date.now() +
|
|
3104
|
-
})
|
|
3084
|
+
exp: Date.now() + _b().constants.TIME.days(14),
|
|
3085
|
+
});
|
|
3105
3086
|
res.status(200);
|
|
3106
3087
|
return res.end ? res.end("ok") : res.send("ok");
|
|
3107
3088
|
} catch (e) {
|
|
@@ -162,10 +162,15 @@
|
|
|
162
162
|
|
|
163
163
|
// ---- constants ----------------------------------------------------------
|
|
164
164
|
|
|
165
|
+
// `_b()` is a hoisted function declaration (defined below); resolving the
|
|
166
|
+
// framework constants here at module-eval is safe — the index entry point
|
|
167
|
+
// exposes `framework` before the require cascade.
|
|
168
|
+
var C = _b().constants;
|
|
169
|
+
|
|
165
170
|
var CACHE_NAMESPACE = "subscription-analytics-cache";
|
|
166
171
|
|
|
167
|
-
var DEFAULT_TTL_MS = 5
|
|
168
|
-
var ONE_DAY_MS =
|
|
172
|
+
var DEFAULT_TTL_MS = C.TIME.minutes(5); // 5 minutes
|
|
173
|
+
var ONE_DAY_MS = C.TIME.days(1);
|
|
169
174
|
var ONE_YEAR_MS = 365 * ONE_DAY_MS;
|
|
170
175
|
var DEFAULT_LTV_WINDOW_MS = 90 * ONE_DAY_MS;
|
|
171
176
|
|
|
@@ -55,6 +55,7 @@ function _b() {
|
|
|
55
55
|
if (!bShop) bShop = require("./index");
|
|
56
56
|
return bShop.framework;
|
|
57
57
|
}
|
|
58
|
+
var C = _b().constants;
|
|
58
59
|
|
|
59
60
|
// ---- constants ----------------------------------------------------------
|
|
60
61
|
|
|
@@ -89,12 +90,12 @@ var FREQUENCIES = [
|
|
|
89
90
|
// step skipNext / changeFrequency surface; a calendar-accurate
|
|
90
91
|
// scheduler is out of scope for v1.
|
|
91
92
|
var PERIOD_MS = Object.freeze({
|
|
92
|
-
weekly: 7
|
|
93
|
-
biweekly: 14
|
|
94
|
-
monthly: 30
|
|
95
|
-
quarterly: 90
|
|
96
|
-
semiannual: 182
|
|
97
|
-
annual: 365
|
|
93
|
+
weekly: C.TIME.days(7),
|
|
94
|
+
biweekly: C.TIME.days(14),
|
|
95
|
+
monthly: C.TIME.days(30),
|
|
96
|
+
quarterly: C.TIME.days(90),
|
|
97
|
+
semiannual: C.TIME.days(182),
|
|
98
|
+
annual: C.TIME.days(365),
|
|
98
99
|
});
|
|
99
100
|
|
|
100
101
|
// Plan-interval → frequency fallback. When the row has no `frequency`
|
|
@@ -115,7 +116,7 @@ function _frequencyFromPlan(plan) {
|
|
|
115
116
|
return "monthly";
|
|
116
117
|
}
|
|
117
118
|
|
|
118
|
-
var REACTIVATE_GRACE_MS = 90
|
|
119
|
+
var REACTIVATE_GRACE_MS = C.TIME.days(90);
|
|
119
120
|
var MAX_REASON_LEN = 280;
|
|
120
121
|
var MAX_SKIP_COUNT = 12;
|
|
121
122
|
var MAX_QUANTITY = 1000000;
|
|
@@ -599,7 +600,7 @@ function create(opts) {
|
|
|
599
600
|
if (now - row.cancelled_at > REACTIVATE_GRACE_MS) {
|
|
600
601
|
var gErr = new Error(
|
|
601
602
|
"subscriptionControls.reactivate: refused — cancellation is older than the " +
|
|
602
|
-
(REACTIVATE_GRACE_MS / (
|
|
603
|
+
(REACTIVATE_GRACE_MS / C.TIME.days(1)) + "-day grace window"
|
|
603
604
|
);
|
|
604
605
|
gErr.code = "SUBSCRIPTION_REACTIVATE_GRACE_EXPIRED";
|
|
605
606
|
throw gErr;
|
|
@@ -72,6 +72,7 @@ function _b() {
|
|
|
72
72
|
if (!bShop) bShop = require("./index");
|
|
73
73
|
return bShop.framework;
|
|
74
74
|
}
|
|
75
|
+
var C = _b().constants;
|
|
75
76
|
|
|
76
77
|
// ---- constants ----------------------------------------------------------
|
|
77
78
|
|
|
@@ -94,7 +95,7 @@ var MAX_LIST_LIMIT = 200;
|
|
|
94
95
|
var DEFAULT_LIST_LIMIT = 50;
|
|
95
96
|
// Default redemption window: 365 days. Operators wanting a different
|
|
96
97
|
// horizon pass `expires_at_ms` (absolute) at purchase time.
|
|
97
|
-
var DEFAULT_EXPIRY_MS = 365
|
|
98
|
+
var DEFAULT_EXPIRY_MS = C.TIME.days(365);
|
|
98
99
|
|
|
99
100
|
// ---- validators ---------------------------------------------------------
|
|
100
101
|
|
package/lib/subscriptions.js
CHANGED
|
@@ -214,7 +214,9 @@ function _extractFromStripeObject(obj) {
|
|
|
214
214
|
// Stripe nests current_period_start/_end at the top level when the
|
|
215
215
|
// event is `customer.subscription.*`. Both are unix seconds; we
|
|
216
216
|
// store epoch ms.
|
|
217
|
+
// allow:raw-time-literal — Stripe sends unix SECONDS; the * 1000 converts a runtime field to epoch ms, not a fixed duration
|
|
217
218
|
var periodStart = obj && obj.current_period_start != null ? obj.current_period_start * 1000 : null;
|
|
219
|
+
// allow:raw-time-literal — Stripe sends unix SECONDS; the * 1000 converts a runtime field to epoch ms, not a fixed duration
|
|
218
220
|
var periodEnd = obj && obj.current_period_end != null ? obj.current_period_end * 1000 : null;
|
|
219
221
|
return {
|
|
220
222
|
stripe_subscription_id: obj && obj.id,
|
package/lib/support-tickets.js
CHANGED
|
@@ -75,10 +75,10 @@ var ALLOWED_STATUSES = [
|
|
|
75
75
|
// ticket is considered breached. Closed / resolved tickets never
|
|
76
76
|
// breach. Drives `slaCheck()` for the scheduler.
|
|
77
77
|
var SLA_MS = {
|
|
78
|
-
urgent: 1
|
|
79
|
-
high: 4
|
|
80
|
-
normal: 24
|
|
81
|
-
low: 72
|
|
78
|
+
urgent: _b().constants.TIME.hours(1),
|
|
79
|
+
high: _b().constants.TIME.hours(4),
|
|
80
|
+
normal: _b().constants.TIME.hours(24),
|
|
81
|
+
low: _b().constants.TIME.hours(72),
|
|
82
82
|
};
|
|
83
83
|
|
|
84
84
|
// FSM. Encoded as an explicit allow-list keyed by from-state. Every
|
package/lib/tax-cert-renewals.js
CHANGED
|
@@ -71,6 +71,7 @@ function _b() {
|
|
|
71
71
|
if (!bShop) bShop = require("./index");
|
|
72
72
|
return bShop.framework;
|
|
73
73
|
}
|
|
74
|
+
var C = _b().constants;
|
|
74
75
|
|
|
75
76
|
var JURISDICTION_RE = /^[A-Z]{2}(-[A-Z0-9]{1,3})?$/;
|
|
76
77
|
var SLUG_RE = /^[a-z0-9](?:[a-z0-9._-]{0,126}[a-z0-9])?$/;
|
|
@@ -81,7 +82,7 @@ var MAX_ESCALATE_DAYS = 365;
|
|
|
81
82
|
var MAX_ESCALATED_TO_LEN = 256;
|
|
82
83
|
var DEFAULT_CHANNELS = Object.freeze(["email"]);
|
|
83
84
|
|
|
84
|
-
var DAY_MS =
|
|
85
|
+
var DAY_MS = C.TIME.days(1);
|
|
85
86
|
|
|
86
87
|
var STATUSES = Object.freeze(["queued", "sent", "escalated", "renewed", "expired"]);
|
|
87
88
|
|
package/lib/tax-remittance.js
CHANGED
|
@@ -90,6 +90,7 @@ function _b() {
|
|
|
90
90
|
if (!bShop) bShop = require("./index");
|
|
91
91
|
return bShop.framework;
|
|
92
92
|
}
|
|
93
|
+
var C = _b().constants;
|
|
93
94
|
|
|
94
95
|
// ---- constants ----------------------------------------------------------
|
|
95
96
|
|
|
@@ -107,7 +108,7 @@ var MAX_PENALTY_REASON_LEN = 1000;
|
|
|
107
108
|
|
|
108
109
|
var DEFAULT_DAYS_LATE_MIN = 1;
|
|
109
110
|
var MAX_DAYS_LATE_MIN = 3650; // ~10 years
|
|
110
|
-
var MS_PER_DAY =
|
|
111
|
+
var MS_PER_DAY = C.TIME.days(1);
|
|
111
112
|
|
|
112
113
|
var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
113
114
|
|
package/lib/theme-assets.js
CHANGED
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.41",
|
|
7
|
+
"tag": "v0.12.41",
|
|
8
8
|
"license": "Apache-2.0",
|
|
9
9
|
"author": "blamejs contributors",
|
|
10
10
|
"source": "https://github.com/blamejs/blamejs",
|
|
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.12.x
|
|
10
10
|
|
|
11
|
+
- v0.12.41 (2026-05-24) — **`b.did` — W3C DID resolution (did:key + did:web) feeding the credential verifiers.** Resolve W3C Decentralized Identifiers (DID Core 1.0) to verification keys — the link that lets a credential's issuer be named by a DID rather than a raw key. Resolve the issuer DID of a b.vc / b.mdoc / b.scitt credential to a node:crypto KeyObject and hand it to the verifier. did:key encodes the public key in the identifier (multicodec + base58btc), so resolution is deterministic and offline — Ed25519, P-256, P-384, and secp256k1 round-trip; did:web places the DID document at an HTTPS URL derived from the identifier, with the network fetch left to the operator (the framework parses the operator-fetched document and extracts its verification methods, as publicKeyMultibase or publicKeyJwk). b.did.keyToDid encodes a KeyObject as a did:key (an issuer naming itself), b.did.parse splits the identifier (and returns the did:web URL to fetch), and b.did.resolve returns the document and verification keys. DID Core 1.0 is a W3C Recommendation; the method specs (did:key W3C CCG report, did:web DID method registry — EUDI-mandated) are deployed-stable. Composes node:crypto; no new runtime dependency. **Added:** *`b.did.resolve(did, opts?)` / `b.did.keyToDid(publicKey)` / `b.did.parse(did)`* — `resolve` returns `{ didDocument, verificationMethods: [{ id, controller, type, publicKey }] }` with each `publicKey` a `node:crypto` KeyObject ready for `b.vc.verify` / `b.mdoc.verifyIssuerSigned` / `b.scitt.verifyStatement`. did:key resolves deterministically and offline (base58btc + multicodec → Ed25519 raw key or EC compressed point, rebuilt via SPKI); did:web requires the operator to pass the fetched DID document as `opts.document` (the URL to GET is on `b.did.parse(did).url`) and the document `id` must match the requested DID. A publicKeyJwk in a DID document is imported only after its `kty`/`crv` is allowlisted (Ed25519 / P-256 / P-384 / secp256k1) — an unexpected key type from an untrusted document is refused, not blindly imported. `keyToDid` encodes an Ed25519 / P-256 / P-384 / secp256k1 KeyObject as a did:key; `parse` derives the did:web HTTPS URL (`host[:port][:path]` → `https://host/path/did.json`, or `/.well-known/did.json`). Unknown methods, malformed base58, unsupported multicodec codes, and unsupported key types are each refused.
|
|
12
|
+
|
|
11
13
|
- v0.12.40 (2026-05-24) — **`b.mdoc` — ISO 18013-5 mdoc / mDL issuer-data verification.** Verify the issuer-signed data of an ISO/IEC 18013-5 mdoc — the credential format behind mobile driving licences (mDL) and the ISO track of the EU Digital Identity Wallet. This is the relying-party side: confirm that the data elements a holder presents were signed by the issuer and have not been altered. An mdoc's IssuerSigned carries the disclosed data elements and an issuerAuth that is a COSE_Sign1 (b.cose) over a Mobile Security Object (MSO) holding a per-element digest. b.mdoc.verifyIssuerSigned verifies the COSE signature with the issuer certificate from the COSE x5chain header, parses the MSO, enforces its validityInfo window, and recomputes each disclosed element's digest (the full Tag-24 IssuerSignedItemBytes) to match it against the MSO constant-time — the integrity check that makes selective disclosure trustworthy. An absent or mismatched digest is refused. Signing algorithms follow b.cose verification (the classical ES256/384/512 + EdDSA that real mDL issuers use; the caller names the allowlist); opts.trustAnchorsPem additionally verifies the issuer certificate chain. This completes the credential trio alongside W3C VCDM (b.vc) and IETF SD-JWT VC (b.auth.sdJwtVc). Composes b.cose + b.cbor; no new runtime dependency. **Added:** *`b.mdoc.verifyIssuerSigned(issuerSigned, opts)`* — Takes the CBOR `IssuerSigned` map (the operator extracts it from the device response / QR) and returns `{ docType, version, digestAlgorithm, validityInfo, namespaces, signerCert, alg }`. Verifies the COSE_Sign1 `issuerAuth` against the mandatory `opts.algorithms` allowlist using the issuer certificate from its `x5chain` (label 33) header; parses the Tag-24 Mobile Security Object; enforces the MSO `validityInfo` window against `opts.at` (default now; must be a valid Date; malformed dates fail closed); and recomputes the digest of every disclosed `IssuerSignedItem` (over the full Tag-24 bytes, with the MSO `digestAlgorithm` — SHA-256/384/512) to match the MSO `valueDigests` constant-time — an absent or mismatched digest is refused with `mdoc/digest-mismatch`. `opts.expectedDocType` pins the document type; `opts.trustAnchorsPem` (a PEM string or array) additionally verifies the issuer certificate chain and validity at the asserted time. A malformed `x5chain` certificate is refused with a clean `mdoc/bad-cert`. The mdoc device-authentication half (the SessionTranscript-bound holder-binding proof) is a presentation-protocol concern and is not part of issuer-data verification.
|
|
12
14
|
|
|
13
15
|
- v0.12.39 (2026-05-24) — **`b.vc` — W3C Verifiable Credentials 2.0 (issue / verify, JOSE + COSE securing).** Issue and verify W3C Verifiable Credentials (VC Data Model 2.0, a W3C Recommendation) secured per Securing Verifiable Credentials using JOSE and COSE (VC-JOSE-COSE, also a W3C Recommendation, May 2025). A verifiable credential is a tamper-evident, signed set of claims an issuer makes about a subject — a diploma, a membership, a license, an age assertion. Two securing mechanisms are supported, both signing the credential itself (no JWT/CWT claims wrapper): JOSE produces a compact JWS with the vc+jwt media type, signed with ES256/384/512 or EdDSA; COSE produces a COSE_Sign1 (application/vc+cose) over b.cose, which also accepts ML-DSA-87 for PQC-forward deployments. b.vc.verify auto-detects the form from the input, requires an algorithm allowlist, always refuses the JOSE none algorithm, re-checks the VCDM 2.0 structural rules, and enforces the validFrom / validUntil window. This is the W3C credential model, distinct from the IETF SD-JWT VC already at b.auth.sdJwtVc. Composes b.cose; no new runtime dependency. **Added:** *`b.vc.issue(credential, opts)` / `b.vc.verify(secured, opts)`* — `issue` validates the credential against the VCDM 2.0 structural rules (the `credentials/v2` context first, a `VerifiableCredential` type, an issuer, a credential subject) and signs it: `securing: "jose"` returns a compact JWS string (`typ` header `vc+jwt`), `securing: "cose"` returns COSE_Sign1 bytes (`typ` header `application/vc+cose`, content type `application/vc`) via `b.cose`. The credential is the exact signed payload — no JWT/CWT claims are injected. `verify` auto-detects the securing form from the input (compact-JWS string vs. COSE_Sign1 bytes), verifies the signature against the mandatory `opts.algorithms` allowlist (the JOSE `none` algorithm is always refused), re-checks the structural rules, enforces the `validFrom` / `validUntil` window against `opts.at` (default now; must be a valid Date), and optionally matches `opts.expectedIssuer` against the credential issuer id. Returns `{ credential, securing, alg, issuer }`.
|
|
@@ -133,6 +133,7 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
|
|
|
133
133
|
- **Trusted timestamping** — `b.tsa` RFC 3161 timestamp client: `buildRequest` a TimeStampReq, `parseResponse`, and `verifyToken` against your data — the message imprint, sent nonce, critical/sole `id-kp-timeStamping` EKU, and CMS signature are all checked, with optional certificate-chain verification. Timestamp a release artifact, audit checkpoint, or signed statement against any RFC 3161 TSA. Composes `b.cms` + the in-tree ASN.1 DER codec
|
|
134
134
|
- **Verifiable Credentials** — `b.vc` W3C Verifiable Credentials Data Model 2.0 (VC-JOSE-COSE): `issue` / `verify` a signed credential as a compact JWS (`vc+jwt`, ES256/384/512 + EdDSA) or a COSE_Sign1 (`vc+cose`, + ML-DSA-87) over `b.cose`. VCDM structural + `validFrom`/`validUntil` checks; the JOSE `none` algorithm is always refused. The W3C model, distinct from the IETF SD-JWT VC at `b.auth.sdJwtVc`
|
|
135
135
|
- **Mobile credentials (mDL)** — `b.mdoc` ISO/IEC 18013-5 issuer-data verification: `verifyIssuerSigned` checks the COSE_Sign1 IssuerAuth (issuer cert from the `x5chain` header), the Mobile Security Object validity window, and every disclosed element's digest against the MSO `valueDigests` (the selective-disclosure integrity check), with optional issuer-chain verification. The ISO credential ecosystem alongside `b.vc` and `b.auth.sdJwtVc`. Composes `b.cose` + `b.cbor`
|
|
136
|
+
- **Decentralized Identifiers** — `b.did` W3C DID resolution (DID Core 1.0): `resolve` a `did:key` (deterministic, offline — Ed25519 / P-256 / P-384 / secp256k1) or `did:web` (operator-fetched document) to `node:crypto` verification keys, so a credential's issuer DID resolves to the key that verifies it (`b.vc` / `b.mdoc` / `b.scitt`). `keyToDid` names a key as a `did:key`; document JWKs are kty/crv-allowlisted before import
|
|
136
137
|
- **Document parsers** — `b.parsers` (XML / TOML / YAML / .env); `b.config` (schema-validated env)
|
|
137
138
|
- **File-type detection** — `b.fileType` magic-byte content classification with deny-on-upload categories (image / document / archive / executable / etc.)
|
|
138
139
|
### Content-safety gates
|