@blamejs/core 0.11.32 → 0.11.33

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.33 (2026-05-21) — **IMAP QRESYNC (RFC 7162 §3.2) — VANISHED responses + SELECT delta on `b.mail.server.imap`.** Closes the v0.11.27 deferral. The IMAP listener now advertises QRESYNC in CAPABILITY, accepts `ENABLE QRESYNC` (which implicitly engages CONDSTORE per §3.2.5), and parses the `(QRESYNC (<uidvalidity> <modseq> [<knownUids>] [<knownSequenceMatchData>]))` parameter list on SELECT / EXAMINE. When the client's UIDVALIDITY matches the backend's, the listener emits a single `* VANISHED (EARLIER) <uid-set>` listing UIDs the server expunged since the client's snapshot — operators implement the actual delta computation via `mailStore.selectFolder(actor, mailbox, { qresync }) → { ..., vanishedEarlier }`. Stale-UIDVALIDITY clients fall through to a full re-SELECT. **Added:** *CAPABILITY advertises `QRESYNC`* — Sits next to the existing `CONDSTORE` advertisement. Both extensions are server-advertised; clients ENABLE before relying on the responses. RFC 7162 §3.2.5 — QRESYNC implies CONDSTORE. · *`ENABLE QRESYNC` engages both flags* — The handler flips `state.enabledQResync = true` AND `state.enabledCondStore = true` and emits `* ENABLED QRESYNC` + `OK ENABLE completed`. Already-engaged QRESYNC re-issues skip the advertisement line (only newly-engaged extensions appear). · *`SELECT mailbox (QRESYNC (<uidvalidity> <modseq> [<knownUids>] [<knownSeq>]))`* — The QRESYNC parameter list is stripped from the SELECT args before the mailbox-name validator runs and forwarded to `mailStore.selectFolder` as `opts.qresync = { uidvalidity, modseq, knownUids, knownSeq }`. Backends compute the delta and return `vanishedEarlier` (sequence-set string) in the existing select-info object. Non-finite uidvalidity / modseq values refuse with `BAD SELECT QRESYNC params must be (<uidvalidity> <modseq> ...) numerics` before the backend call. · *Implicit CONDSTORE+QRESYNC engagement on parameterised SELECT* — Per RFC 7162 §3.2.4, a client that issues `SELECT INBOX (QRESYNC (...))` without a prior `ENABLE QRESYNC` flips both flags implicitly for the session. Subsequent FETCH responses include `MODSEQ (<n>)` just as they would after explicit ENABLE. · *`* VANISHED (EARLIER) <uid-set>` emission* — The listener emits a single VANISHED untagged response between `HIGHESTMODSEQ` and the tagged OK when (a) the client supplied a QRESYNC parameter, (b) the client's UIDVALIDITY matches the backend's, AND (c) the backend supplied a non-empty `vanishedEarlier`. Mismatched UIDVALIDITY suppresses the VANISHED line so the client correctly falls through to a full re-sync (RFC 7162 §3.2.5). **Security:** *QRESYNC parameter parse is bounded* — The regex anchors on `\(\s*QRESYNC\s*\(\s*([^)]+)\)` — `[^)]*` not `.*` — so a malformed parameter list cannot consume the entire SELECT args. Both `uidvalidity` and `modseq` parse strictly as `\d+` via `parseInt(...)` + `isFinite` check; non-numeric values refuse before the backend dispatch. · *VANISHED suppressed on UIDVALIDITY mismatch* — RFC 7162 §3.2.5 — when the client's `uidvalidity` does NOT match the server's current value, the mailbox has been reset (e.g., recreated under the same name) and the client's UID cache is invalid. The listener intentionally drops the VANISHED line so the client forces a fresh full-sync instead of acting on a delta that references a different message set. **References:** [RFC 7162 §3.2 (QRESYNC — Quick Mailbox Resynchronization)](https://www.rfc-editor.org/rfc/rfc7162.html) · [RFC 9051 (IMAP4rev2)](https://www.rfc-editor.org/rfc/rfc9051.html) · [RFC 5161 (IMAP ENABLE Extension)](https://www.rfc-editor.org/rfc/rfc5161.html)
12
+
11
13
  - 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
14
 
13
15
  - 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.
@@ -646,8 +646,11 @@ function create(opts) {
646
646
  var caps = ["IMAP4rev2"];
647
647
  if (!state.tls) caps.push("STARTTLS");
648
648
  // RFC 7162 §3 — CONDSTORE is server-advertised; clients ENABLE
649
- // before relying on MODSEQ in untagged FETCH responses.
649
+ // before relying on MODSEQ in untagged FETCH responses. QRESYNC
650
+ // (§3.2) adds the VANISHED responses on SELECT + post-EXPUNGE
651
+ // and implicitly engages CONDSTORE per §3.2.5.
650
652
  caps.push("CONDSTORE");
653
+ caps.push("QRESYNC");
651
654
  // v0.11.28 — opt-in extensions (advertised so capable clients can
652
655
  // exercise them; each handler refuses gracefully when the operator
653
656
  // backend doesn't supply the corresponding hook).
@@ -690,10 +693,18 @@ function create(opts) {
690
693
  state.enabledCondStore = true;
691
694
  enabled.push("CONDSTORE");
692
695
  }
696
+ } else if (name === "QRESYNC") {
697
+ // RFC 7162 §3.2.5 — QRESYNC implicitly engages CONDSTORE.
698
+ // The client signals it can consume `* VANISHED (EARLIER)`
699
+ // responses on SELECT / EXAMINE + post-EXPUNGE; the listener
700
+ // flips both flags and the SELECT handler honours the
701
+ // QRESYNC parameter list when present.
702
+ if (!state.enabledQResync) {
703
+ state.enabledQResync = true;
704
+ state.enabledCondStore = true;
705
+ enabled.push("QRESYNC");
706
+ }
693
707
  }
694
- // QRESYNC (RFC 7162 §3.2.5) implies CONDSTORE — accepted only
695
- // when the operator backend supplies the QRESYNC vanished /
696
- // expunged-set surface; v1 of the listener stops at CONDSTORE.
697
708
  }
