@blamejs/blamejs-shop 0.0.129 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +1 -1
  3. package/lib/admin.js +275 -9
  4. package/lib/affiliates.js +4 -3
  5. package/lib/analytics.js +3 -2
  6. package/lib/api-keys.js +1 -1
  7. package/lib/assembly-instructions.js +2 -1
  8. package/lib/auto-replenish.js +4 -3
  9. package/lib/backorder.js +2 -1
  10. package/lib/business-hours.js +8 -1
  11. package/lib/carrier-accounts.js +1 -1
  12. package/lib/carrier-rates.js +1 -1
  13. package/lib/cart-abandonment.js +3 -2
  14. package/lib/cart-bulk-ops.js +2 -1
  15. package/lib/cart-recovery.js +5 -4
  16. package/lib/cart.js +6 -2
  17. package/lib/catalog-drafts.js +1 -1
  18. package/lib/click-and-collect.js +3 -2
  19. package/lib/clickstream.js +4 -3
  20. package/lib/config.js +2 -1
  21. package/lib/cookie-consent.js +2 -1
  22. package/lib/credit-limits.js +2 -1
  23. package/lib/currency-display.js +2 -1
  24. package/lib/customer-activity.js +3 -2
  25. package/lib/customer-impersonation.js +3 -3
  26. package/lib/customer-merge.js +4 -3
  27. package/lib/customer-portal.js +4 -4
  28. package/lib/customer-risk-profile.js +2 -1
  29. package/lib/customer-segments.js +2 -1
  30. package/lib/customer-surveys.js +6 -3
  31. package/lib/delivery-estimate.js +2 -2
  32. package/lib/demand-forecast.js +2 -1
  33. package/lib/discount-analytics.js +2 -2
  34. package/lib/dunning.js +4 -1
  35. package/lib/email-warmup.js +6 -1
  36. package/lib/email.js +1 -8
  37. package/lib/error-log.js +3 -2
  38. package/lib/event-log.js +3 -2
  39. package/lib/fraud-screen.js +3 -1
  40. package/lib/fulfillment-sla.js +3 -1
  41. package/lib/index.js +11 -3
  42. package/lib/inventory-allocations.js +3 -0
  43. package/lib/inventory-snapshots.js +2 -1
  44. package/lib/invoice-renderer.js +2 -1
  45. package/lib/line-gift-wrap.js +6 -1
  46. package/lib/live-chat.js +2 -1
  47. package/lib/loyalty-redemption.js +2 -1
  48. package/lib/newsletter.js +6 -1
  49. package/lib/operator-activity-feed.js +4 -3
  50. package/lib/operator-sessions.js +7 -7
  51. package/lib/order-exchanges.js +1 -0
  52. package/lib/order-timeline.js +2 -1
  53. package/lib/payment-retries.js +2 -1
  54. package/lib/payment.js +5 -4
  55. package/lib/pixel-events.js +6 -5
  56. package/lib/preorder.js +2 -1
  57. package/lib/print-queue.js +2 -1
  58. package/lib/product-compare.js +2 -1
  59. package/lib/product-qa.js +2 -1
  60. package/lib/push-notifications.js +6 -5
  61. package/lib/recently-viewed.js +7 -2
  62. package/lib/recommendations.js +7 -2
  63. package/lib/referral-leaderboard.js +2 -1
  64. package/lib/refund-automation.js +1 -1
  65. package/lib/refund-policy.js +1 -1
  66. package/lib/reorder-reminders.js +2 -1
  67. package/lib/reorder-thresholds.js +2 -1
  68. package/lib/robots-config.js +1 -0
  69. package/lib/sales-reports.js +17 -14
  70. package/lib/sales-tax-filings.js +2 -1
  71. package/lib/save-for-later.js +2 -1
  72. package/lib/search-suggestions.js +1 -1
  73. package/lib/shipping-insurance.js +2 -1
  74. package/lib/shipping-labels.js +3 -2
  75. package/lib/shipping-zones.js +1 -0
  76. package/lib/shrinkage-report.js +9 -8
  77. package/lib/sms-dispatcher.js +6 -5
  78. package/lib/stock-alerts.js +1 -1
  79. package/lib/stock-receipts.js +2 -1
  80. package/lib/store-credit.js +2 -1
  81. package/lib/storefront-forms.js +1 -1
  82. package/lib/storefront.js +93 -112
  83. package/lib/subscription-analytics.js +7 -2
  84. package/lib/subscription-controls.js +9 -8
  85. package/lib/subscription-gifts.js +2 -1
  86. package/lib/subscriptions.js +2 -0
  87. package/lib/support-tickets.js +4 -4
  88. package/lib/tax-cert-renewals.js +2 -1
  89. package/lib/tax-remittance.js +2 -1
  90. package/lib/theme-assets.js +1 -1
  91. package/lib/vendor/MANIFEST.json +2 -2
  92. package/lib/vendor/blamejs/CHANGELOG.md +16 -0
  93. package/lib/vendor/blamejs/README.md +6 -4
  94. package/lib/vendor/blamejs/SECURITY.md +2 -0
  95. package/lib/vendor/blamejs/api-snapshot.json +255 -2
  96. package/lib/vendor/blamejs/index.js +1 -0
  97. package/lib/vendor/blamejs/lib/cose.js +284 -10
  98. package/lib/vendor/blamejs/lib/crypto.js +119 -0
  99. package/lib/vendor/blamejs/lib/did.js +416 -0
  100. package/lib/vendor/blamejs/lib/mdoc.js +122 -0
  101. package/lib/vendor/blamejs/lib/network-dnssec.js +328 -0
  102. package/lib/vendor/blamejs/lib/network.js +1 -0
  103. package/lib/vendor/blamejs/lib/vc.js +231 -33
  104. package/lib/vendor/blamejs/package.json +1 -1
  105. package/lib/vendor/blamejs/release-notes/v0.12.41.json +18 -0
  106. package/lib/vendor/blamejs/release-notes/v0.12.42.json +18 -0
  107. package/lib/vendor/blamejs/release-notes/v0.12.43.json +18 -0
  108. package/lib/vendor/blamejs/release-notes/v0.12.44.json +18 -0
  109. package/lib/vendor/blamejs/release-notes/v0.12.45.json +18 -0
  110. package/lib/vendor/blamejs/release-notes/v0.12.46.json +18 -0
  111. package/lib/vendor/blamejs/release-notes/v0.12.47.json +18 -0
  112. package/lib/vendor/blamejs/release-notes/v0.12.48.json +22 -0
  113. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +47 -2
  114. package/lib/vendor/blamejs/test/layer-0-primitives/cose.test.js +101 -2
  115. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-self-test.test.js +74 -0
  116. package/lib/vendor/blamejs/test/layer-0-primitives/did.test.js +176 -0
  117. package/lib/vendor/blamejs/test/layer-0-primitives/dnssec.test.js +130 -0
  118. package/lib/vendor/blamejs/test/layer-0-primitives/mdoc.test.js +52 -0
  119. package/lib/vendor/blamejs/test/layer-0-primitives/vc.test.js +63 -0
  120. package/lib/vendor-invoices.js +1 -1
  121. package/lib/webhook-receiver.js +8 -2
  122. package/lib/webhook-subscriptions.js +1 -1
  123. package/lib/webhooks.js +6 -5
  124. package/lib/winback-campaigns.js +2 -1
  125. package/lib/wishlist-alerts.js +2 -1
  126. package/lib/wishlist-digest.js +2 -1
  127. package/package.json +1 -1
