@blamejs/blamejs-shop 0.2.23 → 0.2.24
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/asset-manifest.json +1 -1
- package/lib/cart.js +24 -9
- package/lib/storefront.js +92 -17
- package/lib/vendor/MANIFEST.json +3 -3
- package/lib/vendor/blamejs/CHANGELOG.md +2 -0
- package/lib/vendor/blamejs/GOVERNANCE.md +2 -3
- package/lib/vendor/blamejs/LTS-CALENDAR.md +6 -2
- package/lib/vendor/blamejs/SECURITY.md +1 -1
- package/lib/vendor/blamejs/api-snapshot.json +2 -2
- package/lib/vendor/blamejs/lib/ai-content-detect.js +4 -3
- package/lib/vendor/blamejs/lib/archive-read.js +10 -7
- package/lib/vendor/blamejs/lib/break-glass.js +1 -1
- package/lib/vendor/blamejs/lib/cms-codec.js +10 -9
- package/lib/vendor/blamejs/lib/mail-crypto-smime.js +10 -6
- package/lib/vendor/blamejs/lib/mtls-ca.js +2 -2
- package/lib/vendor/blamejs/lib/safe-archive.js +8 -7
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.13.43.json +39 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ws-client.test.js +4 -1
- 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.24 (2026-05-29) — **Scope cart-line edits to your own cart, harden structured data, and redact auth errors.** A set of request-path hardening fixes. Cart line edit and remove are now scoped to your own session cart, so a cart-line id belonging to another shopper can no longer be used to change quantities on or delete their item — a mismatched id is a no-op, not a mutation. The product structured-data (JSON-LD) embedded in page heads now neutralizes every `</script` close-tag variant (with trailing space, slash, or newline), not just the exact `</script>`, closing a markup-injection path from admin-entered product fields. Account sign-in, registration, and passkey error responses now return a generic message plus a request id instead of the raw internal error string, with the full error logged server-side for the operator. And the worker DB/R2 bridge secret is checked at boot: a secret shorter than 32 characters logs a loud warning so a weak secret is visible in the boot log (the documented generator produces 43 characters, so a by-the-docs deploy stays quiet). **Security:** *Cart-line edits are scoped to your cart* — `POST /cart/lines/:id/update` and `/remove` now resolve the requester's session cart and scope the change to it (`WHERE id = ? AND cart_id = ?`), so a cart-line id from another shopper's cart can't be used to alter or delete their line — it resolves to a no-op rather than a cross-cart mutation. Legitimate edits to your own cart are unchanged. · *Structured-data close-tag escaping hardened* — JSON-LD embedded in page heads (edge and origin, byte-identical) now escapes every `</script` sequence the HTML parser treats as a tag-closer — including `</script `, `</script/`, and `</script` followed by a newline — not only the exact `</script>`. This removes a markup-injection vector from admin-entered product fields that flow into structured data. · *Account auth errors no longer leak internals* — The sign-in, registration, and passkey ceremony endpoints now return a generic message and a request id on an unexpected server error instead of the raw error string; the real error is logged server-side, correlated by that request id. Client-shape (400) and other expected responses are unchanged. · *Bridge-secret entropy warning at boot* — The worker DB/R2 bridge secret (`D1_BRIDGE_SECRET`) authorizes the database and object-storage bridge, so a weak value is high-impact. The container now logs a loud boot warning in production when it is shorter than 32 characters. The documented generator produces 43 characters; if you see the warning, regenerate the secret with the command it prints.
|
|
12
|
+
|
|
11
13
|
- v0.2.23 (2026-05-29) — **Rate limiting and cross-site request isolation on the request lifecycle.** Two request-lifecycle defenses are now composed into every storefront and admin request. A per-client-IP rate limit backs the whole site (a generous global budget that a normal browsing and checkout session never trips) with tighter budgets on the abusable POST/auth endpoints — sign-in, passkey enrolment, checkout, gift-card balance lookup, account registration, newsletter, review/question submit, and survey response — so credential and passkey spraying, gift-card brute-forcing, and checkout flooding are shut down. Separately, a fetch-metadata gate refuses cross-site state-changing requests using the browser's Sec-Fetch headers, adding CSRF defense-in-depth on top of the SameSite session cookies. The client IP is read from the edge-forwarded header (the container sits behind the Worker), so limits apply per real visitor; the payment-webhook and health-probe paths are exempt. No change for normal use. **Added:** *Per-client rate limiting* — A generous global token-bucket limit per client IP backs the whole site, with tight fixed-window budgets on the abusable POST/auth endpoints (login, passkey register/add, checkout, gift-card balance, register, newsletter, review/question, survey). Keyed on the real client IP forwarded from the edge; payment-webhook and health-probe paths are exempt. The single-instance container uses an in-memory backend. · *Cross-site request isolation (fetch-metadata)* — Cross-site state-changing requests (the CSRF vector) are refused using the browser's Sec-Fetch-Site/Mode/Dest headers — same-origin requests and direct navigations pass; payment webhooks are exempt. This restores defense-in-depth alongside the SameSite session cookies.
|
|
12
14
|
|
|
13
15
|
- v0.2.22 (2026-05-29) — **See your full order history, track shipments, reorder, and request returns.** The customer order surface is now complete end-to-end. A new order-history list shows every order (cursor-paginated), and each order page gains a status timeline, a carrier tracking panel (tracking number + the carrier's public tracking link), a one-click Reorder that rebuilds a cart from the order, and a Request-a-return button for eligible orders — the same Reorder/Return actions also appear per row on the account dashboard and the history list. On the operator side, the admin order screen gets a shipment panel to attach a carrier + tracking number and record shipment events, and the returns console's Refund now issues the payment-provider refund first (then records the RMA) whenever the order has a captured charge, so a return can't be marked refunded while the customer is left un-refunded; manual/guest orders with no charge fall back to an explicit record-only step. **Added:** *Order history + tracking + reorder + returns (customer)* — `/account/orders` lists all your orders with pagination; each order page shows a status timeline and carrier tracking, plus Reorder (`POST /orders/:id/reorder` rebuilds a cart from the order's lines at current prices) and Request-a-return for eligible orders. The dashboard and history rows carry the same actions. · *Shipment + tracking panel (admin)* — The admin order detail screen can attach a shipment (carrier + tracking number) and record shipment events; marking delivered advances the order's lifecycle. Tracking the order surfaces the carrier's public tracking link to the customer. **Changed:** *Return refunds move the money* — The returns console Refund action now issues the payment-provider refund first and records the RMA only on success when the order has a captured charge, so an approved return is never marked refunded without the customer actually being refunded. Orders with no captured charge use an explicit record-only step that points to the order.
|
package/lib/asset-manifest.json
CHANGED
package/lib/cart.js
CHANGED
|
@@ -184,28 +184,43 @@ function create(opts) {
|
|
|
184
184
|
return { id: id, cart_id: cartId, variant_id: input.variant_id, sku: variantRow.sku, qty: input.qty, unit_amount_minor: unitAmount, unit_currency: unitCurrency, added_at: ts, updated_at: ts };
|
|
185
185
|
},
|
|
186
186
|
|
|
187
|
-
|
|
187
|
+
// Scope the mutation to (lineId, cartId): a caller who learns
|
|
188
|
+
// another visitor's cart_lines.id (it's rendered in their own cart
|
|
189
|
+
// HTML as the update/remove form action) can't mutate it because
|
|
190
|
+
// the row only matches when it also belongs to the caller's session
|
|
191
|
+
// cart. A cross-cart id matches zero rows and becomes a no-op
|
|
192
|
+
// (returns null), never a mutation. cartId is required — every
|
|
193
|
+
// caller resolves the session cart first.
|
|
194
|
+
updateLine: async function (lineId, cartId, patch) {
|
|
188
195
|
_uuid(lineId, "line id");
|
|
196
|
+
_uuid(cartId, "cart_id");
|
|
189
197
|
if (!patch || typeof patch !== "object") throw new TypeError("cart.updateLine: patch object required");
|
|
190
198
|
if (patch.qty == null) throw new TypeError("cart.updateLine: qty is the only updatable field");
|
|
191
199
|
_qty(patch.qty);
|
|
192
200
|
var ts = _now();
|
|
193
201
|
var r = await query(
|
|
194
|
-
"UPDATE cart_lines SET qty = ?1, updated_at = ?2 WHERE id = ?3",
|
|
195
|
-
[patch.qty, ts, lineId],
|
|
202
|
+
"UPDATE cart_lines SET qty = ?1, updated_at = ?2 WHERE id = ?3 AND cart_id = ?4",
|
|
203
|
+
[patch.qty, ts, lineId, cartId],
|
|
196
204
|
);
|
|
197
205
|
if (r.rowCount === 0) return null;
|
|
198
|
-
var row = (await query("SELECT * FROM cart_lines WHERE id = ?1", [lineId])).rows[0];
|
|
199
|
-
await query("UPDATE carts SET updated_at = ?1 WHERE id = ?2", [ts,
|
|
206
|
+
var row = (await query("SELECT * FROM cart_lines WHERE id = ?1 AND cart_id = ?2", [lineId, cartId])).rows[0];
|
|
207
|
+
await query("UPDATE carts SET updated_at = ?1 WHERE id = ?2", [ts, cartId]);
|
|
200
208
|
return row;
|
|
201
209
|
},
|
|
202
210
|
|
|
203
|
-
|
|
211
|
+
// Same cart-scoping as updateLine: a row only deletes when it
|
|
212
|
+
// belongs to the caller's session cart, so a cross-cart id is a
|
|
213
|
+
// no-op (returns false) rather than deleting another visitor's line.
|
|
214
|
+
removeLine: async function (lineId, cartId) {
|
|
204
215
|
_uuid(lineId, "line id");
|
|
205
|
-
|
|
216
|
+
_uuid(cartId, "cart_id");
|
|
217
|
+
var row = (await query(
|
|
218
|
+
"SELECT cart_id FROM cart_lines WHERE id = ?1 AND cart_id = ?2",
|
|
219
|
+
[lineId, cartId],
|
|
220
|
+
)).rows[0];
|
|
206
221
|
if (!row) return false;
|
|
207
|
-
await query("DELETE FROM cart_lines WHERE id = ?1", [lineId]);
|
|
208
|
-
await query("UPDATE carts SET updated_at = ?1 WHERE id = ?2", [_now(),
|
|
222
|
+
await query("DELETE FROM cart_lines WHERE id = ?1 AND cart_id = ?2", [lineId, cartId]);
|
|
223
|
+
await query("UPDATE carts SET updated_at = ?1 WHERE id = ?2", [_now(), cartId]);
|
|
209
224
|
return true;
|
|
210
225
|
},
|
|
211
226
|
|
package/lib/storefront.js
CHANGED
|
@@ -44,6 +44,35 @@ var b = require("./vendor/blamejs");
|
|
|
44
44
|
// finds no store and falls back to the English baseline.
|
|
45
45
|
var _localeAls = new AsyncLocalStorage();
|
|
46
46
|
|
|
47
|
+
// Storefront error logger. Routes through the framework's structured
|
|
48
|
+
// log sink (not console) so operators can redirect / quiet / structured-
|
|
49
|
+
// log every emission point, and so the per-request id the createApp
|
|
50
|
+
// requestId middleware allocates is auto-bound to each line. Used by the
|
|
51
|
+
// 5xx auth-route handlers to record the real failure server-side while
|
|
52
|
+
// the client only ever sees a generic message + the correlating id.
|
|
53
|
+
var _log = b.log.create({});
|
|
54
|
+
|
|
55
|
+
// Generic 500 for an auth/ceremony route: log the real error server-side
|
|
56
|
+
// (correlated by the framework request id) and return a fixed message to
|
|
57
|
+
// the client so no internal error string (stack frame, DB column, vault
|
|
58
|
+
// internals) leaks. The request id — set by the createApp requestId
|
|
59
|
+
// middleware on req.requestId and echoed in the X-Request-Id response
|
|
60
|
+
// header — rides in the body so an operator can grep the logs for a
|
|
61
|
+
// customer's failed ceremony. `where` names the route for the log line
|
|
62
|
+
// only; it is not reflected to the client.
|
|
63
|
+
function _authServerError(req, res, e, where) {
|
|
64
|
+
var rid = (req && req.requestId) || null;
|
|
65
|
+
_log.error("storefront auth route failed", {
|
|
66
|
+
route: where,
|
|
67
|
+
request_id: rid,
|
|
68
|
+
err: (e && e.message) || String(e),
|
|
69
|
+
});
|
|
70
|
+
res.status(500);
|
|
71
|
+
var ref = rid ? " (ref " + rid + ")" : "";
|
|
72
|
+
var msg = "Something went wrong. Please try again." + ref;
|
|
73
|
+
return res.end ? res.end(msg) : res.send(msg);
|
|
74
|
+
}
|
|
75
|
+
|
|
47
76
|
// Payment-webhook signatures (Stripe's HMAC, PayPal's, …) are computed over
|
|
48
77
|
// the EXACT raw request bytes, but the global JSON body-parser reparses and
|
|
49
78
|
// discards them. This middleware buffers the raw body for the given POST
|
|
@@ -3561,10 +3590,16 @@ function _buildCompare(productId) {
|
|
|
3561
3590
|
}
|
|
3562
3591
|
|
|
3563
3592
|
// Schema.org JSON-LD block. JSON.stringify covers the standard escapes;
|
|
3564
|
-
// the
|
|
3565
|
-
// value.
|
|
3593
|
+
// the `</script` → `<\/script` rewrite neutralises any literal closing
|
|
3594
|
+
// tag in a value. The HTML tokenizer ends a <script> on `</script`
|
|
3595
|
+
// followed by whitespace, `/`, or `>`, so matching only the exact
|
|
3596
|
+
// `</script>` byte sequence misses `</script `, `</script\n`,
|
|
3597
|
+
// `</script/` — all of which still break out. Matching `</script`
|
|
3598
|
+
// (any trailing byte) closes every variant. Mirrors the edge
|
|
3599
|
+
// renderer's `jsonLdScript` byte-for-byte (this output is dual-rendered
|
|
3600
|
+
// edge + container; the render-parity tests gate on the two agreeing).
|
|
3566
3601
|
function _jsonLdScript(data) {
|
|
3567
|
-
var serialised = JSON.stringify(data).replace(/<\/
|
|
3602
|
+
var serialised = JSON.stringify(data).replace(/<\/script/gi, "<\\/script");
|
|
3568
3603
|
return "<script type=\"application/ld+json\">" + serialised + "</script>";
|
|
3569
3604
|
}
|
|
3570
3605
|
|
|
@@ -8067,8 +8102,11 @@ function mount(router, deps) {
|
|
|
8067
8102
|
return res.end ? res.end(JSON.stringify(startOpts)) : res.send(JSON.stringify(startOpts));
|
|
8068
8103
|
} catch (e) {
|
|
8069
8104
|
if (e && e.code === "vault/not-initialized") return _serviceUnavailable(res, "auth not configured");
|
|
8070
|
-
|
|
8071
|
-
|
|
8105
|
+
if (e instanceof TypeError) {
|
|
8106
|
+
res.status(400);
|
|
8107
|
+
return res.end ? res.end((e && e.message) || "register-begin failed") : res.send((e && e.message) || "register-begin failed");
|
|
8108
|
+
}
|
|
8109
|
+
return _authServerError(req, res, e, "register-begin");
|
|
8072
8110
|
}
|
|
8073
8111
|
});
|
|
8074
8112
|
|
|
@@ -8125,8 +8163,11 @@ function mount(router, deps) {
|
|
|
8125
8163
|
return res.end ? res.end("ok") : res.send("ok");
|
|
8126
8164
|
} catch (e) {
|
|
8127
8165
|
if (e && e.code === "vault/not-initialized") return _serviceUnavailable(res, "auth not configured");
|
|
8128
|
-
|
|
8129
|
-
|
|
8166
|
+
if (e instanceof TypeError) {
|
|
8167
|
+
res.status(400);
|
|
8168
|
+
return res.end ? res.end((e && e.message) || "register-finish failed") : res.send((e && e.message) || "register-finish failed");
|
|
8169
|
+
}
|
|
8170
|
+
return _authServerError(req, res, e, "register-finish");
|
|
8130
8171
|
}
|
|
8131
8172
|
});
|
|
8132
8173
|
|
|
@@ -8166,8 +8207,11 @@ function mount(router, deps) {
|
|
|
8166
8207
|
return res.end ? res.end(JSON.stringify(startOpts)) : res.send(JSON.stringify(startOpts));
|
|
8167
8208
|
} catch (e) {
|
|
8168
8209
|
if (e && e.code === "vault/not-initialized") return _serviceUnavailable(res, "auth not configured");
|
|
8169
|
-
|
|
8170
|
-
|
|
8210
|
+
if (e instanceof TypeError) {
|
|
8211
|
+
res.status(400);
|
|
8212
|
+
return res.end ? res.end((e && e.message) || "login-begin failed") : res.send((e && e.message) || "login-begin failed");
|
|
8213
|
+
}
|
|
8214
|
+
return _authServerError(req, res, e, "login-begin");
|
|
8171
8215
|
}
|
|
8172
8216
|
});
|
|
8173
8217
|
|
|
@@ -8230,8 +8274,11 @@ function mount(router, deps) {
|
|
|
8230
8274
|
return res.end ? res.end("ok") : res.send("ok");
|
|
8231
8275
|
} catch (e) {
|
|
8232
8276
|
if (e && e.code === "vault/not-initialized") return _serviceUnavailable(res, "auth not configured");
|
|
8233
|
-
|
|
8234
|
-
|
|
8277
|
+
if (e instanceof TypeError) {
|
|
8278
|
+
res.status(400);
|
|
8279
|
+
return res.end ? res.end((e && e.message) || "login-finish failed") : res.send((e && e.message) || "login-finish failed");
|
|
8280
|
+
}
|
|
8281
|
+
return _authServerError(req, res, e, "login-finish");
|
|
8235
8282
|
}
|
|
8236
8283
|
});
|
|
8237
8284
|
|
|
@@ -8507,8 +8554,11 @@ function mount(router, deps) {
|
|
|
8507
8554
|
return res.end ? res.end(JSON.stringify(startOpts)) : res.send(JSON.stringify(startOpts));
|
|
8508
8555
|
} catch (e) {
|
|
8509
8556
|
if (e && e.code === "vault/not-initialized") return _serviceUnavailable(res, "auth not configured");
|
|
8510
|
-
|
|
8511
|
-
|
|
8557
|
+
if (e instanceof TypeError) {
|
|
8558
|
+
res.status(400);
|
|
8559
|
+
return res.end ? res.end((e && e.message) || "add-begin failed") : res.send((e && e.message) || "add-begin failed");
|
|
8560
|
+
}
|
|
8561
|
+
return _authServerError(req, res, e, "add-begin");
|
|
8512
8562
|
}
|
|
8513
8563
|
});
|
|
8514
8564
|
|
|
@@ -8560,8 +8610,11 @@ function mount(router, deps) {
|
|
|
8560
8610
|
return res.end ? res.end("ok") : res.send("ok");
|
|
8561
8611
|
} catch (e) {
|
|
8562
8612
|
if (e && e.code === "vault/not-initialized") return _serviceUnavailable(res, "auth not configured");
|
|
8563
|
-
|
|
8564
|
-
|
|
8613
|
+
if (e instanceof TypeError) {
|
|
8614
|
+
res.status(400);
|
|
8615
|
+
return res.end ? res.end((e && e.message) || "add-finish failed") : res.send((e && e.message) || "add-finish failed");
|
|
8616
|
+
}
|
|
8617
|
+
return _authServerError(req, res, e, "add-finish");
|
|
8565
8618
|
}
|
|
8566
8619
|
});
|
|
8567
8620
|
|
|
@@ -10346,8 +10399,19 @@ function mount(router, deps) {
|
|
|
10346
10399
|
res.status(400);
|
|
10347
10400
|
return res.end ? res.end("Invalid request") : res.send("Invalid request");
|
|
10348
10401
|
}
|
|
10402
|
+
// Resolve the requester's own session cart and scope the mutation
|
|
10403
|
+
// to it (same pattern as POST /cart/lines/:line_id/save). The line
|
|
10404
|
+
// id is rendered in every visitor's own cart HTML, so without this
|
|
10405
|
+
// a caller who learns another visitor's line id could change its
|
|
10406
|
+
// qty. A line that isn't in the caller's cart returns null → 404.
|
|
10407
|
+
var sid = _readSidCookie(req);
|
|
10408
|
+
var cart = sid ? await deps.cart.bySession(sid) : null;
|
|
10409
|
+
if (!cart) {
|
|
10410
|
+
res.status(404);
|
|
10411
|
+
return res.end ? res.end("Line not found") : res.send("Line not found");
|
|
10412
|
+
}
|
|
10349
10413
|
try {
|
|
10350
|
-
var updated = await deps.cart.updateLine(lineId, { qty: qty });
|
|
10414
|
+
var updated = await deps.cart.updateLine(lineId, cart.id, { qty: qty });
|
|
10351
10415
|
if (!updated) {
|
|
10352
10416
|
res.status(404);
|
|
10353
10417
|
return res.end ? res.end("Line not found") : res.send("Line not found");
|
|
@@ -10368,8 +10432,19 @@ function mount(router, deps) {
|
|
|
10368
10432
|
res.status(400);
|
|
10369
10433
|
return res.end ? res.end("Invalid request") : res.send("Invalid request");
|
|
10370
10434
|
}
|
|
10435
|
+
// Scope the delete to the requester's own session cart (same as the
|
|
10436
|
+
// update route) so a known-but-foreign line id can't delete another
|
|
10437
|
+
// visitor's line. No session cart → nothing of the caller's to
|
|
10438
|
+
// remove; redirect to /cart as the no-op success path.
|
|
10439
|
+
var sid = _readSidCookie(req);
|
|
10440
|
+
var cart = sid ? await deps.cart.bySession(sid) : null;
|
|
10441
|
+
if (!cart) {
|
|
10442
|
+
res.status(303);
|
|
10443
|
+
res.setHeader && res.setHeader("location", "/cart");
|
|
10444
|
+
return res.end ? res.end() : res.send("");
|
|
10445
|
+
}
|
|
10371
10446
|
try {
|
|
10372
|
-
await deps.cart.removeLine(lineId);
|
|
10447
|
+
await deps.cart.removeLine(lineId, cart.id);
|
|
10373
10448
|
} catch (e) {
|
|
10374
10449
|
res.status(e instanceof TypeError ? 400 : 500);
|
|
10375
10450
|
return res.end ? res.end((e && e.message) || "Error") : res.send((e && e.message) || "Error");
|
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.43",
|
|
7
|
+
"tag": "v0.13.43",
|
|
8
8
|
"license": "Apache-2.0",
|
|
9
9
|
"author": "blamejs contributors",
|
|
10
10
|
"source": "https://github.com/blamejs/blamejs",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"server": "lib/vendor/blamejs/"
|
|
14
14
|
},
|
|
15
15
|
"bundler": "shallow git clone of release tag from github.com/blamejs/blamejs",
|
|
16
|
-
"bundledAt": "2026-05-
|
|
16
|
+
"bundledAt": "2026-05-30"
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
}
|
|
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.13.x
|
|
10
10
|
|
|
11
|
+
- v0.13.43 (2026-05-29) — **LTS window stated consistently as 24 months, experimental primitives declared semver-exempt, and stale version references cleaned up.** Documentation and operator-facing string hygiene ahead of the 1.0 stability contract. The LTS support window is now stated as 24 months everywhere (GOVERNANCE.md and the LTS calendar previously disagreed — 24 vs 18). The LTS calendar gains an explicit clause that primitives marked experimental are exempt from the stability/LTS contract, so operators can tell at a glance which surfaces may change between minors. Several error messages and doc blocks that pinned to long-past version numbers ("lands in v0.10.9", "not supported in v0.12.7", "ships in v0.6.45+") are restated version-agnostically with their escape hatch, and the S/MIME module now points operators at the live PGP encrypt/decrypt path for confidentiality today. No API or behavior changes. **Changed:** *LTS support window is consistently 24 months* — `GOVERNANCE.md` promised a 24-month LTS window while `LTS-CALENDAR.md` and `SECURITY.md` stated 18 — a six-month contradiction in the single most load-bearing number of the support contract. All three now state 24 months of security-only patches per major. The calendar table and the supported-versions prose are aligned. · *Experimental primitives are declared exempt from the stability contract* — `LTS-CALENDAR.md` now states explicitly that primitives documented as experimental (shown as "experimental" on their wiki page, and via the `experimental` segment in namespaces like `b.jose.jwe.experimental`) are not covered by the stability contract or the LTS window — they may change signature, behavior, or wire format, or be removed, in any minor without a deprecation cycle. This lets the framework ship primitives that track in-flight standards without freezing an unsettled format, and tells operators precisely which surfaces are not yet frozen. **Fixed:** *Stale version references removed from operator-facing errors and docs* — Error messages and documentation that pinned to long-past versions are restated version-agnostically with the relevant escape hatch: ZIP64 and unsupported-compression errors in archive reading, the CMS AuthEnvelopedData / fielded-decoder notes, the mTLS CRL-engine error, the safe-archive format-detection summary (which also now correctly lists the supported zip / tar / tar.gz set rather than claiming only zip), and the AI-content IPTC-reader note. None changed behavior; they no longer read as broken promises against the published version history. · *S/MIME confidentiality deferral points to the working PGP path* — `b.mail.crypto.smime` ships sign + verify; encrypt/decrypt is deferred. The deferral note previously cited an open-ended internal condition; it now names the escape hatch directly — use `b.mail.crypto.pgp.encrypt` / `decrypt` for mail confidentiality today — and states the concrete trigger that would re-open S/MIME-specific (X.509-recipient) encryption. · *Governance doc no longer references an internal file operators cannot see* — `GOVERNANCE.md` cited a rule number in a contributor-only file that does not ship in the repository. The deprecation-policy statement is now self-contained.
|
|
12
|
+
|
|
11
13
|
- v0.13.42 (2026-05-29) — **S/MIME trust-chain validation binds the leaf to the key that verified the signature.** When b.mail.crypto.smime.verify is given trust anchors, it validates the supplied certificate chain — but it picked the chain leaf unconditionally (the first cert) and never tied it to signerPublicKey, the key that actually verified the signature. A SignedData blob could therefore carry a validly-chained certificate for one identity while the signature was verified under an unrelated key, and the chain-validated result would imply a cert↔signer binding the code never made. Chain validation now selects the leaf as the certificate whose public key matches signerPublicKey, and refuses (signer-not-in-chain) when no certificate in the chain carries that key — so a chain-validated signature is bound to the cert it claims. **Security:** *Trust-chain leaf is bound to the verified signer key* — `smime.verify({ trustAnchorCertsPem })` validated the supplied chain starting from `chain[0]` without checking that the leaf's public key was the one that verified the signature (the operator-supplied `signerPublicKey`). A crafted `SignedData` could pair a validly-chained certificate for identity A with a signature verified under an unrelated key, and the chain-valid result would assert a binding that didn't hold. The chain walk now selects the leaf as the certificate whose public key equals `signerPublicKey` (matched against the certificate's SPKI, raw key or full-encoding form), and throws `mail-crypto/smime/signer-not-in-chain` when no certificate in `SignedData.certificates` carries that key. A certificate whose key cannot be extracted is treated as a non-match, so validation fails closed rather than trusting an unverifiable binding.
|
|
12
14
|
|
|
13
15
|
- v0.13.41 (2026-05-29) — **Agent registry reads can be tenant-scoped; compliance-erasure docs clarify actor is an audit field.** The agent orchestrator's registry reads (list and lookup) gated only on the flat agent-registry:read scope, so any holder could enumerate every tenant's agents and resolve a handle to one — even though the event bus already scopes subscribe and delivery by tenant. The orchestrator now mirrors that: with the new tenantScope option enabled, list returns only the actor's own tenant's agents and lookup refuses a cross-tenant name, unless the actor holds the framework cross-tenant-admin scope. Off by default, so single-tenant deployments are unaffected. Separately, the subject-erasure docs now state explicitly that the recorded actor is an audit field, not authentication — the caller must be authorized upstream. **Added:** *Tenant-scoped agent registry reads (opts.tenantScope)* — `b.agent.orchestrator.create({ tenantScope: true })` now scopes `list` and `lookup` to the calling actor's tenant: `list` filters out agents in other tenants and `lookup` returns null for a cross-tenant name, unless the actor holds the cross-tenant-admin scope (`b.agent.tenant.CROSS_TENANT_ADMIN_SCOPE`). This closes a cross-tenant metadata-enumeration and handle-acquisition path — `agent-registry:read` alone no longer exposes other tenants' agents — and mirrors the tenant scoping the event bus enforces on subscribe and delivery. The option defaults off; existing single-tenant orchestrators behave exactly as before. The `tenantId` argument to `list` remains a convenience filter, distinct from this authorization boundary. **Changed:** *Subject-erasure docs clarify the actor is an audit field, not authentication* — `b.subject.erase` and `b.subject.eraseHard` gate the deletion on operator acknowledgements and the legal-hold registry, not on caller identity. Their documentation now states explicitly that the recorded `actor` is an audit-record field, not authentication — the caller MUST be authenticated and authorized by the route before invoking. No behavior change; this removes an implicit assumption that could otherwise be read as the primitive authorizing the call.
|
|
@@ -39,9 +39,8 @@ then, the maintainer is final on technical direction.
|
|
|
39
39
|
- **Operator-impacting changes.** Pre-1.0 the framework reserves the
|
|
40
40
|
right to break operator-facing surface in any minor version; major
|
|
41
41
|
versions ship deprecation warnings at least one minor before
|
|
42
|
-
removal
|
|
43
|
-
|
|
44
|
-
[LTS-CALENDAR.md](LTS-CALENDAR.md).
|
|
42
|
+
removal. Post-1.0 the same contract applies across majors with a
|
|
43
|
+
24-month LTS window per [LTS-CALENDAR.md](LTS-CALENDAR.md).
|
|
45
44
|
- **Releases.** Patch (`0.0.x`) is the default; minor (`0.x.0`)
|
|
46
45
|
requires an explicit decision the maintainer documents in the
|
|
47
46
|
release notes; major (`x.0.0`) requires a deprecation cycle. The
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# LTS calendar
|
|
2
2
|
|
|
3
3
|
`@blamejs/core` ships on a published major cadence. Each major receives
|
|
4
|
-
**
|
|
4
|
+
**24 months of security-only patches** starting the day the next major is
|
|
5
5
|
published. Feature backports are not promised.
|
|
6
6
|
|
|
7
7
|
| Version | First release | Security patches through | Node minimum | KEM | Cipher | KDF | Sigs |
|
|
8
8
|
|---------------|---------------|-----------------------------|---------------|----------------------|-----------------------|----------|-----------------------|
|
|
9
9
|
| `v0.x` (pre-1.0) | 2026-04-25 | until v1.0 ships | 24 | ML-KEM-1024 + P-384 | XChaCha20-Poly1305 | SHAKE256 | SLH-DSA-SHAKE-256f |
|
|
10
|
-
| `v1.x` | TBD | first release +
|
|
10
|
+
| `v1.x` | TBD | first release + 24 months | current LTS | ML-KEM-1024 + P-384 | XChaCha20-Poly1305 | SHAKE256 | SLH-DSA-SHAKE-256f |
|
|
11
11
|
|
|
12
12
|
## What "security patches" means
|
|
13
13
|
|
|
@@ -27,3 +27,7 @@ The "Node minimum" column is the lowest Node major the framework supports for th
|
|
|
27
27
|
## Pre-1.0 caveat
|
|
28
28
|
|
|
29
29
|
`v0.x` has no LTS commitment. Every release may change something operators depend on; the algorithm posture is intentionally evolving. Read [CHANGELOG.md](CHANGELOG.md) before upgrading across more than a few patches at a time. The LTS calendar takes effect at v1.0.
|
|
30
|
+
|
|
31
|
+
## Experimental primitives are exempt
|
|
32
|
+
|
|
33
|
+
Primitives documented `@status experimental` (shown as "experimental" on each wiki page, and via the `experimental` segment in namespaces such as `b.jose.jwe.experimental`) are **not** covered by the stability contract or the LTS window. They may change signature, behavior, or wire format — or be removed — in any minor, without the deprecation cycle that stable primitives get. This applies on the LTS line too. The exemption exists so the framework can ship primitives that track in-flight standards (draft RFCs, pre-IANA codepoints, newly published W3C surfaces) without freezing an unsettled format for a major's full support window. A primitive graduates to stable by dropping the `@status experimental` marker in a release whose notes call out the graduation.
|
|
@@ -178,7 +178,7 @@ We coordinate with the reporter on disclosure — typical embargo is 14 days pos
|
|
|
178
178
|
|
|
179
179
|
Pre-1.0, the supported version is the most-recent published patch on the most-recent minor. Older minors do not receive security backports unless the issue is critical AND the operator base on the older minor is non-trivial.
|
|
180
180
|
|
|
181
|
-
Once 1.0 ships, the LTS calendar takes effect: each major gets
|
|
181
|
+
Once 1.0 ships, the LTS calendar takes effect: each major gets 24 months of security-only patches after the next major's release.
|
|
182
182
|
|
|
183
183
|
| Version range | Security patches |
|
|
184
184
|
|---|---|
|
|
@@ -30,9 +30,10 @@
|
|
|
30
30
|
* IPTC `digitalSourceType` PhotoMetadata reading is forward-watch —
|
|
31
31
|
* the framework ships no XMP / EXIF parser yet, so operators that
|
|
32
32
|
* want IPTC detection pre-parse with their tool of choice and pass
|
|
33
|
-
* the field via `opts.ipmd`. AB-853 names C2PA as "widely adopted"
|
|
34
|
-
* IPTC PhotoMetadata reader
|
|
35
|
-
* decision
|
|
33
|
+
* the field via `opts.ipmd`. AB-853 names C2PA as "widely adopted".
|
|
34
|
+
* A built-in IPTC PhotoMetadata reader is deferred pending a vendoring
|
|
35
|
+
* decision for an XMP/EXIF parser; the `opts.ipmd` escape hatch covers
|
|
36
|
+
* the gap until then.
|
|
36
37
|
*
|
|
37
38
|
* @card
|
|
38
39
|
* Inbound provenance detector — composes C2PA verify + CAC implicit-label parser + operator-supplied IPTC field, returns a normalized report for AB-853 / EU AI Act Art. 50 / CAC disclosure UIs.
|
|
@@ -214,10 +214,11 @@ async function _readCentralDirectory(adapter, eocd) {
|
|
|
214
214
|
"multi-disk archives are not supported (diskNumber=" + eocd.diskNumber + ")");
|
|
215
215
|
}
|
|
216
216
|
if (eocd.totalEntries === 0xffff || eocd.cdSize === 0xffffffff || eocd.cdOffset === 0xffffffff) {
|
|
217
|
-
// ZIP64 sentinel —
|
|
218
|
-
//
|
|
217
|
+
// ZIP64 sentinel — unsupported. Archives at >4 GiB / >65535 entries
|
|
218
|
+
// use tar instead (the escape hatch); ZIP64 read support is deferred
|
|
219
|
+
// until an operator surfaces a need.
|
|
219
220
|
throw new ArchiveReadError("archive-read/zip64-unsupported",
|
|
220
|
-
"ZIP64 archives are
|
|
221
|
+
"ZIP64 archives are unsupported (operators at >4 GiB / >65535 entries should switch to tar)");
|
|
221
222
|
}
|
|
222
223
|
if (eocd.cdSize === 0 || eocd.totalEntries === 0) {
|
|
223
224
|
return [];
|
|
@@ -256,7 +257,7 @@ async function _readCentralDirectory(adapter, eocd) {
|
|
|
256
257
|
}
|
|
257
258
|
if (compressedSize === 0xffffffff || uncompressedSize === 0xffffffff || lfhOffset === 0xffffffff) {
|
|
258
259
|
throw new ArchiveReadError("archive-read/zip64-unsupported",
|
|
259
|
-
"central directory entry " + n + " carries ZIP64 sentinel sizes (
|
|
260
|
+
"central directory entry " + n + " carries ZIP64 sentinel sizes (unsupported — use tar for >4 GiB / >65535 entries)");
|
|
260
261
|
}
|
|
261
262
|
// ZIP names are CP437 or UTF-8 (per FLAG_UTF8_NAME bit). Decode
|
|
262
263
|
// as UTF-8 unconditionally — Codex P2 territory if operators in
|
|
@@ -425,7 +426,7 @@ async function _decompressEntry(adapter, entry, dataStart, bombPolicy) {
|
|
|
425
426
|
}
|
|
426
427
|
throw new ArchiveReadError("archive-read/unsupported-method",
|
|
427
428
|
"entry " + JSON.stringify(entry.name) + " uses method=" + entry.method +
|
|
428
|
-
" — only STORE (0) and DEFLATE (8) supported
|
|
429
|
+
" — only STORE (0) and DEFLATE (8) are supported");
|
|
429
430
|
}
|
|
430
431
|
|
|
431
432
|
// ---- Public read.zip factory ---------------------------------------------
|
|
@@ -668,8 +669,10 @@ function zip(adapter, opts) {
|
|
|
668
669
|
// entry-type policy opts in.
|
|
669
670
|
if (entry.isEncrypted && !extractOpts.allowEncrypted) {
|
|
670
671
|
throw new ArchiveReadError("archive-read/encrypted-entry",
|
|
671
|
-
"entry " + JSON.stringify(entry.name) + " is encrypted — " +
|
|
672
|
-
"
|
|
672
|
+
"entry " + JSON.stringify(entry.name) + " is encrypted — this " +
|
|
673
|
+
"low-level reader does not decrypt; use b.safeArchive for " +
|
|
674
|
+
"encrypted-archive handling, or pass allowEncrypted to extract " +
|
|
675
|
+
"the raw entry");
|
|
673
676
|
}
|
|
674
677
|
var typeRefusal = _enforceEntryTypePolicy(entry, entryTypePolicy);
|
|
675
678
|
if (typeRefusal) {
|
|
@@ -523,7 +523,7 @@ function _validatePolicySet(table, opts) {
|
|
|
523
523
|
"' not in allowed factors [" + ALLOWED_FACTORS.join(",") + "]");
|
|
524
524
|
}
|
|
525
525
|
}
|
|
526
|
-
// Model B (cryptographic mode)
|
|
526
|
+
// Model B (cryptographic mode). When enabled,
|
|
527
527
|
// glass-locked columns must be encrypted with `b.breakGlass.encryptCell`
|
|
528
528
|
// at write time (the framework can't auto-encrypt at write because
|
|
529
529
|
// policy-set may post-date existing data; operators run the migration
|
|
@@ -42,15 +42,16 @@
|
|
|
42
42
|
* refuses EnvelopedData and accepts only the §5083 ContentInfo
|
|
43
43
|
* OID. Cheap escape hatch: operators on such a peer compose
|
|
44
44
|
* `b.asn1Der` directly to rewrap an EnvelopedData blob into an
|
|
45
|
-
* AuthEnvelopedData ContentInfo.
|
|
46
|
-
*
|
|
47
|
-
*
|
|
45
|
+
* AuthEnvelopedData ContentInfo. A built-in encode path is deferred
|
|
46
|
+
* until an interop case requires a peer that refuses EnvelopedData;
|
|
47
|
+
* the `b.asn1Der` rewrap covers the gap until then.
|
|
48
48
|
* - **`b.cms.decode` parse-tree of inner SignedData / EnvelopedData**
|
|
49
|
-
* beyond the ContentInfo wrapper.
|
|
49
|
+
* beyond the ContentInfo wrapper. `b.cms.decode` returns the inner
|
|
50
50
|
* SEQUENCE bytes as `content` (an asn1-der node); callers that
|
|
51
|
-
* need fielded access walk it via `b.asn1Der.readSequence`.
|
|
52
|
-
* fielded decoders
|
|
53
|
-
*
|
|
51
|
+
* need fielded access walk it via `b.asn1Der.readSequence`. Built-in
|
|
52
|
+
* fielded decoders are deferred until they're actually consumed by a
|
|
53
|
+
* shipping primitive; the `b.asn1Der.readSequence` walk is the escape
|
|
54
|
+
* hatch until then.
|
|
54
55
|
*
|
|
55
56
|
* Refusal posture:
|
|
56
57
|
*
|
|
@@ -302,8 +303,8 @@ function encodeEnvelopedData(opts) {
|
|
|
302
303
|
* string (e.g. `"1.2.840.113549.1.7.2"` for SignedData) and
|
|
303
304
|
* `content` is the inner asn1-der node (SignedData / EnvelopedData /
|
|
304
305
|
* other) — operators walk it via `b.asn1Der.readSequence`. Fielded
|
|
305
|
-
* decoders for SignedData / EnvelopedData
|
|
306
|
-
*
|
|
306
|
+
* decoders for SignedData / EnvelopedData are deferred; the
|
|
307
|
+
* `b.asn1Der.readSequence` walk is the escape hatch until then.
|
|
307
308
|
*
|
|
308
309
|
* Refuses input past `opts.maxBytes` (default 64 MiB), top-level
|
|
309
310
|
* non-SEQUENCE shapes, missing OID + [0] EXPLICIT child pair.
|
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
* @card
|
|
10
10
|
* S/MIME 4.0 sign + verify (PQC-first ML-DSA / SLH-DSA signers) on
|
|
11
11
|
* the b.cms substrate. RFC 8551 multipart/signed with RFC 5652
|
|
12
|
-
* SignedData
|
|
13
|
-
*
|
|
12
|
+
* SignedData. Confidentiality (encrypt/decrypt) is deferred — use
|
|
13
|
+
* `b.mail.crypto.pgp.encrypt`/`decrypt` today; S/MIME-specific
|
|
14
|
+
* (X.509-recipient) encryption re-opens when an operator surfaces a
|
|
15
|
+
* peer that requires it, with the EFAIL defenses below applied then.
|
|
14
16
|
*
|
|
15
17
|
* @intro
|
|
16
18
|
* S/MIME 4.0 (RFC 8551, replacing RFC 5751) `multipart/signed;
|
|
@@ -112,10 +114,12 @@ var ALLOWED_HASHES = ["sha256", "sha384", "sha512"];
|
|
|
112
114
|
var REFUSED_HASHES = ["md5", "sha1"]; // allow:raw-byte-literal — SHAttered / RFC 8551 §2.5
|
|
113
115
|
|
|
114
116
|
// PROFILES + COMPLIANCE_POSTURES — the framework's standard cross-
|
|
115
|
-
// primitive contract. sign() and verify()
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
117
|
+
// primitive contract. sign() and verify() read these to determine which
|
|
118
|
+
// hash + RSA-bit floors apply per operator posture. Confidentiality
|
|
119
|
+
// (encrypt/decrypt) is deferred — b.mail.crypto.pgp.encrypt/decrypt is the
|
|
120
|
+
// confidentiality path today; if S/MIME-specific X.509-recipient
|
|
121
|
+
// encryption is added, it composes the same set with the @intro EFAIL
|
|
122
|
+
// defenses applied.
|
|
119
123
|
var PROFILES = ["strict", "balanced", "permissive"];
|
|
120
124
|
var COMPLIANCE_POSTURES = {
|
|
121
125
|
hipaa: "strict",
|
|
@@ -517,8 +517,8 @@ function create(opts) {
|
|
|
517
517
|
opts3 = opts3 || {};
|
|
518
518
|
if (typeof engine.generateCrl !== "function") {
|
|
519
519
|
throw new MtlsCaError("mtls-ca/engine-no-crl",
|
|
520
|
-
"configured engine does not implement generateCrl(); the
|
|
521
|
-
"engine
|
|
520
|
+
"configured engine does not implement generateCrl(); use the " +
|
|
521
|
+
"framework's bundled CA engine, which supports it");
|
|
522
522
|
}
|
|
523
523
|
var ca = await initCA();
|
|
524
524
|
var revocations = _loadRevocations().revocations;
|
|
@@ -22,10 +22,11 @@
|
|
|
22
22
|
* pipeline manually.
|
|
23
23
|
*
|
|
24
24
|
* Format auto-detection sniffs the first ~512 bytes for magic
|
|
25
|
-
* signatures
|
|
26
|
-
*
|
|
27
|
-
* wrapped
|
|
28
|
-
*
|
|
25
|
+
* signatures: ZIP (LFH magic `0x04034b50` + EOCD magic `0x06054b50`),
|
|
26
|
+
* tar (`ustar` at offset 257), gzip / tar.gz (RFC 1952 magic), and
|
|
27
|
+
* `b.crypto.encryptPacked`-wrapped envelopes (auto-unwrapped before
|
|
28
|
+
* format detection). Unrecognized inputs are flagged
|
|
29
|
+
* `safe-archive/format-unsupported`.
|
|
29
30
|
*
|
|
30
31
|
* The orchestrator refuses the WHOLE archive on any single critical
|
|
31
32
|
* guard issue — no partial extraction. Cleanup is `fs.rm`-recursive
|
|
@@ -148,7 +149,7 @@ async function _collectSourceBytes(source) {
|
|
|
148
149
|
* @opts
|
|
149
150
|
* source: b.archive.adapters.* | Buffer | string,
|
|
150
151
|
* destination: string (target directory; created if missing),
|
|
151
|
-
* format: "auto" | "zip"
|
|
152
|
+
* format: "auto" | "zip" | "tar" | "tar.gz",
|
|
152
153
|
* bombPolicy: b.guardArchive.zipBombPolicy(...) | { ... },
|
|
153
154
|
* entryTypePolicy: b.guardArchive.entryTypePolicy(...) | { ... },
|
|
154
155
|
* guardProfile: "strict" | "balanced" | "permissive" | "hipaa" | ...,
|
|
@@ -275,8 +276,8 @@ async function extract(opts) {
|
|
|
275
276
|
});
|
|
276
277
|
} else {
|
|
277
278
|
throw new SafeArchiveError("safe-archive/format-unsupported",
|
|
278
|
-
"extract: format=" + JSON.stringify(format) + " —
|
|
279
|
-
"
|
|
279
|
+
"extract: format=" + JSON.stringify(format) + " — supported formats are " +
|
|
280
|
+
"zip, tar, tar.gz; b.crypto.encryptPacked-wrapped archives are auto-unwrapped first");
|
|
280
281
|
}
|
|
281
282
|
var result = await reader.extract({
|
|
282
283
|
destination: opts.destination,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.13.43",
|
|
4
|
+
"date": "2026-05-29",
|
|
5
|
+
"headline": "LTS window stated consistently as 24 months, experimental primitives declared semver-exempt, and stale version references cleaned up",
|
|
6
|
+
"summary": "Documentation and operator-facing string hygiene ahead of the 1.0 stability contract. The LTS support window is now stated as 24 months everywhere (GOVERNANCE.md and the LTS calendar previously disagreed — 24 vs 18). The LTS calendar gains an explicit clause that primitives marked experimental are exempt from the stability/LTS contract, so operators can tell at a glance which surfaces may change between minors. Several error messages and doc blocks that pinned to long-past version numbers (\"lands in v0.10.9\", \"not supported in v0.12.7\", \"ships in v0.6.45+\") are restated version-agnostically with their escape hatch, and the S/MIME module now points operators at the live PGP encrypt/decrypt path for confidentiality today. No API or behavior changes.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Changed",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "LTS support window is consistently 24 months",
|
|
13
|
+
"body": "`GOVERNANCE.md` promised a 24-month LTS window while `LTS-CALENDAR.md` and `SECURITY.md` stated 18 — a six-month contradiction in the single most load-bearing number of the support contract. All three now state 24 months of security-only patches per major. The calendar table and the supported-versions prose are aligned."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"title": "Experimental primitives are declared exempt from the stability contract",
|
|
17
|
+
"body": "`LTS-CALENDAR.md` now states explicitly that primitives documented as experimental (shown as \"experimental\" on their wiki page, and via the `experimental` segment in namespaces like `b.jose.jwe.experimental`) are not covered by the stability contract or the LTS window — they may change signature, behavior, or wire format, or be removed, in any minor without a deprecation cycle. This lets the framework ship primitives that track in-flight standards without freezing an unsettled format, and tells operators precisely which surfaces are not yet frozen."
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"heading": "Fixed",
|
|
23
|
+
"items": [
|
|
24
|
+
{
|
|
25
|
+
"title": "Stale version references removed from operator-facing errors and docs",
|
|
26
|
+
"body": "Error messages and documentation that pinned to long-past versions are restated version-agnostically with the relevant escape hatch: ZIP64 and unsupported-compression errors in archive reading, the CMS AuthEnvelopedData / fielded-decoder notes, the mTLS CRL-engine error, the safe-archive format-detection summary (which also now correctly lists the supported zip / tar / tar.gz set rather than claiming only zip), and the AI-content IPTC-reader note. None changed behavior; they no longer read as broken promises against the published version history."
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"title": "S/MIME confidentiality deferral points to the working PGP path",
|
|
30
|
+
"body": "`b.mail.crypto.smime` ships sign + verify; encrypt/decrypt is deferred. The deferral note previously cited an open-ended internal condition; it now names the escape hatch directly — use `b.mail.crypto.pgp.encrypt` / `decrypt` for mail confidentiality today — and states the concrete trigger that would re-open S/MIME-specific (X.509-recipient) encryption."
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"title": "Governance doc no longer references an internal file operators cannot see",
|
|
34
|
+
"body": "`GOVERNANCE.md` cited a rule number in a contributor-only file that does not ship in the repository. The deprecation-policy statement is now self-contained."
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
}
|
|
@@ -254,7 +254,10 @@ async function run() {
|
|
|
254
254
|
var c8 = b.wsClient.connect("ws://127.0.0.1:" + port8 + "/foo", {
|
|
255
255
|
reconnect: false, audit: false, allowInternal: true,
|
|
256
256
|
});
|
|
257
|
-
|
|
257
|
+
// Poll for handshake completion rather than a fixed-budget sleep — the
|
|
258
|
+
// 150ms budget flakes under SMOKE_PARALLEL=64 contention (rule §11b).
|
|
259
|
+
await helpers.waitUntil(function () { return c8.readyState === "open"; },
|
|
260
|
+
{ timeoutMs: 5000, label: "ws-client: c8 handshake completes (readyState open)" });
|
|
258
261
|
check("getter: url", c8.url.indexOf("/foo") !== -1);
|
|
259
262
|
check("getter: readyState open after handshake", c8.readyState === "open");
|
|
260
263
|
c8.close();
|
package/package.json
CHANGED