@blamejs/core 0.10.11 → 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 +1 -0
- package/lib/mail-server-imap.js +8 -0
- package/lib/mail-server-jmap.js +9 -3
- package/lib/mail-server-managesieve.js +4 -0
- package/lib/mail-server-pop3.js +35 -0
- package/lib/mail-server-registry.js +45 -0
- package/lib/mail-server-submission.js +33 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,7 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.10.x
|
|
10
10
|
|
|
11
|
+
- v0.10.12 (2026-05-18) — **`b.agent.tenant` adoption across the mail-server listeners.** The v0.10.11 shared `b.mail.serverRegistry` primitive gains optional `opts.tenantScope` (a `b.agent.tenant.create()` instance) + `opts.agentTenantId` (the tenant this listener serves). When supplied, every method dispatch first gates on `tenantScope.check(state.actor, agentTenantId)` BEFORE guard validation or audit emission; cross-tenant access surfaces as the v0.9.25-typed `agent-tenant/cross-tenant-access-refused` which the listener's catch-path converts to the protocol's `BAD` / `NO` refusal reply. **(a) `b.mail.server.imap.create({ tenantScope, agentTenantId })`** — IMAP dispatch is gated for every command after AUTH. **(b) `b.mail.server.jmap.create({ tenantScope, agentTenantId })`** — JMAP per-method dispatch routes through the tenant scope alongside its existing per-`accountId` isolation. **(c) `b.mail.server.managesieve.create({ tenantScope, agentTenantId })`** — ManageSieve same pattern. **(d) `b.mail.server.submission.create({ tenantScope, agentTenantId })`** — submission listener gates at the AUTH-success boundary (before `state.actor` is committed) so cross-tenant authentication surfaces as `535 5.7.0 Authentication rejected (cross-tenant)` and the SMTP envelope never begins under the wrong tenant. **(e) `b.mail.server.pop3.create({ tenantScope, agentTenantId })`** — same AUTH-success gate; cross-tenant refusal returns `-ERR Authentication rejected (cross-tenant)`. New audit events: `mail.server.submission.cross_tenant_refused` and `mail.server.pop3.cross_tenant_refused`. **Operator impact:** no breaking changes — `tenantScope` / `agentTenantId` are optional; operators not running multi-tenant see identical behavior. Operators with multi-tenant deployments wire `b.agent.tenant.create({...})` once and pass the same scope to every per-tenant listener instance — cross-tenant isolation becomes structural rather than per-handler opt-in. **Deferred to v0.10.12.1:** per-tenant `b.mailStore` seal-key derivation via `tenantScope.derivedKey(tenantId, "seal")` and per-tenant audit namespaces via `tenantScope.auditFor(tenantId)`. Today every mail listener seals through the framework primary vault key — adequate for single-tenant and multi-tenant-trusted deployments; the v0.10.12.1 follow-up adds per-tenant key separation for compromise-isolation use cases. References: [RFC 9051 IMAP4rev2 §3 state machine](https://www.rfc-editor.org/rfc/rfc9051#section-3) · [RFC 8620 JMAP Core §1.6.2 accountId](https://www.rfc-editor.org/rfc/rfc8620#section-1.6.2) · [RFC 6409 Submission §6.1 actor-to-MAIL-FROM identity binding](https://www.rfc-editor.org/rfc/rfc6409#section-6.1) · [RFC 1939 POP3 §6 transaction state](https://www.rfc-editor.org/rfc/rfc1939#section-6) · v0.9.25 `b.agent.tenant` contract.
|
|
11
12
|
- v0.10.11 (2026-05-18) — **Mail-server per-method registration sweep.** New shared primitive `b.mail.serverRegistry` (`lib/mail-server-registry.js`) replaces the hand-rolled `switch (verb)` dispatchers in the IMAP, JMAP, and ManageSieve listener factories. Operators can now override individual command / method handlers (e.g. IMAP `FETCH`, JMAP `Email/query`, ManageSieve `PUTSCRIPT`) via `opts.overrides: { NAME: { fn, maxHandlerBytes, maxHandlerMs } }` without re-implementing wire-protocol state machines or bypassing the guard substrate. **(a) Per-handler resource budgets — required at registration.** Operators MUST supply `maxHandlerBytes` (≤ 256 MiB) and `maxHandlerMs` (≤ 5 min) on every override; the registration throws `mail-server-registry/bad-max-handler-bytes` / `bad-max-handler-ms` on missing or out-of-range budgets. Defends [CVE-2024-34055](https://nvd.nist.gov/vuln/detail/CVE-2024-34055) (Cyrus authenticated OOM) and [CVE-2026-26312](https://nvd.nist.gov/vuln/detail/CVE-2026-26312) (Stalwart malformed nested `message/rfc822` cyclical OOM) by forcing operators to declare the resource ceiling explicitly. **(b) Catalogue gate.** Per-protocol method names outside the IANA / RFC catalogue refuse registration unless `allowExperimental: true` is supplied — opting in audits the registration so operators can grep for off-spec handlers. **(c) Guard chain preserved.** The listener factories run `b.guardImapCommand` / `b.guardJmap` / `b.guardManagesieveCommand` BEFORE the registry lookup; operator overrides cannot bypass the wire-protocol validation, smuggling defenses, or rate-limit budgets. **(d) Handler timeout.** Promise-returning handlers wrap through `b.safeAsync.withTimeout(maxHandlerMs)`; a runaway override raises `mail-server-registry/handler-timeout` rather than pinning the connection. **(e) Defaults seeded.** IMAP picks up 30 verbs (CAPABILITY, NOOP, LOGOUT, ID, STARTTLS, AUTHENTICATE, LOGIN, ENABLE, SELECT, EXAMINE, LIST, STATUS, NAMESPACE, APPEND, CHECK, CLOSE, UNSELECT, EXPUNGE, FETCH, STORE, UID, IDLE, DONE — plus the previously-undispatched SEARCH / CREATE / DELETE / RENAME / SUBSCRIBE / UNSUBSCRIBE / COPY / MOVE which default to `NO not-configured` until operator overrides); ManageSieve picks up 12 verbs (CAPABILITY, NOOP, STARTTLS, LOGOUT, AUTHENTICATE, HAVESPACE, PUTSCRIPT, LISTSCRIPTS, SETACTIVE, GETSCRIPT, DELETESCRIPT, RENAMESCRIPT); JMAP wraps the existing `opts.methods` map with a one-time deprecation audit (`mail.server.jmap.methods_opt_deprecated`) and routes through the same registry — operators migrate to `opts.overrides` with explicit budgets. **(f) New audit event:** `mail.serverRegistry.method_dispatch` carries `{ protocol, name, source: "builtin" | "operator-override" }` on every dispatch; `mail.serverRegistry.experimental_registration` audits opt-in off-catalogue registrations. **Operator impact:** existing JMAP `opts.methods` callers see the deprecation audit but continue to function (legacy auto-budget = 10 MiB / 30 s); existing IMAP / ManageSieve operators have no migration burden — the listener factories continue to accept the same opts shape. Operators wiring NEW overrides MUST supply explicit budgets. References: [RFC 9051 IMAP4rev2](https://www.rfc-editor.org/rfc/rfc9051) · [RFC 8620 JMAP Core](https://www.rfc-editor.org/rfc/rfc8620) · [RFC 8621 JMAP for Mail](https://www.rfc-editor.org/rfc/rfc8621) · [RFC 5804 ManageSieve](https://www.rfc-editor.org/rfc/rfc5804) · [RFC 2971 IMAP4 ID](https://www.rfc-editor.org/rfc/rfc2971) · [RFC 2177 IMAP IDLE](https://www.rfc-editor.org/rfc/rfc2177) · [CVE-2024-34055](https://nvd.nist.gov/vuln/detail/CVE-2024-34055) · [CVE-2026-26312](https://nvd.nist.gov/vuln/detail/CVE-2026-26312).
|
|
12
13
|
- v0.10.10 (2026-05-17) — **PQC envelope completion (experimental).** Two new opt-in PQC-protocol primitives behind explicit experimental namespaces. **(a) `b.jose.jwe.experimental.encrypt` / `.decrypt`** — RFC 7516 compact-serialization JWE with ML-KEM-1024 key encapsulation and XChaCha20-Poly1305 AEAD content encryption. Lives under `b.jose.jwe.experimental` because the JOSE PQC IANA codepoint registration ([draft-ietf-jose-pqc-kem-05](https://datatracker.ietf.org/doc/draft-ietf-jose-pqc-kem/)) hasn't finalized — the namespace name is the contract: codepoints may change between minors without affecting the framework's stable surface. Header carries `{ alg: "ML-KEM-1024", enc: "XC20P", "x-blamejs-experimental": true }`; decrypt refuses any envelope missing the experimental marker (defends a stable-system consumer that accidentally ingests an experimental envelope and treats it as IANA-compliant). Header bytes route through `b.safeJson.parse` for proto-pollution / depth / size defenses; header is byte-capped at 4 KiB. **(b) `b.crypto.hpke.pq.connolly.seal` / `.open` + `b.crypto.hpke.pq.wg.seal` / `.open`** — both active PQ-HPKE drafts behind explicit opt-in: [draft-connolly-cfrg-hpke-mlkem-04](https://datatracker.ietf.org/doc/draft-connolly-cfrg-hpke-mlkem/) (individual; codepoints today) and [draft-ietf-hpke-pq-03](https://datatracker.ietf.org/doc/draft-ietf-hpke-pq/) (WG-adopted). Each wrapper binds a draft-distinguishing label into the RFC 9180 §5.1 `info` parameter so cross-draft substitution (sealing under connolly and opening as wg, or vice versa) refuses by construction — the derived AEAD key diverges and Poly1305 verify fails. Both compose the existing `b.crypto.hpke.seal` / `.open` core (ML-KEM-1024 KEM + HKDF-SHA3-512 + ChaCha20-Poly1305 per project PQC-first policy); the wrappers add the draft-isolation label without touching the wire-format primitives. **New audit namespace:** `jose` (`jose.jwe.experimental.encrypt` / `.decrypt`). **Operator impact:** no breaking changes. The stable `b.crypto.hpke.seal` and the existing `b.crypto.encrypt` envelope shape are unaffected. Operators integrating against systems speaking one of the active PQ-HPKE drafts use the explicit `.pq.connolly` / `.pq.wg` paths; operators wanting IANA-final codepoints wait for graduation to the stable surface (one-minor deprecation window will ship when IANA registration lands). The framework refuses to silently pick a winner between the two drafts. **Deferred to follow-up:** COSE-PQ signatures (draft-ietf-cose-pqc-* — pending IANA codepoint registration), JWE JSON serialization variant (compact-only at this experimental tier), FIPS 203 KAT test vectors against the vendored bundle (test-corpus addition; functional parity is established by the existing v0.8.41 hybrid-KEM verify path). References: [draft-ietf-jose-pqc-kem-05](https://datatracker.ietf.org/doc/draft-ietf-jose-pqc-kem/) · [draft-connolly-cfrg-hpke-mlkem-04](https://datatracker.ietf.org/doc/draft-connolly-cfrg-hpke-mlkem/) · [draft-ietf-hpke-pq-03](https://datatracker.ietf.org/doc/draft-ietf-hpke-pq/) · [RFC 9180 HPKE](https://www.rfc-editor.org/rfc/rfc9180.html) · [RFC 7516 JWE](https://www.rfc-editor.org/rfc/rfc7516.html) · [FIPS 203 ML-KEM](https://csrc.nist.gov/pubs/fips/203/final) · [draft-irtf-cfrg-xchacha XChaCha20-Poly1305](https://datatracker.ietf.org/doc/draft-irtf-cfrg-xchacha/).
|
|
13
14
|
- v0.10.9 (2026-05-17) — **Ergonomic helpers bundle (A / B / C / D / E / H).** Six small DX primitives bundled into one release. **(a) `b.safePath.resolve` / `.resolveOrNull` / `.validate`** — path-traversal-safe multi-segment resolve. Refuses absolute / UNC / drive-letter `rel`, NUL bytes, C0 control chars, bidi-override codepoints (CVE-2021-42574 Trojan Source class), URL-encoded + fullwidth + division-slash path separators, Windows reserved device names CON / PRN / AUX / NUL / COM[0-9] / LPT[0-9] on EVERY platform (closes [CVE-2025-27210](https://nvd.nist.gov/vuln/detail/CVE-2025-27210) cross-mount class), trailing-`.`/trailing-space segments under windows-mode, NTFS Alternate Data Stream markers ([CVE-2024-12217](https://nvd.nist.gov/vuln/detail/CVE-2024-12217) class), and `..` segments that escape `base` after lexical resolve. Optional `opts.realpath: true` adds symlink-escape detection via `fs.realpathSync.native`. Every documented failure mode → coded refusal (`safe-path/absolute-rel` / `null-byte` / `bidi` / `win-reserved` / `escapes-base` / etc.); no best-effort path. **(b) `b.bootGates.run([{ name, fn, timeoutMs?, exitCode?, onFail? }], opts?)`** — sequential boot-invariant runner. Each gate runs in order; on first failure: emits `bootgates.failed` audit, runs the gate's `onFail` callback (swallows + audits onFail throws), writes a single-line failure summary via `opts.log`, and calls the operator-supplied `opts.exit(code)`. The default `exit` throws `BootGatesError("bootgates/no-exit-wired")` rather than calling `process.exit` directly (lib/ never terminates the process — the CLI surface owns that wiring). Each gate runs under a 60s default `timeoutMs` budget configurable per-gate; overall budget via `opts.overallTimeoutMs`. **(c) `b.metrics.snapshot.shadowRegistry({ namespace, counters, gauges, info, cardinalityCap?, onCardinalityExceeded? })`** — namespaced shadow registry that mirrors a subset of a primary registry's metrics for export to systems needing isolated views (sidecar / per-tenant scrape endpoint / compliance-tagged subset). Cardinality cap (default 10000 per metric name) closes the [client_golang CVE-2022-21698](https://nvd.nist.gov/vuln/detail/CVE-2022-21698) unbounded-cardinality DoS class; policy is `drop` (default), `audit-only`, or `refuse`. Emits `metrics.shadow.cardinality_dropped` audit (rate-limited to 1/sec per shadow registry). **(d) Per-instance `agent.reloadCerts({ cert, key, ca })` on `b.pqcAgent.create()` returns** — long-running daemons that rotate TLS material via explicit `b.pqcAgent.create()` agents previously needed a process restart; the new instance method tests the new material via `tls.createSecureContext`, swaps `agent.options` atomically, closes idle keep-alive sockets via `agent.destroy()` (in-flight sockets complete naturally), and emits `pqcagent.reloadCerts` audit. Cert/key mismatch surfaces as `pqcagent/reload-mismatch` with the OpenSSL chain; CA bundle parse failures surface as `pqcagent/reload-bad-ca`. **(e) `b.metrics.snapshot.render(snap, { format: "text", groups })`** — operator-readable text format gains an `opts.groups` map that sections the output (`== HTTP ==` / `== Queue ==` / `== TLS ==`); fields not named in any group fall to `== Other ==`. Group ordering preserved per insertion order. Prometheus / OpenMetrics formats unchanged. **(h) ISO-8601 date strings render-eligible in the text format** — timestamps shaped as `2026-05-17T20:00:00.000Z` (length-bounded at 64 chars) now render verbatim in the text format instead of degrading to `[skipped: non-numeric]`; the Prometheus format gets a parallel `<name>_epoch_ms` gauge so downstream alerting can compute durations per OpenMetrics 1.0 §3.4 (Timestamps MUST be float64 Unix-epoch). Non-ISO strings continue to skip in Prometheus (label-value injection defense). **New compliance scaffold:** new namespace `b.bootGates`. New audit namespaces: `bootgates`, `metrics`. **Operator impact:** no breaking changes. `b.bootGates.run` callers MUST supply `opts.exit: process.exit.bind(process)` from their daemon main() if they want the failure path to terminate the process — the default-throw shape exists so lib/-internal callers can't accidentally `process.exit` from inside a primitive. `b.safePath.resolve` is a brand-new primitive; existing code is unaffected. References: [Node.js path.resolve docs](https://nodejs.org/api/path.html#pathresolvepaths) · [CVE-2025-27210 Windows device-name bypass](https://nvd.nist.gov/vuln/detail/CVE-2025-27210) · [CVE-2024-12217 NTFS ADS](https://nvd.nist.gov/vuln/detail/CVE-2024-12217) · [CVE-2021-42574 Trojan Source](https://nvd.nist.gov/vuln/detail/CVE-2021-42574) · [CVE-2022-21698 Prometheus cardinality DoS](https://nvd.nist.gov/vuln/detail/CVE-2022-21698) · [OpenMetrics 1.0 spec](https://github.com/prometheus/OpenMetrics/blob/v1.0.0/specification/OpenMetrics.md) · [CVE-2026-21637 SNI sync-throw](https://nvd.nist.gov/vuln/detail/CVE-2026-21637).
|
package/lib/mail-server-imap.js
CHANGED
|
@@ -582,6 +582,14 @@ function create(opts) {
|
|
|
582
582
|
protocol: "imap",
|
|
583
583
|
defaults: defaults,
|
|
584
584
|
overrides: opts.overrides || {},
|
|
585
|
+
// b.agent.tenant adoption (v0.10.12). Operators wiring multi-
|
|
586
|
+
// tenant IMAP deployments pass `tenantScope` from
|
|
587
|
+
// `b.agent.tenant.create({...})` plus the per-listener tenant id.
|
|
588
|
+
// The registry then gates every dispatch on
|
|
589
|
+
// `tenantScope.check(state.actor, agentTenantId)` before guard
|
|
590
|
+
// validation or audit emission.
|
|
591
|
+
tenantScope: opts.tenantScope || null,
|
|
592
|
+
agentTenantId: opts.agentTenantId || null,
|
|
585
593
|
notFoundHandler: function (verb, _state, socket, parsed) {
|
|
586
594
|
return _writeTagged(socket, parsed.tag,
|
|
587
595
|
"BAD Verb '" + verb + "' not implemented in v1");
|
package/lib/mail-server-jmap.js
CHANGED
|
@@ -220,9 +220,15 @@ function create(opts) {
|
|
|
220
220
|
};
|
|
221
221
|
}
|
|
222
222
|
var registry = mailServerRegistry.create({
|
|
223
|
-
protocol:
|
|
224
|
-
defaults:
|
|
225
|
-
overrides:
|
|
223
|
+
protocol: "jmap",
|
|
224
|
+
defaults: defaults,
|
|
225
|
+
overrides: opts.overrides || {},
|
|
226
|
+
// b.agent.tenant adoption (v0.10.12). When `opts.tenantScope` is
|
|
227
|
+
// supplied, every method dispatch first gates on
|
|
228
|
+
// `tenantScope.check(state.actor, agentTenantId)` — JMAP's
|
|
229
|
+
// accountId scoping continues to apply inside operator handlers.
|
|
230
|
+
tenantScope: opts.tenantScope || null,
|
|
231
|
+
agentTenantId: opts.agentTenantId || null,
|
|
226
232
|
});
|
|
227
233
|
var sessionState = bCrypto.generateToken(16); // allow:raw-byte-literal — opaque session-state token length
|
|
228
234
|
|
|
@@ -446,6 +446,10 @@ function create(opts) {
|
|
|
446
446
|
protocol: "managesieve",
|
|
447
447
|
defaults: defaults,
|
|
448
448
|
overrides: opts.overrides || {},
|
|
449
|
+
// b.agent.tenant adoption (v0.10.12) — see imap factory for the
|
|
450
|
+
// shape.
|
|
451
|
+
tenantScope: opts.tenantScope || null,
|
|
452
|
+
agentTenantId: opts.agentTenantId || null,
|
|
449
453
|
notFoundHandler: function (verb, _state, socket) {
|
|
450
454
|
return _writeNo(socket, "Unknown verb '" + verb + "'");
|
|
451
455
|
},
|
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;
|
|
@@ -131,6 +131,27 @@ function create(opts) {
|
|
|
131
131
|
throw new MailServerRegistryError("mail-server-registry/unknown-protocol",
|
|
132
132
|
"create: protocol must be 'imap', 'jmap', or 'managesieve' (got '" + opts.protocol + "')");
|
|
133
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
|
+
}
|
|
134
155
|
var catalogue = CATALOGUE[opts.protocol];
|
|
135
156
|
var entries = Object.create(null);
|
|
136
157
|
|
|
@@ -251,6 +272,30 @@ function create(opts) {
|
|
|
251
272
|
*/
|
|
252
273
|
function dispatch(name) {
|
|
253
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
|
+
}
|
|
254
299
|
var entry = entries[name];
|
|
255
300
|
if (!entry) {
|
|
256
301
|
if (typeof opts.notFoundHandler === "function") {
|
|
@@ -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/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
|
]
|