@@ -90,7 +90,7 @@ var MAX_LIST_LIMIT = 200;
90
90
  var DEFAULT_LIST_LIMIT = 50;
91
91
  var DEFAULT_THROTTLE_LIMIT = 5;
92
92
  var MAX_THROTTLE_LIMIT = 1000;
93
- var THROTTLE_WINDOW_MS = 60 * 1000;
93
+ var THROTTLE_WINDOW_MS = _b().constants.TIME.minutes(1);
94
94
 
95
95
  var SESSION_NAMESPACE = "storefront-form-session";
96
96
 
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=\"99\" class=\"cart-line__qty-input\" aria-label=\"Quantity\">\n" +
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
- function _readSidCookie(req) {
2106
- var raw = (req.headers && (req.headers.cookie || req.headers.Cookie)) || "";
2107
- if (!raw) return null;
2108
- var parts = raw.split(";");
2109
- for (var i = 0; i < parts.length; i += 1) {
2110
- var p = parts[i].trim();
2111
- var eq = p.indexOf("=");
2112
- if (eq <= 0) continue;
2113
- if (p.slice(0, eq) === SESSION_COOKIE_NAME) {
2114
- var v = p.slice(eq + 1);
2115
- // Cookie values are URL-encoded.
2116
- try { return decodeURIComponent(v); } catch (_e) { return null; }
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 null;
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
- var raw = (req.headers && (req.headers.cookie || req.headers.Cookie)) || "";
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 _appendCookie(res, header) {
2146
- if (typeof res.appendHeader === "function") res.appendHeader("Set-Cookie", header);
2147
- else if (typeof res.setHeader === "function") res.setHeader("Set-Cookie", header);
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 _setAuthCookie(res, sealed) {
2151
- var attrs = "Max-Age=" + AUTH_COOKIE_MAX + "; Path=/; HttpOnly; Secure; SameSite=Lax";
2152
- _appendCookie(res, AUTH_COOKIE_NAME + "=" + encodeURIComponent(sealed) + "; " + attrs);
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
- _appendCookie(res,
2157
- AUTH_COOKIE_NAME + "=; Max-Age=0; Path=/; HttpOnly; Secure; SameSite=Lax");
2154
+ _cookieJar().clear(res, AUTH_COOKIE_NAME);
2158
2155
  }
2159
-
2160
- function _setChallengeCookie(res, sealed) {
2161
- var attrs = "Max-Age=" + CHALLENGE_COOKIE_MAX + "; Path=/account; HttpOnly; Secure; SameSite=Lax";
2162
- _appendCookie(res, CHALLENGE_COOKIE_NAME + "=" + encodeURIComponent(sealed) + "; " + attrs);
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
- _appendCookie(res,
2167
- CHALLENGE_COOKIE_NAME + "=; Max-Age=0; Path=/account; HttpOnly; Secure; SameSite=Lax");
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 = _readCookie(req, SESSION_COOKIE_NAME);
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 raw = _readCookie(req, AUTH_COOKIE_NAME);
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
- var payCookie = "shop_pay=" + encodeURIComponent(result.payment_intent.client_secret) +
2780
- "; Max-Age=900; Path=/pay/; HttpOnly; Secure; SameSite=Strict";
2781
- if (res.appendHeader) res.appendHeader("Set-Cookie", payCookie);
2782
- else if (res.setHeader) res.setHeader("Set-Cookie", payCookie);
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 rawCookies = (req.headers && (req.headers.cookie || req.headers.Cookie)) || "";
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
- var sealed = _sealEnvelope({
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 rawCookie = _readCookie(req, CHALLENGE_COOKIE_NAME);
2963
- if (!rawCookie) { res.status(400); return res.end ? res.end("missing challenge") : res.send("missing challenge"); }
2964
- var env = _unsealEnvelope(rawCookie);
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, _sealEnvelope({
2977
+ _setAuthCookie(res, {
2995
2978
  customer_id: env.customer_id,
2996
- exp: Date.now() + AUTH_TTL_MS,
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
- var sealed = _sealEnvelope({
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 rawCookie = _readCookie(req, CHALLENGE_COOKIE_NAME);
3052
- if (!rawCookie) { res.status(400); return res.end ? res.end("missing challenge") : res.send("missing challenge"); }
3053
- var env = _unsealEnvelope(rawCookie);
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 = _readCookie(req, SESSION_COOKIE_NAME);
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, _sealEnvelope({
3082
+ _setAuthCookie(res, {
3102
3083
  customer_id: customer.id,
3103
- exp: Date.now() + AUTH_TTL_MS,
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 * 60 * 1000; // 5 minutes
168
- var ONE_DAY_MS = 24 * 60 * 60 * 1000;
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 * 24 * 60 * 60 * 1000,
93
- biweekly: 14 * 24 * 60 * 60 * 1000,
94
- monthly: 30 * 24 * 60 * 60 * 1000,
95
- quarterly: 90 * 24 * 60 * 60 * 1000,
96
- semiannual: 182 * 24 * 60 * 60 * 1000,
97
- annual: 365 * 24 * 60 * 60 * 1000,
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 * 24 * 60 * 60 * 1000;
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 / (24 * 60 * 60 * 1000)) + "-day grace window"
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 * 86400 * 1000;
98
+ var DEFAULT_EXPIRY_MS = C.TIME.days(365);
98
99
 
99
100
  // ---- validators ---------------------------------------------------------
100
101
 
@@ -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,
@@ -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 * 3600 * 1000,
79
- high: 4 * 3600 * 1000,
80
- normal: 24 * 3600 * 1000,
81
- low: 72 * 3600 * 1000,
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
@@ -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 = 86400000;
85
+ var DAY_MS = C.TIME.days(1);
85
86
 
86
87
  var STATUSES = Object.freeze(["queued", "sent", "escalated", "renewed", "expired"]);
87
88
 
@@ -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 = 24 * 60 * 60 * 1000;
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
 
@@ -137,7 +137,7 @@ var ZERO_WIDTH_RE = new RegExp(
137
137
  "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
138
138
  );
139
139
 
140
- var MS_PER_DAY = 86400000;
140
+ var MS_PER_DAY = _b().constants.TIME.days(1);
141
141
 
142
142
  var bShop;
143
143
  function _b() {
@@ -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.40",
7
- "tag": "v0.12.40",
6
+ "version": "0.12.48",
7
+ "tag": "v0.12.48",
8
8
  "license": "Apache-2.0",
9
9
  "author": "blamejs contributors",
10
10
  "source": "https://github.com/blamejs/blamejs",
@@ -8,6 +8,22 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.12.x
10
10
 
11
+ - v0.12.48 (2026-05-25) — **`b.network.dns.dnssec` — local DNSSEC signature verification (RFC 4035).** Verify a DNS answer's RRSIG signature yourself instead of trusting the upstream resolver's AD bit. b.network.dns.dnssec.verifyRrset reconstructs the RFC 4034 §3.1.8.1 signed data — the RRSIG RDATA without the signature, followed by the RRset in canonical form (owner names lowercased, RRs ordered by canonical RDATA, the RRSIG's Original TTL) — and checks the signature against the DNSKEY, enforcing the inception / expiration window. Supports RSA/SHA-256 (alg 8), ECDSA P-256/SHA-256 (13), ECDSA P-384/SHA-384 (14), and Ed25519 (15) — the modern deployed set. verifyDs checks a delegation-signer digest against a DNSKEY (SHA-256 / SHA-384) and keyTag computes the RFC 4034 Appendix B key tag. The verification core is what a chain-walker composes; it defends against a compromised or on-path resolver that lies about authentication. **Added:** *`b.network.dns.dnssec.verifyRrset(opts)`* — Verifies an RRSIG over a canonicalised RRset against a DNSKEY. `opts` carries the owner `name`, the RR `type`, the wire-format `rdatas`, the parsed `rrsig` (algorithm / labels / originalTtl / inception / expiration / keyTag / signerName / signature), and the `dnskey` (algorithm + raw public key). The signed data is rebuilt per RFC 4034 §3.1.8.1: the RRSIG prefix (type covered | algorithm | labels | original TTL | expiration | inception | key tag | canonical signer name) followed by each RR in canonical form (lowercased owner | type | class | original TTL | rdlen | rdata), sorted by `Buffer.compare` on the RDATA. The validity window is enforced against `opts.at` (defaults to now; an invalid Date is refused, not treated as now). An RRSIG whose algorithm disagrees with the DNSKEY is refused before any key is built. RR types that embed domain names in their RDATA (NS, CNAME, SOA, MX, SRV, …) need RDATA-internal name-lowercasing this version does not perform, so they are refused with `dnssec/uncanonicalizable-type` rather than mis-validated; the security-critical DNSKEY / DS and the name-free address / text types (A, AAAA, TXT, CAA, TLSA, …) are fully supported. · *`b.network.dns.dnssec.verifyDs(opts)` / `b.network.dns.dnssec.keyTag(dnskeyRdata)`* — `verifyDs` confirms a delegation-signer record matches a DNSKEY: it checks the key tag, then compares the DS digest (SHA-256 type 2 / SHA-384 type 4) against the digest computed over the canonical owner name and the DNSKEY RDATA, constant-time. `keyTag` computes the RFC 4034 Appendix B 16-bit key tag from a DNSKEY's full RDATA — the identifier an RRSIG or DS uses to select the signing key. Together with `verifyRrset` these are the per-RRset building blocks a recursive chain-walk (root → TLD → zone) composes; the chain-walk itself, NSEC / NSEC3 denial-of-existence, and the bundled IANA root trust anchor are not part of this core.
12
+
13
+ - v0.12.47 (2026-05-25) — **`b.cose.mac0` / `b.cose.macVerify0` — COSE_Mac0 (RFC 9052 §6.2).** Completes the COSE message-type set (COSE_Sign1 / COSE_Encrypt0 / COSE_Mac0) with single shared-key MACs. b.cose.mac0 produces a tagged COSE_Mac0 over a payload using HMAC-SHA-256/384/512 (the COSE-standard MAC algorithms; HMAC is symmetric, so its post-quantum strength is preserved). b.cose.macVerify0 recomputes the tag over the MAC_structure and compares it in constant time, with a mandatory algorithm allowlist. Use when both parties hold a shared key — e.g. an ECDH-derived key — and a non-repudiable signature is not wanted; detached payloads are supported (the proximity mdoc device-MAC variant and MACed CWTs are the consumers). Composes b.cbor + the framework's constant-time compare; no new runtime dependency. **Added:** *`b.cose.mac0(payload, opts)` / `b.cose.macVerify0(coseMac0, opts)`* — `mac0` emits a tagged COSE_Mac0 (tag 17) with `alg` (`HMAC-256/256` | `HMAC-384/384` | `HMAC-512/512`) in the protected header and the HMAC tag computed over the MAC_structure `["MAC0", protected, external_aad, payload]`; `detached: true` emits a nil payload. `macVerify0` reads the algorithm from the protected header (must be in the required `opts.algorithms` allowlist), recomputes the tag, and compares it constant-time — a wrong key, tampered tag, or `external_aad` mismatch is refused with `cose/bad-tag`; a detached payload is supplied via `opts.externalPayload`. `external_aad` binds context into the tag.
14
+
15
+ - v0.12.46 (2026-05-25) — **`b.mdoc.verifyDeviceAuth` — ISO 18013-5 mdoc device authentication.** Completes mdoc verification with the holder-binding half (ISO 18013-5 §9.1.3, signature variant). verifyIssuerSigned proves the data is issuer-signed; verifyDeviceAuth proves the presenter controls the device key the issuer bound into the MSO, so a captured issuer-signed document cannot be replayed by anyone else. The device's COSE_Sign1 (deviceSigned.deviceAuth.deviceSignature) is verified over the detached DeviceAuthentication structure ["DeviceAuthentication", SessionTranscript, DocType, DeviceNameSpacesBytes] using the device key from verifyIssuerSigned().deviceKey (now surfaced) and the operator-supplied SessionTranscript that binds the proof to this exact exchange (the presentation protocol — e.g. OpenID4VP — defines the transcript). Composes the v0.12.45 b.cose detached-payload verify + importKey. The MAC variant (deviceMac / COSE_Mac0, used in proximity flows with a reader ephemeral key) is deferred and refused with mdoc/device-mac-unsupported. No new runtime dependency. **Added:** *`b.mdoc.verifyDeviceAuth(opts)` + `deviceKey` on the verifyIssuerSigned result* — `verifyDeviceAuth({ deviceKey, deviceSigned, docType, sessionTranscript, algorithms })` imports the device key (a COSE_Key via `b.cose.importKey`, or a KeyObject), reconstructs the detached `DeviceAuthentication` payload, and verifies the `deviceSignature` COSE_Sign1 against the mandatory algorithm allowlist — a mismatched `sessionTranscript` or `docType` fails the signature. `verifyIssuerSigned` now returns `deviceKey` (the MSO `deviceKeyInfo.deviceKey`) so the two checks chain. The MAC variant (`deviceMac`) is refused with `mdoc/device-mac-unsupported` pending COSE_Mac0 + reader-key support.
16
+
17
+ - v0.12.45 (2026-05-25) — **`b.cose` adds detached-payload sign/verify + `b.cose.importKey` (COSE_Key).** Two RFC 9052 / 9053 completions to the COSE substrate, both useable today and the prerequisites for mdoc device authentication and C2PA claim verification. Detached payloads (RFC 9052 §4.1): b.cose.sign with detached:true emits a COSE_Sign1 whose payload slot is nil — the signature still covers the payload, and the caller transmits it out of band; b.cose.verify takes the payload back as opts.externalPayload and binds it into the Sig_structure. A detached token verified without externalPayload is refused, and supplying externalPayload for an attached token is refused as ambiguous. COSE_Key import (RFC 9052 §7): b.cose.importKey turns a COSE_Key CBOR map into a node:crypto public KeyObject for b.cose.verify, accepting EC2 (P-256 / P-384 / P-521) and OKP (Ed25519) with the curve allowlisted so an unexpected key type is refused. No new runtime dependency. **Added:** *Detached COSE_Sign1 payloads + `b.cose.importKey(coseKey)`* — `b.cose.sign(payload, { detached: true })` emits a nil-payload COSE_Sign1 (the signature covers the payload regardless); `b.cose.verify(coseSign1, { externalPayload })` reconstructs the Sig_structure from the supplied payload, refusing a detached token with no `externalPayload` (`cose/detached-no-payload`) and refusing `externalPayload` on an attached token (`cose/payload-ambiguous`). `b.cose.importKey(coseKey)` maps a COSE_Key map (`kty` 2/EC2 with `crv` P-256/384/521, or `kty` 1/OKP with Ed25519) to a public KeyObject, allowlisting `kty`/`crv` and refusing anything else with `cose/unsupported-key` — the verification key embedded in an mdoc MSO or COSE_Key header is consumed this way.
18
+
19
+ - v0.12.44 (2026-05-25) — **`b.did` adds the did:jwk method.** Completes b.did's method set with did:jwk alongside did:key and did:web. did:jwk encodes a public key as a base64url-encoded JWK directly in the identifier, so resolution is deterministic and offline — the same self-contained shape as did:key but in JWK form, which is what OpenID4VCI and the EU Digital Identity Wallet ecosystem commonly use. b.did.resolve("did:jwk:…") returns the verification key as a node:crypto KeyObject (kty/crv allowlisted — Ed25519 / P-256 / P-384 / secp256k1 — so an unexpected key type is refused, not blindly imported), and b.did.keyToDid(publicKey, { method: "jwk" }) produces a did:jwk from a key (the private member is stripped). No new runtime dependency. **Added:** *did:jwk in `b.did.resolve` / `b.did.keyToDid`* — `resolve` decodes the base64url JWK (bounded via `b.safeJson`), allowlists its `kty`/`crv`, and returns `{ didDocument, verificationMethods: [{ publicKey, … }] }` with the key as a KeyObject ready for `b.vc` / `b.mdoc` / `b.scitt`; `keyToDid(publicKey, { method: "jwk" })` encodes a public key as `did:jwk:<base64url-JWK>` (default remains `did:key`). Malformed base64url-JSON is refused with `did/bad-jwk` and an unsupported key type with `did/unsupported-key`.
20
+
21
+ - v0.12.43 (2026-05-25) — **`b.crypto.selfTest` — FIPS 140-3-style power-on self-test for the crypto stack.** A power-on self-test over the framework's cryptographic primitives — the integrity check a FIPS 140-3-validated module runs at start-up. The hash / XOF checks are known-answer tests against NIST FIPS 202 published vectors (SHA3-256 / SHA3-512 / SHAKE256), so they confirm the framework's hashing matches the standard rather than merely itself; the AEAD check round-trips XChaCha20-Poly1305 and confirms a tampered ciphertext is rejected; and the post-quantum checks run a pairwise-consistency + negative test for ML-KEM-1024, ML-DSA-87, and SLH-DSA-SHAKE-256f (a fresh keypair must encaps/decaps and sign/verify consistently and reject a tampered signature — FIPS 140-3 §10.3 pairwise consistency, since the runtime exposes no seed-injection API for a fixed-seed KAT). selfTest returns a structured report and, by default, throws on any failure so a broken crypto stack fails closed at boot rather than silently producing bad output. Operators in regulated deployments can run it at start-up as a self-integrity gate. **Added:** *`b.crypto.selfTest(opts?)`* — Runs eight checks — SHA3-512 / SHA3-256 / SHAKE256 known-answer tests (NIST FIPS 202), HMAC-SHA3-512 determinism, XChaCha20-Poly1305 round-trip + tamper-detect, and ML-KEM-1024 / ML-DSA-87 / SLH-DSA-SHAKE-256f pairwise-consistency + negative tests — and returns `{ ok, results: [{ name, ok, detail? }], failures, ranAt }`. Throws `crypto/self-test-failed` (with the report attached) on any failure unless `opts.throwOnFailure` is `false`. Exercises the framework's real primitive paths so a self-test failure means the shipped crypto is broken.
22
+
23
+ - v0.12.42 (2026-05-24) — **`b.vc.present` / `b.vc.verifyPresentation` — W3C Verifiable Presentations.** Completes b.vc with the holder side: a Verifiable Presentation is a holder-signed envelope wrapping one or more credentials, proving the presenter controls the key the credentials were issued to. b.vc.present builds and signs a VerifiablePresentation (each credential enveloped per VC-JOSE-COSE) as a compact JWS (vp+jwt) or COSE_Sign1 (application/vp+cose), matching b.vc.issue's algorithms; an optional nonce / audience is embedded in the signed presentation for holder-binding and replay protection. b.vc.verifyPresentation verifies the holder signature (auto-detected jose/cose, mandatory algorithm allowlist, JOSE none refused), the VCDM structure, and the embedded nonce / audience / expectedHolder when given, and — with verifyCredentials: true — verifies each enveloped credential through b.vc.verify and returns them. The holder is typically a DID, resolved to a key via b.did. Composes b.cose; no new runtime dependency. **Added:** *`b.vc.present(opts)` / `b.vc.verifyPresentation(secured, opts)`* — `present` wraps `opts.credentials` (secured VCs — compact-JWS strings or COSE_Sign1 bytes, each enveloped as an `EnvelopedVerifiableCredential` data: URI) in a `VerifiablePresentation` signed by the holder, with optional `nonce` / `audience` embedded for binding. `verifyPresentation` verifies the holder signature against the mandatory `opts.algorithms` allowlist (JOSE `none` always refused), re-checks the VCDM structure, enforces `expectedHolder` / `nonce` / `audience` when supplied, and with `verifyCredentials: true` verifies each enveloped credential through `b.vc.verify` (using `opts.credentialOpts`), returning `{ presentation, holder, credentials, securing, alg }`. The enveloped-credential count is bounded. A `vp+jwt` presentation is refused by `b.vc.verify` and a `vc+jwt` credential is refused by `verifyPresentation` — the media-type binding keeps the two surfaces distinct.
24
+
25
+ - 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.
26
+
11
27
  - 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
28
 
13
29
  - 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 }`.