@blamejs/core 0.9.49 → 0.10.1

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.
Files changed (77) hide show
  1. package/CHANGELOG.md +951 -908
  2. package/index.js +25 -0
  3. package/lib/_test/crypto-fixtures.js +67 -0
  4. package/lib/agent-event-bus.js +52 -6
  5. package/lib/agent-idempotency.js +169 -16
  6. package/lib/agent-orchestrator.js +263 -9
  7. package/lib/agent-posture-chain.js +163 -5
  8. package/lib/agent-saga.js +146 -16
  9. package/lib/agent-snapshot.js +349 -19
  10. package/lib/agent-stream.js +34 -2
  11. package/lib/agent-tenant.js +179 -23
  12. package/lib/agent-trace.js +84 -21
  13. package/lib/auth/aal.js +8 -1
  14. package/lib/auth/ciba.js +6 -1
  15. package/lib/auth/dpop.js +7 -2
  16. package/lib/auth/fal.js +17 -8
  17. package/lib/auth/jwt-external.js +128 -4
  18. package/lib/auth/oauth.js +232 -10
  19. package/lib/auth/oid4vci.js +67 -7
  20. package/lib/auth/openid-federation.js +71 -25
  21. package/lib/auth/passkey.js +140 -6
  22. package/lib/auth/sd-jwt-vc.js +67 -5
  23. package/lib/circuit-breaker.js +10 -2
  24. package/lib/compliance.js +176 -8
  25. package/lib/crypto-field.js +114 -14
  26. package/lib/crypto.js +216 -20
  27. package/lib/db.js +1 -0
  28. package/lib/guard-jmap.js +321 -0
  29. package/lib/guard-managesieve-command.js +566 -0
  30. package/lib/guard-pop3-command.js +317 -0
  31. package/lib/guard-smtp-command.js +58 -3
  32. package/lib/mail-agent.js +20 -7
  33. package/lib/mail-arc-sign.js +12 -8
  34. package/lib/mail-auth.js +323 -34
  35. package/lib/mail-crypto-pgp.js +934 -0
  36. package/lib/mail-crypto-smime.js +340 -0
  37. package/lib/mail-crypto.js +108 -0
  38. package/lib/mail-dav.js +1224 -0
  39. package/lib/mail-deploy.js +492 -0
  40. package/lib/mail-dkim.js +431 -26
  41. package/lib/mail-journal.js +435 -0
  42. package/lib/mail-scan.js +502 -0
  43. package/lib/mail-server-imap.js +64 -26
  44. package/lib/mail-server-jmap.js +488 -0
  45. package/lib/mail-server-managesieve.js +853 -0
  46. package/lib/mail-server-mx.js +40 -30
  47. package/lib/mail-server-pop3.js +836 -0
  48. package/lib/mail-server-rate-limit.js +13 -0
  49. package/lib/mail-server-submission.js +70 -24
  50. package/lib/mail-server-tls.js +445 -0
  51. package/lib/mail-sieve.js +557 -0
  52. package/lib/mail-spam-score.js +284 -0
  53. package/lib/mail.js +99 -0
  54. package/lib/metrics.js +80 -3
  55. package/lib/middleware/dpop.js +58 -3
  56. package/lib/middleware/idempotency-key.js +255 -42
  57. package/lib/middleware/protected-resource-metadata.js +114 -2
  58. package/lib/network-dns-resolver.js +33 -0
  59. package/lib/network-tls.js +46 -0
  60. package/lib/outbox.js +62 -12
  61. package/lib/pqc-agent.js +13 -5
  62. package/lib/retry.js +23 -9
  63. package/lib/router.js +23 -1
  64. package/lib/safe-ical.js +634 -0
  65. package/lib/safe-icap.js +502 -0
  66. package/lib/safe-mime.js +15 -0
  67. package/lib/safe-sieve.js +684 -0
  68. package/lib/safe-smtp.js +57 -0
  69. package/lib/safe-url.js +37 -0
  70. package/lib/safe-vcard.js +473 -0
  71. package/lib/self-update-standalone-verifier.js +32 -3
  72. package/lib/self-update.js +153 -33
  73. package/lib/vendor/MANIFEST.json +161 -156
  74. package/lib/vendor-data.js +127 -9
  75. package/lib/vex.js +324 -59
  76. package/package.json +1 -1
  77. package/sbom.cdx.json +6 -6
