@blamejs/blamejs-shop 0.2.3 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -0
- package/lib/admin.js +167 -1
- package/lib/asset-manifest.json +7 -3
- package/lib/storefront.js +193 -0
- package/lib/vendor/MANIFEST.json +2 -2
- package/lib/vendor/blamejs/CHANGELOG.md +4 -0
- package/lib/vendor/blamejs/api-snapshot.json +54 -58
- package/lib/vendor/blamejs/lib/acme.js +8 -5
- package/lib/vendor/blamejs/lib/archive-read.js +24 -20
- package/lib/vendor/blamejs/lib/break-glass.js +4 -5
- package/lib/vendor/blamejs/lib/crypto.js +8 -5
- package/lib/vendor/blamejs/lib/guard-dsn.js +3 -2
- package/lib/vendor/blamejs/lib/guard-list-id.js +3 -2
- package/lib/vendor/blamejs/lib/guard-regex.js +4 -4
- package/lib/vendor/blamejs/lib/guard-smtp-command.js +1 -2
- package/lib/vendor/blamejs/lib/mail-agent.js +47 -37
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.13.15.json +27 -0
- package/lib/vendor/blamejs/release-notes/v0.13.16.json +27 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +4 -8
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-agent.test.js +1 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/watcher.test.js +6 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.2.x
|
|
10
10
|
|
|
11
|
+
- v0.2.4 (2026-05-27) — **Announcement bar — publish a sitewide promo or notice strip from the admin console.** Operators can now publish a sitewide announcement bar — a strip at the very top of every storefront page for a sale, a shipping notice, or an urgent alert. Manage it from a new Announcements screen in the admin console: set the message, an optional call-to-action link, a theme (urgency, promo, info, or success), the audience (everyone, signed-out visitors, or signed-in customers), an optional start/expiry schedule, and whether visitors can dismiss it. The highest-priority active announcement shows automatically (urgency outranks promo outranks info outranks success). The bar renders on both the edge and container paths so it appears on every page including the cached ones, and a dismissed bar stays hidden across navigation. Until an announcement is published, nothing changes. **Added:** *Sitewide announcement bar* — A page-top strip rendered on every storefront page (home, search, product, cart, blog, and the storefront chrome) in one of four themes. The single highest-priority active announcement for the viewer is shown, chosen by theme rank (urgency > promo > info > success). Audience targeting covers everyone, signed-out visitors, and signed-in customers, with an optional scheduled start and expiry. An optional call-to-action link is included; its URL is restricted to https:// or a site-relative path. · *Admin console: Announcements* — A new Announcements screen (and matching JSON API) to create announcements, filter the list to those active right now, and archive ones that are done. The screen appears in the console only when the announcement primitive is wired. · *Dismiss without a round-trip* — Dismissible announcements carry a close control. With JavaScript it's hidden in place and remembered in a local cookie so it doesn't reappear as you browse; with JavaScript off, a form submission records the dismissal and returns you to the page. The bar is always server-rendered identically for everyone, so a dismissed bar never leaks back in from a cached page — it's hidden per-visitor on the client.
|
|
12
|
+
|
|
11
13
|
- v0.2.3 (2026-05-27) — **Admin console adopts the dark violet brand — the operator dashboard now matches the storefront.** The admin console has been restyled from its previous light theme and orange accent onto the same dark violet identity as the storefront: a neutral near-black canvas with violet reserved for accents, the self-hosted Hanken Grotesk and Space Mono typefaces in place of Inter, and Space Mono carrying the terminal-style chrome (header mark, navigation, micro-labels, figures, SKUs, order ids, and code). Stat cards, tables, status pills, forms, buttons, banners, nav cards, and the sign-in card are all recoloured for the dark surface with contrast preserved — order statuses keep their meaning (green for paid/shipped, amber for pending, violet for refunded, muted for cancelled), form inputs read clearly on the dark wells, and the revenue sparkline is drawn in the brand violet. Operators on the default theme get the new console on upgrade; no admin functionality, routes, or data change. **Changed:** *Dark violet admin theme* — The admin stylesheet is rebuilt on the storefront's palette — near-black surfaces, neutral hairline borders, and violet reserved for accents (active nav, primary buttons, the `/ admin` mark, focus states, and the revenue sparkline) rather than the former light paper and orange. Self-hosted Hanken Grotesk (display/body) and Space Mono (chrome/mono) replace Inter, loaded under the strict `font-src 'self'` policy. · *Components recoloured for the dark surface* — Stat cards, the orders table, status pills, filter chips, forms, buttons (primary/ghost/danger), banners, nav cards, the gift-card detail grid, and the sign-in card are all restyled for dark with contrast maintained. Order-status pills keep their semantics — green for paid/shipped/fulfilling/delivered, amber for pending, violet for refunded, muted for cancelled. Form inputs sit on recessed dark wells with light text and a violet focus ring.
|
|
12
14
|
|
|
13
15
|
- v0.2.2 (2026-05-27) — **Bespoke line-art illustrations across the storefront — category tiles, image placeholders, and every empty state.** The storefront's stand-in graphics are now a single, cohesive set of hand-drawn line illustrations in the brand violet, replacing the previous mix of gradient-only tiles, single-letter image marks, and emoji. The six featured-collection tiles carry a matching icon (apparel, hardware, digital, subscriptions, bundles, gift cards). Products without an image — in the grid and on the product page — show a bespoke "no image yet" placeholder instead of a letter glyph. Every empty state has its own illustration: cart, search, wishlist, saved-for-later, addresses, returns, and orders. The artwork is inline SVG (no extra files fetched, consistent with the strict asset policy) and adapts to the surface — the fine detail strokes follow the text colour, so the same set reads correctly on a dark or a light background. **Changed:** *Category tiles carry line-art icons* — Each of the six featured-collection tiles now shows a violet line icon for its category, centered over the existing tile gradient. The tile links and labels are unchanged. · *Bespoke image placeholder replaces the letter mark* — When a product has no image, the grid card and the product-page gallery render a line-art "no image yet" placeholder — an isometric package on a faint grid (the product page adds a shield motif and a terminal-prompt accent) — instead of the previous single uppercase letter. The no-image grid card now uses the same media-card layout as image-bearing cards for a consistent grid. · *Every empty state has matching artwork* — The empty cart, no-search-results, empty wishlist, saved-for-later, addresses, returns, and orders states now show a bespoke line illustration in place of the previous emoji or unicode glyphs, so the storefront and account sections share one visual language. · *Inline, theme-adaptive SVG* — All of the new artwork is inline SVG with presentation attributes only — nothing is fetched from a CDN and no inline styles are used, consistent with the strict content-security and asset policies. The detail strokes use currentColor, so the set adapts to the background it sits on. The edge and container renderers emit identical markup.
|
package/lib/admin.js
CHANGED
|
@@ -300,7 +300,7 @@ function mount(router, deps) {
|
|
|
300
300
|
// Which optional console sections are wired — gates their nav links so a
|
|
301
301
|
// signed-in admin is never sent to a route that wasn't mounted. Passed
|
|
302
302
|
// into every authed render call as `nav_available`.
|
|
303
|
-
var navAvailable = { returns: !!returns, reviews: !!reviews, productQa: !!productQa, subscriptions: !!deps.subscriptions, webhooks: !!deps.webhooks, collections: !!deps.collections, customers: !!deps.customers, giftcards: !!deps.giftcards };
|
|
303
|
+
var navAvailable = { returns: !!returns, reviews: !!reviews, productQa: !!productQa, subscriptions: !!deps.subscriptions, webhooks: !!deps.webhooks, collections: !!deps.collections, customers: !!deps.customers, giftcards: !!deps.giftcards, announcementBar: !!deps.announcementBar };
|
|
304
304
|
|
|
305
305
|
try { b.audit.registerNamespace(AUDIT_NAMESPACE); } catch (_e) { /* idempotent */ }
|
|
306
306
|
|
|
@@ -2098,6 +2098,79 @@ function mount(router, deps) {
|
|
|
2098
2098
|
));
|
|
2099
2099
|
}
|
|
2100
2100
|
|
|
2101
|
+
// ---- announcements --------------------------------------------------
|
|
2102
|
+
// Sitewide promo/notice strip. Content-negotiated like the other console
|
|
2103
|
+
// screens: bearer → the JSON contract; signed-in browser → the HTML
|
|
2104
|
+
// table + create form. The console exposes the all / guest / logged_in
|
|
2105
|
+
// audiences; the primitive's segment audience needs an isMember handle
|
|
2106
|
+
// that isn't wired (see server.js), so it isn't offered here.
|
|
2107
|
+
if (deps.announcementBar) {
|
|
2108
|
+
var announcements = deps.announcementBar;
|
|
2109
|
+
|
|
2110
|
+
router.get("/admin/announcements", _pageOrApi(true,
|
|
2111
|
+
R(async function (req, res) {
|
|
2112
|
+
var url = req.url ? new URL(req.url, "http://localhost") : null;
|
|
2113
|
+
var activeS = url && url.searchParams.get("active");
|
|
2114
|
+
var rows = await announcements.listAnnouncements(activeS === "1" ? { active_only: true } : {});
|
|
2115
|
+
_json(res, 200, { rows: rows });
|
|
2116
|
+
}),
|
|
2117
|
+
async function (req, res) {
|
|
2118
|
+
var url = req.url ? new URL(req.url, "http://localhost") : null;
|
|
2119
|
+
var activeS = url && url.searchParams.get("active");
|
|
2120
|
+
var rows = await announcements.listAnnouncements(activeS === "1" ? { active_only: true } : {});
|
|
2121
|
+
_sendHtml(res, 200, renderAdminAnnouncements({
|
|
2122
|
+
shop_name: deps.shop_name, nav_available: navAvailable, announcements: rows,
|
|
2123
|
+
active_filter: activeS,
|
|
2124
|
+
created: url && url.searchParams.get("created"),
|
|
2125
|
+
archived: url && url.searchParams.get("archived"),
|
|
2126
|
+
notice: (url && url.searchParams.get("err")) ? "That action couldn't be completed for the announcement." : null,
|
|
2127
|
+
}));
|
|
2128
|
+
},
|
|
2129
|
+
));
|
|
2130
|
+
|
|
2131
|
+
router.post("/admin/announcements", _pageOrApi(false,
|
|
2132
|
+
W("announcement.define", async function (req, res) {
|
|
2133
|
+
var row = await announcements.defineAnnouncement(req.body || {});
|
|
2134
|
+
_json(res, 201, row);
|
|
2135
|
+
return { id: row.slug };
|
|
2136
|
+
}),
|
|
2137
|
+
async function (req, res) {
|
|
2138
|
+
try {
|
|
2139
|
+
await announcements.defineAnnouncement(_announcementFromForm(req.body || {}));
|
|
2140
|
+
} catch (e) {
|
|
2141
|
+
if (!(e instanceof TypeError)) throw e;
|
|
2142
|
+
var rows = await announcements.listAnnouncements({});
|
|
2143
|
+
return _sendHtml(res, 400, renderAdminAnnouncements({
|
|
2144
|
+
shop_name: deps.shop_name, nav_available: navAvailable, announcements: rows,
|
|
2145
|
+
notice: (e && e.message || "Couldn't save that announcement.").replace(/^announcementBar[.:]\s*/, ""),
|
|
2146
|
+
}));
|
|
2147
|
+
}
|
|
2148
|
+
b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".announcement.define", outcome: "success" });
|
|
2149
|
+
_redirect(res, "/admin/announcements?created=1");
|
|
2150
|
+
},
|
|
2151
|
+
));
|
|
2152
|
+
|
|
2153
|
+
router.post("/admin/announcements/:slug/archive", _pageOrApi(false,
|
|
2154
|
+
W("announcement.archive", async function (req, res) {
|
|
2155
|
+
var row;
|
|
2156
|
+
try { row = await announcements.archiveAnnouncement(req.params.slug); }
|
|
2157
|
+
catch (e) { if (e instanceof TypeError) return _problem(res, 400, "bad-request", e.message); throw e; }
|
|
2158
|
+
if (!row) return _problem(res, 404, "announcement-not-found");
|
|
2159
|
+
_json(res, 200, row);
|
|
2160
|
+
return { id: row.slug };
|
|
2161
|
+
}),
|
|
2162
|
+
async function (req, res) {
|
|
2163
|
+
var slug = req.params.slug;
|
|
2164
|
+
var row = null;
|
|
2165
|
+
try { row = await announcements.archiveAnnouncement(slug); }
|
|
2166
|
+
catch (e) { if (!(e instanceof TypeError)) throw e; }
|
|
2167
|
+
if (!row) return _redirect(res, "/admin/announcements?err=1");
|
|
2168
|
+
b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".announcement.archive", outcome: "success", metadata: { slug: slug } });
|
|
2169
|
+
_redirect(res, "/admin/announcements?archived=1");
|
|
2170
|
+
},
|
|
2171
|
+
));
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2101
2174
|
// ---- analytics ------------------------------------------------------
|
|
2102
2175
|
|
|
2103
2176
|
var analytics = deps.analytics || null;
|
|
@@ -2772,6 +2845,7 @@ var ADMIN_NAV_ITEMS = [
|
|
|
2772
2845
|
{ key: "questions", href: "/admin/questions", label: "Q&A", requires: "productQa" },
|
|
2773
2846
|
{ key: "subscriptions", href: "/admin/subscription-plans", label: "Subscriptions", requires: "subscriptions" },
|
|
2774
2847
|
{ key: "collections", href: "/admin/collections", label: "Collections", requires: "collections" },
|
|
2848
|
+
{ key: "announcements", href: "/admin/announcements", label: "Announcements", requires: "announcementBar" },
|
|
2775
2849
|
{ key: "giftcards", href: "/admin/gift-cards", label: "Gift cards", requires: "giftcards" },
|
|
2776
2850
|
{ key: "webhooks", href: "/admin/webhooks", label: "Webhooks", requires: "webhooks" },
|
|
2777
2851
|
{ key: "integrations", href: "/admin/integrations", label: "Integrations" },
|
|
@@ -3910,6 +3984,98 @@ function renderAdminCollections(opts) {
|
|
|
3910
3984
|
return _renderAdminShell(opts.shop_name, "Collections", bodyHtml, "collections", opts.nav_available);
|
|
3911
3985
|
}
|
|
3912
3986
|
|
|
3987
|
+
// A <datetime-local> form value → epoch ms (integer) for the lib's
|
|
3988
|
+
// starts_at/expires_at, or null when blank/unparseable (open-ended). The
|
|
3989
|
+
// announcement primitive validates the result; a blank schedule field just
|
|
3990
|
+
// omits the bound.
|
|
3991
|
+
function _epochFromForm(v) {
|
|
3992
|
+
if (typeof v !== "string" || !v.trim()) return null;
|
|
3993
|
+
var t = Date.parse(v.trim());
|
|
3994
|
+
return Number.isFinite(t) ? t : null;
|
|
3995
|
+
}
|
|
3996
|
+
|
|
3997
|
+
// Coerce the create form into the shape announcementBar.defineAnnouncement
|
|
3998
|
+
// expects: trimmed slug, strict boolean `dismissible`, integer epoch
|
|
3999
|
+
// schedule bounds, and link_url/link_label as a both-or-neither pair.
|
|
4000
|
+
function _announcementFromForm(body) {
|
|
4001
|
+
body = body || {};
|
|
4002
|
+
var out = {
|
|
4003
|
+
slug: typeof body.slug === "string" ? body.slug.trim() : body.slug,
|
|
4004
|
+
message: body.message,
|
|
4005
|
+
theme: body.theme,
|
|
4006
|
+
audience: body.audience,
|
|
4007
|
+
dismissible: body.dismissible === "on" || body.dismissible === "1" || body.dismissible === true,
|
|
4008
|
+
};
|
|
4009
|
+
var lu = typeof body.link_url === "string" ? body.link_url.trim() : "";
|
|
4010
|
+
var ll = typeof body.link_label === "string" ? body.link_label.trim() : "";
|
|
4011
|
+
if (lu || ll) { out.link_url = lu; out.link_label = ll; }
|
|
4012
|
+
var sa = _epochFromForm(body.starts_at); if (sa != null) out.starts_at = sa;
|
|
4013
|
+
var ea = _epochFromForm(body.expires_at); if (ea != null) out.expires_at = ea;
|
|
4014
|
+
return out;
|
|
4015
|
+
}
|
|
4016
|
+
|
|
4017
|
+
function renderAdminAnnouncements(opts) {
|
|
4018
|
+
opts = opts || {};
|
|
4019
|
+
var rows = opts.announcements || [];
|
|
4020
|
+
var created = opts.created ? "<div class=\"banner banner--ok\">Announcement saved.</div>" : "";
|
|
4021
|
+
var archived = opts.archived ? "<div class=\"banner banner--ok\">Announcement archived.</div>" : "";
|
|
4022
|
+
var notice = opts.notice ? "<div class=\"banner banner--err\">" + _htmlEscape(opts.notice) + "</div>" : "";
|
|
4023
|
+
|
|
4024
|
+
var af = opts.active_filter;
|
|
4025
|
+
var chips = "<div class=\"order-filters\">" +
|
|
4026
|
+
"<a class=\"chip" + (af == null ? " chip--on" : "") + "\" href=\"/admin/announcements\">All</a>" +
|
|
4027
|
+
"<a class=\"chip" + (af === "1" ? " chip--on" : "") + "\" href=\"/admin/announcements?active=1\">Active now</a>" +
|
|
4028
|
+
"</div>";
|
|
4029
|
+
|
|
4030
|
+
var bodyRows = rows.map(function (a) {
|
|
4031
|
+
var isArchived = a.archived_at != null;
|
|
4032
|
+
return "<tr>" +
|
|
4033
|
+
"<td><code class=\"order-id\">" + _htmlEscape(a.slug) + "</code></td>" +
|
|
4034
|
+
"<td>" + _htmlEscape(a.message) + "</td>" +
|
|
4035
|
+
"<td><span class=\"status-pill\">" + _htmlEscape(a.theme) + "</span></td>" +
|
|
4036
|
+
"<td>" + _htmlEscape(a.audience) + "</td>" +
|
|
4037
|
+
"<td><span class=\"status-pill " + (isArchived ? "cancelled" : "paid") + "\">" + (isArchived ? "archived" : "active") + "</span></td>" +
|
|
4038
|
+
"<td><div class=\"actions-row\">" +
|
|
4039
|
+
(isArchived ? "" :
|
|
4040
|
+
"<form method=\"post\" action=\"/admin/announcements/" + _htmlEscape(encodeURIComponent(a.slug)) + "/archive\" class=\"form-inline\">" +
|
|
4041
|
+
"<button class=\"btn btn--danger\" type=\"submit\">Archive</button></form>") +
|
|
4042
|
+
"</div></td>" +
|
|
4043
|
+
"</tr>";
|
|
4044
|
+
}).join("");
|
|
4045
|
+
|
|
4046
|
+
var table = rows.length
|
|
4047
|
+
? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Slug</th><th scope=\"col\">Message</th><th scope=\"col\">Theme</th><th scope=\"col\">Audience</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
|
|
4048
|
+
: "<p class=\"empty\">No announcements" + (af === "1" ? " active right now" : " yet") + ".</p>";
|
|
4049
|
+
|
|
4050
|
+
var themeOpts = ["urgency", "promo", "info", "success"].map(function (t) {
|
|
4051
|
+
return "<option value=\"" + t + "\"" + (t === "info" ? " selected" : "") + ">" + t + "</option>";
|
|
4052
|
+
}).join("");
|
|
4053
|
+
var audienceOpts = ["all", "guest", "logged_in"].map(function (au) {
|
|
4054
|
+
return "<option value=\"" + au + "\">" + au + "</option>";
|
|
4055
|
+
}).join("");
|
|
4056
|
+
|
|
4057
|
+
var createForm =
|
|
4058
|
+
"<div class=\"panel mt mw-40\">" +
|
|
4059
|
+
"<h3 class=\"subhead\">Create an announcement</h3>" +
|
|
4060
|
+
"<p class=\"meta\">The highest-priority active announcement shows at the top of every page (urgency > promo > info > success). Leave the schedule blank for an open-ended notice.</p>" +
|
|
4061
|
+
"<form method=\"post\" action=\"/admin/announcements\">" +
|
|
4062
|
+
_setupField("Slug", "slug", "", "text", "Lowercase, hyphenated — a stable id.", " maxlength=\"64\" required") +
|
|
4063
|
+
"<label class=\"form-field\"><span>Message</span><textarea name=\"message\" maxlength=\"500\" required></textarea></label>" +
|
|
4064
|
+
_setupField("Link URL (optional)", "link_url", "", "text", "https:// or a /-rooted path.", " maxlength=\"2048\"") +
|
|
4065
|
+
_setupField("Link label (optional)", "link_label", "", "text", "", " maxlength=\"120\"") +
|
|
4066
|
+
"<label class=\"form-field\"><span>Theme</span><select name=\"theme\">" + themeOpts + "</select></label>" +
|
|
4067
|
+
"<label class=\"form-field\"><span>Audience</span><select name=\"audience\">" + audienceOpts + "</select></label>" +
|
|
4068
|
+
"<label class=\"form-field\"><span>Starts at (optional)</span><input type=\"datetime-local\" name=\"starts_at\"></label>" +
|
|
4069
|
+
"<label class=\"form-field\"><span>Expires at (optional)</span><input type=\"datetime-local\" name=\"expires_at\"></label>" +
|
|
4070
|
+
"<label class=\"kv\"><input type=\"checkbox\" name=\"dismissible\" value=\"on\" checked> Visitors can dismiss this</label>" +
|
|
4071
|
+
"<div class=\"actions-row\"><button class=\"btn\" type=\"submit\">Create announcement</button></div>" +
|
|
4072
|
+
"</form>" +
|
|
4073
|
+
"</div>";
|
|
4074
|
+
|
|
4075
|
+
var bodyHtml = "<section><h2>Announcements</h2>" + created + archived + notice + chips + table + createForm + "</section>";
|
|
4076
|
+
return _renderAdminShell(opts.shop_name, "Announcements", bodyHtml, "announcements", opts.nav_available);
|
|
4077
|
+
}
|
|
4078
|
+
|
|
3913
4079
|
function renderAdminCollection(opts) {
|
|
3914
4080
|
opts = opts || {};
|
|
3915
4081
|
var col = opts.collection;
|
package/lib/asset-manifest.json
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.2.
|
|
2
|
+
"version": "0.2.4",
|
|
3
3
|
"assets": {
|
|
4
4
|
"css/admin.css": {
|
|
5
5
|
"integrity": "sha384-FgqvXcZygYkJjjOtXAeu9xi6AtYqkchPnJKcS0OeHm8lynhd9r4RfdpoT7P7j7/l",
|
|
6
6
|
"fingerprinted": "css/admin.afd7b964c1f6fe1b.css"
|
|
7
7
|
},
|
|
8
8
|
"css/main.css": {
|
|
9
|
-
"integrity": "sha384-
|
|
10
|
-
"fingerprinted": "css/main.
|
|
9
|
+
"integrity": "sha384-fMfWbfkIZ29Z4RjHyn3ol0SOf/VodnKqDjkrU7OCQLvzm6hFHRL3kcwMGNnXzdai",
|
|
10
|
+
"fingerprinted": "css/main.73ee71264ed8de75.css"
|
|
11
|
+
},
|
|
12
|
+
"js/announcement.js": {
|
|
13
|
+
"integrity": "sha384-z4zcEMn+tScoVnYRE4nEf8N/oyvpxdpaxTNrT4QO/jURChid4+qjAvWkzatCaAPq",
|
|
14
|
+
"fingerprinted": "js/announcement.59eab4872d2f3b4c.js"
|
|
11
15
|
},
|
|
12
16
|
"js/consent.js": {
|
|
13
17
|
"integrity": "sha384-KKMQ0og8HPOykRRPpUyxX7dMhTvKySfVtpGX/jGWzZwNaN/c4OykvRvXpqBHcQST",
|
package/lib/storefront.js
CHANGED
|
@@ -160,6 +160,7 @@ var LAYOUT =
|
|
|
160
160
|
"</head>\n" +
|
|
161
161
|
"<body>\n" +
|
|
162
162
|
" <a class=\"skip-link\" href=\"#main\">{{skip_to_content}}</a>\n" +
|
|
163
|
+
"RAW_ANNOUNCEMENT_BAR" +
|
|
163
164
|
"\n" +
|
|
164
165
|
" <div class=\"utility-bar\" role=\"complementary\">\n" +
|
|
165
166
|
" <div class=\"utility-bar__inner\">\n" +
|
|
@@ -262,6 +263,7 @@ var LAYOUT =
|
|
|
262
263
|
" </footer>\n" +
|
|
263
264
|
CONSENT_BANNER +
|
|
264
265
|
"RAW_CONSENT_SCRIPT" +
|
|
266
|
+
"RAW_ANNOUNCEMENT_SCRIPT" +
|
|
265
267
|
"</body>\n" +
|
|
266
268
|
"</html>\n";
|
|
267
269
|
|
|
@@ -331,6 +333,128 @@ function _islandScript(name, opts) {
|
|
|
331
333
|
(sri ? " integrity=\"" + sri + "\"" : "") + " defer" + policyAttr + "></script>";
|
|
332
334
|
}
|
|
333
335
|
|
|
336
|
+
// ---- announcement bar --------------------------------------------------
|
|
337
|
+
//
|
|
338
|
+
// Sitewide operator promo/notice strip rendered at the top of every page
|
|
339
|
+
// (this container render + the worker/render/*.js LAYOUT mirror it). The
|
|
340
|
+
// active row is resolved per request from a short-TTL in-memory cache of
|
|
341
|
+
// the active announcements — NOT a per-request DB read — because the bar
|
|
342
|
+
// is page-top chrome on every route and the active set changes rarely
|
|
343
|
+
// (operator-managed). The cache is refreshed out-of-band (fire-and-forget)
|
|
344
|
+
// so a request never blocks on it; resolution is synchronous, which is why
|
|
345
|
+
// it can run in the same sync middleware that seeds the locale context
|
|
346
|
+
// (an async middleware's `enterWith` would not reach the handler).
|
|
347
|
+
//
|
|
348
|
+
// Dismissal mirrors the consent banner: the bar is always server-rendered;
|
|
349
|
+
// the `announcement.js` island hides any bar whose slug is listed in the
|
|
350
|
+
// plain `shop_ann_dismissed` cookie and sets that cookie on dismiss. A
|
|
351
|
+
// no-JS POST /announcements/:slug/dismiss records the durable dismissal +
|
|
352
|
+
// sets the same cookie. The cookie-driven hide is what keeps a dismissed
|
|
353
|
+
// bar from reappearing on an edge-cached page (the slug is in the markup;
|
|
354
|
+
// the island removes it client-side per the visitor's cookie).
|
|
355
|
+
var ANNOUNCEMENT_DISMISS_COOKIE = "shop_ann_dismissed";
|
|
356
|
+
var _BAR_THEME_RANK = { urgency: 3, promo: 2, info: 1, success: 0 };
|
|
357
|
+
var _BAR_TTL_MS = 30000;
|
|
358
|
+
var _annCache = { rows: [], at: 0, inflight: false };
|
|
359
|
+
|
|
360
|
+
// Refresh the active-announcement cache if it's older than the TTL. Async
|
|
361
|
+
// + fire-and-forget: the caller never awaits it, so a slow D1 read can't
|
|
362
|
+
// stall a page render — the request resolves against whatever the cache
|
|
363
|
+
// last held (empty on a cold first request, then populated within the TTL).
|
|
364
|
+
function _refreshAnnouncementCache(announcementBar) {
|
|
365
|
+
if (!announcementBar) return;
|
|
366
|
+
var now = Date.now();
|
|
367
|
+
if (_annCache.inflight) return;
|
|
368
|
+
if (now - _annCache.at < _BAR_TTL_MS && _annCache.at !== 0) return;
|
|
369
|
+
_annCache.inflight = true;
|
|
370
|
+
Promise.resolve()
|
|
371
|
+
.then(function () { return announcementBar.listAnnouncements({ active_only: true }); })
|
|
372
|
+
.then(function (rows) { _annCache.rows = Array.isArray(rows) ? rows : []; _annCache.at = Date.now(); })
|
|
373
|
+
.catch(function () { /* drop-silent — keep serving the prior cache */ })
|
|
374
|
+
.then(function () { _annCache.inflight = false; });
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Synchronous pick over the cached active rows: drop announcements whose
|
|
378
|
+
// audience the viewer doesn't match (segment is never matched here — the
|
|
379
|
+
// console doesn't offer it) and return the highest theme-rank survivor
|
|
380
|
+
// (urgency > promo > info > success), ties broken by the cache's own order
|
|
381
|
+
// (updated_at DESC, slug ASC from listAnnouncements).
|
|
382
|
+
//
|
|
383
|
+
// Dismissal is NOT filtered here: the bar is rendered identically for every
|
|
384
|
+
// viewer of an audience (so an edge-cached response is correct for all) and
|
|
385
|
+
// the announcement island hides any bar whose slug is in the visitor's
|
|
386
|
+
// `shop_ann_dismissed` cookie — exactly the consent-banner pattern. This
|
|
387
|
+
// keeps the container + edge renders byte-identical and the shared edge
|
|
388
|
+
// cache key (URL) sound.
|
|
389
|
+
function _resolveActiveAnnouncement(viewerKind) {
|
|
390
|
+
var rows = _annCache.rows;
|
|
391
|
+
if (!rows || !rows.length) return null;
|
|
392
|
+
var best = null;
|
|
393
|
+
var bestRank = -1;
|
|
394
|
+
for (var i = 0; i < rows.length; i += 1) {
|
|
395
|
+
var row = rows[i];
|
|
396
|
+
if (row.audience === "logged_in" && viewerKind !== "logged_in") continue;
|
|
397
|
+
if (row.audience === "guest" && viewerKind !== "guest") continue;
|
|
398
|
+
if (row.audience === "segment") continue;
|
|
399
|
+
var rank = _BAR_THEME_RANK[row.theme];
|
|
400
|
+
if (rank == null) rank = -1;
|
|
401
|
+
if (rank > bestRank) { best = row; bestRank = rank; }
|
|
402
|
+
}
|
|
403
|
+
return best;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Parse the plain `shop_ann_dismissed` cookie into an array of slugs. The
|
|
407
|
+
// cookie is a comma-separated slug list (slugs are [a-z0-9-], so no
|
|
408
|
+
// escaping is needed); anything that isn't a well-shaped slug is ignored.
|
|
409
|
+
function _readDismissedSlugs(req) {
|
|
410
|
+
var raw = _readCookie(req, ANNOUNCEMENT_DISMISS_COOKIE);
|
|
411
|
+
if (!raw || typeof raw !== "string") return [];
|
|
412
|
+
var out = [];
|
|
413
|
+
var parts = raw.split(",");
|
|
414
|
+
for (var i = 0; i < parts.length; i += 1) {
|
|
415
|
+
var s = parts[i].trim();
|
|
416
|
+
if (s && /^[a-z0-9-]{1,64}$/.test(s) && out.indexOf(s) === -1) out.push(s);
|
|
417
|
+
}
|
|
418
|
+
return out;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Render the bar markup for a hydrated announcement row. Returns "" for a
|
|
422
|
+
// null row so a no-announcement page renders unchanged. `currentColor`-free
|
|
423
|
+
// — the theme color comes from the `--<theme>` modifier class in main.css.
|
|
424
|
+
function _buildAnnouncementBar(row) {
|
|
425
|
+
if (!row) return "";
|
|
426
|
+
var esc = function (s) { return b.template.escapeHtml(String(s == null ? "" : s)); };
|
|
427
|
+
var theme = _BAR_THEME_RANK[row.theme] != null ? row.theme : "info";
|
|
428
|
+
var slug = esc(row.slug);
|
|
429
|
+
var link = "";
|
|
430
|
+
if (row.link_url && row.link_label) {
|
|
431
|
+
// Defense-in-depth at the href sink: only emit the link for an https://
|
|
432
|
+
// URL or a /-rooted absolute path (not protocol-relative `//`). The
|
|
433
|
+
// lib's `_linkUrl` already enforces exactly this at write time, but
|
|
434
|
+
// re-checking here means a `javascript:` / `data:` scheme can never
|
|
435
|
+
// reach the rendered href even if a row ever arrived by another path —
|
|
436
|
+
// HTML-escaping alone does not neutralise those schemes.
|
|
437
|
+
var href = String(row.link_url);
|
|
438
|
+
if (/^https:\/\//i.test(href) || (href.charAt(0) === "/" && href.charAt(1) !== "/")) {
|
|
439
|
+
link = " <a class=\"announcement-bar__link\" href=\"" + esc(href) + "\">" + esc(row.link_label) + "</a>";
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
var dismiss = "";
|
|
443
|
+
if (row.dismissible) {
|
|
444
|
+
dismiss =
|
|
445
|
+
"<form class=\"announcement-bar__dismiss\" method=\"post\" action=\"/announcements/" + slug + "/dismiss\">" +
|
|
446
|
+
"<input type=\"hidden\" name=\"return_to\" value=\"/\" data-announcement-return>" +
|
|
447
|
+
"<button type=\"submit\" class=\"announcement-bar__x\" aria-label=\"Dismiss this announcement\">×</button>" +
|
|
448
|
+
"</form>";
|
|
449
|
+
}
|
|
450
|
+
return "<aside class=\"announcement-bar announcement-bar--" + theme + "\" role=\"region\" aria-label=\"Store announcement\" data-announcement-slug=\"" + slug + "\">" +
|
|
451
|
+
"<div class=\"announcement-bar__inner\">" +
|
|
452
|
+
"<p class=\"announcement-bar__msg\">" + esc(row.message) + link + "</p>" +
|
|
453
|
+
dismiss +
|
|
454
|
+
"</div>" +
|
|
455
|
+
"</aside>";
|
|
456
|
+
}
|
|
457
|
+
|
|
334
458
|
// Multi-currency display switcher — a GET form in the footer listing the
|
|
335
459
|
// operator's display currencies. Selecting one POSTs to /currency, which
|
|
336
460
|
// sets the sealed `shop_ccy` cookie and redirects back. The currently
|
|
@@ -441,6 +565,17 @@ function _wrap(opts) {
|
|
|
441
565
|
var localeCtx = (opts.locale_ctx && opts.locale_ctx.chrome)
|
|
442
566
|
? opts.locale_ctx
|
|
443
567
|
: ((storeCtx && storeCtx.chrome) ? storeCtx : _DEFAULT_LOCALE_CTX);
|
|
568
|
+
// Announcement bar — the active row is resolved by the sync request
|
|
569
|
+
// middleware and carried on the locale ALS store; an explicit
|
|
570
|
+
// `opts.announcement` (a renderer or unit test threading its own) wins.
|
|
571
|
+
// Absent both, no bar renders.
|
|
572
|
+
var announcementRow = (opts.announcement !== undefined)
|
|
573
|
+
? opts.announcement
|
|
574
|
+
: ((storeCtx && storeCtx.announcement) || null);
|
|
575
|
+
var announcementBarHtml = _buildAnnouncementBar(announcementRow);
|
|
576
|
+
var announcementScript = (announcementRow && announcementRow.dismissible)
|
|
577
|
+
? _islandScript("announcement.js", { id: "announcement-island" })
|
|
578
|
+
: "";
|
|
444
579
|
var chrome = localeCtx.chrome;
|
|
445
580
|
var cartCount = opts.cart_count == null ? 0 : opts.cart_count;
|
|
446
581
|
// The cart aria-label carries the count: when the resolved string is
|
|
@@ -481,7 +616,9 @@ function _wrap(opts) {
|
|
|
481
616
|
|
|
482
617
|
return _render(LAYOUT, vars)
|
|
483
618
|
.replace("RAW_CSS_INTEGRITY", themeCssIntegrity)
|
|
619
|
+
.replace("RAW_ANNOUNCEMENT_BAR", announcementBarHtml)
|
|
484
620
|
.replace("RAW_CONSENT_SCRIPT", _islandScript("consent.js", { id: "consent-island", policy: _activeConsentPolicy }))
|
|
621
|
+
.replace("RAW_ANNOUNCEMENT_SCRIPT", announcementScript)
|
|
485
622
|
.replace("RAW_CURRENCY_SWITCHER", switcherHtml)
|
|
486
623
|
.replace("RAW_LOCALE_SWITCHER", localeCtx.switcher_html || "")
|
|
487
624
|
.replace("RAW_BODY_PLACEHOLDER", opts.body);
|
|
@@ -4853,6 +4990,62 @@ function mount(router, deps) {
|
|
|
4853
4990
|
});
|
|
4854
4991
|
}
|
|
4855
4992
|
|
|
4993
|
+
// Announcement-bar resolution — synchronous, so its `enterWith` reaches
|
|
4994
|
+
// the page handler (an async middleware's would not). Runs AFTER the
|
|
4995
|
+
// locale middleware so it can extend that request's ALS context rather
|
|
4996
|
+
// than replace it. The active set comes from a short-TTL in-memory cache
|
|
4997
|
+
// refreshed out-of-band here (fire-and-forget — never blocks the render);
|
|
4998
|
+
// resolution itself reads the cache + the dismissed-slug cookie + a
|
|
4999
|
+
// coarse logged-in/guest signal (auth-cookie presence — exact identity
|
|
5000
|
+
// isn't needed to bucket audience). Best-effort: any failure drops the
|
|
5001
|
+
// bar, never the page.
|
|
5002
|
+
if (typeof router.use === "function" && deps.announcementBar) {
|
|
5003
|
+
router.use(function announcementMiddleware(req, _res, next) {
|
|
5004
|
+
try {
|
|
5005
|
+
_refreshAnnouncementCache(deps.announcementBar);
|
|
5006
|
+
var viewerKind = _cookieJar().read(req, AUTH_COOKIE_NAME) ? "logged_in" : "guest";
|
|
5007
|
+
var row = _resolveActiveAnnouncement(viewerKind);
|
|
5008
|
+
var cur = _localeAls.getStore() || {};
|
|
5009
|
+
_localeAls.enterWith(Object.assign({}, cur, { announcement: row }));
|
|
5010
|
+
} catch (_e) { /* drop-silent — no bar this request */ }
|
|
5011
|
+
next();
|
|
5012
|
+
});
|
|
5013
|
+
|
|
5014
|
+
// Dismiss an announcement (no-JS path; the island intercepts the click
|
|
5015
|
+
// and stays on-page). Records the durable dismissal keyed on the
|
|
5016
|
+
// session id when one is present, always appends the slug to the plain
|
|
5017
|
+
// `shop_ann_dismissed` cookie (the cookie is what hides the bar on a
|
|
5018
|
+
// cached edge response), and 303-redirects to a validated same-origin
|
|
5019
|
+
// return path. Unknown slug / no session still sets the cookie + redirects
|
|
5020
|
+
// — a hostile or stale slug can never 500 the route.
|
|
5021
|
+
router.post("/announcements/:slug/dismiss", async function (req, res) {
|
|
5022
|
+
var slug = (req.params && typeof req.params.slug === "string") ? req.params.slug : "";
|
|
5023
|
+
var body = req.body || {};
|
|
5024
|
+
var to = (typeof body.return_to === "string") ? body.return_to : "/";
|
|
5025
|
+
if (to.charAt(0) !== "/" || to.charAt(1) === "/" || to.indexOf("\\") !== -1 || /[\x00-\x1f\x7f]/.test(to)) {
|
|
5026
|
+
to = "/";
|
|
5027
|
+
}
|
|
5028
|
+
if (/^[a-z0-9-]{1,64}$/.test(slug)) {
|
|
5029
|
+
// Durable record (best-effort — keyed on the cart session id; absent
|
|
5030
|
+
// a session the cookie still carries the dismissal for this browser).
|
|
5031
|
+
try {
|
|
5032
|
+
var sid = _readSidCookie(req);
|
|
5033
|
+
if (sid) await deps.announcementBar.recordDismissal({ slug: slug, session_id: sid });
|
|
5034
|
+
} catch (_e) { /* slug not found / no session — cookie still set below */ }
|
|
5035
|
+
// Append the slug to the dismissed-cookie set (deduped, capped).
|
|
5036
|
+
var slugs = _readDismissedSlugs(req);
|
|
5037
|
+
if (slugs.indexOf(slug) === -1) slugs.push(slug);
|
|
5038
|
+
if (slugs.length > 50) slugs = slugs.slice(slugs.length - 50);
|
|
5039
|
+
_cookieJar().write(res, ANNOUNCEMENT_DISMISS_COOKIE, slugs.join(","), {
|
|
5040
|
+
expires: new Date(Date.now() + b.constants.TIME.days(180)), httpOnly: false,
|
|
5041
|
+
});
|
|
5042
|
+
}
|
|
5043
|
+
res.status(303);
|
|
5044
|
+
res.setHeader && res.setHeader("location", to);
|
|
5045
|
+
return res.end ? res.end() : res.send("");
|
|
5046
|
+
});
|
|
5047
|
+
}
|
|
5048
|
+
|
|
4856
5049
|
// Persist a locale choice. A GET form (works with JS off) from the
|
|
4857
5050
|
// footer switcher submits `lang` (the chosen tag) + `to` (the path to
|
|
4858
5051
|
// return to). We validate the tag against the active locale set, set
|
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.16",
|
|
7
|
+
"tag": "v0.13.16",
|
|
8
8
|
"license": "Apache-2.0",
|
|
9
9
|
"author": "blamejs contributors",
|
|
10
10
|
"source": "https://github.com/blamejs/blamejs",
|
|
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.13.x
|
|
10
10
|
|
|
11
|
+
- v0.13.16 (2026-05-27) — **`b.mail.agent` docs now describe the facade accurately, and not-yet-wired verbs point to the primitive to use.** b.mail.agent's module documentation claimed it was "the standardization contract for every mail protocol" that JMAP / IMAP / POP3 all route through — but no protocol server actually dispatches through the agent (the framework's own JMAP EmailSubmission handler composes b.mail.send.deliver directly), and the compose / send / reply / forward, sieve.list / sieve.activate, identity / vacation / mdn.* and export / job / import verbs throw mail-agent/not-implemented. The docs are corrected to describe what the agent is: a mailbox-access facade (RBAC + posture + audit + dispatch around a mail store) whose read surface plus the mailbox-mutation and Sieve-upload methods are wired, with the remaining verbs not yet routed through it. Those verbs' error message now names the underlying primitive to compose directly (b.mail.send.deliver, b.mail.sieve, b.mailMdn, …) instead of citing a version tag that had long passed. The public WIRED_AT export (a method→version map that no longer reflected reality) is replaced by COMPOSE_HINT (a method→primitive-to-compose map). No behaviour change: the same methods are wired or throw exactly as before. **Changed:** *`b.mail.agent` documentation corrected; not-implemented errors point to the primitive to compose* — The `@module` / `@card` no longer claim the agent is the universal protocol-dispatch contract — it's documented as a mailbox-access facade with a wired read + mutation + Sieve-upload surface, and the compose/send/identity/vacation/MDN/export verbs documented as not yet routed through it (compose the underlying primitive directly until a protocol server adopts the agent). The `mail-agent/not-implemented` error now names that primitive (e.g. `b.mail.send.deliver`) rather than a passed version tag. **Removed:** *`b.mail.agent.WIRED_AT` export replaced by `COMPOSE_HINT`* — The `WIRED_AT` export mapped each method to a framework version that was supposed to "light it up" — versions that have all shipped without the wiring, so the map was misleading. It is replaced by `COMPOSE_HINT`, mapping each not-yet-wired method to the primitive an operator composes directly. Operators reading `b.mail.agent.WIRED_AT` should read `b.mail.agent.COMPOSE_HINT` instead (pre-1.0: no compatibility shim).
|
|
12
|
+
|
|
13
|
+
- v0.13.15 (2026-05-27) — **Corrected more source citations and made deferred/reserved options honest in their docs.** A second accuracy pass over source threat-annotations and option docs. Three citation corrections: the base64url strict-decode guard cited CVE-2022-0235 (which is actually a node-fetch cookie-leak, unrelated) — it now names the weakness class it defends (CWE-347 / CWE-1286 signature canonicalization); the glob consecutive-wildcard ReDoS cap cited the wrong library (the CVE-2026-26996 ReDoS is minimatch, not picomatch — the adjacent picomatch one is CVE-2026-33671); and CVE-2026-32178 is reframed to the CWE-138 header-injection-spoofing class the public record actually documents (and dropped from the end-of-data SMTP-smuggling list, which is a different class). Several options/statuses are now honest about not-yet-implemented surface: b.archive.read.zip.fromTrustedStream is marked experimental (its methods throw and its options aren't honored yet — the example now shows the supported buffer-then-random-access path); b.acme revokeCert's useCertKey / certPrivateKey are marked reserved (the cert-key path throws; account-key signing is the supported default); and a stale message claiming passkey break-glass factors were a future feature is removed (passkeys are a live allowed factor). No runtime behaviour changes beyond message/doc text. **Changed:** *Deferred / reserved surface now documented honestly* — `b.archive.read.zip.fromTrustedStream` is marked `experimental` — its `inspect`/`entries`/`extract` throw and its `bombPolicy`/`audit` options aren't honored yet; the documented example now shows the supported path (buffer the stream, then use the random-access reader). `b.acme` `revokeCert`'s `useCertKey` / `certPrivateKey` options are marked reserved (the cert-key-signed-revocation path throws; account-key signing, the default, covers mainstream CAs). A `b.breakGlass` policy error and comment that called passkey factors a future feature are corrected — passkeys are a live allowed factor. **Fixed:** *Corrected misattributed CVE citations in source threat-annotations* — `b.crypto.fromBase64Url`'s strict-decode guard cited CVE-2022-0235 (a node-fetch header-leak, unrelated to base64/JWT decoding); it now cites the weakness class it actually defends — CWE-347 / CWE-1286 signature canonicalization. `b.guardRegex`'s consecutive-`*` cap attributed CVE-2026-26996 to picomatch; that ReDoS is in minimatch (the picomatch ReDoS it also defends is CVE-2026-33671) — the library name is corrected. CVE-2026-32178 is reframed to the CWE-138 header-injection spoofing class the public advisory documents, and removed from the end-of-data SMTP-smuggling trio (a distinct class). No behaviour change — the defenses are unchanged.
|
|
14
|
+
|
|
11
15
|
- v0.13.14 (2026-05-27) — **DNSSEC chain validation now bounds KeyTrap (CVE-2023-50387) amplification with hard caps.** b.network.dns.dnssec.verifyChain tried every DNSKEY whose 16-bit key tag matched an RRSIG, with no cap on how many candidates or total signature verifications a single response could drive. A hostile zone publishing many DNSKEYs sharing one key tag (plus matching RRSIGs) could force O(keys x signatures) full public-key verifications from one query — the KeyTrap denial-of-service (CVE-2023-50387). Validation is now bounded by non-configurable caps that match the BIND / Unbound mitigations: at most 4 same-tag candidate keys are tried per RRSIG, at most 64 DNSKEYs per zone link and 16 DS records per delegation are accepted, the chain is at most 128 links deep, and the whole response is held to a signature-validation budget that scales with chain depth (so a legitimate deep delegation is never false-rejected while bounded collisions stay bounded); exceeding any of these refuses the response rather than performing the work. Separately, a domain name that encodes to more than 255 octets is now refused at canonicalization (RFC 1035 §2.3.4), which also bounds the NSEC3 closest-encloser label enumeration, and the NSEC3 iteration ceiling is lowered from 500 to 150 to match the BIND 9.16.33+ / Unbound 1.17.1 fix for the sibling CVE-2023-50868. **Security:** *`verifyChain` caps colliding-key fan-out and total signature validations (KeyTrap / CVE-2023-50387)* — A zone advertising many same-key-tag DNSKEYs and RRSIGs can no longer drive unbounded public-key verifications. New refusals: `dnssec/too-many-colliding-keys` (>4 same-tag candidates per RRSIG), `dnssec/too-many-dnskeys` (>64 DNSKEYs per zone link), `dnssec/too-many-ds` (>16 DS records per delegation), `dnssec/too-many-links` (chain deeper than 128), and `dnssec/validation-budget-exceeded` (signature validations beyond the depth-scaled budget). The caps are intentionally non-configurable — they sit well above any legitimate zone, and the budget scales with chain depth so deep delegations validate normally. · *Domain-name octet cap + lower NSEC3 iteration ceiling* — A name that canonicalizes to more than 255 octets is refused (`dnssec/bad-name`, RFC 1035 §2.3.4), which bounds the per-label NSEC3 closest-encloser enumeration (CVE-2023-50868 class). The default NSEC3 iteration ceiling drops from 500 to 150, matching the BIND 9.16.33+ / Unbound 1.17.1 post-CVE defaults (RFC 9276 recommends 0).
|
|
12
16
|
|
|
13
17
|
- 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).
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 1,
|
|
3
|
-
"frameworkVersion": "0.13.
|
|
4
|
-
"createdAt": "2026-05-
|
|
3
|
+
"frameworkVersion": "0.13.16",
|
|
4
|
+
"createdAt": "2026-05-27T16:55:42.429Z",
|
|
5
5
|
"exports": {
|
|
6
6
|
"a2a": {
|
|
7
7
|
"type": "object",
|
|
@@ -37552,54 +37552,17 @@
|
|
|
37552
37552
|
"agent": {
|
|
37553
37553
|
"type": "object",
|
|
37554
37554
|
"members": {
|
|
37555
|
-
"
|
|
37556
|
-
"type": "object",
|
|
37557
|
-
"members": {
|
|
37558
|
-
"export": {
|
|
37559
|
-
"type": "primitive",
|
|
37560
|
-
"valueType": "boolean"
|
|
37561
|
-
},
|
|
37562
|
-
"search": {
|
|
37563
|
-
"type": "primitive",
|
|
37564
|
-
"valueType": "boolean"
|
|
37565
|
-
}
|
|
37566
|
-
}
|
|
37567
|
-
},
|
|
37568
|
-
"MailAgentError": {
|
|
37569
|
-
"type": "function",
|
|
37570
|
-
"arity": 4
|
|
37571
|
-
},
|
|
37572
|
-
"SCOPE_FOR_METHOD": {
|
|
37555
|
+
"COMPOSE_HINT": {
|
|
37573
37556
|
"type": "object",
|
|
37574
37557
|
"members": {
|
|
37575
37558
|
"compose": {
|
|
37576
37559
|
"type": "primitive",
|
|
37577
37560
|
"valueType": "string"
|
|
37578
37561
|
},
|
|
37579
|
-
"delete": {
|
|
37580
|
-
"type": "primitive",
|
|
37581
|
-
"valueType": "string"
|
|
37582
|
-
},
|
|
37583
37562
|
"export": {
|
|
37584
37563
|
"type": "primitive",
|
|
37585
37564
|
"valueType": "string"
|
|
37586
37565
|
},
|
|
37587
|
-
"expunge": {
|
|
37588
|
-
"type": "primitive",
|
|
37589
|
-
"valueType": "string"
|
|
37590
|
-
},
|
|
37591
|
-
"fetch": {
|
|
37592
|
-
"type": "primitive",
|
|
37593
|
-
"valueType": "string"
|
|
37594
|
-
},
|
|
37595
|
-
"flag": {
|
|
37596
|
-
"type": "primitive",
|
|
37597
|
-
"valueType": "string"
|
|
37598
|
-
},
|
|
37599
|
-
"folders": {
|
|
37600
|
-
"type": "primitive",
|
|
37601
|
-
"valueType": "string"
|
|
37602
|
-
},
|
|
37603
37566
|
"forward": {
|
|
37604
37567
|
"type": "primitive",
|
|
37605
37568
|
"valueType": "string"
|
|
@@ -37628,56 +37591,73 @@
|
|
|
37628
37591
|
"type": "primitive",
|
|
37629
37592
|
"valueType": "string"
|
|
37630
37593
|
},
|
|
37631
|
-
"
|
|
37594
|
+
"reply": {
|
|
37632
37595
|
"type": "primitive",
|
|
37633
37596
|
"valueType": "string"
|
|
37634
37597
|
},
|
|
37635
|
-
"
|
|
37598
|
+
"send": {
|
|
37636
37599
|
"type": "primitive",
|
|
37637
37600
|
"valueType": "string"
|
|
37638
37601
|
},
|
|
37639
|
-
"
|
|
37602
|
+
"sieve.activate": {
|
|
37640
37603
|
"type": "primitive",
|
|
37641
37604
|
"valueType": "string"
|
|
37642
37605
|
},
|
|
37643
|
-
"
|
|
37606
|
+
"sieve.list": {
|
|
37644
37607
|
"type": "primitive",
|
|
37645
37608
|
"valueType": "string"
|
|
37646
37609
|
},
|
|
37647
|
-
"
|
|
37610
|
+
"vacation.set": {
|
|
37648
37611
|
"type": "primitive",
|
|
37649
37612
|
"valueType": "string"
|
|
37613
|
+
}
|
|
37614
|
+
}
|
|
37615
|
+
},
|
|
37616
|
+
"HEAVY_METHODS": {
|
|
37617
|
+
"type": "object",
|
|
37618
|
+
"members": {
|
|
37619
|
+
"export": {
|
|
37620
|
+
"type": "primitive",
|
|
37621
|
+
"valueType": "boolean"
|
|
37650
37622
|
},
|
|
37651
|
-
"
|
|
37623
|
+
"search": {
|
|
37624
|
+
"type": "primitive",
|
|
37625
|
+
"valueType": "boolean"
|
|
37626
|
+
}
|
|
37627
|
+
}
|
|
37628
|
+
},
|
|
37629
|
+
"MailAgentError": {
|
|
37630
|
+
"type": "function",
|
|
37631
|
+
"arity": 4
|
|
37632
|
+
},
|
|
37633
|
+
"SCOPE_FOR_METHOD": {
|
|
37634
|
+
"type": "object",
|
|
37635
|
+
"members": {
|
|
37636
|
+
"compose": {
|
|
37652
37637
|
"type": "primitive",
|
|
37653
37638
|
"valueType": "string"
|
|
37654
37639
|
},
|
|
37655
|
-
"
|
|
37640
|
+
"delete": {
|
|
37656
37641
|
"type": "primitive",
|
|
37657
37642
|
"valueType": "string"
|
|
37658
37643
|
},
|
|
37659
|
-
"
|
|
37644
|
+
"export": {
|
|
37660
37645
|
"type": "primitive",
|
|
37661
37646
|
"valueType": "string"
|
|
37662
37647
|
},
|
|
37663
|
-
"
|
|
37648
|
+
"expunge": {
|
|
37664
37649
|
"type": "primitive",
|
|
37665
37650
|
"valueType": "string"
|
|
37666
37651
|
},
|
|
37667
|
-
"
|
|
37652
|
+
"fetch": {
|
|
37668
37653
|
"type": "primitive",
|
|
37669
37654
|
"valueType": "string"
|
|
37670
|
-
}
|
|
37671
|
-
|
|
37672
|
-
},
|
|
37673
|
-
"WIRED_AT": {
|
|
37674
|
-
"type": "object",
|
|
37675
|
-
"members": {
|
|
37676
|
-
"compose": {
|
|
37655
|
+
},
|
|
37656
|
+
"flag": {
|
|
37677
37657
|
"type": "primitive",
|
|
37678
37658
|
"valueType": "string"
|
|
37679
37659
|
},
|
|
37680
|
-
"
|
|
37660
|
+
"folders": {
|
|
37681
37661
|
"type": "primitive",
|
|
37682
37662
|
"valueType": "string"
|
|
37683
37663
|
},
|
|
@@ -37709,10 +37689,22 @@
|
|
|
37709
37689
|
"type": "primitive",
|
|
37710
37690
|
"valueType": "string"
|
|
37711
37691
|
},
|
|
37692
|
+
"move": {
|
|
37693
|
+
"type": "primitive",
|
|
37694
|
+
"valueType": "string"
|
|
37695
|
+
},
|
|
37696
|
+
"quota": {
|
|
37697
|
+
"type": "primitive",
|
|
37698
|
+
"valueType": "string"
|
|
37699
|
+
},
|
|
37712
37700
|
"reply": {
|
|
37713
37701
|
"type": "primitive",
|
|
37714
37702
|
"valueType": "string"
|
|
37715
37703
|
},
|
|
37704
|
+
"search": {
|
|
37705
|
+
"type": "primitive",
|
|
37706
|
+
"valueType": "string"
|
|
37707
|
+
},
|
|
37716
37708
|
"send": {
|
|
37717
37709
|
"type": "primitive",
|
|
37718
37710
|
"valueType": "string"
|
|
@@ -37729,6 +37721,10 @@
|
|
|
37729
37721
|
"type": "primitive",
|
|
37730
37722
|
"valueType": "string"
|
|
37731
37723
|
},
|
|
37724
|
+
"thread": {
|
|
37725
|
+
"type": "primitive",
|
|
37726
|
+
"valueType": "string"
|
|
37727
|
+
},
|
|
37732
37728
|
"vacation.set": {
|
|
37733
37729
|
"type": "primitive",
|
|
37734
37730
|
"valueType": "string"
|
|
@@ -1079,14 +1079,17 @@ function create(opts) {
|
|
|
1079
1079
|
* the DER-encoded cert (base64url-encoded automatically) plus an
|
|
1080
1080
|
* optional `reason` code per RFC 5280 §5.3.1 (0=unspecified,
|
|
1081
1081
|
* 1=keyCompromise, 3=affiliationChanged, 4=superseded, 5=cessationOfOperation).
|
|
1082
|
-
* Signs with the account key
|
|
1083
|
-
*
|
|
1084
|
-
*
|
|
1082
|
+
* Signs with the account key — the only supported path today, and
|
|
1083
|
+
* sufficient for mainstream CAs. (The cert-key-signed variant —
|
|
1084
|
+
* `useCertKey` / `certPrivateKey` — is reserved and not yet
|
|
1085
|
+
* implemented; passing `useCertKey:true` throws.)
|
|
1085
1086
|
*
|
|
1086
1087
|
* @opts
|
|
1087
1088
|
* reason: number, // RFC 5280 §5.3.1 reason code; default 0 (unspecified)
|
|
1088
|
-
* useCertKey: boolean, //
|
|
1089
|
-
*
|
|
1089
|
+
* useCertKey: boolean, // RESERVED — cert-key-signed revocation is not yet
|
|
1090
|
+
* // implemented; account-key signing (the default)
|
|
1091
|
+
* // covers mainstream CAs. Passing true throws.
|
|
1092
|
+
* certPrivateKey: KeyObject, // RESERVED — consumed only by the cert-key path above
|
|
1090
1093
|
*
|
|
1091
1094
|
* @example
|
|
1092
1095
|
* await acme.revokeCert(certDerBuffer, { reason: 4 }); // 4 = superseded
|
|
@@ -772,7 +772,7 @@ function zip(adapter, opts) {
|
|
|
772
772
|
* @primitive b.archive.read.zip.fromTrustedStream
|
|
773
773
|
* @signature b.archive.read.zip.fromTrustedStream(adapter, opts?)
|
|
774
774
|
* @since 0.12.7
|
|
775
|
-
* @status
|
|
775
|
+
* @status experimental
|
|
776
776
|
* @related b.archive.read.zip, b.archive.adapters.trustedStream
|
|
777
777
|
*
|
|
778
778
|
* Forward-scan-only ZIP reader for trusted Readable sources. No
|
|
@@ -781,20 +781,23 @@ function zip(adapter, opts) {
|
|
|
781
781
|
* `b.archive.zip().toStream()` output back into a reader for round-trip
|
|
782
782
|
* verification).
|
|
783
783
|
*
|
|
784
|
-
*
|
|
785
|
-
* `
|
|
784
|
+
* NOT YET IMPLEMENTED: the streaming LFH walker is not built —
|
|
785
|
+
* `inspect()` / `entries()` / `extract()` throw
|
|
786
|
+
* `archive-read/trusted-stream-*-deferred`, and `bombPolicy` / `audit`
|
|
787
|
+
* are accepted but not yet honored. Re-opens when a streaming
|
|
788
|
+
* consumer needs it. Until then, collect the stream into a buffer and
|
|
789
|
+
* use the random-access reader, which is the supported path for both
|
|
790
|
+
* trusted round-trip verification and adversarial input.
|
|
786
791
|
*
|
|
787
792
|
* @opts
|
|
788
|
-
* bombPolicy: {
|
|
789
|
-
*
|
|
790
|
-
* audit: b.audit,
|
|
793
|
+
* bombPolicy: { ... }, // reserved — not yet honored
|
|
794
|
+
* audit: b.audit, // reserved — not yet honored
|
|
791
795
|
*
|
|
792
796
|
* @example
|
|
793
|
-
*
|
|
794
|
-
* var
|
|
795
|
-
*
|
|
796
|
-
* );
|
|
797
|
-
* for await (var e of reader.entries()) console.log(e.name, e.size);
|
|
797
|
+
* // Supported path: buffer the stream, then read random-access.
|
|
798
|
+
* var bytes = await someStreamToBuffer(producedZipStream);
|
|
799
|
+
* var reader = b.archive.read.zip(b.archive.adapters.buffer(bytes));
|
|
800
|
+
* var entries = await reader.inspect();
|
|
798
801
|
*/
|
|
799
802
|
function fromTrustedStream(adapter, opts) {
|
|
800
803
|
if (!adapter || adapter.kind !== "trusted-sequential") {
|
|
@@ -805,25 +808,26 @@ function fromTrustedStream(adapter, opts) {
|
|
|
805
808
|
var bombPolicy = _normalizeBombPolicy(opts.bombPolicy);
|
|
806
809
|
void bombPolicy;
|
|
807
810
|
|
|
808
|
-
//
|
|
809
|
-
//
|
|
810
|
-
//
|
|
811
|
-
//
|
|
812
|
-
//
|
|
811
|
+
// The streaming LFH walker is not built — only the API surface +
|
|
812
|
+
// adapter validation exist. Extraction via streaming inflate +
|
|
813
|
+
// data-descriptor scanning re-opens when a streaming consumer needs
|
|
814
|
+
// it; until then the supported path is to buffer the stream and use
|
|
815
|
+
// the random-access reader (which handles both trusted round-trip
|
|
816
|
+
// verification and adversarial input).
|
|
813
817
|
async function inspect() {
|
|
814
818
|
throw new ArchiveReadError("archive-read/trusted-stream-inspect-deferred",
|
|
815
|
-
"fromTrustedStream.inspect() is
|
|
816
|
-
"
|
|
819
|
+
"fromTrustedStream.inspect() is not implemented — collect the stream into a buffer and " +
|
|
820
|
+
"use b.archive.read.zip(b.archive.adapters.buffer(bytes))");
|
|
817
821
|
}
|
|
818
822
|
|
|
819
823
|
async function* entries() {
|
|
820
824
|
throw new ArchiveReadError("archive-read/trusted-stream-entries-deferred",
|
|
821
|
-
"fromTrustedStream.entries() is
|
|
825
|
+
"fromTrustedStream.entries() is not implemented — collect into a buffer and use the random-access reader");
|
|
822
826
|
}
|
|
823
827
|
|
|
824
828
|
async function extract() {
|
|
825
829
|
throw new ArchiveReadError("archive-read/trusted-stream-extract-deferred",
|
|
826
|
-
"fromTrustedStream.extract() is
|
|
830
|
+
"fromTrustedStream.extract() is not implemented — collect into a buffer and use the random-access reader");
|
|
827
831
|
}
|
|
828
832
|
|
|
829
833
|
return {
|
|
@@ -130,9 +130,9 @@ function _ensureFactorLockout() {
|
|
|
130
130
|
// keys + encryption-context binding (cross-cell tampering / accidental
|
|
131
131
|
// row-swap fails closed). It does NOT defend against vault-key
|
|
132
132
|
// compromise alone — the DEK is still vault-recoverable. True
|
|
133
|
-
// second-factor cryptographic gating
|
|
134
|
-
//
|
|
135
|
-
//
|
|
133
|
+
// second-factor cryptographic gating uses passkey integration (the
|
|
134
|
+
// passkey private key lives on the YubiKey, not in the framework, so a
|
|
135
|
+
// vault leak alone can't unwrap).
|
|
136
136
|
|
|
137
137
|
// In-memory DEK cache. Keyed by table name. Cleared on _resetForTest.
|
|
138
138
|
var dekCache = new Map();
|
|
@@ -520,8 +520,7 @@ function _validatePolicySet(table, opts) {
|
|
|
520
520
|
if (ALLOWED_FACTORS.indexOf(opts.factors[j]) === -1) {
|
|
521
521
|
throw new BreakGlassError("breakglass/bad-policy",
|
|
522
522
|
"policy.set: factors[" + j + "] '" + opts.factors[j] +
|
|
523
|
-
"' not in
|
|
524
|
-
" (passkey lands in v0.5.2)");
|
|
523
|
+
"' not in allowed factors [" + ALLOWED_FACTORS.join(",") + "]");
|
|
525
524
|
}
|
|
526
525
|
}
|
|
527
526
|
// Model B (cryptographic mode) ships in v0.5.1. When enabled,
|
|
@@ -789,10 +789,12 @@ function toBase64Url(buf) {
|
|
|
789
789
|
*
|
|
790
790
|
* Strict mode (default) refuses non-canonical input — chars outside
|
|
791
791
|
* the RFC 4648 §5 alphabet, length-mod-4-of-1, mixed `+/` from
|
|
792
|
-
* standard base64, trailing garbage. Defends
|
|
793
|
-
* footgun where
|
|
794
|
-
*
|
|
795
|
-
*
|
|
792
|
+
* standard base64, trailing garbage. Defends the CWE-347 /
|
|
793
|
+
* CWE-1286 signature-canonicalization footgun where a permissive
|
|
794
|
+
* base64url decoder silently tolerates a tampered JWS / JWT signature
|
|
795
|
+
* (non-canonical bytes decoding to the same buffer). Operators with a
|
|
796
|
+
* documented lossy legacy payload opt out per call via
|
|
797
|
+
* `{ strict: false }`.
|
|
796
798
|
*
|
|
797
799
|
* @opts
|
|
798
800
|
* strict: boolean // default: true — refuse non-canonical input
|
|
@@ -817,7 +819,8 @@ function fromBase64Url(s, opts) {
|
|
|
817
819
|
// OAuth `state` round-tripping) MUST reject non-canonical / malformed
|
|
818
820
|
// input. The Node base64url decoder silently tolerates trailing
|
|
819
821
|
// garbage, mixed `+/` from standard base64, missing padding errors,
|
|
820
|
-
// and length-mod-4 shapes —
|
|
822
|
+
// and length-mod-4 shapes — the CWE-347 / CWE-1286 signature-
|
|
823
|
+
// canonicalization footgun. Strict mode
|
|
821
824
|
// (the default) refuses anything outside the RFC 4648 §5 alphabet +
|
|
822
825
|
// length rules. Operators with a known-lossy legacy payload pass
|
|
823
826
|
// `{ strict: false }` to opt out per call.
|
|
@@ -84,8 +84,9 @@
|
|
|
84
84
|
* generating a DSN (the existing `b.mail.bounce` primitive does
|
|
85
85
|
* this); this guard parses INBOUND DSNs and gates the parse
|
|
86
86
|
* surface bounds, not the bounce-generation policy.
|
|
87
|
-
* - **DSN header-injection class** (CVE-2026-32178 .NET
|
|
88
|
-
* System.Net.Mail
|
|
87
|
+
* - **DSN header-injection class** (CVE-2026-32178 — .NET CWE-138
|
|
88
|
+
* special-element / header-injection spoofing, the System.Net.Mail
|
|
89
|
+
* vector per MSRC, at outbound; the inbound parse path here)
|
|
89
90
|
* — refuses CR/LF/NUL/C0 in header lines.
|
|
90
91
|
* - **CSAF / iSchedule prose tampering** — operator inspecting
|
|
91
92
|
* the prose part for the original recipient runs into the
|
|
@@ -37,8 +37,9 @@
|
|
|
37
37
|
* octets. Total header value capped at 998 bytes per RFC 5322
|
|
38
38
|
* §2.1.1 line cap.
|
|
39
39
|
* - **CRLF + control-char refusal** — header-injection defense
|
|
40
|
-
* (CVE-2026-32178 .NET
|
|
41
|
-
*
|
|
40
|
+
* (CVE-2026-32178 — .NET CWE-138 header-injection spoofing, the
|
|
41
|
+
* System.Net.Mail vector per MSRC, on the wire-protocol surface;
|
|
42
|
+
* this primitive's job is the SEMANTIC shape).
|
|
42
43
|
* - **Phrase-injection refusal** — Operator-supplied display
|
|
43
44
|
* phrase mustn't carry CRLF / `<` / `>` outside the angle
|
|
44
45
|
* brackets (a separate Bcc/Cc header smuggled into the phrase
|
|
@@ -248,14 +248,14 @@ function _detectIssues(input, opts) {
|
|
|
248
248
|
}
|
|
249
249
|
|
|
250
250
|
// Consecutive-star wildcard cap (CVE-2026-26996). Operator-supplied
|
|
251
|
-
// glob fragments compile to picomatch / RegExp; a long run
|
|
252
|
-
// against a non-matching literal walks O(4^N). Three-or-more
|
|
251
|
+
// glob fragments compile to minimatch / picomatch / RegExp; a long run
|
|
252
|
+
// of `*` against a non-matching literal walks O(4^N). Three-or-more
|
|
253
253
|
// consecutive `*` is the canonical bad shape; `**` (recursive glob)
|
|
254
254
|
// stays permitted, gated by the profile's `maxConsecutiveStars`.
|
|
255
255
|
function _detectConsecutiveStar(input, opts, issues) {
|
|
256
256
|
if (opts.consecutiveStarPolicy === "allow") return;
|
|
257
|
-
// CVE-2026-26996 is a
|
|
258
|
-
// `***+literal` walks O(4^N) when
|
|
257
|
+
// CVE-2026-26996 is a minimatch glob-shape backtracking class —
|
|
258
|
+
// `***+literal` walks O(4^N) when minimatch translates the run to a
|
|
259
259
|
// backtracking-heavy regex. Native ECMAScript regex syntax cannot
|
|
260
260
|
// produce three consecutive `*` quantifiers (it's a SyntaxError),
|
|
261
261
|
// so applying this detector to `inputKind: "regex"` strings only
|
|
@@ -15,8 +15,7 @@
|
|
|
15
15
|
* ## Smuggling defense — bare-CR / bare-LF refusal
|
|
16
16
|
*
|
|
17
17
|
* The SMTP smuggling class (`CVE-2023-51764` Postfix, `CVE-2023-51765`
|
|
18
|
-
* Sendmail, `CVE-2023-51766` Exim
|
|
19
|
-
* `System.Net.Mail`) exploits implementations that accept the
|
|
18
|
+
* Sendmail, `CVE-2023-51766` Exim) exploits implementations that accept the
|
|
20
19
|
* non-standard end-of-data sequence `<LF>.<LF>` or `<LF>.<CR><LF>`
|
|
21
20
|
* instead of the standard `<CR><LF>.<CR><LF>`. The introduced break-
|
|
22
21
|
* out lets a malicious peer inject a second message past SPF / DMARC
|
|
@@ -7,19 +7,26 @@
|
|
|
7
7
|
* @featured true
|
|
8
8
|
*
|
|
9
9
|
* @intro
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
10
|
+
* A mailbox-access facade that owns RBAC, posture enforcement, audit
|
|
11
|
+
* emission, dispatch (local / worker-pool / queue), and worker
|
|
12
|
+
* isolation around a mail store, so a protocol server built on top
|
|
13
|
+
* can stay a thin shell. It is designed to be the shared dispatch
|
|
14
|
+
* layer mail-protocol servers route through; today the read surface
|
|
15
|
+
* and the mailbox-mutation + Sieve-upload methods are wired, while the
|
|
16
|
+
* compose/send and identity/vacation/MDN/export verbs are not yet
|
|
17
|
+
* wired into the facade (see below).
|
|
17
18
|
*
|
|
18
|
-
* `agent.create()` returns the facade. Methods backed by
|
|
19
|
-
* `b.mailStore`
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
19
|
+
* `agent.create()` returns the facade. Methods backed by
|
|
20
|
+
* `b.mailStore` (folders / fetch / search / move / flag / delete /
|
|
21
|
+
* expunge, plus `sieve.put`) run immediately. The remaining verbs —
|
|
22
|
+
* compose / send / reply / forward, sieve.list / sieve.activate,
|
|
23
|
+
* identity / vacation / mdn.*, export / job / import — throw
|
|
24
|
+
* `mail-agent/not-implemented`: they are not yet routed through the
|
|
25
|
+
* agent. Until they are, compose the underlying primitive directly
|
|
26
|
+
* (`b.mail.send.deliver` for outbound, `b.mail.sieve` for Sieve,
|
|
27
|
+
* `b.mailMdn` for MDN, etc.) — which is what the framework's own JMAP
|
|
28
|
+
* `emailSubmissionSet` handler does. They wire into the facade when a
|
|
29
|
+
* protocol server adopts the agent as its dispatch layer.
|
|
23
30
|
*
|
|
24
31
|
* ```js
|
|
25
32
|
* var agent = b.mail.agent.create({
|
|
@@ -58,9 +65,11 @@
|
|
|
58
65
|
* on every entrypoint.
|
|
59
66
|
*
|
|
60
67
|
* @card
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
68
|
+
* Mailbox-access facade — RBAC + posture + audit + dispatch around a
|
|
69
|
+
* mail store, so a protocol server on top stays a thin shell. Read +
|
|
70
|
+
* mailbox-mutation + Sieve-upload methods are wired; compose/send and
|
|
71
|
+
* identity/vacation/MDN/export verbs compose the underlying primitive
|
|
72
|
+
* directly until a protocol server routes them through the agent.
|
|
64
73
|
*/
|
|
65
74
|
|
|
66
75
|
var lazyRequire = require("./lazy-require");
|
|
@@ -118,25 +127,25 @@ var SCOPE_FOR_METHOD = Object.freeze({
|
|
|
118
127
|
import: "mail:import",
|
|
119
128
|
});
|
|
120
129
|
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
"sieve.
|
|
131
|
-
"sieve.activate": "
|
|
132
|
-
"identity.set": "
|
|
133
|
-
"vacation.set": "
|
|
134
|
-
"mdn.send": "
|
|
135
|
-
"mdn.parse": "
|
|
136
|
-
"mdn.allowList": "
|
|
137
|
-
export: "
|
|
138
|
-
job: "
|
|
139
|
-
import: "
|
|
130
|
+
// Verbs not yet routed through the agent facade. The error points the
|
|
131
|
+
// operator at the underlying primitive to compose directly (the
|
|
132
|
+
// escape hatch) — defer-with-condition: these wire into the agent when
|
|
133
|
+
// a protocol server adopts it as its dispatch layer.
|
|
134
|
+
var COMPOSE_HINT = Object.freeze({
|
|
135
|
+
compose: "b.mail.send.deliver",
|
|
136
|
+
send: "b.mail.send.deliver",
|
|
137
|
+
reply: "b.mail.send.deliver",
|
|
138
|
+
forward: "b.mail.send.deliver",
|
|
139
|
+
"sieve.list": "b.mail.sieve",
|
|
140
|
+
"sieve.activate": "b.mail.sieve",
|
|
141
|
+
"identity.set": "your identity store + b.mail.sieve",
|
|
142
|
+
"vacation.set": "b.mail.sieve (vacation extension)",
|
|
143
|
+
"mdn.send": "b.mailMdn",
|
|
144
|
+
"mdn.parse": "b.mailMdn",
|
|
145
|
+
"mdn.allowList": "b.mailMdn",
|
|
146
|
+
export: "b.mailStore / b.auditTools",
|
|
147
|
+
job: "the dispatch queue directly",
|
|
148
|
+
import: "b.mailStore",
|
|
140
149
|
});
|
|
141
150
|
|
|
142
151
|
/**
|
|
@@ -653,9 +662,10 @@ function _notImplemented(ctx, method, args) {
|
|
|
653
662
|
// the slice lights up.
|
|
654
663
|
if (ctx.posture) guardMailQuery.validateActor(args && args.actor, ctx.posture);
|
|
655
664
|
_checkPermission(ctx, method, args);
|
|
656
|
-
ctx.auditEmit("mail.agent.not_implemented", args && args.actor, { method: method,
|
|
665
|
+
ctx.auditEmit("mail.agent.not_implemented", args && args.actor, { method: method, composeDirectly: COMPOSE_HINT[method] });
|
|
657
666
|
return Promise.reject(new MailAgentError("mail-agent/not-implemented",
|
|
658
|
-
"agent." + method + "
|
|
667
|
+
"agent." + method + " is not yet routed through the agent facade — compose " +
|
|
668
|
+
COMPOSE_HINT[method] + " directly"));
|
|
659
669
|
}
|
|
660
670
|
|
|
661
671
|
// ---- Internals ------------------------------------------------------------
|
|
@@ -771,7 +781,7 @@ module.exports = {
|
|
|
771
781
|
consumer: consumer,
|
|
772
782
|
MailAgentError: MailAgentError,
|
|
773
783
|
SCOPE_FOR_METHOD: SCOPE_FOR_METHOD,
|
|
774
|
-
|
|
784
|
+
COMPOSE_HINT: COMPOSE_HINT,
|
|
775
785
|
HEAVY_METHODS: HEAVY_METHODS,
|
|
776
786
|
// Re-export the guard family so callers can introspect without
|
|
777
787
|
// separate requires.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.13.15",
|
|
4
|
+
"date": "2026-05-27",
|
|
5
|
+
"headline": "Corrected more source citations and made deferred/reserved options honest in their docs",
|
|
6
|
+
"summary": "A second accuracy pass over source threat-annotations and option docs. Three citation corrections: the base64url strict-decode guard cited CVE-2022-0235 (which is actually a node-fetch cookie-leak, unrelated) — it now names the weakness class it defends (CWE-347 / CWE-1286 signature canonicalization); the glob consecutive-wildcard ReDoS cap cited the wrong library (the CVE-2026-26996 ReDoS is minimatch, not picomatch — the adjacent picomatch one is CVE-2026-33671); and CVE-2026-32178 is reframed to the CWE-138 header-injection-spoofing class the public record actually documents (and dropped from the end-of-data SMTP-smuggling list, which is a different class). Several options/statuses are now honest about not-yet-implemented surface: b.archive.read.zip.fromTrustedStream is marked experimental (its methods throw and its options aren't honored yet — the example now shows the supported buffer-then-random-access path); b.acme revokeCert's useCertKey / certPrivateKey are marked reserved (the cert-key path throws; account-key signing is the supported default); and a stale message claiming passkey break-glass factors were a future feature is removed (passkeys are a live allowed factor). No runtime behaviour changes beyond message/doc text.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Fixed",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "Corrected misattributed CVE citations in source threat-annotations",
|
|
13
|
+
"body": "`b.crypto.fromBase64Url`'s strict-decode guard cited CVE-2022-0235 (a node-fetch header-leak, unrelated to base64/JWT decoding); it now cites the weakness class it actually defends — CWE-347 / CWE-1286 signature canonicalization. `b.guardRegex`'s consecutive-`*` cap attributed CVE-2026-26996 to picomatch; that ReDoS is in minimatch (the picomatch ReDoS it also defends is CVE-2026-33671) — the library name is corrected. CVE-2026-32178 is reframed to the CWE-138 header-injection spoofing class the public advisory documents, and removed from the end-of-data SMTP-smuggling trio (a distinct class). No behaviour change — the defenses are unchanged."
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"heading": "Changed",
|
|
19
|
+
"items": [
|
|
20
|
+
{
|
|
21
|
+
"title": "Deferred / reserved surface now documented honestly",
|
|
22
|
+
"body": "`b.archive.read.zip.fromTrustedStream` is marked `experimental` — its `inspect`/`entries`/`extract` throw and its `bombPolicy`/`audit` options aren't honored yet; the documented example now shows the supported path (buffer the stream, then use the random-access reader). `b.acme` `revokeCert`'s `useCertKey` / `certPrivateKey` options are marked reserved (the cert-key-signed-revocation path throws; account-key signing, the default, covers mainstream CAs). A `b.breakGlass` policy error and comment that called passkey factors a future feature are corrected — passkeys are a live allowed factor."
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.13.16",
|
|
4
|
+
"date": "2026-05-27",
|
|
5
|
+
"headline": "`b.mail.agent` docs now describe the facade accurately, and not-yet-wired verbs point to the primitive to use",
|
|
6
|
+
"summary": "b.mail.agent's module documentation claimed it was \"the standardization contract for every mail protocol\" that JMAP / IMAP / POP3 all route through — but no protocol server actually dispatches through the agent (the framework's own JMAP EmailSubmission handler composes b.mail.send.deliver directly), and the compose / send / reply / forward, sieve.list / sieve.activate, identity / vacation / mdn.* and export / job / import verbs throw mail-agent/not-implemented. The docs are corrected to describe what the agent is: a mailbox-access facade (RBAC + posture + audit + dispatch around a mail store) whose read surface plus the mailbox-mutation and Sieve-upload methods are wired, with the remaining verbs not yet routed through it. Those verbs' error message now names the underlying primitive to compose directly (b.mail.send.deliver, b.mail.sieve, b.mailMdn, …) instead of citing a version tag that had long passed. The public WIRED_AT export (a method→version map that no longer reflected reality) is replaced by COMPOSE_HINT (a method→primitive-to-compose map). No behaviour change: the same methods are wired or throw exactly as before.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Changed",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "`b.mail.agent` documentation corrected; not-implemented errors point to the primitive to compose",
|
|
13
|
+
"body": "The `@module` / `@card` no longer claim the agent is the universal protocol-dispatch contract — it's documented as a mailbox-access facade with a wired read + mutation + Sieve-upload surface, and the compose/send/identity/vacation/MDN/export verbs documented as not yet routed through it (compose the underlying primitive directly until a protocol server adopts the agent). The `mail-agent/not-implemented` error now names that primitive (e.g. `b.mail.send.deliver`) rather than a passed version tag."
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"heading": "Removed",
|
|
19
|
+
"items": [
|
|
20
|
+
{
|
|
21
|
+
"title": "`b.mail.agent.WIRED_AT` export replaced by `COMPOSE_HINT`",
|
|
22
|
+
"body": "The `WIRED_AT` export mapped each method to a framework version that was supposed to \"light it up\" — versions that have all shipped without the wiring, so the map was misleading. It is replaced by `COMPOSE_HINT`, mapping each not-yet-wired method to the primitive an operator composes directly. Operators reading `b.mail.agent.WIRED_AT` should read `b.mail.agent.COMPOSE_HINT` instead (pre-1.0: no compatibility shim)."
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
@@ -541,23 +541,19 @@ var STALE_DEFER_ALLOWLIST = {
|
|
|
541
541
|
// operator-feeds-metadata escape hatch. Defer-with-condition.
|
|
542
542
|
"lib/ai-content-detect.js": ["IPTC PhotoMetadata reader lands in v0.10.9"],
|
|
543
543
|
// GAP BACKLOG (being worked down — these are real overdue items):
|
|
544
|
-
// archive-read ZIP64 read
|
|
544
|
+
// archive-read ZIP64 read (promised v0.12.8) — building. (The
|
|
545
|
+
// fromTrustedStream defer was reworded to a version-free "not
|
|
546
|
+
// implemented / re-opens when needed" in v0.13.15, so it no longer
|
|
547
|
+
// needs an allowlist entry.)
|
|
545
548
|
"lib/archive-read.js": [
|
|
546
549
|
"not supported in v0.12.7. Will land",
|
|
547
550
|
"switch to tar — lands v0.12.8",
|
|
548
551
|
"carries ZIP64 sentinel sizes (not supported in v0.12.7)",
|
|
549
|
-
"deferred to v0.12.8 alongside the tar reader",
|
|
550
|
-
"fromTrustedStream.inspect() is deferred to v0.12.8",
|
|
551
|
-
"fromTrustedStream.entries() is deferred to v0.12.8",
|
|
552
|
-
"fromTrustedStream.extract() is deferred to v0.12.8",
|
|
553
552
|
],
|
|
554
553
|
"lib/safe-archive.js": [
|
|
555
554
|
"tar lands v0.12.8, gz v0.12.9",
|
|
556
555
|
"fromTrustedStream` is deferred to v0.12.8",
|
|
557
556
|
],
|
|
558
|
-
// STALE comments (feature shipped; phrased as future) — correcting to past
|
|
559
|
-
// tense as the backlog is worked; allowlisted until then.
|
|
560
|
-
"lib/break-glass.js": ["passkey lands in v0.5.2"],
|
|
561
557
|
};
|
|
562
558
|
|
|
563
559
|
function testNoStaleDefers() {
|
|
@@ -46,7 +46,7 @@ function testSurface() {
|
|
|
46
46
|
check("mail.agent.consumer is fn", typeof b.mail.agent.consumer === "function");
|
|
47
47
|
check("MailAgentError is fn", typeof b.mail.agent.MailAgentError === "function");
|
|
48
48
|
check("SCOPE_FOR_METHOD frozen", Object.isFrozen(b.mail.agent.SCOPE_FOR_METHOD));
|
|
49
|
-
check("
|
|
49
|
+
check("COMPOSE_HINT frozen", Object.isFrozen(b.mail.agent.COMPOSE_HINT));
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
async function testCreateRequiresStore() {
|
|
@@ -100,8 +100,14 @@ async function run() {
|
|
|
100
100
|
// Poll until the watcher surfaces the target event. macOS fs.watch
|
|
101
101
|
// on CI runners can take 2-3s to deliver write events under load;
|
|
102
102
|
// the helper widens the default wait budget to 15s for that drift.
|
|
103
|
+
// Re-write a.txt on every poll: FSEvents may not be listening yet
|
|
104
|
+
// when the initial write (above) lands, and an event for a write
|
|
105
|
+
// that happened before the watch primed is dropped entirely — no
|
|
106
|
+
// amount of polling recovers it. Re-touching until the watch
|
|
107
|
+
// catches one closes that start-up race without a fixed prime-sleep.
|
|
103
108
|
try {
|
|
104
109
|
await waitForWatcher(function () {
|
|
110
|
+
fs.writeFileSync(path.join(tmpDir, "a.txt"), "hello");
|
|
105
111
|
w._flushForTest();
|
|
106
112
|
return changes.some(function (e) {
|
|
107
113
|
return e.relativePath === "a.txt" && e.type === "file";
|
package/package.json
CHANGED