@blamejs/core 0.11.31 → 0.11.32

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 CHANGED
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.11.x
10
10
 
11
+ - v0.11.32 (2026-05-21) — **`b.mail.crypto.pgp.encrypt` / `.decrypt` / `.wkd` promoted to stable — WKD IDN-homograph defense.** The PGP encrypt / decrypt / Web Key Directory (WKD) primitives that shipped under `b.mail.crypto.pgp.experimental.*` at v0.10.16 are promoted to top-level stable. `b.mail.crypto.pgp.encrypt` / `b.mail.crypto.pgp.decrypt` / `b.mail.crypto.pgp.wkd.fetch` / `b.mail.crypto.pgp.wkd.computeUrl` are now the canonical paths; the v0.10.16 `experimental` aliases continue to work so existing operator code keeps importing through the old path until they migrate at their own pace. WKD picks up a hard IDN-homograph refusal at `wkd.computeUrl` — Cyrillic / Greek / full-width / Han confusable domain characters refuse with `mail-crypto/pgp/bad-domain` before any HTTP fetch runs. Operators with internationalised domains MUST Punycode-encode upstream (RFC 3492 `xn--` form). **Added:** *Top-level `b.mail.crypto.pgp.encrypt` / `.decrypt` / `.wkd.{fetch,computeUrl}`* — Same function bodies that shipped under `b.mail.crypto.pgp.experimental.*` at v0.10.16 are now exported at the top of the namespace. The framework-private envelope (BJ-PGP-PQ magic + version) is unchanged; the IANA-pending RFC 9580bis ML-KEM PKESK codepoints will land as an alternate-encoding option in a follow-up slice. The `experimental` alias keeps the v0.10.16 import paths working — operator migration is opt-in. `b.mail.crypto.pgp.encrypt === b.mail.crypto.pgp.experimental.encrypt` (same reference) so a feature-detection test like `typeof b.mail.crypto.pgp.encrypt === 'function'` is the new operator-facing pattern. **Security:** *WKD IDN-homograph refusal at `wkd.computeUrl`* — `wkd.computeUrl(email)` now refuses any domain containing characters outside the LDH+dot ASCII subset (RFC 952 / RFC 1123 §2). The threat model: Cyrillic `а` (U+0430), Greek `ο` (U+03BF), full-width `A` (U+FF21), and Han homographs visually impersonate Latin letters in `paypa1.com` / `gοogle.com` style phishing host strings; a naive `toLowerCase` + concat into the WKD URL would route the key fetch to the attacker's domain. Operators with legitimate internationalised domains MUST Punycode-encode upstream (the `xn--` form is plain ASCII LDH and passes). The framework's `b.httpClient` already refuses non-ASCII hostnames at the SSRF guard layer; this primitive surfaces the same refusal at the WKD entry point so the error surface is consistent. · *RFC 5321 + RFC 1035 length caps* — Email length capped at 320 octets (RFC 5321 §4.5.3.1 maximum). Domain length capped at 253 octets (RFC 1035 §2.3.4). Empty labels (`bad..example.com`), leading-dot, and trailing-dot domains refuse before any URL construction. Adversarial-length inputs cannot reach the tokenizer / hasher path. **References:** [draft-koch-openpgp-webkey-service (Web Key Directory)](https://datatracker.ietf.org/doc/draft-koch-openpgp-webkey-service/) · [RFC 9580 (OpenPGP)](https://www.rfc-editor.org/rfc/rfc9580.html) · [RFC 3492 (Punycode — IDNA `xn--` form)](https://www.rfc-editor.org/rfc/rfc3492.html) · [RFC 5891 (IDNA 2008)](https://www.rfc-editor.org/rfc/rfc5891.html) · [RFC 5321 §4.5.3.1 (SMTP — local-part + domain octet maximums)](https://www.rfc-editor.org/rfc/rfc5321.html) · [RFC 1035 §2.3.4 (domain label length)](https://www.rfc-editor.org/rfc/rfc1035.html) · [Unicode TR 39 (Security Mechanisms — confusable identifiers)](https://www.unicode.org/reports/tr39/)
12
+
11
13
  - v0.11.31 (2026-05-21) — **`b.calendar` — JSCalendar (RFC 8984) primitive + JMAP Calendars method catalogue.** JSCalendar is the JSON-native calendar grammar JMAP Calendars rides on. The framework now ships `b.calendar` — a thin layer over the existing `b.safeIcal.parse` (RFC 5545 bounded parser shipped earlier) that exposes validate / fromIcal / toIcal / expandRecurrence. The JMAP method catalogue at `b.mail.serverRegistry` picks up the 15 Calendar / CalendarEvent / CalendarEventNotification / ParticipantIdentity methods per the draft-ietf-jmap-calendars spec, so operators wire them through the existing `b.mail.server.jmap.create({ methods: { ... } })` dispatch path without `allowExperimental: true` escape-hatches.
12
14
 
13
15
  v1 scope: validate JSCalendar Event / Task shape per RFC 8984 §5/§6, bidirectional VEVENT ↔ JSCalendar conversion, RRULE expansion for FREQ=DAILY/WEEKLY/MONTHLY/YEARLY/HOURLY/MINUTELY/SECONDLY with INTERVAL / COUNT / UNTIL / BYDAY / BYMONTH / BYMONTHDAY. BYSETPOS / BYWEEKNO / BYYEARDAY filters + non-Gregorian calendars (RFC 7529) + JSCalendar Group objects + VTODO/VJOURNAL mapping are deferred-with-condition for follow-up slices. **Added:** *`b.calendar.validate(jsCal)` — JSCalendar Event/Task shape gate (RFC 8984 §5/§6)* — Asserts `@type` ∈ { 'Event', 'Task' }, non-empty `uid` (≤ 1024 bytes), `updated` matches UTCDateTime grammar, Event's optional `start` matches LocalDateTime, `duration` is RFC 8601 PnYnMnDTnHnMnS, every `recurrenceRules[i].@type` is 'RecurrenceRule' with `frequency` in the JSCAL_FREQUENCIES catalogue, every `alerts.{id}.action` is 'display' or 'email'. Returns the input on success; throws `CalendarError` with a structured `.code` (`calendar/no-uid`, `calendar/bad-recurrence`, etc.) on refusal so operator-side error handling has a stable surface. · *`b.calendar.fromIcal(text, opts?)` + `b.calendar.toIcal(jsCal, opts?)`* — Bidirectional bridge: iCalendar (RFC 5545) ↔ JSCalendar (RFC 8984). `fromIcal` runs the text through `b.safeIcal.parse` (already CVE-bounded for parser DoS) and maps the first VEVENT into an Event object (UID → uid, DTSTAMP → updated, DTSTART → start, DURATION → duration, SUMMARY → title, DESCRIPTION → description, LOCATION → locations[L1].name, RRULE → recurrenceRules[0]). `toIcal` round-trips the same shape back through a CRLF-folded VCALENDAR envelope per RFC 5545 §3.1 (75-octet content-line cap). Operators wire it on the EmailSubmission send path so calendar invitations from a JSCalendar back-end emit RFC 5545 over MIME, and inbound iCalendar attachments parse straight into the JMAP method's Event return shape. · *`b.calendar.expandRecurrence(event, { from, to, max })` — bounded RRULE expansion* — Emits ISO 8601 UTC instance timestamps for an Event in the operator's `[from, to]` window. Supports FREQ=DAILY/WEEKLY/MONTHLY/YEARLY/HOURLY/MINUTELY/SECONDLY with INTERVAL, COUNT, UNTIL. Bounded by `MAX_EXPAND_INSTANCES` (4096) and `MAX_EXPAND_SPAN_MS` (10 years) — refuses with `calendar/oversize-expansion-span` when the window exceeds 10 years (CVE-2024-39687-class recurrence-bomb defense, mirroring the parser-side caps already in `b.safeIcal`). LocalDateTime starts without a `timeZone` are treated as floating-UTC during expansion so wall-clock semantics survive `Date.parse` host-locale interpretation. · *JMAP method catalogue: Calendar / CalendarEvent / CalendarEventNotification / ParticipantIdentity* — 15 new methods added to `JMAP_METHODS` in `lib/mail-server-registry.js` per draft-ietf-jmap-calendars: `Calendar/{get,changes,set,query,queryChanges}`, `CalendarEvent/{get,changes,query,queryChanges,set,copy}`, `CalendarEventNotification/{get,changes,query,queryChanges,set}`, `ParticipantIdentity/{get,changes,set}`. Operators wire concrete handlers through the existing `b.mail.server.jmap.create({ methods: { 'CalendarEvent/get': async function (actor, args) { ... } } })` path — no `allowExperimental: true` escape-hatch is required. **Security:** *Recurrence-bomb expansion caps* — `MAX_EXPAND_INSTANCES = 4096` + `MAX_EXPAND_SPAN_MS = 10 years` clamp the expansion budget at the framework boundary so an operator who forwards an unbounded JSCalendar from a hostile peer cannot DoS the host. The caps mirror `b.safeIcal`'s RRULE COUNT + BY-entries caps (which already defend the CVE-2024-39687 class). The 10-year span refusal carries `code: 'calendar/oversize-expansion-span'` so operators distinguish bomb defense from legitimate too-wide-window misconfiguration. · *UID + duration + UTCDateTime parsing is strict* — `validate` refuses `uid` over 1024 bytes (anti-DoS), `updated` that doesn't match the RFC 8984 §1.4.3 `\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z` UTCDateTime grammar exactly (no `+00:00` aliases, no fractional-second-only suffixes), and `duration` that isn't `PnYnMnDTnHnMnS`. Hostile JSCalendar payloads from federation peers can't slip non-canonical-shape values into the operator's storage layer through the validator. · *iCalendar parsing routed through bounded `b.safeIcal.parse`* — `fromIcal` does NOT roll its own RFC 5545 parser — it forwards to `b.safeIcal.parse` which is already audit-hardened (CVE-2024-39929 / CVE-2025-30258 mitigation, bounded depth, capped RRULE COUNT + BY-entries). The JSCalendar layer composes the existing security substrate rather than re-litigating it. **References:** [RFC 8984 (JSCalendar — JSON Representation of Calendar Data)](https://www.rfc-editor.org/rfc/rfc8984.html) · [RFC 5545 (iCalendar — Internet Calendaring and Scheduling Core Object)](https://www.rfc-editor.org/rfc/rfc5545.html) · [draft-ietf-jmap-calendars (JMAP for Calendars)](https://datatracker.ietf.org/doc/draft-ietf-jmap-calendars/) · [RFC 8620 (JMAP Core)](https://www.rfc-editor.org/rfc/rfc8620.html) · [RFC 8601 (Duration grammar PnYnMnDTnHnMnS)](https://www.rfc-editor.org/rfc/rfc3339.html) · [RFC 7986 (New properties for iCalendar)](https://www.rfc-editor.org/rfc/rfc7986.html) · [CVE-2024-39687 (iCalendar recurrence-bomb expansion)](https://nvd.nist.gov/vuln/detail/CVE-2024-39687)
@@ -1190,10 +1190,38 @@ function wkdComputeUrl(email, opts) {
1190
1190
  throw new MailCryptoError("mail-crypto/pgp/bad-email",
1191
1191
  "wkd.computeUrl: email must be a 'local@domain' string");
1192
1192
  }
1193
+ // RFC 5321 §4.5.3.1 — practical email-length cap. 320 octets is the
1194
+ // upper bound (64 local + 1 @ + 255 domain). Refuse beyond that BEFORE
1195
+ // any further processing to defend tokenisation paths against
1196
+ // adversarial-length inputs.
1197
+ if (email.length > 320) { // allow:raw-byte-literal — RFC 5321 max email length
1198
+ throw new MailCryptoError("mail-crypto/pgp/bad-email",
1199
+ "wkd.computeUrl: email length " + email.length + " exceeds RFC 5321 max 320 octets");
1200
+ }
1193
1201
  var at = email.indexOf("@");
1194
1202
  var localRaw = email.slice(0, at);
1195
1203
  var localLower = localRaw.toLowerCase();
1196
1204
  var domain = email.slice(at + 1).toLowerCase();
1205
+ // IDN-homograph defense — refuse domains with bytes outside the
1206
+ // LDH+dot ASCII subset (RFC 952 / RFC 1123 §2). Operators with IDN
1207
+ // (internationalised) domains MUST Punycode-encode upstream
1208
+ // (RFC 3492 `xn--` form). Cyrillic / Greek / Han homograph attacks
1209
+ // (`paypa1` lookalike etc.) are the threat model — the WKD URL has
1210
+ // to be an unambiguous host string, and the framework's b.httpClient
1211
+ // already refuses non-ASCII hostnames at the SSRF guard, so this
1212
+ // surface is the canonical refusal point.
1213
+ if (!/^[a-z0-9.-]+$/.test(domain)) {
1214
+ throw new MailCryptoError("mail-crypto/pgp/bad-domain",
1215
+ "wkd.computeUrl: domain must be ASCII LDH+dot (Punycode-encode IDN domains upstream; RFC 3492 xn-- form)");
1216
+ }
1217
+ if (domain.indexOf("..") !== -1 || domain.charAt(0) === "." || domain.charAt(domain.length - 1) === ".") {
1218
+ throw new MailCryptoError("mail-crypto/pgp/bad-domain",
1219
+ "wkd.computeUrl: domain must not contain empty labels");
1220
+ }
1221
+ if (domain.length > 253) { // allow:raw-byte-literal — RFC 1035 §2.3.4 max domain length
1222
+ throw new MailCryptoError("mail-crypto/pgp/bad-domain",
1223
+ "wkd.computeUrl: domain length " + domain.length + " exceeds RFC 1035 max 253 octets");
1224
+ }
1197
1225
  var hashed = bCrypto.kdf(Buffer.from(localLower, "utf8"), 20); // allow:raw-byte-literal — 20-byte hash per draft-koch §3.1
1198
1226
  var encoded = _zbase32Encode(hashed);
1199
1227
  var advancedHost = opts.advancedHost || ("openpgpkey." + domain);
@@ -1230,6 +1258,19 @@ function _zbase32Encode(buf) {
1230
1258
  module.exports = {
1231
1259
  sign: sign,
1232
1260
  verify: verify,
1261
+ // v0.11.32 — encrypt / decrypt / wkd promoted to stable top-level
1262
+ // surface. The framework-private envelope (BJ-PGP-PQ magic + version)
1263
+ // is the same one the experimental namespace shipped at v0.10.16;
1264
+ // the IANA-pending RFC 9580bis ML-KEM PKESK codepoints will be
1265
+ // wired as an alternate-encoding option in a follow-up slice. Until
1266
+ // then the `experimental` alias keeps the v0.10.16 import paths
1267
+ // working — operators migrate at their own pace.
1268
+ encrypt: experimentalEncrypt,
1269
+ decrypt: experimentalDecrypt,
1270
+ wkd: {
1271
+ computeUrl: wkdComputeUrl,
1272
+ fetch: wkdFetch,
1273
+ },
1233
1274
  experimental: {
1234
1275
  encrypt: experimentalEncrypt,
1235
1276
  decrypt: experimentalDecrypt,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.11.31",
3
+ "version": "0.11.32",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
package/sbom.cdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:19702043-8392-4b48-9d1c-120d93f6889f",
5
+ "serialNumber": "urn:uuid:3c063952-23ab-48f4-9274-e30b6d7bed0d",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-21T17:20:28.895Z",
8
+ "timestamp": "2026-05-21T17:46:32.271Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/core@0.11.31",
22
+ "bom-ref": "@blamejs/core@0.11.32",
23
23
  "type": "application",
24
24
  "name": "blamejs",
25
- "version": "0.11.31",
25
+ "version": "0.11.32",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "The Node framework that owns its stack.",
29
- "purl": "pkg:npm/%40blamejs/core@0.11.31",
29
+ "purl": "pkg:npm/%40blamejs/core@0.11.32",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/core@0.11.31",
57
+ "ref": "@blamejs/core@0.11.32",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]