@@ -0,0 +1,488 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.mail.server.jmap
4
+ * @nav Mail
5
+ * @title Mail JMAP Server
6
+ * @order 548
7
+ *
8
+ * @intro
9
+ * JMAP Core (RFC 8620) + JMAP Mail (RFC 8621) listener. Where IMAP
10
+ * is a TCP text-protocol with a connection state-machine, JMAP is
11
+ * HTTP-mounted JSON-RPC — operators mount the handler under their
12
+ * existing `b.router` / `b.createApp` and the JMAP semantics ride
13
+ * the HTTP request lifecycle (auth → body parse → handler →
14
+ * response).
15
+ *
16
+ * ## Public surface
17
+ *
18
+ * ```js
19
+ * var jmap = b.mail.server.jmap.create({
20
+ * mailStore: b.mailStore.create({ backend: b.db.handle() }),
21
+ * methods: {
22
+ * "Mailbox/get": async function (actor, args) {...},
23
+ * "Email/query": async function (actor, args) {...},
24
+ * "Email/get": async function (actor, args) {...},
25
+ * },
26
+ * serverCapabilities: {
27
+ * "urn:ietf:params:jmap:mail": { maxMailboxesPerEmail: null },
28
+ * "urn:ietf:params:jmap:submission": null,
29
+ * },
30
+ * });
31
+ *
32
+ * // Mount on the framework's router:
33
+ * app.use("/.well-known/jmap", jmap.discoveryHandler);
34
+ * app.use("/jmap/session", b.middleware.bearerAuth(...), jmap.sessionHandler);
35
+ * app.use("/jmap/api", b.middleware.bearerAuth(...), jmap.apiHandler);
36
+ * ```
37
+ *
38
+ * The listener owns the request envelope (`b.guardJmap.validate`),
39
+ * back-reference resolution (RFC 8620 §3.7), the per-call dispatch,
40
+ * and the standard error mapping (RFC 8620 §3.6.1). Operators wire
41
+ * the actual method implementations — JMAP semantics are too varied
42
+ * (Mailbox / Email / Thread / SearchSnippet / Identity /
43
+ * EmailSubmission) to enshrine in v1.
44
+ *
45
+ * ## Capability discovery (RFC 8620 §2)
46
+ *
47
+ * GET `/.well-known/jmap` redirects to the session resource per
48
+ * §2.2. GET `/jmap/session` returns the session object with the
49
+ * server's capabilities, account list (operator-supplied via
50
+ * `opts.accountsFor(actor)`), and endpoint URLs.
51
+ *
52
+ * ## Request shape (RFC 8620 §3.3)
53
+ *
54
+ * POST `/jmap/api` with body:
55
+ *
56
+ * ```json
57
+ * {
58
+ * "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
59
+ * "methodCalls": [
60
+ * ["Mailbox/get", { "accountId": "A1" }, "c0"],
61
+ * ["Email/query", { "filter": { "inMailbox": "#c0/list/0/id" } }, "c1"]
62
+ * ]
63
+ * }
64
+ * ```
65
+ *
66
+ * Response shape:
67
+ *
68
+ * ```json
69
+ * {
70
+ * "methodResponses": [
71
+ * ["Mailbox/get", { ... }, "c0"],
72
+ * ["Email/query", { ... }, "c1"]
73
+ * ],
74
+ * "sessionState": "<opaque-token>"
75
+ * }
76
+ * ```
77
+ *
78
+ * ## Caps (RFC 8620 §3.6)
79
+ *
80
+ * Enforced via `b.guardJmap.validate` — `maxCallsInRequest`,
81
+ * `maxSizeRequest`, `maxObjectsInGet/Set`, `maxBackRefDepth`. Per-
82
+ * account method-call concurrent cap via `b.mail.server.rateLimit`
83
+ * when wired.
84
+ *
85
+ * ## Error vocabulary (RFC 8620 §3.6.1)
86
+ *
87
+ * Standard errors emitted as the methodResponse object:
88
+ *
89
+ * - `urn:ietf:params:jmap:error:requestTooLarge`
90
+ * - `urn:ietf:params:jmap:error:invalidArguments`
91
+ * - `urn:ietf:params:jmap:error:invalidResultReference`
92
+ * - `urn:ietf:params:jmap:error:unknownCapability`
93
+ * - `urn:ietf:params:jmap:error:limit/<name>`
94
+ * - `urn:ietf:params:jmap:error:forbidden`
95
+ * - `urn:ietf:params:jmap:error:accountNotFound`
96
+ * - `urn:ietf:params:jmap:error:serverFail` (opaque last-resort)
97
+ *
98
+ * ## What v1 does NOT ship
99
+ *
100
+ * - **Push channel (SSE + WebSocket per RFC 8887)** — operator wires
101
+ * `b.sse` or `b.websocket` to the `pushSubscribe` hook. v1.5
102
+ * bundles a turnkey push handler.
103
+ * - **Blob upload/download endpoints** — operator wires their own
104
+ * `/jmap/upload` / `/jmap/download` handlers; the framework
105
+ * supplies `b.storage` + `b.objectStore` + the guard-* family
106
+ * for the actual upload path.
107
+ * - **EmailSubmission (RFC 8621 §7)** — operator wires the bridge
108
+ * to `b.mail.server.submission`'s outbound agent.
109
+ * - **Calendars / Contacts (RFC 9610)**, **Sieve (RFC 9404)**,
110
+ * **MDN (RFC 9007)** — opt-in capabilities.
111
+ *
112
+ * @card
113
+ * JMAP Core (RFC 8620) + JMAP Mail (RFC 8621) listener. HTTP-mounted
114
+ * JSON-RPC. Composes b.guardJmap (request-envelope validator) +
115
+ * operator-supplied method handlers + b.mailStore. Per-account back-
116
+ * reference resolution (RFC 8620 §3.7) + standard error vocabulary
117
+ * (RFC 8620 §3.6.1) handled at the listener boundary.
118
+ */
119
+
120
+ var lazyRequire = require("./lazy-require");
121
+ var C = require("./constants");
122
+ var bCrypto = require("./crypto");
123
+ var safeJson = require("./safe-json");
124
+ var validateOpts = require("./validate-opts");
125
+ var guardJmap = require("./guard-jmap");
126
+ var { defineClass } = require("./framework-error");
127
+
128
+ var audit = lazyRequire(function () { return require("./audit"); });
129
+
130
+ var MailServerJmapError = defineClass("MailServerJmapError", { alwaysPermanent: true });
131
+
132
+ var DEFAULT_PROFILE = "strict";
133
+ var WELL_KNOWN_PATH = "/.well-known/jmap";
134
+ void C; // reserved for future cap constants
135
+ void WELL_KNOWN_PATH;
136
+
137
+ /**
138
+ * @primitive b.mail.server.jmap.create
139
+ * @signature b.mail.server.jmap.create(opts)
140
+ * @since 0.9.50
141
+ * @status stable
142
+ * @related b.mail.server.imap.create, b.guardJmap.validate, b.mailStore.create
143
+ *
144
+ * Build a JMAP Core + JMAP Mail listener. Returns a handle exposing
145
+ * `apiHandler` / `sessionHandler` / `discoveryHandler` (Express-style
146
+ * `(req, res, next)` functions) and `dispatch(actor, body)` for
147
+ * operators with a non-Express transport.
148
+ *
149
+ * @opts
150
+ * mailStore: b.mailStore handle (operator-supplied backend),
151
+ * methods: { "<Type>/<verb>": async fn(actor, args, ctx) },
152
+ * // operator-supplied JMAP method handlers
153
+ * serverCapabilities: { "<URI>": <capability-record> },
154
+ * // capabilities the server advertises beyond core
155
+ * accountsFor: async function (actor) → { primaryAccounts, accounts },
156
+ * // operator-supplied accountId enumeration
157
+ * profile: "strict" | "balanced" | "permissive",
158
+ * posture: "hipaa" | "pci-dss" | "gdpr" | "soc2",
159
+ * audit: b.audit // optional
160
+ *
161
+ * @example
162
+ * var jmap = b.mail.server.jmap.create({
163
+ * mailStore: b.mailStore.create({ backend: b.db.handle() }),
164
+ * methods: {
165
+ * "Mailbox/get": async function (actor, args) {
166
+ * return { accountId: args.accountId, list: [], notFound: [] };
167
+ * },
168
+ * },
169
+ * serverCapabilities: { "urn:ietf:params:jmap:mail": {} },
170
+ * accountsFor: async function (actor) {
171
+ * return {
172
+ * primaryAccounts: { "urn:ietf:params:jmap:mail": "A1" },
173
+ * accounts: { A1: { name: actor.username } },
174
+ * };
175
+ * },
176
+ * });
177
+ *
178
+ * app.post("/jmap/api", b.middleware.bearerAuth({ verify: verify }), jmap.apiHandler);
179
+ */
180
+ function create(opts) {
181
+ validateOpts.requireObject(opts, "mail.server.jmap.create",
182
+ MailServerJmapError, "mail-server-jmap/bad-opts");
183
+ if (!opts.mailStore) {
184
+ throw new MailServerJmapError("mail-server-jmap/no-mail-store",
185
+ "mail.server.jmap.create: mailStore is required (compose b.mailStore.create({ backend: ... }))");
186
+ }
187
+ if (typeof opts.methods !== "object" || opts.methods === null || Array.isArray(opts.methods)) {
188
+ throw new MailServerJmapError("mail-server-jmap/no-methods",
189
+ "mail.server.jmap.create: opts.methods must be an object mapping method-name → async fn(actor, args, ctx)");
190
+ }
191
+ if (typeof opts.accountsFor !== "function") {
192
+ throw new MailServerJmapError("mail-server-jmap/no-accounts-for",
193
+ "mail.server.jmap.create: opts.accountsFor(actor) async function is required for the session resource");
194
+ }
195
+ var profile = opts.profile || DEFAULT_PROFILE;
196
+ var posture = opts.posture || null;
197
+ var serverCapabilities = opts.serverCapabilities || {};
198
+ var methods = opts.methods;
199
+ var sessionState = bCrypto.generateToken(16); // allow:raw-byte-literal — opaque session-state token length
200
+
201
+ function _emit(action, metadata, outcome) {
202
+ try {
203
+ audit().safeEmit({
204
+ action: action,
205
+ outcome: outcome || "success",
206
+ metadata: metadata || {},
207
+ });
208
+ } catch (_e) { /* drop-silent — audit best-effort */ }
209
+ }
210
+
211
+ // ---- Back-reference resolution (RFC 8620 §3.7) -------------------------
212
+ //
213
+ // Walks the args tree replacing `#<refClientId>/<jsonPointer>` shapes
214
+ // with the resolved value from the prior method-response. Returns the
215
+ // resolved args object on success or throws an
216
+ // `urn:ietf:params:jmap:error:invalidResultReference`.
217
+ function _resolveBackRefs(args, priorResponses) {
218
+ if (args === null || typeof args !== "object") return args;
219
+ if (Array.isArray(args)) {
220
+ var out = [];
221
+ for (var i = 0; i < args.length; i += 1) out.push(_resolveBackRefs(args[i], priorResponses));
222
+ return out;
223
+ }
224
+ var obj = {};
225
+ var keys = Object.keys(args);
226
+ for (var k = 0; k < keys.length; k += 1) {
227
+ var key = keys[k];
228
+ var val = args[key];
229
+ if (key.charCodeAt(0) === 0x23) { // allow:raw-byte-literal — `#` (0x23) is the JMAP back-ref-key prefix
230
+ // `#<srcClientId>` → key strips the `#`; value is { resultOf, name, path }
231
+ var targetKey = key.slice(1);
232
+ if (!val || typeof val !== "object" || Array.isArray(val) ||
233
+ typeof val.resultOf !== "string" || typeof val.name !== "string" ||
234
+ typeof val.path !== "string") {
235
+ throw new MailServerJmapError("urn:ietf:params:jmap:error:invalidResultReference",
236
+ "back-ref `#" + targetKey + "` malformed (expected { resultOf, name, path })");
237
+ }
238
+ var src = priorResponses[val.resultOf];
239
+ if (!src || src.name !== val.name) {
240
+ throw new MailServerJmapError("urn:ietf:params:jmap:error:invalidResultReference",
241
+ "back-ref `#" + targetKey + "` → no prior response with clientId='" + val.resultOf +
242
+ "' and name='" + val.name + "'");
243
+ }
244
+ var resolved = _pointerLookup(src.result, val.path);
245
+ if (resolved === undefined) {
246
+ throw new MailServerJmapError("urn:ietf:params:jmap:error:invalidResultReference",
247
+ "back-ref `#" + targetKey + "` → path '" + val.path + "' resolved to undefined");
248
+ }
249
+ obj[targetKey] = resolved;
250
+ } else {
251
+ obj[key] = _resolveBackRefs(val, priorResponses);
252
+ }
253
+ }
254
+ return obj;
255
+ }
256
+
257
+ // Minimal JSON Pointer (RFC 6901 §3) — `/foo/bar/0` traversal. JMAP
258
+ // back-references constrain the shape (single object → single array
259
+ // → single field) so we don't need the full RFC 6901 escape grammar
260
+ // here. Bounded recursion via the back-ref-depth cap upstream.
261
+ function _pointerLookup(node, path) {
262
+ if (typeof path !== "string") return undefined;
263
+ if (path === "" || path === "/") return node;
264
+ var parts = path.split("/");
265
+ var cur = node;
266
+ for (var i = 0; i < parts.length; i += 1) {
267
+ var seg = parts[i];
268
+ if (seg === "" && i === 0) continue;
269
+ if (cur === null || typeof cur !== "object") return undefined;
270
+ // RFC 6901 ~1 / ~0 escapes — minimal grammar.
271
+ seg = seg.replace(/~1/g, "/").replace(/~0/g, "~"); // allow:regex-no-length-cap — seg length bounded by path which is bounded by maxLineBytes upstream
272
+ if (Array.isArray(cur)) {
273
+ if (seg === "*") return cur;
274
+ var idx = parseInt(seg, 10);
275
+ if (!isFinite(idx) || idx < 0 || idx >= cur.length) return undefined;
276
+ cur = cur[idx];
277
+ } else {
278
+ if (!Object.prototype.hasOwnProperty.call(cur, seg)) return undefined;
279
+ cur = cur[seg];
280
+ }
281
+ }
282
+ return cur;
283
+ }
284
+
285
+ // ---- Dispatch ------------------------------------------------------------
286
+ //
287
+ // `dispatch(actor, body)` is the operator-callable form — accepts a
288
+ // pre-parsed request body + an authenticated actor, returns a
289
+ // response object suitable for JSON-serialization to the client.
290
+ async function dispatch(actor, body) {
291
+ if (!actor) {
292
+ return _refusalResponse("urn:ietf:params:jmap:error:forbidden",
293
+ "actor is required (operator must wire b.middleware.bearerAuth before this handler)");
294
+ }
295
+ var parsed;
296
+ try {
297
+ parsed = guardJmap.validate(body, {
298
+ profile: profile,
299
+ posture: posture,
300
+ serverCapabilities: serverCapabilities,
301
+ });
302
+ } catch (e) {
303
+ var errType = (e && e.code) || "urn:ietf:params:jmap:error:invalidArguments";
304
+ _emit("mail.server.jmap.request_refused",
305
+ { type: errType, reason: (e && e.message) || "" }, "denied");
306
+ return _refusalResponse(errType, (e && e.message) || "request refused");
307
+ }
308
+
309
+ var methodResponses = [];
310
+ var byClientId = Object.create(null);
311
+ for (var i = 0; i < parsed.methodCalls.length; i += 1) {
312
+ var call = parsed.methodCalls[i];
313
+ var methodName = call[0];
314
+ var rawArgs = call[1];
315
+ var clientId = call[2];
316
+ var resolvedArgs;
317
+ try {
318
+ resolvedArgs = _resolveBackRefs(rawArgs, byClientId);
319
+ } catch (e) {
320
+ var refType = (e && e.code) || "urn:ietf:params:jmap:error:invalidResultReference";
321
+ methodResponses.push(["error", { type: refType, description: (e && e.message) || "" }, clientId]);
322
+ continue;
323
+ }
324
+ var handler = methods[methodName];
325
+ if (typeof handler !== "function") {
326
+ methodResponses.push(["error",
327
+ { type: "urn:ietf:params:jmap:error:unknownMethod",
328
+ description: "Method '" + methodName + "' not implemented on this server" }, clientId]);
329
+ continue;
330
+ }
331
+ try {
332
+ // JMAP methodCalls execute sequentially by spec (RFC 8620 §3.7 —
333
+ // back-references require strict ordering). The await-in-loop
334
+ // pattern is intentional here.
335
+ var result = await handler(actor, resolvedArgs, {
336
+ using: parsed.using,
337
+ createdIds: parsed.createdIds,
338
+ methodName: methodName,
339
+ clientId: clientId,
340
+ });
341
+ if (result && typeof result === "object" && result.type &&
342
+ typeof result.type === "string" && result.type.indexOf("urn:ietf:params:jmap:error:") === 0) {
343
+ // Operator-emitted error shape — preserve as-is.
344
+ methodResponses.push(["error", result, clientId]);
345
+ byClientId[clientId] = { name: "error", result: result };
346
+ } else {
347
+ methodResponses.push([methodName, result || {}, clientId]);
348
+ byClientId[clientId] = { name: methodName, result: result || {} };
349
+ }
350
+ } catch (e) {
351
+ _emit("mail.server.jmap.method_threw",
352
+ { method: methodName, clientId: clientId,
353
+ error: (e && e.message) || String(e) }, "failure");
354
+ methodResponses.push(["error",
355
+ { type: "urn:ietf:params:jmap:error:serverFail",
356
+ description: "Method threw" }, clientId]);
357
+ }
358
+ }
359
+
360
+ _emit("mail.server.jmap.request",
361
+ { methodCallCount: parsed.methodCalls.length, using: parsed.using });
362
+
363
+ return {
364
+ methodResponses: methodResponses,
365
+ sessionState: sessionState,
366
+ createdIds: parsed.createdIds,
367
+ };
368
+ }
369
+
370
+ function _refusalResponse(type, description) {
371
+ return {
372
+ type: type,
373
+ description: description,
374
+ methodResponses: [],
375
+ sessionState: sessionState,
376
+ };
377
+ }
378
+
379
+ // ---- HTTP handlers -----------------------------------------------------
380
+
381
+ function apiHandler(req, res) {
382
+ // Operator wires b.middleware.bearerAuth before this handler, so
383
+ // `req.user` is the authenticated actor. If req.user is missing,
384
+ // refuse with 401 + Problem Details body.
385
+ var actor = req.user || (req.actor || null);
386
+ var rawBody = req.body;
387
+ if (rawBody === undefined) {
388
+ // b.middleware.bodyParser may not have run. Refuse cleanly.
389
+ res.statusCode = 400;
390
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
391
+ res.end(JSON.stringify({
392
+ type: "urn:ietf:params:jmap:error:invalidArguments",
393
+ description: "request body missing (wire b.middleware.bodyParser before this handler)",
394
+ }));
395
+ return;
396
+ }
397
+ dispatch(actor, rawBody).then(function (response) {
398
+ // Map typed JMAP refusals to the right HTTP status. `forbidden`
399
+ // means the dispatcher refused because actor was missing /
400
+ // wrong-tenant — clients + proxies need 401 to trigger their
401
+ // re-auth flow (a 400 looks like a malformed request, which it
402
+ // isn't). Everything else stays 400 per RFC 8620 §3.6.1.
403
+ if (response && response.type === "urn:ietf:params:jmap:error:forbidden") {
404
+ res.statusCode = 401; // allow:raw-byte-literal — HTTP status codes
405
+ } else if (response && response.type) {
406
+ res.statusCode = 400; // allow:raw-byte-literal — HTTP status codes
407
+ } else {
408
+ res.statusCode = 200; // allow:raw-byte-literal — HTTP status codes
409
+ }
410
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
411
+ res.end(JSON.stringify(response));
412
+ }, function (err) {
413
+ _emit("mail.server.jmap.handler_threw",
414
+ { error: (err && err.message) || String(err) }, "failure");
415
+ res.statusCode = 500;
416
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
417
+ res.end(JSON.stringify({
418
+ type: "urn:ietf:params:jmap:error:serverFail",
419
+ description: "Server error",
420
+ }));
421
+ });
422
+ }
423
+
424
+ function sessionHandler(req, res) {
425
+ var actor = req.user || (req.actor || null);
426
+ if (!actor) {
427
+ res.statusCode = 401;
428
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
429
+ res.end(JSON.stringify({
430
+ type: "urn:ietf:params:jmap:error:forbidden",
431
+ description: "Authentication required",
432
+ }));
433
+ return;
434
+ }
435
+ Promise.resolve().then(function () { return opts.accountsFor(actor); })
436
+ .then(function (accountInfo) {
437
+ var info = accountInfo || { primaryAccounts: {}, accounts: {} };
438
+ var session = {
439
+ capabilities: Object.assign({}, { "urn:ietf:params:jmap:core": {} }, serverCapabilities),
440
+ accounts: info.accounts || {},
441
+ primaryAccounts: info.primaryAccounts || {},
442
+ username: actor.username || actor.id || "unknown",
443
+ apiUrl: opts.apiUrl || "/jmap/api",
444
+ downloadUrl: opts.downloadUrl || "/jmap/download/{accountId}/{blobId}/{name}?accept={type}",
445
+ uploadUrl: opts.uploadUrl || "/jmap/upload/{accountId}",
446
+ eventSourceUrl: opts.eventSourceUrl || "/jmap/eventsource?types={types}&closeafter={closeafter}&ping={ping}",
447
+ state: sessionState,
448
+ };
449
+ res.statusCode = 200;
450
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
451
+ res.end(safeJson.stringify ? safeJson.stringify(session) : JSON.stringify(session)); // allow:bare-canonicalize-walk — JSON response, not signed payload
452
+ })
453
+ .catch(function (err) {
454
+ _emit("mail.server.jmap.session_threw",
455
+ { error: (err && err.message) || String(err) }, "failure");
456
+ res.statusCode = 500;
457
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
458
+ res.end(JSON.stringify({
459
+ type: "urn:ietf:params:jmap:error:serverFail",
460
+ description: "Session resource failed",
461
+ }));
462
+ });
463
+ }
464
+
465
+ function discoveryHandler(req, res) {
466
+ // RFC 8620 §2.2 — well-known endpoint redirects (or directly returns)
467
+ // the session URL. We redirect to /jmap/session per the most common
468
+ // pattern; operators with a non-root mount path override via
469
+ // opts.sessionUrl.
470
+ res.statusCode = 302;
471
+ res.setHeader("Location", opts.sessionUrl || "/jmap/session");
472
+ res.end();
473
+ }
474
+
475
+ return {
476
+ create: create,
477
+ dispatch: dispatch,
478
+ apiHandler: apiHandler,
479
+ sessionHandler: sessionHandler,
480
+ discoveryHandler: discoveryHandler,
481
+ MailServerJmapError: MailServerJmapError,
482
+ };
483
+ }
484
+
485
+ module.exports = {
486
+ create: create,
487
+ MailServerJmapError: MailServerJmapError,
488
+ };