698
709
  _writeUntagged(socket, "ENABLED" + (enabled.length ? " " + enabled.join(" ") : ""));
699
710
  _writeTagged(socket, tag, "OK ENABLE completed");
@@ -1148,15 +1159,47 @@ function create(opts) {
1148
1159
 
1149
1160
  function _handleSelect(state, socket, tag, args, examine) {
1150
1161
  if (!_requireAuth(state, socket, tag)) return;
1151
- var name = _unquote(args.trim());
1162
+ var trimmed = (args || "").trim();
1163
+ // RFC 7162 §3.2.4 — `SELECT mailbox (QRESYNC (<uidvalidity>
1164
+ // <modseq> [<knownUids>] [<knownSequenceMatchData>]))`. The
1165
+ // QRESYNC parameter is wrapped in an outer parenthesis pair after
1166
+ // the mailbox name. Extract it before parsing the mailbox so the
1167
+ // mailbox-name validator sees just the name.
1168
+ var qresyncParam = null;
1169
+ var qresyncMatch = trimmed.match(/^(\S+|"[^"]+")\s+\(\s*QRESYNC\s*\(\s*([^)]+)\)\s*(?:\(\s*([^)]+)\)\s*)?\)\s*$/i); // allow:regex-no-length-cap — args length already capped upstream
1170
+ if (qresyncMatch) {
1171
+ var inner = qresyncMatch[2].trim().split(/\s+/);
1172
+ qresyncParam = {
1173
+ uidvalidity: parseInt(inner[0], 10),
1174
+ modseq: parseInt(inner[1], 10),
1175
+ knownUids: inner[2] || null,
1176
+ knownSeq: qresyncMatch[3] || null,
1177
+ };
1178
+ if (!isFinite(qresyncParam.uidvalidity) || !isFinite(qresyncParam.modseq)) {
1179
+ _writeTagged(socket, tag, "BAD SELECT QRESYNC params must be (<uidvalidity> <modseq> ...) numerics");
1180
+ return;
1181
+ }
1182
+ trimmed = qresyncMatch[1];
1183
+ }
1184
+ var name = _unquote(trimmed);
1152
1185
  if (!_validateMailboxName(name, { allowLegacyMUtf7: allowLegacyMUtf7 })) {
1153
1186
  _writeTagged(socket, tag, "BAD Mailbox name refused");
1154
1187
  return;
1155
1188
  }
1189
+ // QRESYNC requires CONDSTORE to be engaged; if the client sent
1190
+ // the parameter without having issued ENABLE first, RFC 7162
1191
+ // §3.2.4 lets the server flip the flags implicitly.
1192
+ if (qresyncParam && !state.enabledQResync) {
1193
+ state.enabledQResync = true;
1194
+ state.enabledCondStore = true;
1195
+ }
1156
1196
  Promise.resolve()
1157
1197
  .then(function () {
1158
1198
  if (typeof mailStore.selectFolder === "function") {
1159
- return mailStore.selectFolder(state.actor, name, { readOnly: examine });
1199
+ return mailStore.selectFolder(state.actor, name, {
1200
+ readOnly: examine,
1201
+ qresync: qresyncParam,
1202
+ });
1160
1203
  }
1161
1204
  // RFC 9051 §2.3.1.1 — UIDVALIDITY MUST be strictly increasing
1162
1205
  // and 32-bit unique across the mailbox lifetime. The earlier
@@ -1183,9 +1226,26 @@ function create(opts) {
1183
1226
  if (info.modseq !== undefined) {
1184
1227
  _writeUntagged(socket, "OK [HIGHESTMODSEQ " + info.modseq + "]");
1185
1228
  }
1229
+ // RFC 7162 §3.2.5 — when SELECT carried a QRESYNC parameter
1230
+ // AND the client's UIDVALIDITY matches, emit a single
1231
+ // `* VANISHED (EARLIER) <uid-set>` listing UIDs the server
1232
+ // expunged since the client's snapshot. The backend supplies
1233
+ // this via `info.vanishedEarlier` (sequence-set string) — the
1234
+ // listener does the wire emission. Mismatched UIDVALIDITY
1235
+ // means the client's cache is stale and MUST re-SELECT; we
1236
+ // skip the VANISHED line in that case so the client falls
1237
+ // through to a full re-sync. RFC 7162 §3.2.5.2 says the
1238
+ // server MAY also include changed-since-modseq FETCH lines
1239
+ // — those flow through the normal FETCH path with
1240
+ // CHANGEDSINCE so we leave them to the operator.
1241
+ if (qresyncParam && info.vanishedEarlier &&
1242
+ info.uidvalidity === qresyncParam.uidvalidity) {
1243
+ _writeUntagged(socket, "VANISHED (EARLIER) " + info.vanishedEarlier);
1244
+ }
1186
1245
  _emit("mail.server.imap.select", {
1187
1246
  connectionId: state.id, mailbox: name,
1188
1247
  modseq: info.modseq || 0, exists: info.exists,
1248
+ qresync: qresyncParam !== null,
1189
1249
  });
1190
1250
  _writeTagged(socket, tag, "OK [" + (examine ? "READ-ONLY" : "READ-WRITE") + "] " +
1191
1251
  (examine ? "EXAMINE" : "SELECT") + " completed");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.11.32",
3
+ "version": "0.11.33",
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:3c063952-23ab-48f4-9274-e30b6d7bed0d",
5
+ "serialNumber": "urn:uuid:10fe079a-bfcc-4ffe-84e1-55a6b5b11ccf",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-21T17:46:32.271Z",
8
+ "timestamp": "2026-05-21T18:47:05.672Z",
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.32",
22
+ "bom-ref": "@blamejs/core@0.11.33",
23
23
  "type": "application",
24
24
  "name": "blamejs",
25
- "version": "0.11.32",
25
+ "version": "0.11.33",
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.32",
29
+ "purl": "pkg:npm/%40blamejs/core@0.11.33",
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.32",
57
+ "ref": "@blamejs/core@0.11.33",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]