@blamejs/core 0.10.9 → 0.10.11

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.
@@ -0,0 +1,318 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.mail.serverRegistry
4
+ * @nav Mail
5
+ * @title Mail Server Method Registry
6
+ *
7
+ * @intro
8
+ * Shared per-method dispatch registry for the IMAP / JMAP /
9
+ * ManageSieve listener factories. Replaces the hand-rolled
10
+ * `switch (verb)` dispatchers with a single primitive that:
11
+ *
12
+ * - runs the protocol-specific guard chain BEFORE the handler
13
+ * lookup (so operator-supplied overrides cannot bypass
14
+ * `b.guardImapCommand` / `b.guardJmap` / `b.guardManagesieveCommand`),
15
+ * - enforces per-handler resource budgets (`maxHandlerBytes`,
16
+ * `maxHandlerMs`) — refused at registration time if not supplied,
17
+ * - emits a `mail.serverRegistry.method_dispatch` audit event with
18
+ * handler-source (`builtin` | `operator-override`),
19
+ * - rejects registrations of method names outside the per-protocol
20
+ * IANA / RFC catalogue (unless `allowExperimental: true`, which
21
+ * itself audits).
22
+ *
23
+ * Operators wanting to override IMAP `FETCH`, JMAP `Email/query`,
24
+ * ManageSieve `PUTSCRIPT`, etc. supply
25
+ * `opts.overrides: { "FETCH": { fn, maxHandlerBytes, maxHandlerMs } }`
26
+ * to the listener factory and the registry routes dispatch through
27
+ * the override without touching wire-protocol state, audit
28
+ * lifecycle, or the guard substrate.
29
+ *
30
+ * Per-handler resource budgets are required (no auto-defaults) per
31
+ * the framework's security-defaults-on rule: an operator-supplied
32
+ * FETCH override that omits a budget could regress CVE-2024-34055
33
+ * (Cyrus authenticated OOM via unbounded allocation); the
34
+ * stricter-mode contract forces the operator to acknowledge the
35
+ * resource ceiling explicitly.
36
+ *
37
+ * @card
38
+ * Per-method dispatch registry for the mail-server listeners. Guard-chain preserving, per-handler resource-budget required, audit on every dispatch.
39
+ */
40
+
41
+ var safeAsync = require("./safe-async");
42
+ var validateOpts = require("./validate-opts");
43
+ var audit = require("./audit");
44
+ var { defineClass } = require("./framework-error");
45
+
46
+ var MailServerRegistryError = defineClass("MailServerRegistryError", { alwaysPermanent: true });
47
+
48
+ // Per-protocol RFC catalogue. Names outside these tables refuse at
49
+ // register() unless `allowExperimental: true`.
50
+ var IMAP_VERBS = Object.freeze({
51
+ CAPABILITY: 1, NOOP: 1, LOGOUT: 1, STARTTLS: 1, AUTHENTICATE: 1,
52
+ LOGIN: 1, ID: 1, ENABLE: 1, SELECT: 1, EXAMINE: 1, CREATE: 1,
53
+ DELETE: 1, RENAME: 1, SUBSCRIBE: 1, UNSUBSCRIBE: 1, LIST: 1,
54
+ NAMESPACE: 1, STATUS: 1, APPEND: 1, IDLE: 1, CHECK: 1, CLOSE: 1,
55
+ UNSELECT: 1, EXPUNGE: 1, SEARCH: 1, FETCH: 1, STORE: 1, COPY: 1,
56
+ MOVE: 1, UID: 1, DONE: 1,
57
+ });
58
+
59
+ var MANAGESIEVE_VERBS = Object.freeze({
60
+ AUTHENTICATE: 1, STARTTLS: 1, LOGOUT: 1, CAPABILITY: 1, HAVESPACE: 1,
61
+ PUTSCRIPT: 1, LISTSCRIPTS: 1, SETACTIVE: 1, GETSCRIPT: 1,
62
+ DELETESCRIPT: 1, RENAMESCRIPT: 1, NOOP: 1,
63
+ });
64
+
65
+ // JMAP method names match the `Type/verb` shape. The catalogue table
66
+ // enumerates the RFC 8620 + RFC 8621 set; operator-registered types
67
+ // must opt in via `allowExperimental: true` with audit emission.
68
+ var JMAP_METHODS = Object.freeze({
69
+ "Core/echo": 1,
70
+ "Mailbox/get": 1, "Mailbox/changes": 1, "Mailbox/query": 1,
71
+ "Mailbox/queryChanges": 1, "Mailbox/set": 1,
72
+ "Thread/get": 1, "Thread/changes": 1,
73
+ "Email/get": 1, "Email/changes": 1, "Email/query": 1,
74
+ "Email/queryChanges": 1, "Email/set": 1, "Email/copy": 1,
75
+ "Email/import": 1, "Email/parse": 1,
76
+ "SearchSnippet/get": 1,
77
+ "Identity/get": 1, "Identity/changes": 1, "Identity/set": 1,
78
+ "EmailSubmission/get": 1, "EmailSubmission/changes": 1,
79
+ "EmailSubmission/query": 1, "EmailSubmission/queryChanges": 1,
80
+ "EmailSubmission/set": 1,
81
+ "VacationResponse/get": 1, "VacationResponse/set": 1,
82
+ });
83
+
84
+ var CATALOGUE = Object.freeze({
85
+ imap: IMAP_VERBS,
86
+ jmap: JMAP_METHODS,
87
+ managesieve: MANAGESIEVE_VERBS,
88
+ });
89
+
90
+ // Maximum resource budget caps. Operators cannot raise above these
91
+ // even with explicit values — protects against accidental
92
+ // configuration that lifts the CVE-2024-34055 / CVE-2026-26312 OOM
93
+ // ceiling.
94
+ var MAX_HANDLER_BYTES_CEILING = 256 * 1024 * 1024; // allow:raw-byte-literal — 256 MiB per-handler ceiling
95
+ var MAX_HANDLER_MS_CEILING = 5 * 60 * 1000; // allow:raw-time-literal — 5 minute per-handler ceiling
96
+
97
+ /**
98
+ * @primitive b.mail.serverRegistry.create
99
+ * @signature b.mail.serverRegistry.create(opts)
100
+ * @since 0.10.11
101
+ * @status stable
102
+ * @related b.mail.server.imap.create, b.mail.server.jmap.create, b.mail.server.managesieve.create
103
+ *
104
+ * Build a per-method dispatch registry for one of the mail-server
105
+ * listeners. Returns `{ register, unregister, dispatch, list,
106
+ * has, source, MailServerRegistryError }`.
107
+ *
108
+ * @opts
109
+ * protocol: "imap" | "jmap" | "managesieve",
110
+ * defaults: { [name]: { fn, maxHandlerBytes, maxHandlerMs } },
111
+ * overrides: { [name]: { fn, maxHandlerBytes, maxHandlerMs } },
112
+ * notFoundHandler: function (name, ctx), // optional; returns the protocol's "not configured" reply
113
+ *
114
+ * @example
115
+ * var reg = b.mail.serverRegistry.create({
116
+ * protocol: "imap",
117
+ * defaults: { CAPABILITY: { fn: _capabilityHandler,
118
+ * maxHandlerBytes: 8 * 1024,
119
+ * maxHandlerMs: 5 * 1000 } },
120
+ * overrides: opts.overrides || {},
121
+ * });
122
+ * await reg.dispatch("CAPABILITY", state, socket, parsed);
123
+ */
124
+ function create(opts) {
125
+ validateOpts.requireObject(opts, "b.mail.serverRegistry.create",
126
+ MailServerRegistryError, "mail-server-registry/bad-opts");
127
+ validateOpts.requireNonEmptyString(opts.protocol,
128
+ "b.mail.serverRegistry.create: protocol", MailServerRegistryError,
129
+ "mail-server-registry/bad-protocol");
130
+ if (!CATALOGUE[opts.protocol]) {
131
+ throw new MailServerRegistryError("mail-server-registry/unknown-protocol",
132
+ "create: protocol must be 'imap', 'jmap', or 'managesieve' (got '" + opts.protocol + "')");
133
+ }
134
+ var catalogue = CATALOGUE[opts.protocol];
135
+ var entries = Object.create(null);
136
+
137
+ function _validateEntry(name, entry, source) {
138
+ if (!entry || typeof entry !== "object") {
139
+ throw new MailServerRegistryError("mail-server-registry/bad-entry",
140
+ "register: entry for '" + name + "' must be an object");
141
+ }
142
+ if (typeof entry.fn !== "function") {
143
+ throw new MailServerRegistryError("mail-server-registry/bad-handler-fn",
144
+ "register: entry.fn for '" + name + "' must be a function");
145
+ }
146
+ if (typeof entry.maxHandlerBytes !== "number" || !isFinite(entry.maxHandlerBytes) ||
147
+ entry.maxHandlerBytes < 1 || entry.maxHandlerBytes > MAX_HANDLER_BYTES_CEILING ||
148
+ Math.floor(entry.maxHandlerBytes) !== entry.maxHandlerBytes) {
149
+ throw new MailServerRegistryError("mail-server-registry/bad-max-handler-bytes",
150
+ "register: '" + name + "' entry.maxHandlerBytes must be a positive integer ≤ " +
151
+ MAX_HANDLER_BYTES_CEILING + " (got " + entry.maxHandlerBytes + ") — stricter-mode " +
152
+ "registration refuses entries without an explicit budget (defends CVE-2024-34055 / " +
153
+ "CVE-2026-26312 OOM class)");
154
+ }
155
+ if (typeof entry.maxHandlerMs !== "number" || !isFinite(entry.maxHandlerMs) ||
156
+ entry.maxHandlerMs < 1 || entry.maxHandlerMs > MAX_HANDLER_MS_CEILING ||
157
+ Math.floor(entry.maxHandlerMs) !== entry.maxHandlerMs) {
158
+ throw new MailServerRegistryError("mail-server-registry/bad-max-handler-ms",
159
+ "register: '" + name + "' entry.maxHandlerMs must be a positive integer ≤ " +
160
+ MAX_HANDLER_MS_CEILING + " (got " + entry.maxHandlerMs + ")");
161
+ }
162
+ if (!catalogue[name] && entry.allowExperimental !== true) {
163
+ throw new MailServerRegistryError("mail-server-registry/unknown-method",
164
+ "register: '" + name + "' is not in the " + opts.protocol + " catalogue; pass " +
165
+ "allowExperimental: true to opt out of the catalogue gate (audited)");
166
+ }
167
+ if (entry.allowExperimental === true && !catalogue[name]) {
168
+ try {
169
+ audit.safeEmit({
170
+ action: "mail.serverRegistry.experimental_registration",
171
+ outcome: "denied",
172
+ metadata: { protocol: opts.protocol, name: name, source: source,
173
+ severity: "warning" },
174
+ });
175
+ } catch (_e) { /* drop-silent */ }
176
+ }
177
+ }
178
+
179
+ function register(name, entry) {
180
+ if (typeof name !== "string" || name.length === 0) {
181
+ throw new MailServerRegistryError("mail-server-registry/bad-name",
182
+ "register: name must be a non-empty string");
183
+ }
184
+ var source = entry && entry.source === "operator-override"
185
+ ? "operator-override" : "operator-override"; // user-facing register defaults to override
186
+ _validateEntry(name, entry, source);
187
+ entries[name] = {
188
+ fn: entry.fn,
189
+ maxHandlerBytes: entry.maxHandlerBytes,
190
+ maxHandlerMs: entry.maxHandlerMs,
191
+ source: source,
192
+ allowExperimental: entry.allowExperimental === true,
193
+ };
194
+ }
195
+
196
+ function _internalRegister(name, entry, source) {
197
+ _validateEntry(name, entry, source);
198
+ entries[name] = {
199
+ fn: entry.fn,
200
+ maxHandlerBytes: entry.maxHandlerBytes,
201
+ maxHandlerMs: entry.maxHandlerMs,
202
+ source: source,
203
+ allowExperimental: entry.allowExperimental === true,
204
+ };
205
+ }
206
+
207
+ // Seed defaults first, then operator overrides shadow them.
208
+ if (opts.defaults && typeof opts.defaults === "object") {
209
+ var dnames = Object.keys(opts.defaults);
210
+ for (var di = 0; di < dnames.length; di += 1) {
211
+ _internalRegister(dnames[di], opts.defaults[dnames[di]], "builtin");
212
+ }
213
+ }
214
+ if (opts.overrides && typeof opts.overrides === "object") {
215
+ var onames = Object.keys(opts.overrides);
216
+ for (var oi = 0; oi < onames.length; oi += 1) {
217
+ _internalRegister(onames[oi], opts.overrides[onames[oi]], "operator-override");
218
+ }
219
+ }
220
+
221
+ function unregister(name) {
222
+ if (entries[name]) {
223
+ delete entries[name];
224
+ return true;
225
+ }
226
+ return false;
227
+ }
228
+
229
+ function has(name) { return entries[name] !== undefined; }
230
+ function source(name) { return entries[name] ? entries[name].source : null; }
231
+
232
+ function list() {
233
+ var out = [];
234
+ var names = Object.keys(entries).sort(); // allow:bare-canonicalize-walk — deterministic output ordering
235
+ for (var i = 0; i < names.length; i += 1) {
236
+ out.push({
237
+ name: names[i],
238
+ source: entries[names[i]].source,
239
+ hasBudget: true,
240
+ });
241
+ }
242
+ return out;
243
+ }
244
+
245
+ /**
246
+ * Dispatch a registered method. `name` is the per-protocol verb /
247
+ * method-name; `args` is variadic forwarded to the handler. The
248
+ * registry wraps the handler in `safeAsync.withTimeout` so a
249
+ * runaway handler can't pin the connection. On not-found, returns
250
+ * the protocol's `notFoundHandler` result (or throws if none).
251
+ */
252
+ function dispatch(name) {
253
+ var argsArr = Array.prototype.slice.call(arguments, 1);
254
+ var entry = entries[name];
255
+ if (!entry) {
256
+ if (typeof opts.notFoundHandler === "function") {
257
+ return opts.notFoundHandler.apply(null, [name].concat(argsArr));
258
+ }
259
+ throw new MailServerRegistryError("mail-server-registry/not-configured",
260
+ "dispatch: '" + name + "' has no registered handler (" + opts.protocol +
261
+ " protocol; supply via opts.defaults or opts.overrides)");
262
+ }
263
+ var t0 = Date.now();
264
+ try {
265
+ audit.safeEmit({
266
+ action: "mail.serverRegistry.method_dispatch",
267
+ outcome: "success",
268
+ metadata: { protocol: opts.protocol, name: name, source: entry.source },
269
+ });
270
+ } catch (_e) { /* drop-silent */ }
271
+ var result;
272
+ try { result = entry.fn.apply(null, argsArr); }
273
+ catch (err) {
274
+ throw new MailServerRegistryError("mail-server-registry/handler-threw",
275
+ "dispatch: '" + name + "' handler threw (" + ((err && err.message) || String(err)) + ")");
276
+ }
277
+ // Wrap promise-returning handlers in safeAsync.withTimeout so a
278
+ // runaway handler can't pin the connection past maxHandlerMs.
279
+ // safeAsync raises its own `async/timeout` error; map it into a
280
+ // typed MailServerRegistryError so the listener catch path sees a
281
+ // single error class.
282
+ if (result && typeof result.then === "function") {
283
+ var timeoutMs = entry.maxHandlerMs;
284
+ var handlerName = name;
285
+ return safeAsync.withTimeout(result, timeoutMs,
286
+ { name: opts.protocol + "/" + handlerName })
287
+ .catch(function (err) {
288
+ if (err && err.code === "async/timeout") {
289
+ throw new MailServerRegistryError("mail-server-registry/handler-timeout",
290
+ "dispatch: '" + handlerName + "' exceeded maxHandlerMs=" + timeoutMs + " (" +
291
+ (Date.now() - t0) + "ms elapsed)");
292
+ }
293
+ throw err;
294
+ });
295
+ }
296
+ return result;
297
+ }
298
+
299
+ return {
300
+ register: register,
301
+ unregister: unregister,
302
+ dispatch: dispatch,
303
+ list: list,
304
+ has: has,
305
+ source: source,
306
+ protocol: opts.protocol,
307
+ MailServerRegistryError: MailServerRegistryError,
308
+ };
309
+ }
310
+
311
+ module.exports = {
312
+ create: create,
313
+ CATALOGUE: CATALOGUE,
314
+ IMAP_VERBS: Object.keys(IMAP_VERBS),
315
+ JMAP_METHODS: Object.keys(JMAP_METHODS),
316
+ MANAGESIEVE_VERBS: Object.keys(MANAGESIEVE_VERBS),
317
+ MailServerRegistryError: MailServerRegistryError,
318
+ };
package/lib/mcp.js CHANGED
@@ -192,8 +192,18 @@ function _checkRedirectUri(uri, allowlist, errorClass) {
192
192
  "mcp: redirect_uri did not parse");
