@blamejs/core 0.10.9 → 0.10.12
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 +3 -0
- package/index.js +9 -0
- package/lib/audit.js +1 -0
- package/lib/auth/dpop.js +5 -1
- package/lib/auth/fido-mds3.js +19 -3
- package/lib/auth/jwt.js +2 -1
- package/lib/auth/oauth.js +17 -5
- package/lib/auth/status-list.js +7 -1
- package/lib/crypto-hpke-pq.js +187 -0
- package/lib/crypto.js +11 -3
- package/lib/guard-list-id.js +6 -1
- package/lib/jose-jwe-experimental.js +228 -0
- package/lib/mail-server-imap.js +140 -29
- package/lib/mail-server-jmap.js +44 -4
- package/lib/mail-server-managesieve.js +72 -17
- package/lib/mail-server-pop3.js +35 -0
- package/lib/mail-server-registry.js +363 -0
- package/lib/mail-server-submission.js +33 -0
- package/lib/mcp.js +12 -2
- package/lib/network-dns.js +9 -0
- package/lib/pagination.js +2 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/mail-server-pop3.js
CHANGED
|
@@ -196,6 +196,38 @@ function create(opts) {
|
|
|
196
196
|
var profile = opts.profile || "strict";
|
|
197
197
|
var authConfig = opts.auth || null;
|
|
198
198
|
var mailStore = opts.mailStore;
|
|
199
|
+
// b.agent.tenant adoption (v0.10.12) — cross-tenant authentication
|
|
200
|
+
// is refused at the AUTH-success boundary BEFORE the listener
|
|
201
|
+
// accepts the actor into transaction state. The scope's `.check`
|
|
202
|
+
// method is validated at create() time so a malformed scope object
|
|
203
|
+
// surfaces as a configuration error rather than rejecting every
|
|
204
|
+
// otherwise-valid auth as "cross-tenant".
|
|
205
|
+
var tenantScope = opts.tenantScope || null;
|
|
206
|
+
var agentTenantId = opts.agentTenantId || null;
|
|
207
|
+
if (tenantScope && typeof tenantScope.check !== "function") {
|
|
208
|
+
throw new MailServerPop3Error("mail-server-pop3/bad-tenant-scope",
|
|
209
|
+
"create: opts.tenantScope must be a b.agent.tenant.create() instance " +
|
|
210
|
+
"(missing .check); a malformed scope would refuse every auth as cross-tenant");
|
|
211
|
+
}
|
|
212
|
+
if (tenantScope && !agentTenantId) {
|
|
213
|
+
throw new MailServerPop3Error("mail-server-pop3/no-agent-tenant-id",
|
|
214
|
+
"create: opts.tenantScope requires opts.agentTenantId");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function _assertTenantOrRefuse(state, socket, result) {
|
|
218
|
+
if (!tenantScope || !agentTenantId) return true;
|
|
219
|
+
try { tenantScope.check(result.actor, agentTenantId); return true; }
|
|
220
|
+
catch (tenantErr) {
|
|
221
|
+
_emit("mail.server.pop3.cross_tenant_refused",
|
|
222
|
+
{ connectionId: state.id,
|
|
223
|
+
actorTenant: (result.actor && result.actor.tenantId) || null,
|
|
224
|
+
agentTenant: agentTenantId,
|
|
225
|
+
code: (tenantErr && tenantErr.code) || null },
|
|
226
|
+
"denied");
|
|
227
|
+
_writeErr(socket, "Authentication rejected (cross-tenant)");
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
199
231
|
|
|
200
232
|
var rateLimit;
|
|
201
233
|
if (opts.rateLimit === false) {
|
|
@@ -467,6 +499,7 @@ function create(opts) {
|
|
|
467
499
|
})
|
|
468
500
|
.then(function (result) {
|
|
469
501
|
if (result && result.ok && result.actor) {
|
|
502
|
+
if (!_assertTenantOrRefuse(state, socket, result)) return;
|
|
470
503
|
state.actor = result.actor;
|
|
471
504
|
_enterTransaction(state, socket, "PASS");
|
|
472
505
|
return;
|
|
@@ -527,6 +560,7 @@ function create(opts) {
|
|
|
527
560
|
})
|
|
528
561
|
.then(function (result) {
|
|
529
562
|
if (result && result.ok && result.actor) {
|
|
563
|
+
if (!_assertTenantOrRefuse(state, socket, result)) return;
|
|
530
564
|
state.actor = result.actor;
|
|
531
565
|
_enterTransaction(state, socket, "APOP");
|
|
532
566
|
return;
|
|
@@ -595,6 +629,7 @@ function create(opts) {
|
|
|
595
629
|
})
|
|
596
630
|
.then(function (result) {
|
|
597
631
|
if (result && result.ok && result.actor) {
|
|
632
|
+
if (!_assertTenantOrRefuse(state, socket, result)) return;
|
|
598
633
|
state.actor = result.actor;
|
|
599
634
|
_enterTransaction(state, socket, "AUTH/" + mech);
|
|
600
635
|
return;
|
|
@@ -0,0 +1,363 @@
|
|
|
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
|
+
// Tenant scope (v0.10.12 — b.agent.tenant adoption).
|
|
135
|
+
// When `opts.tenantScope` is supplied alongside `opts.agentTenantId`,
|
|
136
|
+
// every dispatch first gates on `tenantScope.check(actor,
|
|
137
|
+
// agentTenantId)`. Actor without matching tenantId surfaces as a
|
|
138
|
+
// typed `agent-tenant/cross-tenant-access-refused` per the v0.9.25
|
|
139
|
+
// contract — the listener's catch path converts that into the
|
|
140
|
+
// protocol's `BAD AUTH` / `NO not authorized` reply.
|
|
141
|
+
//
|
|
142
|
+
// Optional: when omitted, dispatch behaves identically to v0.10.11
|
|
143
|
+
// (no per-tenant gate; operators that don't run multi-tenant don't
|
|
144
|
+
// pay the check cost).
|
|
145
|
+
var tenantScope = opts.tenantScope || null;
|
|
146
|
+
var agentTenantId = opts.agentTenantId || null;
|
|
147
|
+
if (tenantScope && typeof tenantScope.check !== "function") {
|
|
148
|
+
throw new MailServerRegistryError("mail-server-registry/bad-tenant-scope",
|
|
149
|
+
"create: opts.tenantScope must be a b.agent.tenant.create() instance (missing .check)");
|
|
150
|
+
}
|
|
151
|
+
if (tenantScope && !agentTenantId) {
|
|
152
|
+
throw new MailServerRegistryError("mail-server-registry/no-agent-tenant-id",
|
|
153
|
+
"create: opts.tenantScope requires opts.agentTenantId (the tenant this listener serves)");
|
|
154
|
+
}
|
|
155
|
+
var catalogue = CATALOGUE[opts.protocol];
|
|
156
|
+
var entries = Object.create(null);
|
|
157
|
+
|
|
158
|
+
function _validateEntry(name, entry, source) {
|
|
159
|
+
if (!entry || typeof entry !== "object") {
|
|
160
|
+
throw new MailServerRegistryError("mail-server-registry/bad-entry",
|
|
161
|
+
"register: entry for '" + name + "' must be an object");
|
|
162
|
+
}
|
|
163
|
+
if (typeof entry.fn !== "function") {
|
|
164
|
+
throw new MailServerRegistryError("mail-server-registry/bad-handler-fn",
|
|
165
|
+
"register: entry.fn for '" + name + "' must be a function");
|
|
166
|
+
}
|
|
167
|
+
if (typeof entry.maxHandlerBytes !== "number" || !isFinite(entry.maxHandlerBytes) ||
|
|
168
|
+
entry.maxHandlerBytes < 1 || entry.maxHandlerBytes > MAX_HANDLER_BYTES_CEILING ||
|
|
169
|
+
Math.floor(entry.maxHandlerBytes) !== entry.maxHandlerBytes) {
|
|
170
|
+
throw new MailServerRegistryError("mail-server-registry/bad-max-handler-bytes",
|
|
171
|
+
"register: '" + name + "' entry.maxHandlerBytes must be a positive integer ≤ " +
|
|
172
|
+
MAX_HANDLER_BYTES_CEILING + " (got " + entry.maxHandlerBytes + ") — stricter-mode " +
|
|
173
|
+
"registration refuses entries without an explicit budget (defends CVE-2024-34055 / " +
|
|
174
|
+
"CVE-2026-26312 OOM class)");
|
|
175
|
+
}
|
|
176
|
+
if (typeof entry.maxHandlerMs !== "number" || !isFinite(entry.maxHandlerMs) ||
|
|
177
|
+
entry.maxHandlerMs < 1 || entry.maxHandlerMs > MAX_HANDLER_MS_CEILING ||
|
|
178
|
+
Math.floor(entry.maxHandlerMs) !== entry.maxHandlerMs) {
|
|
179
|
+
throw new MailServerRegistryError("mail-server-registry/bad-max-handler-ms",
|
|
180
|
+
"register: '" + name + "' entry.maxHandlerMs must be a positive integer ≤ " +
|
|
181
|
+
MAX_HANDLER_MS_CEILING + " (got " + entry.maxHandlerMs + ")");
|
|
182
|
+
}
|
|
183
|
+
if (!catalogue[name] && entry.allowExperimental !== true) {
|
|
184
|
+
throw new MailServerRegistryError("mail-server-registry/unknown-method",
|
|
185
|
+
"register: '" + name + "' is not in the " + opts.protocol + " catalogue; pass " +
|
|
186
|
+
"allowExperimental: true to opt out of the catalogue gate (audited)");
|
|
187
|
+
}
|
|
188
|
+
if (entry.allowExperimental === true && !catalogue[name]) {
|
|
189
|
+
try {
|
|
190
|
+
audit.safeEmit({
|
|
191
|
+
action: "mail.serverRegistry.experimental_registration",
|
|
192
|
+
outcome: "denied",
|
|
193
|
+
metadata: { protocol: opts.protocol, name: name, source: source,
|
|
194
|
+
severity: "warning" },
|
|
195
|
+
});
|
|
196
|
+
} catch (_e) { /* drop-silent */ }
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function register(name, entry) {
|
|
201
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
202
|
+
throw new MailServerRegistryError("mail-server-registry/bad-name",
|
|
203
|
+
"register: name must be a non-empty string");
|
|
204
|
+
}
|
|
205
|
+
var source = entry && entry.source === "operator-override"
|
|
206
|
+
? "operator-override" : "operator-override"; // user-facing register defaults to override
|
|
207
|
+
_validateEntry(name, entry, source);
|
|
208
|
+
entries[name] = {
|
|
209
|
+
fn: entry.fn,
|
|
210
|
+
maxHandlerBytes: entry.maxHandlerBytes,
|
|
211
|
+
maxHandlerMs: entry.maxHandlerMs,
|
|
212
|
+
source: source,
|
|
213
|
+
allowExperimental: entry.allowExperimental === true,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function _internalRegister(name, entry, source) {
|
|
218
|
+
_validateEntry(name, entry, source);
|
|
219
|
+
entries[name] = {
|
|
220
|
+
fn: entry.fn,
|
|
221
|
+
maxHandlerBytes: entry.maxHandlerBytes,
|
|
222
|
+
maxHandlerMs: entry.maxHandlerMs,
|
|
223
|
+
source: source,
|
|
224
|
+
allowExperimental: entry.allowExperimental === true,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Seed defaults first, then operator overrides shadow them.
|
|
229
|
+
if (opts.defaults && typeof opts.defaults === "object") {
|
|
230
|
+
var dnames = Object.keys(opts.defaults);
|
|
231
|
+
for (var di = 0; di < dnames.length; di += 1) {
|
|
232
|
+
_internalRegister(dnames[di], opts.defaults[dnames[di]], "builtin");
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (opts.overrides && typeof opts.overrides === "object") {
|
|
236
|
+
var onames = Object.keys(opts.overrides);
|
|
237
|
+
for (var oi = 0; oi < onames.length; oi += 1) {
|
|
238
|
+
_internalRegister(onames[oi], opts.overrides[onames[oi]], "operator-override");
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function unregister(name) {
|
|
243
|
+
if (entries[name]) {
|
|
244
|
+
delete entries[name];
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function has(name) { return entries[name] !== undefined; }
|
|
251
|
+
function source(name) { return entries[name] ? entries[name].source : null; }
|
|
252
|
+
|
|
253
|
+
function list() {
|
|
254
|
+
var out = [];
|
|
255
|
+
var names = Object.keys(entries).sort(); // allow:bare-canonicalize-walk — deterministic output ordering
|
|
256
|
+
for (var i = 0; i < names.length; i += 1) {
|
|
257
|
+
out.push({
|
|
258
|
+
name: names[i],
|
|
259
|
+
source: entries[names[i]].source,
|
|
260
|
+
hasBudget: true,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
return out;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Dispatch a registered method. `name` is the per-protocol verb /
|
|
268
|
+
* method-name; `args` is variadic forwarded to the handler. The
|
|
269
|
+
* registry wraps the handler in `safeAsync.withTimeout` so a
|
|
270
|
+
* runaway handler can't pin the connection. On not-found, returns
|
|
271
|
+
* the protocol's `notFoundHandler` result (or throws if none).
|
|
272
|
+
*/
|
|
273
|
+
function dispatch(name) {
|
|
274
|
+
var argsArr = Array.prototype.slice.call(arguments, 1);
|
|
275
|
+
// Tenant scope check — pre-dispatch, pre-guard, pre-audit.
|
|
276
|
+
//
|
|
277
|
+
// Two argument shapes occur across the three listeners:
|
|
278
|
+
// - IMAP / ManageSieve dispatch with `(state, socket, parsed)` —
|
|
279
|
+
// state.actor is the actor.
|
|
280
|
+
// - JMAP dispatches with `(actor, resolvedArgs, ctx)` — the
|
|
281
|
+
// first argument IS the actor object directly.
|
|
282
|
+
//
|
|
283
|
+
// Detect both shapes: if argsArr[0].actor exists, use it; else if
|
|
284
|
+
// argsArr[0] itself carries a `tenantId` field, treat it as the
|
|
285
|
+
// actor. The dispatch shapes are documented at the listener
|
|
286
|
+
// factory layer; the registry's job here is uniform enforcement.
|
|
287
|
+
if (tenantScope && argsArr.length > 0 && argsArr[0]) {
|
|
288
|
+
var actor = argsArr[0].actor ||
|
|
289
|
+
(typeof argsArr[0] === "object" && argsArr[0] !== null &&
|
|
290
|
+
Object.prototype.hasOwnProperty.call(argsArr[0], "tenantId")
|
|
291
|
+
? argsArr[0] : null);
|
|
292
|
+
if (actor) {
|
|
293
|
+
// tenantScope.check throws AgentTenantError on cross-tenant;
|
|
294
|
+
// we let the typed error propagate so the listener's
|
|
295
|
+
// catch-path converts it to the protocol's refusal reply.
|
|
296
|
+
tenantScope.check(actor, agentTenantId);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
var entry = entries[name];
|
|
300
|
+
if (!entry) {
|
|
301
|
+
if (typeof opts.notFoundHandler === "function") {
|
|
302
|
+
return opts.notFoundHandler.apply(null, [name].concat(argsArr));
|
|
303
|
+
}
|
|
304
|
+
throw new MailServerRegistryError("mail-server-registry/not-configured",
|
|
305
|
+
"dispatch: '" + name + "' has no registered handler (" + opts.protocol +
|
|
306
|
+
" protocol; supply via opts.defaults or opts.overrides)");
|
|
307
|
+
}
|
|
308
|
+
var t0 = Date.now();
|
|
309
|
+
try {
|
|
310
|
+
audit.safeEmit({
|
|
311
|
+
action: "mail.serverRegistry.method_dispatch",
|
|
312
|
+
outcome: "success",
|
|
313
|
+
metadata: { protocol: opts.protocol, name: name, source: entry.source },
|
|
314
|
+
});
|
|
315
|
+
} catch (_e) { /* drop-silent */ }
|
|
316
|
+
var result;
|
|
317
|
+
try { result = entry.fn.apply(null, argsArr); }
|
|
318
|
+
catch (err) {
|
|
319
|
+
throw new MailServerRegistryError("mail-server-registry/handler-threw",
|
|
320
|
+
"dispatch: '" + name + "' handler threw (" + ((err && err.message) || String(err)) + ")");
|
|
321
|
+
}
|
|
322
|
+
// Wrap promise-returning handlers in safeAsync.withTimeout so a
|
|
323
|
+
// runaway handler can't pin the connection past maxHandlerMs.
|
|
324
|
+
// safeAsync raises its own `async/timeout` error; map it into a
|
|
325
|
+
// typed MailServerRegistryError so the listener catch path sees a
|
|
326
|
+
// single error class.
|
|
327
|
+
if (result && typeof result.then === "function") {
|
|
328
|
+
var timeoutMs = entry.maxHandlerMs;
|
|
329
|
+
var handlerName = name;
|
|
330
|
+
return safeAsync.withTimeout(result, timeoutMs,
|
|
331
|
+
{ name: opts.protocol + "/" + handlerName })
|
|
332
|
+
.catch(function (err) {
|
|
333
|
+
if (err && err.code === "async/timeout") {
|
|
334
|
+
throw new MailServerRegistryError("mail-server-registry/handler-timeout",
|
|
335
|
+
"dispatch: '" + handlerName + "' exceeded maxHandlerMs=" + timeoutMs + " (" +
|
|
336
|
+
(Date.now() - t0) + "ms elapsed)");
|
|
337
|
+
}
|
|
338
|
+
throw err;
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
return result;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
register: register,
|
|
346
|
+
unregister: unregister,
|
|
347
|
+
dispatch: dispatch,
|
|
348
|
+
list: list,
|
|
349
|
+
has: has,
|
|
350
|
+
source: source,
|
|
351
|
+
protocol: opts.protocol,
|
|
352
|
+
MailServerRegistryError: MailServerRegistryError,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
module.exports = {
|
|
357
|
+
create: create,
|
|
358
|
+
CATALOGUE: CATALOGUE,
|
|
359
|
+
IMAP_VERBS: Object.keys(IMAP_VERBS),
|
|
360
|
+
JMAP_METHODS: Object.keys(JMAP_METHODS),
|
|
361
|
+
MANAGESIEVE_VERBS: Object.keys(MANAGESIEVE_VERBS),
|
|
362
|
+
MailServerRegistryError: MailServerRegistryError,
|
|
363
|
+
};
|
|
@@ -266,6 +266,18 @@ function create(opts) {
|
|
|
266
266
|
throw new MailServerSubmissionError("mail-server-submission/no-tls-context",
|
|
267
267
|
"mail.server.submission.create: tlsContext is required");
|
|
268
268
|
}
|
|
269
|
+
// b.agent.tenant shape validation at create() time — a malformed
|
|
270
|
+
// scope object would refuse every auth as cross-tenant, masking the
|
|
271
|
+
// configuration error as an auth outage.
|
|
272
|
+
if (opts.tenantScope && typeof opts.tenantScope.check !== "function") {
|
|
273
|
+
throw new MailServerSubmissionError("mail-server-submission/bad-tenant-scope",
|
|
274
|
+
"create: opts.tenantScope must be a b.agent.tenant.create() instance " +
|
|
275
|
+
"(missing .check); a malformed scope would refuse every auth as cross-tenant");
|
|
276
|
+
}
|
|
277
|
+
if (opts.tenantScope && !opts.agentTenantId) {
|
|
278
|
+
throw new MailServerSubmissionError("mail-server-submission/no-agent-tenant-id",
|
|
279
|
+
"create: opts.tenantScope requires opts.agentTenantId");
|
|
280
|
+
}
|
|
269
281
|
numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
|
|
270
282
|
["maxLineBytes", "maxMessageBytes", "maxRcptsPerMessage", "idleTimeoutMs"],
|
|
271
283
|
"mail.server.submission.", MailServerSubmissionError, "mail-server-submission/bad-bound");
|
|
@@ -742,6 +754,27 @@ function create(opts) {
|
|
|
742
754
|
// successful verify, not whatever state.authPending happens
|
|
743
755
|
// to be at the post-null read (which is always null).
|
|
744
756
|
var successfulMechanism = state.authPending && state.authPending.mechanism;
|
|
757
|
+
// b.agent.tenant gate (v0.10.12). When the listener is
|
|
758
|
+
// wired with `opts.tenantScope` + `opts.agentTenantId`,
|
|
759
|
+
// every authenticated actor must belong to the listener's
|
|
760
|
+
// tenant. Cross-tenant authentication surfaces here as a
|
|
761
|
+
// `535 5.7.0` refusal — the actor never reaches authenticated
|
|
762
|
+
// state, mail submission never begins under the wrong tenant.
|
|
763
|
+
if (opts.tenantScope && opts.agentTenantId) {
|
|
764
|
+
try { opts.tenantScope.check(result.actor, opts.agentTenantId); }
|
|
765
|
+
catch (tenantErr) {
|
|
766
|
+
state.authPending = null;
|
|
767
|
+
_emit("mail.server.submission.cross_tenant_refused",
|
|
768
|
+
{ connectionId: state.id,
|
|
769
|
+
actorTenant: (result.actor && result.actor.tenantId) || null,
|
|
770
|
+
agentTenant: opts.agentTenantId,
|
|
771
|
+
code: (tenantErr && tenantErr.code) || null },
|
|
772
|
+
"denied");
|
|
773
|
+
_writeReply(socket, REPLY_535_AUTH_FAILED,
|
|
774
|
+
"5.7.0 Authentication rejected (cross-tenant)");
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
745
778
|
state.authenticated = true;
|
|
746
779
|
state.actor = result.actor;
|
|
747
780
|
state.authPending = null;
|
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
|
-
|
|
196
|
-
|
|
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)");
|
package/lib/network-dns.js
CHANGED
|
@@ -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
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:
|
|
5
|
+
"serialNumber": "urn:uuid:c0157c5c-5ef3-45a3-aef2-004727f9fd8c",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-18T06:22:30.244Z",
|
|
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.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.10.12",
|
|
23
23
|
"type": "library",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.10.
|
|
25
|
+
"version": "0.10.12",
|
|
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.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.10.12",
|
|
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.
|
|
57
|
+
"ref": "@blamejs/core@0.10.12",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|