@blamejs/blamejs-shop 0.1.38 → 0.2.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.
- package/CHANGELOG.md +6 -0
- package/lib/asset-manifest.json +3 -3
- package/lib/storefront.js +94 -21
- package/lib/vendor/MANIFEST.json +2 -2
- package/lib/vendor/blamejs/CHANGELOG.md +10 -0
- package/lib/vendor/blamejs/README.md +1 -1
- package/lib/vendor/blamejs/SECURITY.md +2 -1
- package/lib/vendor/blamejs/api-snapshot.json +2 -2
- package/lib/vendor/blamejs/lib/archive-read.js +17 -0
- package/lib/vendor/blamejs/lib/auth/fal.js +12 -0
- package/lib/vendor/blamejs/lib/auth/jwt-external.js +15 -11
- package/lib/vendor/blamejs/lib/auth/jwt.js +2 -2
- package/lib/vendor/blamejs/lib/auth/oauth.js +7 -6
- package/lib/vendor/blamejs/lib/auth/oid4vci.js +3 -3
- package/lib/vendor/blamejs/lib/auth/saml.js +15 -12
- package/lib/vendor/blamejs/lib/auth/sd-jwt-vc.js +3 -3
- package/lib/vendor/blamejs/lib/calendar.js +9 -2
- package/lib/vendor/blamejs/lib/circuit-breaker.js +5 -4
- package/lib/vendor/blamejs/lib/crypto-hpke.js +1 -1
- package/lib/vendor/blamejs/lib/crypto-oprf.js +2 -2
- package/lib/vendor/blamejs/lib/crypto.js +2 -2
- package/lib/vendor/blamejs/lib/framework-error.js +2 -1
- package/lib/vendor/blamejs/lib/guard-filename.js +90 -5
- package/lib/vendor/blamejs/lib/guard-jwt.js +3 -2
- package/lib/vendor/blamejs/lib/guard-smtp-command.js +2 -2
- package/lib/vendor/blamejs/lib/mail-auth.js +2 -2
- package/lib/vendor/blamejs/lib/mail-crypto-pgp.js +1 -1
- package/lib/vendor/blamejs/lib/mail-crypto-smime.js +7 -7
- package/lib/vendor/blamejs/lib/mail-crypto.js +1 -1
- package/lib/vendor/blamejs/lib/mail-dav.js +5 -4
- package/lib/vendor/blamejs/lib/mail-deploy.js +3 -2
- package/lib/vendor/blamejs/lib/mail-server-imap.js +1 -1
- package/lib/vendor/blamejs/lib/mail-server-jmap.js +1 -1
- package/lib/vendor/blamejs/lib/mail-server-managesieve.js +1 -1
- package/lib/vendor/blamejs/lib/mail-server-mx.js +142 -47
- package/lib/vendor/blamejs/lib/mail-server-submission.js +3 -3
- package/lib/vendor/blamejs/lib/mail-store.js +2 -2
- package/lib/vendor/blamejs/lib/mail.js +2 -2
- package/lib/vendor/blamejs/lib/network-tls.js +10 -7
- package/lib/vendor/blamejs/lib/safe-decompress.js +8 -6
- package/lib/vendor/blamejs/lib/safe-ical.js +12 -12
- package/lib/vendor/blamejs/lib/safe-mime.js +6 -6
- package/lib/vendor/blamejs/lib/safe-sieve.js +1 -1
- package/lib/vendor/blamejs/lib/safe-smtp.js +1 -1
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.13.10.json +44 -0
- package/lib/vendor/blamejs/release-notes/v0.13.11.json +27 -0
- package/lib/vendor/blamejs/release-notes/v0.13.12.json +36 -0
- package/lib/vendor/blamejs/release-notes/v0.13.13.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.13.9.json +27 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/archive-read.test.js +76 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/auth-jwt-defenses.test.js +4 -4
- package/lib/vendor/blamejs/test/layer-0-primitives/calendar.test.js +31 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +104 -7
- package/lib/vendor/blamejs/test/layer-0-primitives/fal.test.js +26 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto-smime.test.js +5 -5
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-dav.test.js +1 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-mx.test.js +166 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/rate-limit-cluster.test.js +45 -27
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-ical.test.js +2 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/sandbox.test.js +12 -12
- package/lib/vendor/blamejs/test/layer-0-primitives/scheduler-exactly-once.test.js +15 -9
- package/lib/vendor/blamejs/test/layer-0-primitives/websocket-channels.test.js +0 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,12 @@ Pre-1.0 the surface is intentionally evolving — every release may
|
|
|
6
6
|
change something operators depend on. Read each entry before
|
|
7
7
|
upgrading across more than a few patches at a time.
|
|
8
8
|
|
|
9
|
+
## v0.2.x
|
|
10
|
+
|
|
11
|
+
- v0.2.1 (2026-05-27) — **Theme tuning — stronger contrast, a cleaner product buy-box, and natural code-sample colors.** A polish pass over the new dark theme. The palette is rebalanced so the brand violet reads as a deliberate accent rather than the whole field: surfaces, borders, and shadows are now neutral dark, body text and prices are high-contrast near-white, and violet is concentrated on calls-to-action, the hero accent, and the newsletter band. The product page's buy-box is now a single contained card — price, variant, quantity, and add-to-cart group cleanly with proper spacing and alignment instead of blending into the column, and the wishlist/compare controls sit in their own separated row. The homepage code sample uses natural syntax-highlight colours (green strings, blue functions, violet keywords) with the usual window dots. The home collection tiles are calmer dark tiles with a subtle violet glow rather than solid magenta swatches. **Changed:** *Rebalanced palette for contrast* — Surfaces (`--surface`, card and raised tiers) and borders are now neutral dark instead of violet-tinted; secondary text and prices are high-contrast near-white; default shadows are neutral. The violet accent is reserved for primary buttons, the hero accent word, focus states, and the newsletter/framework bands — so contrast and hierarchy are clearer and the page no longer reads as a single wash of magenta. · *Product buy-box is a contained, aligned card* — Price, variant chip(s), quantity, and the full-width add-to-cart now sit in one bordered buy-box card with consistent spacing, the price rendered in high-contrast white, and the post-quantum-secured-checkout trust line as a calm note beneath it. The wishlist and compare controls are grouped into their own row, separated from the buy-box. No change to add-to-cart, variant, or currency behaviour. · *Natural code-sample colours and calmer collection tiles* — The homepage server-rendered code sample now uses a natural console palette — green strings, blue function names, violet keywords, grey comments — with red/yellow/green window dots. The home collection tiles are dark with a subtle, position-varied violet glow instead of solid violet gradient fills.
|
|
12
|
+
|
|
13
|
+
- v0.2.0 (2026-05-27) — **A new storefront visual identity — a dark, violet brand theme with self-hosted typefaces and a redesigned product buy-box.** The default storefront theme has been redesigned around the project's actual brand: a dark, near-black canvas with a violet-to-magenta accent system that matches the blamejs shield-and-terminal mark, replacing the previous light theme and its unrelated orange accent. The stylesheet is restructured onto a semantic design-token layer (a purpose tier — surface, text, border, accent, price — sitting over the raw palette), so colour, contrast, and surfaces are now consistent and themeable rather than hard-coded per component. Two distinctive open-licensed typefaces are self-hosted in place of Inter: Hanken Grotesk for display and body, Space Mono for the terminal-style chrome (navigation, labels, SKUs, prices). The product page's variant selector is rebuilt from a data table into a proper buy-box: a prominent price, the variant(s) as selectable chips, a full-width add-to-cart, and a post-quantum-secured-checkout trust line — the add-to-cart, variant, and currency-conversion behaviour is unchanged. Accessibility is preserved (contrast targets met on the dark surfaces, focus-visible, reduced-motion guards, the consent banner restyled as legible dark glass). Operators on the default theme get the new look on upgrade; a custom theme passed via the theme primitive is unaffected. **Changed:** *Dark violet brand theme replaces the light theme* — The default theme is now a dark, near-black storefront with a violet→magenta accent family (matching the logo's shield/terminal mark) carried across the chrome, hero, cards, forms, badges, switchers, and footer. The previous light canvas and its off-brand orange accent are gone. Depth comes from hard-edged and soft violet shadows and a faint circuit-grid texture; motion (a staggered page-load reveal, a glitch treatment on the hero accent word, a terminal cursor) is CSS-only and disabled under `prefers-reduced-motion`. · *Semantic design-token layer + self-hosted typefaces* — The stylesheet now has a primitive→semantic token structure (`--surface`/`--surface-raised`, `--text`/`--text-muted`, `--border`, `--color-accent`, `--color-price`, glow tokens) so components reference purpose, not raw values — fixing the previously undefined `--font-sans`/`--danger-l` tokens and a duplicated button definition along the way. Hanken Grotesk (display/body) and Space Mono (mono chrome) are self-hosted as OFL woff2, replacing Inter; nothing is fetched from a font CDN, consistent with the strict `font-src 'self'` policy. · *Product buy-box redesign* — The product page's variant table is replaced by a buy-box: a large monospace price, each variant as a selectable pill chip (with a clear selected state, fully operable with JavaScript off), a full-width add-to-cart, and a trust line stating the checkout is post-quantum secured. The underlying add-to-cart form, multi-variant selection, and multi-currency price formatting are unchanged; products with many variants keep a compact fallback. The PDP trust badge wording "Ships from origin" is replaced with a real delivery estimate.
|
|
14
|
+
|
|
9
15
|
## v0.1.x
|
|
10
16
|
|
|
11
17
|
- v0.1.38 (2026-05-26) — **Security hardening + accessibility and UI completeness across the storefront and admin.** This release closes two data-exposure gaps, tightens admin input validation, and brings a broad accessibility and visual-completeness pass to both the storefront and the operator console. A freshly issued gift-card code is no longer placed in a redirect URL where it could persist in browser history or server logs — it's now revealed once in the page itself. Signed-in order pages now enforce ownership so a customer can only view their own orders. On accessibility: admin tables carry proper header scope, every admin page has its own title, a skip-to-content link and current-page navigation state are in place, search-filter selection is exposed correctly to assistive technology, status changes are announced, and destructive actions (refunds, plan archival, endpoint deletion, subscription cancellation, address removal) now require a confirmation step. Several surfaces that shipped without styling — the faceted-search sidebar, the subscriptions page, loyalty and referral badges, and the locale switcher — are now fully styled; product cards gain depth, accent-colored text meets contrast requirements, touch targets meet the 44px minimum on mobile, and success messages and empty states are consistent. **Changed:** *Confirmation step on destructive actions* — Irreversible or money-moving actions now route through a server-rendered confirmation page that states the consequence before proceeding: order refunds and subscription-plan archival and webhook deletion in the admin, and subscription cancellation (including the immediate-cancel option, which now spells out the forfeited time) and saved-address removal (with an undo link) in the customer account. · *Success confirmations and consistent empty states* — Account actions — saving or removing an address, toggling the wishlist, moving a saved item, requesting a return (which now echoes the RMA code) — confirm success with an announced status message instead of redirecting silently. Empty states across the account area now share one consistent treatment. **Fixed:** *Accessibility across storefront and admin* — Admin data tables now use `scope` on column and row headers; every admin page renders its own document title; a skip-to-content link and `aria-current` navigation state are in place. On the storefront, search-facet links expose their selected state correctly to screen readers (replacing an invalid attribute), and account status changes are announced via live regions. Accent-colored text (prices, labels, links) now uses a darker shade that meets the 4.5:1 contrast minimum, and interactive controls meet the 44px touch-target minimum on small screens. · *Styling for surfaces that shipped unstyled* — The faceted-search sidebar, the subscriptions management page, loyalty and referral status badges, and the footer locale switcher now render with full styling instead of unstyled browser defaults. Product and collection cards gain a resting elevation, a duplicate button definition was consolidated to one consistent style, and disabled-button and placeholder text now meet contrast requirements. **Security:** *Gift-card one-time code is no longer exposed in a URL* — Issuing a gift card from the admin console previously redirected to a URL containing the plaintext code, which could persist in browser history, the redirect `Location` header, and access logs. The code is now shown once directly in the response body (the same pattern used for webhook signing secrets) and never placed in a URL. · *Order pages enforce ownership for signed-in customers* — `GET /orders/:id` now verifies that a signed-in customer owns the order before rendering it (returning 404 otherwise), closing a gap where an order's contents could be viewed by anyone with the order id. The guest post-purchase confirmation path is unchanged. · *Strict validation on admin money inputs* — Gift-card amounts and subscription-plan amounts/intervals/trials are now parsed with strict integer validation, so malformed input (e.g. `50abc`) is rejected rather than silently coerced.
|
package/lib/asset-manifest.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.1
|
|
2
|
+
"version": "0.2.1",
|
|
3
3
|
"assets": {
|
|
4
4
|
"css/admin.css": {
|
|
5
5
|
"integrity": "sha384-oUi6NkXA0ULSy8+8+LG0FV6jsgr7Y11Xf3VdnwUESnvUPaN7anYEC+QOAUXwgsap",
|
|
6
6
|
"fingerprinted": "css/admin.7b692a8965624a0c.css"
|
|
7
7
|
},
|
|
8
8
|
"css/main.css": {
|
|
9
|
-
"integrity": "sha384-
|
|
10
|
-
"fingerprinted": "css/main.
|
|
9
|
+
"integrity": "sha384-kqtBG2WosuGXNlIpwHZ1MgvxuEGCTeHAHoI94W0nKesG6Q0Z+oICKfpiV6aomg0G",
|
|
10
|
+
"fingerprinted": "css/main.639d11f926797395.css"
|
|
11
11
|
},
|
|
12
12
|
"js/consent.js": {
|
|
13
13
|
"integrity": "sha384-KKMQ0og8HPOykRRPpUyxX7dMhTvKySfVtpGX/jGWzZwNaN/c4OykvRvXpqBHcQST",
|
package/lib/storefront.js
CHANGED
|
@@ -548,12 +548,12 @@ var HOME_HERO =
|
|
|
548
548
|
" </div>\n" +
|
|
549
549
|
" <div class=\"hero__inner\">\n" +
|
|
550
550
|
" <div class=\"hero__copy\">\n" +
|
|
551
|
-
" <p class=\"eyebrow eyebrow--on-dark\"
|
|
552
|
-
" <h1 class=\"hero__title\">
|
|
553
|
-
" <p class=\"hero__lede\">
|
|
551
|
+
" <p class=\"eyebrow eyebrow--on-dark\">~/blamejs.shop — secure commerce · v" + require("../package.json").version + "</p>\n" +
|
|
552
|
+
" <h1 class=\"hero__title\">Sell anything.<br>Trust <span class=\"glitch glitch--live\" data-text=\"nothing.\">nothing.</span><span class=\"term-cursor\" aria-hidden=\"true\"></span></h1>\n" +
|
|
553
|
+
" <p class=\"hero__lede\">An open-source, server-rendered ecommerce framework with post-quantum cryptography baked into every session, cart, and checkout. No client-side validation theater. No npm runtime dependencies. Just hardened HTML.</p>\n" +
|
|
554
554
|
" <div class=\"hero__cta\">\n" +
|
|
555
|
-
" <a href=\"#catalog\" class=\"btn-primary\"
|
|
556
|
-
" <a href=\"https://github.com/blamejs/blamejs.shop\" class=\"btn-ghost btn-ghost--on-dark\" rel=\"noopener\">
|
|
555
|
+
" <a href=\"#catalog\" class=\"btn-primary\">$ npx create-shop</a>\n" +
|
|
556
|
+
" <a href=\"https://github.com/blamejs/blamejs.shop\" class=\"btn-ghost btn-ghost--on-dark\" rel=\"noopener\">Read the threat model</a>\n" +
|
|
557
557
|
" </div>\n" +
|
|
558
558
|
" <dl class=\"hero__stats\">\n" +
|
|
559
559
|
" <div><dt>Products live</dt><dd>{{product_count}}</dd></div>\n" +
|
|
@@ -1104,6 +1104,91 @@ var VARIANT_ROW =
|
|
|
1104
1104
|
" </td>\n" +
|
|
1105
1105
|
"</tr>\n";
|
|
1106
1106
|
|
|
1107
|
+
// PDP buy-box. A single cart-add form posting `variant_id` + `qty` to
|
|
1108
|
+
// /cart/lines (unchanged endpoint + field names). Multi-variant
|
|
1109
|
+
// selection is server-rendered radio chips sharing `name="variant_id"`
|
|
1110
|
+
// — the checked radio is what POSTs, so variant choice works with zero
|
|
1111
|
+
// client JS. The lead price renders large + mono + violet; each chip
|
|
1112
|
+
// carries its own price so a shopper sees per-variant pricing before
|
|
1113
|
+
// they pick. Above twelve variants the chip wall gets unwieldy, so the
|
|
1114
|
+
// existing compact variant table (VARIANT_ROW) is the fallback — it
|
|
1115
|
+
// keeps a per-row add form, so the same endpoint contract holds.
|
|
1116
|
+
// `variants` is the pre-formatted array [{ id, sku, title, price }]
|
|
1117
|
+
// the renderers already build; `escAttr` is the path's HTML escaper.
|
|
1118
|
+
// Mirrored byte-for-byte by worker/render/product.js#_buildBuyBox.
|
|
1119
|
+
var BUYBOX_CHIP_LIMIT = 12;
|
|
1120
|
+
|
|
1121
|
+
function _buildBuyBox(variants, escAttr) {
|
|
1122
|
+
if (!variants || variants.length === 0) {
|
|
1123
|
+
return "<div class=\"pdp__variants\">\n" +
|
|
1124
|
+
" <h2 class=\"pdp__variants-title\">Choose a variant</h2>\n" +
|
|
1125
|
+
" <div class=\"table-scroll\">\n" +
|
|
1126
|
+
" <table class=\"variant-table\">\n" +
|
|
1127
|
+
" <thead><tr><th>Variant</th><th>SKU</th><th>Price</th><th class=\"variant-table__action-h\">Action</th></tr></thead>\n" +
|
|
1128
|
+
" <tbody><tr><td colspan=\"4\" class=\"empty\">No variants available.</td></tr></tbody>\n" +
|
|
1129
|
+
" </table>\n" +
|
|
1130
|
+
" </div>\n" +
|
|
1131
|
+
" </div>";
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
var trustLine =
|
|
1135
|
+
"<div class=\"pdp__meta\">\n" +
|
|
1136
|
+
" <span class=\"pdp__badge\"><span class=\"shield\" aria-hidden=\"true\"><span class=\"shield__glyph\">>_</span></span> Post-quantum secured checkout · ML-KEM-1024 key agreement · ML-DSA-65 receipt signature.</span>\n" +
|
|
1137
|
+
" </div>";
|
|
1138
|
+
|
|
1139
|
+
// Many variants → keep the compact table (still a per-row add form).
|
|
1140
|
+
if (variants.length > BUYBOX_CHIP_LIMIT) {
|
|
1141
|
+
var rows = variants.map(function (v) {
|
|
1142
|
+
return _render(VARIANT_ROW, { title: v.title, sku: v.sku, price: v.price, variant_id: v.id });
|
|
1143
|
+
}).join("");
|
|
1144
|
+
return "<div class=\"pdp__variants\">\n" +
|
|
1145
|
+
" <h2 class=\"pdp__variants-title\">Choose a variant</h2>\n" +
|
|
1146
|
+
" <div class=\"table-scroll\">\n" +
|
|
1147
|
+
" <table class=\"variant-table\">\n" +
|
|
1148
|
+
" <thead><tr><th>Variant</th><th>SKU</th><th>Price</th><th class=\"variant-table__action-h\">Action</th></tr></thead>\n" +
|
|
1149
|
+
" <tbody>" + rows + "</tbody>\n" +
|
|
1150
|
+
" </table>\n" +
|
|
1151
|
+
" </div>\n" +
|
|
1152
|
+
" </div>\n" +
|
|
1153
|
+
" " + trustLine;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
var lead = variants[0];
|
|
1157
|
+
var single = variants.length === 1;
|
|
1158
|
+
var chips = "";
|
|
1159
|
+
if (!single) {
|
|
1160
|
+
for (var i = 0; i < variants.length; i += 1) {
|
|
1161
|
+
var v = variants[i];
|
|
1162
|
+
chips +=
|
|
1163
|
+
"<label class=\"pdp__badge\">" +
|
|
1164
|
+
"<input type=\"radio\" name=\"variant_id\" value=\"" + escAttr(v.id) + "\"" + (i === 0 ? " checked" : "") + ">" +
|
|
1165
|
+
" <span class=\"variant-row__title\">" + escAttr(v.title) + "</span>" +
|
|
1166
|
+
" <span class=\"variant-row__sku\"><code>" + escAttr(v.sku) + "</code></span>" +
|
|
1167
|
+
" <span class=\"variant-row__price price\">" + escAttr(v.price) + "</span>" +
|
|
1168
|
+
"</label>";
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
var variantBlock = single
|
|
1173
|
+
? "<p class=\"variant-row__sku\"><code>" + escAttr(lead.sku) + "</code></p>" +
|
|
1174
|
+
"<input type=\"hidden\" name=\"variant_id\" value=\"" + escAttr(lead.id) + "\">"
|
|
1175
|
+
: "<fieldset class=\"pdp__variants\">\n" +
|
|
1176
|
+
" <legend class=\"pdp__variants-title\">Choose a variant</legend>\n" +
|
|
1177
|
+
" <div class=\"pdp__meta\">" + chips + "</div>\n" +
|
|
1178
|
+
" </fieldset>";
|
|
1179
|
+
|
|
1180
|
+
return "<div class=\"pdp__buybox\">\n" +
|
|
1181
|
+
" <p class=\"featured-product__price\">" + escAttr(lead.price) + "</p>\n" +
|
|
1182
|
+
" <form method=\"post\" action=\"/cart/lines\">\n" +
|
|
1183
|
+
" " + variantBlock + "\n" +
|
|
1184
|
+
" <label class=\"pdp__variants-title\" for=\"buybox-qty\">Quantity</label>\n" +
|
|
1185
|
+
" <input id=\"buybox-qty\" type=\"number\" name=\"qty\" value=\"1\" min=\"1\" max=\"99\" class=\"variant-row__qty\" aria-label=\"Quantity\">\n" +
|
|
1186
|
+
" <button type=\"submit\" class=\"btn-primary cart-page__checkout\">$ add to cart</button>\n" +
|
|
1187
|
+
" </form>\n" +
|
|
1188
|
+
" </div>\n" +
|
|
1189
|
+
" " + trustLine;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1107
1192
|
var PRODUCT_PAGE =
|
|
1108
1193
|
"<section class=\"pdp\">\n" +
|
|
1109
1194
|
" <nav class=\"breadcrumb\" aria-label=\"Breadcrumb\">\n" +
|
|
@@ -1120,18 +1205,10 @@ var PRODUCT_PAGE =
|
|
|
1120
1205
|
" <p class=\"pdp__description\">{{description}}</p>\n" +
|
|
1121
1206
|
" <div class=\"pdp__meta\">\n" +
|
|
1122
1207
|
" <span class=\"pdp__badge pdp__badge--ok\"><span class=\"dot dot--live\" aria-hidden=\"true\"></span> In stock</span>\n" +
|
|
1123
|
-
" <span class=\"pdp__badge\">Ships
|
|
1208
|
+
" <span class=\"pdp__badge\">Ships in 1–2 business days</span>\n" +
|
|
1124
1209
|
" <span class=\"pdp__badge\">Stripe-secured checkout</span>\n" +
|
|
1125
1210
|
" </div>\n" +
|
|
1126
|
-
"
|
|
1127
|
-
" <h2 class=\"pdp__variants-title\">Choose a variant</h2>\n" +
|
|
1128
|
-
" <div class=\"table-scroll\">\n" +
|
|
1129
|
-
" <table class=\"variant-table\">\n" +
|
|
1130
|
-
" <thead><tr><th>Variant</th><th>SKU</th><th>Price</th><th class=\"variant-table__action-h\">Action</th></tr></thead>\n" +
|
|
1131
|
-
" <tbody>{{variant_rows}}</tbody>\n" +
|
|
1132
|
-
" </table>\n" +
|
|
1133
|
-
" </div>\n" +
|
|
1134
|
-
" </div>\n" +
|
|
1211
|
+
" RAW_BUYBOX_PLACEHOLDER\n" +
|
|
1135
1212
|
" RAW_QTYBREAK_PLACEHOLDER\n" +
|
|
1136
1213
|
" RAW_WISHLIST_PLACEHOLDER\n" +
|
|
1137
1214
|
" RAW_COMPARE_PLACEHOLDER\n" +
|
|
@@ -2994,10 +3071,7 @@ function renderProduct(opts) {
|
|
|
2994
3071
|
asset_css_main: opts.theme.assetUrl("css/main.css"),
|
|
2995
3072
|
});
|
|
2996
3073
|
}
|
|
2997
|
-
var
|
|
2998
|
-
return _render(VARIANT_ROW, { title: v.title, sku: v.sku, price: v.price, variant_id: v.id });
|
|
2999
|
-
}).join("");
|
|
3000
|
-
if (!rows) rows = "<tr><td colspan=\"4\" class=\"empty\">No variants available.</td></tr>";
|
|
3074
|
+
var buyboxHtml = _buildBuyBox(rendered, b.template.escapeHtml);
|
|
3001
3075
|
var galleryHtml = _buildPdpGallery(opts.product, opts.media || [], opts.asset_prefix || "/assets/");
|
|
3002
3076
|
var reviewsHtml = _buildReviews(opts.review_summary, opts.reviews, opts.review_cta);
|
|
3003
3077
|
var qaHtml = _buildProductQa(opts.qa_questions, opts.qa_cta);
|
|
@@ -3008,10 +3082,9 @@ function renderProduct(opts) {
|
|
|
3008
3082
|
var body = _render(PRODUCT_PAGE, {
|
|
3009
3083
|
title: opts.product.title,
|
|
3010
3084
|
description: description,
|
|
3011
|
-
variant_rows: "RAW_ROWS_PLACEHOLDER",
|
|
3012
3085
|
})
|
|
3013
3086
|
.replace("RAW_GALLERY_PLACEHOLDER", galleryHtml)
|
|
3014
|
-
.replace("
|
|
3087
|
+
.replace("RAW_BUYBOX_PLACEHOLDER", buyboxHtml)
|
|
3015
3088
|
.replace("RAW_QTYBREAK_PLACEHOLDER", qtyBreaksHtml)
|
|
3016
3089
|
.replace("RAW_WISHLIST_PLACEHOLDER", wishlistHtml)
|
|
3017
3090
|
.replace("RAW_COMPARE_PLACEHOLDER", compareHtml)
|
package/lib/vendor/MANIFEST.json
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
"_about": "blamejs.shop vendors a single framework — blamejs — which itself bundles every server-side crypto/identity dependency. The transitive packages blamejs ships are surfaced in its own MANIFEST.json at lib/vendor/blamejs/lib/vendor/MANIFEST.json — Trivy / Grype rely on that nested data for CVE attribution.",
|
|
4
4
|
"packages": {
|
|
5
5
|
"blamejs": {
|
|
6
|
-
"version": "0.13.
|
|
7
|
-
"tag": "v0.13.
|
|
6
|
+
"version": "0.13.13",
|
|
7
|
+
"tag": "v0.13.13",
|
|
8
8
|
"license": "Apache-2.0",
|
|
9
9
|
"author": "blamejs contributors",
|
|
10
10
|
"source": "https://github.com/blamejs/blamejs",
|
|
@@ -8,6 +8,16 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.13.x
|
|
10
10
|
|
|
11
|
+
- v0.13.13 (2026-05-27) — **Archive extraction-path verification now refuses Windows reserved names, NTFS data streams, and trailing-dot/space per segment.** b.guardFilename.verifyExtractionPath (the per-entry gate b.archive.read.zip.extract / b.safeArchive run on every extracted file) checked traversal, absolute paths, drive-letter and UNC prefixes, null bytes, PATH_MAX overflow, and realpath containment — but not the per-segment Windows write-target hazards the disk validate / sanitize paths already reject. An archive entry named CON, NUL.txt, subdir/LPT1, file.txt:hidden, or secret.txt. stayed inside the extraction root, so the containment and realpath checks passed it, yet on Windows it would resolve to a device, write a hidden NTFS stream, or (after Windows strips the trailing dot/space) overwrite a sibling file. These are now refused: any path segment that collides with a Windows reserved device name, uses NTFS alternate-data-stream syntax (name:stream), or carries a trailing dot or leading/trailing whitespace. The checks are platform-unconditional — a verifier running on Linux still refuses names that are only dangerous on the Windows host that ultimately extracts the archive — with a per-check opt-out (reservedNamePolicy / adsPolicy / leadingTrailingPolicy: "allow") for Linux-only targets. **Security:** *`verifyExtractionPath` refuses per-segment Windows extraction hazards (reserved names / NTFS ADS / trailing dot-space)* — Closes a within-root write-target-redirection gap: an extracted entry could stay inside the destination yet, on Windows, resolve to a device (`CON` / `NUL` / `COM1` / `LPT1`), write a hidden alternate data stream (`file.txt:payload`), or overwrite a sibling after Windows strips a trailing dot/space (`config.`). The verification gate now rejects all three per path segment. Refusal is platform-unconditional (the verifier may run on a different OS than the extractor); set `reservedNamePolicy` / `adsPolicy` / `leadingTrailingPolicy` to `"allow"` to opt a check out on a Linux-only target. Single-entry, name-only residuals — 8.3 short-name aliasing, case-insensitive cross-entry collisions, and archive symlink/hardlink entry-target validation — remain the extract orchestrator's responsibility (it owns the case-folded seen-set and the link-target gate).
|
|
12
|
+
|
|
13
|
+
- v0.13.12 (2026-05-27) — **Inbound MX listener now runs the connection-level gate cascade it documented — HELO identity, DNS blocklist, and greylisting.** b.mail.server.mx.create documented helo / rbl / greylist gate options, but the listener never invoked them — an operator who wired them got silent acceptance of mail those gates would have rejected. They are now wired into the live SMTP state machine: the HELO-identity gate evaluates at HELO/EHLO and refuses a spoofed or malformed identity with 550; the DNS-blocklist gate evaluates the connecting IP once per connection and refuses a listed source with 554; the greylisting gate defers a first-seen (ip, sender, recipient) tuple with a 450 tempfail so legitimate senders retry and pass. Each gate is skipped when the operator doesn't wire it. Because these gates do DNS and store lookups, the per-connection command pump was reworked to process commands asynchronously and strictly in arrival order, so pipelined commands (RFC 2920) cannot overtake a gate still resolving and the existing SMTP-smuggling and STARTTLS-stripping defenses are unchanged. The message-authentication gate (SPF/DKIM/DMARC alignment via b.guardEnvelope) needs the inbound SPF + DKIM verification results as inputs; that inbound-auth pipeline lands as a follow-up, and the documentation no longer implies that gate is active today. **Added:** *HELO-identity / RBL / greylist gates wired into `b.mail.server.mx`* — When wired, `opts.helo` (FCrDNS / HELO-shape / self-name checks) refuses a bad HELO identity at HELO/EHLO with 550; `opts.rbl` refuses a connecting IP found on a DNS blocklist with 554 (evaluated once per connection); `opts.greylist` defers a first-seen (ip, sender, recipient) tuple with 450 4.7.1. Their verdicts surface on the `rcpt_to` event (`rblListed`, `greylist`) and the `helo` event (`heloVerdict`), with dedicated `helo_gate_refused` / `rbl_refused` / `greylist_deferred` audit events. A gate the operator doesn't supply is skipped, never synthesized. **Changed:** *MX command pump processes commands asynchronously and in arrival order* — Gate evaluation involves DNS and store lookups, so the per-connection command pump now awaits each command before the next. Pipelined commands are serialized so a gate resolving cannot let a later command answer ahead of an earlier one; reply ordering, the bare-LF SMTP-smuggling refusal, and the STARTTLS-stripping defense are unchanged. No change to the listener's external behaviour when no gates are wired. **Deprecated:** *SPF/DKIM/DMARC-alignment gate documentation corrected to match what is active* — The `envelope` (SPF/DKIM/DMARC alignment) and `dmarc` gate options were documented as wireable but require inbound SPF + DKIM verification results the listener does not yet produce. They are removed from the documented option set until the inbound-authentication pipeline (composing `b.mail.spf` + `b.mail.dmarc` + DKIM verification) lands; run those checks on the delivered message via the agent handoff in the meantime.
|
|
14
|
+
|
|
15
|
+
- v0.13.11 (2026-05-27) — **Test-suite reliability: replaced fixed-delay waits in the rate-limiter and scheduler suites with condition polling.** No runtime behaviour changes. The rate-limiter, scheduler, and websocket-channel test suites waited for asynchronous work to settle by draining a fixed number of event-loop ticks before asserting. Under heavily parallel CI that budget was occasionally too short, so an assertion read state before the async work (a cluster-backend counter update, a scheduler tick-claim) had landed — an intermittent failure unrelated to the code under test. Those waits now poll the observable condition (helpers.waitUntil) and exit as soon as it holds, with a generous upper bound, so they pass quickly on fast machines and reliably under load. A build gate is added so the fixed-tick-drain shape cannot be reintroduced. **Fixed:** *Flaky fixed-budget waits in the rate-limiter / scheduler / sandbox test suites made contention-tolerant* — The rate-limit-cluster and scheduler-exactly-once suites drained a fixed count of event-loop ticks before asserting on asynchronously-updated state; under contended CI the budget could expire before the work settled, producing intermittent failures. They now wait on the actual observable condition (a written response, a settled counter). The sandbox suite's success-path cases gave the worker a 5 s execution budget that cold worker-thread startup under heavily parallel Windows CI could just exceed; those are raised to the framework's 10 s ceiling. Affects test code only — no change to shipped framework behaviour. The unused tick-drain helper in the websocket-channel suite was removed. **Detectors:** *Build gate rejects the fixed-tick-drain wait shape in tests* — A new test-suite lint rule flags the counted microtask/tick-drain idiom (reassigning a promise to its own `.then()` in a loop to wait a fixed number of ticks), the sibling of the existing fixed-`setTimeout`-sleep rule. A single event-loop yield is unaffected; only the drain-as-wait shape is rejected, directing the wait to condition polling instead.
|
|
16
|
+
|
|
17
|
+
- v0.13.10 (2026-05-27) — **Documented-but-inert options wired up, a non-existent CVE reference removed, and a silent iCalendar cap-bypass fixed.** A sweep for places where a documented option or citation did not match what the code does. The most operator-relevant fix: b.calendar.fromIcal documented a safeIcalOpts option that forwards parser caps (byte size, RRULE limits, nesting depth) to b.safeIcal.parse, but the value was never forwarded — so an operator who set tight caps through it got the default profile instead, silently. That is corrected; the nested options now reach the parser. b.archive.read.zip documented an AbortSignal option that was never honored; it now aborts the read at the entry boundary. b.auth.fal documented a bearerOnly alias that had no effect; it now forces the no-proof-of-possession path and refuses the contradictory combination of bearerOnly:true with a holder-of-key binding. Separately, the auth verification paths cited CVE-2026-23993 (13 places) for the "reject an unknown alg before key lookup" guard — that CVE id does not exist (the registry has no record of it); the citation is replaced with the weakness class (CWE-347 / CWE-757) and the real, verifiable neighboring CVEs. The circuit-breaker error-code note that promised a rename "in v0.10" is corrected to the actual plan (v1.0), and the build gate that catches overdue version promises now also catches two-part version numbers. **Changed:** *`b.auth.fal` `bearerOnly` is now a real alias and refuses contradictions* — `bearerOnly: true` now forces the no-proof-of-possession path (equivalent to `hokBinding: null`), as documented. Passing `bearerOnly: true` together with a non-null `hokBinding` is a contradictory assurance request and is now refused at the call rather than silently resolved one way. **Fixed:** *`b.calendar.fromIcal` now forwards `safeIcalOpts` to the parser* — The documented `safeIcalOpts` option (parser caps: max bytes, RRULE COUNT/BYxxx limits, nesting depth) was not being passed to `b.safeIcal.parse` — when supplied under the documented nested key it was silently ignored and the parser ran with its default profile. Both forms now reach the parser: the documented nested `{ safeIcalOpts: { ... } }` and the top-level `{ profile, ... }` that earlier releases accepted, with the nested form winning on conflict. No caller regresses. · *`b.archive.read.zip` honors the documented `signal` (AbortSignal)* — The `signal` option was documented but never read. A large or slow archive read can now be aborted cooperatively — the reader checks the signal at each entry boundary (`inspect`, `entries`, `extractEntries`, `extract`) and rejects with an `archive-read/aborted` error. · *Removed a non-existent CVE reference from the JWT/JWE verification paths* — The "reject an unknown/unsupported `alg` before any key lookup" guard in `b.auth.jwt.verifyExternal`, `b.auth.oauth.verifyIdToken`, `b.auth.oid4vci`, and `b.auth.sd-jwt-vc` cited a CVE id that the registry has no record of. The behaviour is unchanged; the citation is now the weakness class it defends (CWE-347 improper signature verification / CWE-757 algorithm downgrade) alongside the real, verifiable alg-confusion / JWE-bypass CVEs already cited beside it. **Detectors:** *Overdue-version-promise gate now catches two-part version numbers* — The build gate that flags a deferral whose promised landing version has already shipped previously matched only three-part versions (`vN.N.N`); a two-part promise (`vN.N`) slipped past it. It now matches both. The `b.circuitBreaker` `CIRCUIT_OPEN` error-code note that pointed at a passed version is corrected to its actual plan (rename at v1.0, with a deprecation warning a minor ahead).
|
|
18
|
+
|
|
19
|
+
- v0.13.9 (2026-05-26) — **Corrected CVE citations in source threat annotations + a build gate that refuses malformed CVE identifiers.** Several source-comment threat annotations cited CVE identifiers that were rejected by the numbering authority (never assigned to a real issue), attributed to the wrong product, or structurally malformed (a placeholder with a non-numeric sequence). The annotated defenses are unchanged — every cap, refusal, and constant-time comparison behaves exactly as before; only the reference labels were corrected, each to a verifiable CVE or to the underlying weakness class (CWE / RFC) where no single CVE fits. Notable corrections: the S/MIME SHA-1 / MD5 certificate-signature refusal now cites the SHAttered collision and RFC 8551 §2.5 instead of a rejected candidate id; decompression-output caps cite CWE-409 and CVE-2025-0725 instead of a fabricated placeholder; the iCalendar RRULE / nesting / byte caps describe the calendar-bomb recursion-DoS class instead of an unrelated SSRF advisory; and the SAML signature-wrapping (XSW) defense now cites the actively-exploited CVE-2024-45409 (ruby-saml, CVSS 10.0) and CVE-2025-25291 / -25292 that the duplicate-element refusal defeats. A new build-time detector refuses any CVE token whose sequence number is not all-numeric, so a placeholder identifier can never reach a release again. **Fixed:** *Corrected rejected / misattributed / malformed CVE references in source threat annotations* — Threat-annotation comments across the mail, crypto, auth, guard, and safe modules carried CVE identifiers that were rejected by the CVE numbering authority, attributed to the wrong product, or written as non-numeric placeholders. Each was corrected to a verifiable CVE or to the weakness class (CWE / RFC) it defends. No runtime behaviour changed — the defenses these comments describe are unchanged. The S/MIME certificate check's SHA-1 / MD5 refusal message now names the SHAttered collision and RFC 8551 §2.5; the SAML XSW defense now names CVE-2024-45409 and CVE-2025-25291 / -25292. **Detectors:** *`malformed-cve-identifier` — refuses structurally-invalid CVE tokens at build time* — A CVE identifier's sequence number is always numeric (`CVE-<year>-<digits>`). The new detector refuses any CVE token whose post-year segment contains a letter — the placeholder shape that lets a fabricated reference slip past review. It cannot verify that a well-formed id is real or correctly attributed (that stays a review responsibility), but it makes the structurally-invalid class impossible to ship.
|
|
20
|
+
|
|
11
21
|
- v0.13.8 (2026-05-26) — **In-memory archive extraction for read-only / serverless filesystems.** Archive readers gain an in-memory extraction path so an uploaded archive can be opened and its contents read without writing anything to disk — the case a read-only or ephemeral serverless filesystem requires. b.archive.read.zip(...).extractEntries() and b.archive.read.tar(...).extractEntries() are async generators that yield each regular file entry as { name, bytes, size }, applying the same bomb-policy caps, b.guardArchive metadata cascade (which refuses a Zip-Slip / traversal archive wholesale), and entry-type-policy refusals as the disk extract() — only the disk realpath agreement check is omitted, since nothing is written and the caller owns where the returned bytes land. Directory and link entries carry no content and are not yielded. The guard cascade is factored into one shared path so disk and in-memory extraction refuse identically. Also a documentation fix: b.archive.gz no longer claims a b.archive.zip().toGzip() convenience exists — a ZIP is already DEFLATE-compressed per entry, so gzip-wrapping it gains nothing; gzip the uncompressed tar stream (the canonical .tar.gz) instead. **Added:** *`extractEntries()` — in-memory archive extraction (ZIP + tar)* — `b.archive.read.zip(source).extractEntries(opts?)` and `b.archive.read.tar(source).extractEntries(opts?)` are async generators yielding `{ name, bytes, size }` per regular file entry, never touching disk — for serverless / read-only filesystems where the disk `extract({ destination })` path cannot run. Same bomb-policy, guard-archive cascade, and entry-type-policy refusals as disk extraction; the bytes are byte-identical to what `extract()` writes. **Fixed:** *Removed the inaccurate `b.archive.zip().toGzip()` doc claim* — The `b.archive.gz` documentation described a `b.archive.zip().toGzip()` convenience method that does not (and should not) exist: a ZIP is already DEFLATE-compressed per entry, so gzip-wrapping it would compress already-compressed data for no benefit. `b.archive.tar().toGzip()` (the real `.tar.gz`) is unchanged.
|
|
12
22
|
|
|
13
23
|
- v0.13.7 (2026-05-26) — **Documentation accuracy — several primitives described shipped features as deferred.** A documentation sweep corrected primitive descriptions that still called features deferred after they shipped, so the wiki and inline docs now match the code. b.mdoc documents that device authentication (the ISO 18013-5 §9.1.3 signature variant, verifyDeviceAuth) is verified, not deferred — only the COSE_Mac0 device-auth variant remains refused. b.network.dns.dnssec documents that the root-to-zone chain walk against the IANA trust anchors (verifyChain) and NSEC / NSEC3 denial of existence (verifyDenial / nsec3Hash) ship, where the card previously said they were deferred. b.cose lists COSE_Mac0 and COSE_Encrypt0 among what it ships. The JMAP server documents its push channel, blob upload/download, and EmailSubmission handlers as present, and the submission server documents CHUNKING / BDAT as supported. A new test detector keeps this class of drift from recurring: it fails the build when a comment promises a feature lands in a version that has already shipped. **Fixed:** *Corrected `deferred`/`does-not-ship` docs for features that have shipped* — `b.mdoc` (device authentication, §9.1.3), `b.network.dns.dnssec` (chain walk + NSEC/NSEC3), `b.cose` (COSE_Mac0 + COSE_Encrypt0), the JMAP server (push, blob, EmailSubmission), and the submission server (BDAT/CHUNKING) all carried `@card`/`@intro` text describing shipped capabilities as deferred or not-shipped. The descriptions now match the implemented surface; genuinely-deferred items (the mdoc MAC variant, DNSSEC in-RDATA name canonicalization, COSE multi-signer/multi-recipient) remain documented as such. **Detectors:** *Overdue-defer detector in the codebase-pattern gate* — A new check fails the build when a comment promises a feature "lands in" / is "deferred to" / is "not supported in" a version that the package has already reached — catching stale deferral notes (a feature that shipped but whose comment still says otherwise, or a missed deadline) before they reach a release. An allowlist records the deliberate defer-with-condition exceptions.
|
|
@@ -169,7 +169,7 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
|
|
|
169
169
|
- **Mail (outbound)** — multipart + attachments + DKIM + calendar invites; bounce intake (`b.mail`, `b.mailBounce`)
|
|
170
170
|
- **Mail (outbound delivery)** — turnkey MX-lookup → MTA-STS-fetch → DANE-TLSA → REQUIRETLS handshake → SMTP wire layer → RFC 3464 DSN-on-permanent-failure → deferred-retry scheduling, all wired once (`b.mail.send.deliver`)
|
|
171
171
|
- **Mail (inbound auth)** — SPF / DMARC / ARC verify + ARC chain signing for relays (`b.mail.spf`, `b.mail.dmarc`, `b.mail.arc`)
|
|
172
|
-
- **Mail server listeners** — RFC 5321 MX inbound (`b.mail.server.mx`), RFC 6409 submission with SASL + identity-binding (`b.mail.server.submission`), RFC 9051 IMAP4rev2 with CONDSTORE / QRESYNC / NOTIFY / METADATA / CATENATE (`b.mail.server.imap`), RFC 8620 + RFC 8621 JMAP Core + Mail over HTTP/SSE/WebSocket (`b.mail.server.jmap`), POP3 (`b.mail.server.pop3`), ManageSieve (`b.mail.server.managesieve`)
|
|
172
|
+
- **Mail server listeners** — RFC 5321 MX inbound with connection-level gate cascade (HELO identity / DNS blocklist / greylisting) (`b.mail.server.mx`), RFC 6409 submission with SASL + identity-binding (`b.mail.server.submission`), RFC 9051 IMAP4rev2 with CONDSTORE / QRESYNC / NOTIFY / METADATA / CATENATE (`b.mail.server.imap`), RFC 8620 + RFC 8621 JMAP Core + Mail over HTTP/SSE/WebSocket (`b.mail.server.jmap`), POP3 (`b.mail.server.pop3`), ManageSieve (`b.mail.server.managesieve`)
|
|
173
173
|
- **JMAP EmailSubmission reference** — composes `b.mail.send.deliver` to land the RFC 8621 §7.5 surface end-to-end (`b.mail.server.jmap.emailSubmissionSetHandler`)
|
|
174
174
|
- **Mail crypto** — PQC-first S/MIME via CMS (`b.mail.crypto.cms`) + OpenPGP encrypt/decrypt + WKD key discovery with IDN-homograph defense (`b.mail.crypto.pgp`)
|
|
175
175
|
- **Mail-stack agent** — multi-threaded worker pool + queue dispatch + sealed mail-store backed by SQLite FTS5 (`b.mail.agent`, `b.mailStore`)
|
|
@@ -332,9 +332,10 @@ This is the minimum-viable security posture for a production deployment. The fra
|
|
|
332
332
|
- [ ] For routes that accept YAML (config uploads, CI/CD pipelines, infra-as-code, document-import flows — ANY operator-supplied YAML the server parses): `b.guardYaml.gate({ profile: "strict" })` is wired by default into `b.fileUpload` + `b.staticServe` as of v0.7.12. For inbound YAML bodies that don't go through those primitives, wire `b.guardYaml.parse(body, { profile: "strict" })` before passing the parsed structure to operator handlers — strict refuses deserialization-tag RCE (defends CVE-2026-24009 Docling/PyYAML, CVE-2022-1471 SnakeYAML, CVE-2017-18342 PyYAML class), billion-laughs alias recursion (CVE-2026-27807 MarkUs class), Norway-problem implicit booleans, multi-document streams, leading-zero octals, duplicate keys, merge-key anchor-chains, bidi/null/control chars. Unlike JSON, YAML's threat surface includes language-specific deserialization triggers — `!!python/object/new:...` / `!!java.util.HashMap` / `!!ruby/object` etc. — which the source-level scan catches before any downstream parser (PyYAML / SnakeYAML / js-yaml) sees them
|
|
333
333
|
- [ ] For routes that accept JSON bodies (REST APIs / webhook receivers / config uploads — ANY operator-supplied JSON the server parses): `b.guardJson.gate({ profile: "strict" })` is wired by default into `b.fileUpload` + `b.staticServe` as of v0.7.12. For inbound JSON request bodies that don't go through those primitives, wire `b.guardJson.parse(body, { profile: "strict" })` before passing the parsed structure to operator handlers — strict refuses prototype pollution at source level (catches `__proto__` / `constructor` / `prototype` keys before any parser sees the input — defends CVE-2025-55182 React Server Functions RCE class), duplicate keys (RFC 8259 SHOULD-unique smuggling), NaN/Infinity, comments, JSON5 syntax, BOM, bidi/null/control chars, numeric precision-loss, depth + breadth + array-length + string-length caps. Pair with `topLevelKeyAllowlist: [...]` for routes with a known shape so unauthorized keys refuse before validation
|
|
334
334
|
- [ ] For routes that accept email (inbound webhooks from mail providers, .eml uploads, mailbox imports, message-archival flows, customer-support-ticket-by-email — ANY operator-supplied RFC 822/5322 message the server processes): `b.guardEmail.gate({ profile: "strict" })` is wired by default into `b.fileUpload` + `b.staticServe` as of v0.7.17. For inbound message bytes that don't go through those primitives, wire `b.guardEmail.validateMessage(bytes, { profile: "strict" })` BEFORE the parser sees the message — strict refuses SMTP smuggling (bare CR / bare LF outside CRLF pairs combined with embedded SMTP verbs `MAIL FROM`/`RCPT TO`/`DATA`/`EHLO`/`HELO`/`RSET`/`QUIT` — defends CVE-2023-51764 Postfix / CVE-2023-51765 Sendmail / CVE-2023-51766 Exim / CVE-2026-32178 .NET System.Net.Mail class), CRLF header injection in single-line headers (defends From/Bcc/body smuggling), IDN homograph mixed-script domains in address-bearing headers (Cyrillic / Greek / Armenian / Cherokee codepoints overlapping Latin — operator opts in to legitimate non-Latin via `allowedScripts: ["latin", "cyrillic"]`), Punycode `xn--` labels, display-name spoofing (`"support@apple.com" <attacker@evil>` — display contains @-address that doesn't match envelope domain), IP-literal addresses (`user@[1.2.3.4]` — bypasses DNS/DMARC alignment), RFC 5322 comment syntax in addresses, multiple @ characters, RFC 5321 length caps (local-part 64 / domain 255 / address 320), RFC 5322 line cap (998), BOM injection, bidi/null/control chars in addresses + headers. For per-address validation outside a full message context (form-submitted email, signup, MX-host validation), wire `b.guardEmail.validateAddress(addr, { profile: "strict" })`. Pair with operator's DMARC / SPF / DKIM verifier for envelope-alignment checks — guardEmail is the source-level gate, not the authentication-result interpreter
|
|
335
|
+
- [ ] For an inbound `b.mail.server.mx` listener, wire the connection-level gate cascade so the anti-abuse controls actually run: `helo: b.mail.helo` (FCrDNS / HELO-shape / self-name spoofing → 550), `rbl: b.mail.rbl.create({ providers: [...] })` (connecting-IP DNS blocklist → 554, evaluated once per connection), and `greylist: b.mail.greylist.create({ store })` (first-seen (ip, sender, recipient) → 450 tempfail). A gate you don't wire is skipped, not synthesized — so an unwired RBL is no protection, not a default-allow surprise. SPF/DKIM/DMARC alignment is performed on the delivered message via the agent handoff until the inbound-authentication pipeline lands
|
|
335
336
|
- [ ] For routes that accept markdown (rich-text editors, comment systems, README rendering, documentation submission, GitHub-style wikis, mail-rendered markdown, document-import flows — ANY operator-supplied markdown the server renders): `b.guardMarkdown.gate({ profile: "strict" })` is wired by default into `b.fileUpload` + `b.staticServe` as of v0.7.16. For inbound markdown bodies that don't go through those primitives, wire `b.guardMarkdown.validate(body, { profile: "strict" })` BEFORE passing the source to any markdown renderer (marked / markdown-it / commonmark / remark / parsedown — all of them) — strict refuses dangerous URL schemes in inline links + images + autolinks + reference-link definitions (defends CVE-2025-9540 Markup Markdown class, CVE-2025-24981 MDC class, NuGetGallery GHSA-gwjh-c548-f787, Joplin GHSA-hff8-hjwv-j9q7), whitespace-tolerant dangerous-tag matching (`<script\n>` / `<script\t>` — defends CVE-2026-30838 CommonMark DisallowedRawHtml bypass class), HTML-entity scheme bypass (`javascript:` / `javascript:` decoded BEFORE scheme matching), reference-link smuggling (`[label]: javascript:...`), front-matter YAML/TOML blocks, HTML comments, code-fence language injection (language tag containing `<>"' `` blocks attribute breakout), catastrophic emphasis runs (CVE-2025-6493 CodeMirror Markdown class, CVE-2025-7969 markdown-it class), inline DOCTYPE, bidi/null/control chars, total-bytes + line + link + image + autolink + ref-def + list-depth + blockquote-depth caps. **Layer with `b.guardHtml`**: source-level guardMarkdown then render then output-level guardHtml together close the residual bypass surface that either alone misses (markdown engines surprise; sanitizers also surprise — defense in depth)
|
|
336
337
|
- [ ] For routes that accept XML (SOAP endpoints, sitemap submissions, RSS / Atom feeds, OAI-PMH harvesters, SAML / WS-Federation receivers, document-import flows — ANY operator-supplied XML the server parses): `b.guardXml.gate({ profile: "strict" })` is wired by default into `b.fileUpload` + `b.staticServe` as of v0.7.15. For inbound XML bodies that don't go through those primitives, wire `b.guardXml.validate(body, { profile: "strict" })` before passing the document to any XML parser — strict refuses DOCTYPE declarations unconditionally (XXE + billion-laughs vector — defends CVE-2026-24400 AssertJ class, CVE-2024-8176 libexpat recursive-entity stack-overflow class), `<!ENTITY>` declarations including parameter entities (out-of-band exfiltration vector), external entity references (SYSTEM / PUBLIC with file:// / http:// / ftp:// schemes — local file read + SSRF), `<xi:include>` remote inclusion (CVE-2024-25062 libxml2 use-after-free class), `xsi:schemaLocation` operator-controlled schema fetch, processing instructions (`<?xml-stylesheet ?>` CSS-injection vector), CDATA sections (often used to hide payloads from naive scanners), XML signature wrapping (xmldsig surface), bidi/null/control chars in element text + attribute values, and applies depth + element + per-attribute-value caps. DOCTYPE remains refused at every profile level (strict / balanced / permissive) because billion-laughs is universal. Operators integrating with legacy SOAP that requires DTDs must instead route through a separately-firewalled XML processor with explicit allowlist — the gate has no knob to relax DOCTYPE
|
|
337
|
-
- [ ] **For ZIP-shaped uploads specifically**, reach for `b.safeArchive.extract({ source, destination, guardProfile: "strict" })` — the one-liner composes `b.archive.read.zip`'s random-access reader (LFH/CD skew defense + CD-walk validation), `b.guardArchive.zipBombPolicy` defaults (per-entry + per-archive + ratio caps), `b.guardArchive.entryTypePolicy` defaults (symlink / hardlink / device / fifo / socket entries refused), and `b.guardFilename.verifyExtractionPath`'s dual-check (string-normalize + `fs.realpath`-agreement; refuses pre-resolve names exceeding PATH_MAX=4096 to defend the CVE-2025-4517 TOCTOU class). The fs-coupled realpath check is the depth above `b.guardArchive.checkExtractionPath`'s portable string-only gate; operators with their own extract loop call both. Default refuses ZIP encrypted entries (v0.12.10/11 add the encryption read paths). For tar / gzip / 7z / rar / zstd, the read-side primitives land in v0.12.8 / v0.12.9; until then use the legacy guard-only path below
|
|
338
|
+
- [ ] **For ZIP-shaped uploads specifically**, reach for `b.safeArchive.extract({ source, destination, guardProfile: "strict" })` — the one-liner composes `b.archive.read.zip`'s random-access reader (LFH/CD skew defense + CD-walk validation), `b.guardArchive.zipBombPolicy` defaults (per-entry + per-archive + ratio caps), `b.guardArchive.entryTypePolicy` defaults (symlink / hardlink / device / fifo / socket entries refused), and `b.guardFilename.verifyExtractionPath`'s dual-check (string-normalize + `fs.realpath`-agreement; refuses pre-resolve names exceeding PATH_MAX=4096 to defend the CVE-2025-4517 TOCTOU class; and refuses per-segment Windows write-target hazards — reserved device names like `CON`/`NUL`, NTFS alternate-data-stream `name:stream` syntax, and trailing-dot/whitespace that Windows strips into a sibling overwrite — platform-unconditionally, since the extracting host may differ from the verifying host). The fs-coupled realpath check is the depth above `b.guardArchive.checkExtractionPath`'s portable string-only gate; operators with their own extract loop call both. Default refuses ZIP encrypted entries (v0.12.10/11 add the encryption read paths). For tar / gzip / 7z / rar / zstd, the read-side primitives land in v0.12.8 / v0.12.9; until then use the legacy guard-only path below
|
|
338
339
|
- [ ] For routes that accept archives (zip / tar / gzip / 7z / rar / zstd / etc. — ANY upload that downstream code will extract): use the operator's archive library to enumerate entries, then validate via `b.guardArchive.validateEntries(entries, { profile: "strict" })` BEFORE extracting any file. Strict profile defends against zip-slip path traversal (CVE-2025-3445 / 32779 / 62156 / 66945 / 45582 / 11002 class), symlink + hardlink escape (CVE-2026-26960 class), per-entry + aggregate compression-ratio bombs (zip-bomb defense), total-size + entry-count caps, nested-archive recursion DoS, duplicate entry names (silent-overwrite vector), case-insensitive collisions on Windows / macOS, and per-entry filename safety (composes `b.guardFilename` for path traversal / null-byte / Windows reserved names / NTFS ADS / RTLO bidi / overlong UTF-8 / shell-exec / double-extension detection). Additionally call `b.guardArchive.checkExtractionPath(entryName, extractionRoot)` per entry at extract time AND `path.resolve(extractionRoot, entryName).startsWith(path.resolve(extractionRoot))` after path-resolve to catch any traversal that survived metadata validation
|
|
339
340
|
- [ ] For ANY file-upload route — wire `b.guardFilename.gate({ profile: "strict" })` to validate the filename string before it touches the filesystem. Strict profile rejects path traversal (raw + percent-encoded + UTF-8 overlong), null-byte truncation (defends extension-allowlist bypass), Windows reserved device names (CON / PRN / AUX / ... — even with extensions), NTFS alternate data streams, leading/trailing whitespace + trailing dots (Windows silently strips), Unicode bidi / RTLO file-name spoofing (CVE-2021-42574 in filename context — `Photo01Bygpj.SCR` displays as `RCS.jpg` while OS opens `.SCR`), reserved characters, UNC paths, shell-shortcut + executable extensions (.exe / .bat / .vbs / .lnk / .scr / .dll / .so / etc.), and double-extension bypass (`invoice.pdf.exe`). Operators with non-ASCII filename requirements use `profile: "balanced"`. Operators with multi-component path-shape needs use `profile: "permissive"` and explicitly opt in to `pathSeparatorsPolicy: "allow"`
|
|
340
341
|
- [ ] For routes that accept SVG (avatar uploads, illustration / icon assets, mail attachments, file-upload widgets that allow image/svg+xml): wire `b.guardSvg.gate({ profile: "strict" })` — strict profile rejects every dangerous tag (script / foreignObject / animation family), denies cross-origin `<use>` references (defends server-side rasterization SSRF), refuses every DOCTYPE (defends billion-laughs entity expansion + XXE per CVE-2026-29074 class), refuses SVGZ payloads (operator must ungzip first), and enforces the SMIL animation attributeName allowlist (defends the recent CVE class where `<animate attributeName="href" to="javascript:..."/>` retroactively hijacks an element's href). For uploaded SVGs that need to be rendered, additionally serve under a strict CSP and consider rasterizing server-side to PNG before display
|
|
@@ -503,6 +503,18 @@ function zip(adapter, opts) {
|
|
|
503
503
|
var entryTypePolicy = _normalizeEntryTypePolicy(opts.entryTypePolicy);
|
|
504
504
|
var cdCache = null;
|
|
505
505
|
|
|
506
|
+
// Cooperative cancellation — operators pass an AbortSignal to bound a
|
|
507
|
+
// large/slow archive read. Checked between entries (the natural yield
|
|
508
|
+
// point); a long single-entry decompress is already bounded by the
|
|
509
|
+
// bomb policy.
|
|
510
|
+
function _throwIfAborted() {
|
|
511
|
+
if (opts.signal && opts.signal.aborted) {
|
|
512
|
+
throw new ArchiveReadError("archive-read/aborted",
|
|
513
|
+
"archive read aborted via opts.signal" +
|
|
514
|
+
(opts.signal.reason !== undefined ? ": " + String(opts.signal.reason) : ""));
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
506
518
|
async function _loadCD() {
|
|
507
519
|
if (cdCache) return cdCache;
|
|
508
520
|
var eocd = await _locateEocd(adapter);
|
|
@@ -512,6 +524,7 @@ function zip(adapter, opts) {
|
|
|
512
524
|
}
|
|
513
525
|
|
|
514
526
|
async function inspect() {
|
|
527
|
+
_throwIfAborted();
|
|
515
528
|
var loaded = await _loadCD();
|
|
516
529
|
_enforceBombPolicy(loaded.entries, bombPolicy);
|
|
517
530
|
_emitAudit(opts, "archive.read.inspect", "success", {
|
|
@@ -539,9 +552,11 @@ function zip(adapter, opts) {
|
|
|
539
552
|
}
|
|
540
553
|
|
|
541
554
|
async function* entries() {
|
|
555
|
+
_throwIfAborted();
|
|
542
556
|
var loaded = await _loadCD();
|
|
543
557
|
_enforceBombPolicy(loaded.entries, bombPolicy);
|
|
544
558
|
for (var i = 0; i < loaded.entries.length; i += 1) {
|
|
559
|
+
_throwIfAborted();
|
|
545
560
|
yield loaded.entries[i];
|
|
546
561
|
}
|
|
547
562
|
}
|
|
@@ -593,6 +608,7 @@ function zip(adapter, opts) {
|
|
|
593
608
|
var totalDecompressed = 0;
|
|
594
609
|
var yielded = 0;
|
|
595
610
|
for (var i = 0; i < loaded.entries.length; i += 1) {
|
|
611
|
+
_throwIfAborted();
|
|
596
612
|
var entry = loaded.entries[i];
|
|
597
613
|
if (entry.isEncrypted && !extractOpts.allowEncrypted) {
|
|
598
614
|
throw new ArchiveReadError("archive-read/encrypted-entry",
|
|
@@ -644,6 +660,7 @@ function zip(adapter, opts) {
|
|
|
644
660
|
var totalDecompressed = 0;
|
|
645
661
|
try {
|
|
646
662
|
for (var i = 0; i < loaded.entries.length; i += 1) {
|
|
663
|
+
_throwIfAborted();
|
|
647
664
|
var entry = loaded.entries[i];
|
|
648
665
|
// Skip directory + dangerous-by-default entry types unless the
|
|
649
666
|
// entry-type policy opts in.
|
|
@@ -155,6 +155,18 @@ function fromAssertion(opts) {
|
|
|
155
155
|
"fal.fromAssertion: channel must be 'front' or 'back'");
|
|
156
156
|
}
|
|
157
157
|
var hokBinding = opts.hokBinding;
|
|
158
|
+
// `bearerOnly: true` is the explicit alias for "no proof-of-possession
|
|
159
|
+
// binding" (hokBinding === null). It contradicts a non-null hokBinding;
|
|
160
|
+
// refuse the contradiction at the entry point rather than silently
|
|
161
|
+
// picking one — an operator who sets both has a config bug.
|
|
162
|
+
if (opts.bearerOnly === true) {
|
|
163
|
+
if (hokBinding !== undefined && hokBinding !== null) {
|
|
164
|
+
throw new AuthError("auth/bad-fal-opts",
|
|
165
|
+
"fal.fromAssertion: bearerOnly:true conflicts with hokBinding '" + hokBinding +
|
|
166
|
+
"' (bearerOnly forces no proof-of-possession binding)");
|
|
167
|
+
}
|
|
168
|
+
hokBinding = null;
|
|
169
|
+
}
|
|
158
170
|
if (hokBinding !== undefined && hokBinding !== null) {
|
|
159
171
|
if (hokBinding !== "mtls" && hokBinding !== "dpop" && hokBinding !== "saml-hok") {
|
|
160
172
|
throw new AuthError("auth/bad-fal-opts",
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
*
|
|
29
29
|
* Defenses against the well-known JWT pitfalls:
|
|
30
30
|
*
|
|
31
|
-
* - alg confusion (CVE-2024-54150 / CVE-
|
|
32
|
-
*
|
|
31
|
+
* - alg confusion (CVE-2024-54150 / CVE-2026-22817 Hono class) —
|
|
32
|
+
* `algorithms` is REQUIRED with no default; `none`,
|
|
33
33
|
* `HS256` cannot be accepted unless the operator explicitly listed
|
|
34
34
|
* them, and even then the verifier refuses HS* algs in
|
|
35
35
|
* verifyExternal because HMAC + a public-key JWKS is the canonical
|
|
@@ -175,8 +175,8 @@ function _assertAlgKtyMatch(alg, jwk) {
|
|
|
175
175
|
else if (alg === "ML-DSA-65" || alg === "ML-DSA-87") { expectedKty = "AKP"; }
|
|
176
176
|
else {
|
|
177
177
|
// Unknown alg — caller's alg allowlist should have rejected first;
|
|
178
|
-
// refuse here defensively (
|
|
179
|
-
// that skip downstream verification).
|
|
178
|
+
// refuse here defensively (CWE-347 alg-confusion class — unknown-alg
|
|
179
|
+
// paths that skip downstream verification; cf. CVE-2026-22817).
|
|
180
180
|
throw new AuthError("auth-jwt-external/unsupported-alg",
|
|
181
181
|
"_assertAlgKtyMatch: alg '" + alg + "' has no defined key-type binding");
|
|
182
182
|
}
|
|
@@ -336,7 +336,7 @@ async function verifyExternal(token, opts) {
|
|
|
336
336
|
|
|
337
337
|
// Decode header + payload.
|
|
338
338
|
var parts = token.split(".");
|
|
339
|
-
// CVE-2026-29000 / CVE-2026-
|
|
339
|
+
// CVE-2026-29000 / CVE-2026-22817 / CVE-2026-34950 —
|
|
340
340
|
// JWE-bypass + alg-confusion. A 5-segment compact serialization is a
|
|
341
341
|
// JWE (RFC 7516); accepting it on a JWS verifier is the canonical
|
|
342
342
|
// confused-deputy shape. verifyExternal is JWS-only; refuse JWE
|
|
@@ -350,7 +350,7 @@ async function verifyExternal(token, opts) {
|
|
|
350
350
|
}); } catch (_e) { /* audit best-effort */ }
|
|
351
351
|
throw new AuthError("auth-jwt-external/jwe-refused",
|
|
352
352
|
"5-segment JWE token refused — verifyExternal only handles JWS " +
|
|
353
|
-
"(JWE bypass class — CVE-2026-29000 / CVE-2026-
|
|
353
|
+
"(JWE bypass class — CVE-2026-29000 / CVE-2026-22817 / CVE-2026-34950)");
|
|
354
354
|
}
|
|
355
355
|
if (parts.length !== 3) {
|
|
356
356
|
throw new AuthError("auth-jwt-external/malformed-jwt",
|
|
@@ -371,8 +371,9 @@ async function verifyExternal(token, opts) {
|
|
|
371
371
|
throw new AuthError("auth-jwt-external/unknown-crit",
|
|
372
372
|
"token declares 'crit' header — verifyExternal does not support critical extensions");
|
|
373
373
|
}
|
|
374
|
-
//
|
|
375
|
-
//
|
|
374
|
+
// Alg-allowlist gate (CWE-347 improper-sig-verification / CWE-757
|
|
375
|
+
// algorithm-downgrade) — refuse alg values outside the accepted list
|
|
376
|
+
// BEFORE any key lookup. The early refusal closes the class where an
|
|
376
377
|
// unknown / unsupported alg slips through to a downstream code path
|
|
377
378
|
// that interprets it permissively. The per-listed algorithm check
|
|
378
379
|
// above in the opts-validation loop refuses the OPERATOR'S allowlist
|
|
@@ -381,11 +382,11 @@ async function verifyExternal(token, opts) {
|
|
|
381
382
|
if (opts.algorithms.indexOf(header.alg) === -1) {
|
|
382
383
|
throw new AuthError("auth-jwt-external/alg-not-allowed",
|
|
383
384
|
"token alg='" + header.alg + "' not in allowed list [" + opts.algorithms.join(", ") +
|
|
384
|
-
"] (
|
|
385
|
+
"] (alg-allowlist gate — refused before key lookup)");
|
|
385
386
|
}
|
|
386
387
|
if (SUPPORTED_CLASSICAL_ALGS.indexOf(header.alg) === -1) {
|
|
387
388
|
throw new AuthError("auth-jwt-external/unsupported-alg",
|
|
388
|
-
"token alg='" + header.alg + "' is not in the verifier's supported set (
|
|
389
|
+
"token alg='" + header.alg + "' is not in the verifier's supported set (alg-allowlist gate)");
|
|
389
390
|
}
|
|
390
391
|
|
|
391
392
|
// Resolve key.
|
|
@@ -480,7 +481,10 @@ async function verifyExternal(token, opts) {
|
|
|
480
481
|
// weak iss validation. Constant-time compare defeats prefix-timing
|
|
481
482
|
// narrowing; emit a DISTINCT audit event (separate from sig-verify-
|
|
482
483
|
// fail) so detection signals lights up on the cross-realm shape
|
|
483
|
-
// independently of generic verification failures.
|
|
484
|
+
// independently of generic verification failures. The `typeof ... !==
|
|
485
|
+
// "string"` guard also rejects an array-valued iss (CVE-2025-30144,
|
|
486
|
+
// fast-jwt — an iss array `["attacker", "valid"]` passed an any-match
|
|
487
|
+
// check); only a single string iss is accepted.
|
|
484
488
|
if (typeof payload.iss !== "string" ||
|
|
485
489
|
!_issuerMatches(payload.iss, opts.issuer)) {
|
|
486
490
|
try { audit().safeEmit({
|
|
@@ -234,8 +234,8 @@ async function verify(token, opts) {
|
|
|
234
234
|
// SECURITY: when the resolver uses header.kid as a filename / map
|
|
235
235
|
// key / cache index, it MUST sanitize the kid first. Path-traversal
|
|
236
236
|
// (`../etc/passwd`), null-byte (`key\0..`), control chars, and
|
|
237
|
-
// similar shapes turn a kid lookup into an arbitrary-file-read
|
|
238
|
-
// primitive (
|
|
237
|
+
// similar shapes turn a kid lookup into an arbitrary-file-read or
|
|
238
|
+
// SQLi primitive (the PortSwigger JWT "kid" injection / LFI class). Use
|
|
239
239
|
// `b.guardJwt.kidSafe(header.kid)` — throws on traversal indicators
|
|
240
240
|
// and control bytes, returns the validated kid on success.
|
|
241
241
|
var key;
|
|
@@ -1008,7 +1008,7 @@ function create(opts) {
|
|
|
1008
1008
|
throw new OAuthError("auth-oauth/no-id-token", "verifyIdToken: idToken must be a string");
|
|
1009
1009
|
}
|
|
1010
1010
|
var parts = idToken.split(".");
|
|
1011
|
-
// CVE-2026-29000 / CVE-2026-22817
|
|
1011
|
+
// CVE-2026-29000 / CVE-2026-22817 — mirror
|
|
1012
1012
|
// jwt-external's 5-segment JWE refusal. A 5-segment compact
|
|
1013
1013
|
// serialization is a JWE (RFC 7516); verifyIdToken is a JWS verifier
|
|
1014
1014
|
// and a JWE shape reaching here is the confused-deputy class an OP
|
|
@@ -1023,7 +1023,7 @@ function create(opts) {
|
|
|
1023
1023
|
}); } catch (_e) { /* drop-silent — observability sink */ }
|
|
1024
1024
|
throw new OAuthError("auth-oauth/jwe-refused",
|
|
1025
1025
|
"5-segment JWE id_token refused — verifyIdToken only handles JWS " +
|
|
1026
|
-
"(CVE-2026-29000 / CVE-2026-
|
|
1026
|
+
"(CVE-2026-29000 / CVE-2026-22817 / CVE-2026-34950 JWE-bypass class)");
|
|
1027
1027
|
}
|
|
1028
1028
|
if (parts.length !== 3) {
|
|
1029
1029
|
throw new OAuthError("auth-oauth/malformed-jwt", "ID token does not have 3 parts");
|
|
@@ -1039,13 +1039,14 @@ function create(opts) {
|
|
|
1039
1039
|
if (!header || typeof header.alg !== "string") {
|
|
1040
1040
|
throw new OAuthError("auth-oauth/malformed-jwt", "ID token header missing 'alg'");
|
|
1041
1041
|
}
|
|
1042
|
-
//
|
|
1042
|
+
// Alg-allowlist gate (CWE-347 / CWE-757) — refuse unknown alg BEFORE
|
|
1043
|
+
// any key resolution.
|
|
1043
1044
|
// The acceptedAlgorithms list is the operator's posture; an alg
|
|
1044
1045
|
// outside it never reaches the JWKS lookup or node:crypto.verify.
|
|
1045
1046
|
if (acceptedAlgorithms.indexOf(header.alg) === -1) {
|
|
1046
1047
|
throw new OAuthError("auth-oauth/alg-not-accepted",
|
|
1047
1048
|
"ID token signed with '" + header.alg + "' which is not in the accepted-algorithm list " +
|
|
1048
|
-
"(
|
|
1049
|
+
"(alg-allowlist gate — refused before key lookup)");
|
|
1049
1050
|
}
|
|
1050
1051
|
// RFC 7515 §4.1.11 — refuse JWS with `crit` header. Every other
|
|
1051
1052
|
// verifier in the framework (jwt.js, jwt-external.js, dpop.js)
|
|
@@ -1344,8 +1345,8 @@ function create(opts) {
|
|
|
1344
1345
|
}
|
|
1345
1346
|
var iss = u.searchParams.get("iss");
|
|
1346
1347
|
var sid = u.searchParams.get("sid");
|
|
1347
|
-
//
|
|
1348
|
-
// present (defends against an attacker-controlled IdP forging a
|
|
1348
|
+
// OpenID Connect Front-Channel Logout 1.0 §3: `iss` MUST match the
|
|
1349
|
+
// configured issuer when present (defends against an attacker-controlled IdP forging a
|
|
1349
1350
|
// logout for a session at a different IdP). `sid` is required
|
|
1350
1351
|
// when the RP registered with frontchannel_logout_session_required=true;
|
|
1351
1352
|
// we surface it either way and let the operator decide.
|
|
@@ -108,14 +108,14 @@ function _verifyProofJwt(proofJwt, expectedAud, expectedCNonce, expectedClientId
|
|
|
108
108
|
throw new AuthError("auth-oid4vci/wrong-proof-typ",
|
|
109
109
|
"credential issuance: proof JWT typ must be \"openid4vci-proof+jwt\" (got \"" + header.typ + "\")");
|
|
110
110
|
}
|
|
111
|
-
//
|
|
112
|
-
// verify-side work. The supportedAlgs list is the issuer's posture;
|
|
111
|
+
// Alg-allowlist gate (CWE-347 / CWE-757) — refuse unknown / unsupported
|
|
112
|
+
// alg BEFORE any verify-side work. The supportedAlgs list is the issuer's posture;
|
|
113
113
|
// refusing here mirrors the discipline in oauth.verifyIdToken /
|
|
114
114
|
// jwt-external.verifyExternal.
|
|
115
115
|
if (!header.alg || supportedAlgs.indexOf(header.alg) === -1) {
|
|
116
116
|
throw new AuthError("auth-oid4vci/unsupported-proof-alg",
|
|
117
117
|
"credential issuance: proof JWT alg \"" + header.alg + "\" not in issuer-supported set " +
|
|
118
|
-
"(
|
|
118
|
+
"(alg-allowlist gate — refused before key lookup)");
|
|
119
119
|
}
|
|
120
120
|
// AUTH-5 / RFC 7515 §4.1.11 — refuse non-empty `crit`. Pre-v0.9.x
|
|
121
121
|
// silently ignored, letting an attacker-controlled wallet declare
|