193
193
  }
194
194
  var isHttps = parsed.protocol === "https:";
195
- var isLocal = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" ||
196
- parsed.hostname === "::1";
195
+ // Strip the trailing root-zone dot before the reserved-name compare.
196
+ // RFC 1034 §3.1 `localhost.` is the absolute form of `localhost`
197
+ // and resolves to the same target; without the strip, an attacker
198
+ // who supplies `localhost.` as the redirect_uri host slips past the
199
+ // local-allow path (the URL parser preserves the dot, the equality
200
+ // check fails, the URI gets routed as cleartext non-local — RFC 9700
201
+ // §4.1.1 bypass).
202
+ var rawHost = parsed.hostname || "";
203
+ while (rawHost.length > 0 && rawHost.charAt(rawHost.length - 1) === ".") {
204
+ rawHost = rawHost.slice(0, -1);
205
+ }
206
+ var isLocal = rawHost === "localhost" || rawHost === "127.0.0.1" || rawHost === "::1";
197
207
  if (!isHttps && !isLocal) {
198
208
  throw errorClass.factory("INSECURE_REDIRECT_URI",
199
209
  "mcp: redirect_uri must be HTTPS (or localhost; RFC 9700 sec 4.1.1)");
@@ -54,6 +54,15 @@ var LOCAL_SUFFIXES = [".localhost", ".local", ".test", ".invalid",
54
54
  ".internal", ".intranet", ".lan", ".home", ".corp"];
55
55
  function _isLocalFormHost(host) {
56
56
  if (typeof host !== "string" || host.length === 0) return true;
57
+ // Strip the trailing root-zone dot BEFORE any reserved-name compare.
58
+ // RFC 1034 §3.1 — `foo.` is the absolute form of `foo` (both resolve
59
+ // to the same target). Without the strip, `localhost.` would slip
60
+ // past the reserved-form check and reach a public DoH/DoT provider
61
+ // that maps it to NXDOMAIN, which downstream consumers might then
62
+ // try to resolve via system fallback.
63
+ while (host.length > 0 && host.charAt(host.length - 1) === ".") {
64
+ host = host.slice(0, -1);
65
+ }
57
66
  if (host === "localhost") return true;
58
67
  // IP literal — skip DNS resolution entirely (caller passes through).
59
68
  if (net.isIP(host)) return true;
package/lib/pagination.js CHANGED
@@ -87,7 +87,8 @@ function _b64urlEncode(buf) { return bCrypto.toBase64Url(buf); }
87
87
 
88
88
  function _b64urlDecode(s) {
89
89
  if (typeof s !== "string") throw new PaginationError("pagination/bad-cursor", "cursor must be a string");
90
- return bCrypto.fromBase64Url(s);
90
+ try { return bCrypto.fromBase64Url(s); }
91
+ catch (_e) { throw new PaginationError("pagination/bad-cursor", "cursor is not valid base64url"); }
91
92
  }
92
93
 
93
94
  function _tag(secretBuf, stateJson) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.10.9",
3
+ "version": "0.10.11",
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.6",
5
- "serialNumber": "urn:uuid:d3396b9d-5788-4743-8236-73318b661112",
5
+ "serialNumber": "urn:uuid:2bcbf942-6319-4d65-a08c-1e385ac35198",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-18T01:44:25.642Z",
8
+ "timestamp": "2026-05-18T04:40:34.689Z",
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.10.9",
22
+ "bom-ref": "@blamejs/core@0.10.11",
23
23
  "type": "library",
24
24
  "name": "blamejs",
25
- "version": "0.10.9",
25
+ "version": "0.10.11",
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.10.9",
29
+ "purl": "pkg:npm/%40blamejs/core@0.10.11",
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.10.9",
57
+ "ref": "@blamejs/core@0.10.11",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]