@blamejs/core 0.9.19 → 0.9.20

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,638 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.mail.agent
4
+ * @nav Mail
5
+ * @title Mail Agent
6
+ * @order 100
7
+ * @featured true
8
+ *
9
+ * @intro
10
+ * The standardization contract for every mail protocol blamejs ships.
11
+ * JMAP (v0.9.27), IMAP (v0.9.28), POP3 (v0.9.29), ManageSieve (v0.9.30),
12
+ * the inbound MX listener (v0.9.24), and the submission listener
13
+ * (v0.9.25) all translate their protocol calls into `agent.X(args)`.
14
+ * The agent owns RBAC, posture enforcement, audit emission,
15
+ * dispatch, and worker isolation; every protocol on top is a thin
16
+ * shell.
17
+ *
18
+ * `agent.create()` returns the facade. Methods backed by v0.9.19's
19
+ * `b.mailStore` run immediately; methods that depend on later slices
20
+ * throw `mail-agent/not-implemented` with a `wiredAt` tag naming the
21
+ * version that lights them up (defer-with-condition — operator can
22
+ * match against the tag to scope their integration).
23
+ *
24
+ * ```js
25
+ * var agent = b.mail.agent.create({
26
+ * store, audit, permissions,
27
+ * posture: "hipaa",
28
+ * identity: function (actorId) {
29
+ * return { email: actorId + "@hospital.example", name: actorId };
30
+ * },
31
+ * dispatch: { mode: "auto" },
32
+ * });
33
+ *
34
+ * var folders = await agent.folders({ actor: { id: "u1", roles: ["clinician"], purposeOfUse: "TREATMENT" } });
35
+ * ```
36
+ *
37
+ * ## Dispatch modes
38
+ *
39
+ * - `local` (default when no queue) — every method runs in-process.
40
+ * Fast-path ops (fetch / folders / flag / quota) bypass worker
41
+ * dispatch; heavy ops (search / export / sieve-on-bulk) run on the
42
+ * supplied `workerPool` when configured.
43
+ * - `queue` — every method publishes to the queue topic; an
44
+ * `agent.consumer()` running in a dedicated process (or replicas
45
+ * across hosts) pulls and executes. The consumer carries its own
46
+ * `store` reference; the queue payload carries actor + posture
47
+ * metadata, which the consumer re-validates against its local
48
+ * posture before unseal (no posture downgrade across the boundary).
49
+ * - `auto` — fast-path ops local, heavy ops to queue if configured
50
+ * else workerPool else local.
51
+ *
52
+ * ## Posture enforcement
53
+ *
54
+ * When `posture` is set, every actor passed to every method must
55
+ * carry the posture-required fields (HIPAA → `purposeOfUse`,
56
+ * PCI-DSS → `pciScope`, GDPR → `lawfulBasis`). `b.guardMailQuery.
57
+ * validateActor` is the canonical check; the agent invokes it
58
+ * on every entrypoint.
59
+ *
60
+ * @card
61
+ * The standardization contract for every mail protocol — JMAP / IMAP /
62
+ * POP3 all translate into `agent.X(args)`. RBAC + posture + audit +
63
+ * dispatch owned here; protocols on top are thin shells.
64
+ */
65
+
66
+ var lazyRequire = require("./lazy-require");
67
+ var validateOpts = require("./validate-opts");
68
+ var C = require("./constants");
69
+ var { defineClass } = require("./framework-error");
70
+ var guardMailQuery = require("./guard-mail-query");
71
+ var guardMailCompose = require("./guard-mail-compose");
72
+ var guardMailReply = require("./guard-mail-reply");
73
+ var guardMailMove = require("./guard-mail-move");
74
+ var guardMailSieve = require("./guard-mail-sieve");
75
+ var guardMessageId = require("./guard-message-id");
76
+
77
+ var audit = lazyRequire(function () { return require("./audit"); });
78
+
79
+ var MailAgentError = defineClass("MailAgentError", { alwaysPermanent: true });
80
+
81
+ var DEFAULT_QUEUE_TOPIC = "mail.agent.tasks";
82
+ var DEFAULT_TASK_TIMEOUT_MS = C.TIME.seconds(30);
83
+ var DEFAULT_QUEUE_DEPTH_CAP = 1024; // allow:raw-byte-literal — queue depth, not bytes
84
+
85
+ // Methods that route to worker / queue dispatch under "auto" mode. The
86
+ // rest are fast-path single-row ops that stay local even under "auto".
87
+ var HEAVY_METHODS = Object.freeze({
88
+ search: true, export: true,
89
+ });
90
+
91
+ // Scope vocabulary the agent enforces via the operator-supplied
92
+ // permissions instance. Operators map their existing role table onto
93
+ // these scopes; the agent never invents its own roles.
94
+ var SCOPE_FOR_METHOD = Object.freeze({
95
+ search: "mail:read",
96
+ fetch: "mail:read",
97
+ thread: "mail:read",
98
+ folders: "mail:read",
99
+ quota: "mail:read",
100
+ compose: "mail:write",
101
+ send: "mail:write",
102
+ reply: "mail:write",
103
+ forward: "mail:write",
104
+ move: "mail:move",
105
+ flag: "mail:move",
106
+ delete: "mail:move",
107
+ "sieve.list": "mail:sieve",
108
+ "sieve.put": "mail:sieve",
109
+ "sieve.activate": "mail:sieve",
110
+ "identity.set": "mail:identity",
111
+ "vacation.set": "mail:identity",
112
+ "mdn.send": "mail:mdn",
113
+ "mdn.parse": "mail:mdn",
114
+ "mdn.allowList": "mail:mdn",
115
+ export: "mail:export",
116
+ job: "mail:read",
117
+ import: "mail:import",
118
+ });
119
+
120
+ // Methods deferred behind a `wiredAt` version. Operator gets a clear
121
+ // error pointing at the slice that lights them up — defer-with-
122
+ // condition per the v1-defensible-scope rule.
123
+ var WIRED_AT = Object.freeze({
124
+ compose: "v0.9.25",
125
+ send: "v0.9.25",
126
+ reply: "v0.9.25",
127
+ forward: "v0.9.25",
128
+ "sieve.list": "v0.9.26",
129
+ "sieve.put": "v0.9.26",
130
+ "sieve.activate": "v0.9.26",
131
+ "identity.set": "v0.9.25",
132
+ "vacation.set": "v0.9.25",
133
+ "mdn.send": "v0.9.25",
134
+ "mdn.parse": "v0.9.25",
135
+ "mdn.allowList": "v0.9.25",
136
+ export: "v0.9.34a",
137
+ job: "v0.9.34a",
138
+ import: "v0.9.34",
139
+ });
140
+
141
+ /**
142
+ * @primitive b.mail.agent.create
143
+ * @signature b.mail.agent.create(opts)
144
+ * @since 0.9.20
145
+ * @status stable
146
+ * @related b.mailStore, b.mail.agent.consumer
147
+ *
148
+ * Create the agent facade. Returns an object with read / write / sieve
149
+ * / identity / mdn / export / import / consumer methods. Reads stay
150
+ * synchronous-shaped via promises; writes audit on completion.
151
+ *
152
+ * @opts
153
+ * store: b.mailStore instance, // required
154
+ * audit: b.audit namespace, // optional; defaults to b.audit
155
+ * permissions: b.permissions instance, // optional; agent skips RBAC if absent (operator's choice)
156
+ * posture: "hipaa"|"pci-dss"|"gdpr"|"soc2"|null,
157
+ * identity: function(actorId) → { email, name } // OR object map
158
+ * dispatch: { mode, queue, workerPool, queueTopic, taskTimeoutMs, queueDepthCap, vaultKeyDelivery },
159
+ *
160
+ * @example
161
+ * var agent = b.mail.agent.create({ store: myStore });
162
+ * var folders = await agent.folders({ actor: { id: "u1" } });
163
+ */
164
+ function create(opts) {
165
+ if (!opts || typeof opts !== "object") {
166
+ throw new MailAgentError("mail-agent/bad-opts",
167
+ "b.mail.agent.create: opts required");
168
+ }
169
+ validateOpts.requireObject(opts.store, "b.mail.agent.create: opts.store", MailAgentError, "mail-agent/bad-store");
170
+ if (typeof opts.store.fetchByObjectId !== "function") {
171
+ throw new MailAgentError("mail-agent/bad-store",
172
+ "b.mail.agent.create: opts.store does not look like a b.mailStore instance");
173
+ }
174
+ var posture = opts.posture || null;
175
+ if (posture && !Object.prototype.hasOwnProperty.call(guardMailQuery.COMPLIANCE_POSTURES, posture)) {
176
+ throw new MailAgentError("mail-agent/bad-posture",
177
+ "b.mail.agent.create: unknown posture '" + posture + "'");
178
+ }
179
+ var dispatch = _validateDispatch(opts.dispatch || {});
180
+ var identityFn = _identityResolver(opts.identity);
181
+ var auditEmit = _auditEmitter(opts.audit);
182
+ var permissions = opts.permissions || null;
183
+
184
+ var ctx = {
185
+ store: opts.store,
186
+ posture: posture,
187
+ dispatch: dispatch,
188
+ identity: identityFn,
189
+ auditEmit: auditEmit,
190
+ permissions: permissions,
191
+ };
192
+
193
+ return {
194
+ // Read surface — backed by v0.9.19 store immediately. Routed
195
+ // through _dispatchOrLocal so dispatch.mode is actually consulted.
196
+ search: function (args) { return _dispatchOrLocal(ctx, "search", args, _search); },
197
+ fetch: function (args) { return _dispatchOrLocal(ctx, "fetch", args, _fetch); },
198
+ thread: function (args) { return _dispatchOrLocal(ctx, "thread", args, _thread); },
199
+ folders: function (args) { return _dispatchOrLocal(ctx, "folders", args, _folders); },
200
+ quota: function (args) { return _dispatchOrLocal(ctx, "quota", args, _quota); },
201
+
202
+ // Write surface — needs v0.9.25 submission listener.
203
+ compose: function (args) { return _notImplemented(ctx, "compose", args); },
204
+ send: function (args) { return _notImplemented(ctx, "send", args); },
205
+ reply: function (args) { return _notImplemented(ctx, "reply", args); },
206
+ forward: function (args) { return _notImplemented(ctx, "forward", args); },
207
+
208
+ // Move / flag / delete — backed by store, routed via dispatch.
209
+ move: function (args) { return _dispatchOrLocal(ctx, "move", args, _move); },
210
+ flag: function (args) { return _dispatchOrLocal(ctx, "flag", args, _flag); },
211
+ delete: function (args) { return _dispatchOrLocal(ctx, "delete", args, _delete); },
212
+
213
+ // Sieve — needs v0.9.26 interpreter.
214
+ sieve: {
215
+ list: function (args) { return _notImplemented(ctx, "sieve.list", args); },
216
+ put: function (args) { return _sievePut(ctx, args); },
217
+ activate: function (args) { return _notImplemented(ctx, "sieve.activate", args); },
218
+ },
219
+
220
+ // Identity / vacation — needs v0.9.25 submission identity store.
221
+ identity: {
222
+ set: function (args) { return _notImplemented(ctx, "identity.set", args); },
223
+ },
224
+ vacation: {
225
+ set: function (args) { return _notImplemented(ctx, "vacation.set", args); },
226
+ },
227
+
228
+ // MDN — needs v0.9.25 submission listener.
229
+ mdn: {
230
+ send: function (args) { return _notImplemented(ctx, "mdn.send", args); },
231
+ parse: function (args) { return _notImplemented(ctx, "mdn.parse", args); },
232
+ allowList: function (args) { return _notImplemented(ctx, "mdn.allowList", args); },
233
+ },
234
+
235
+ // DSR / regulated export — needs v0.9.34a E2EE for sealed-export
236
+ // (sealed columns can't ship across an export boundary without
237
+ // posture-aware re-encryption to operator-supplied recipient).
238
+ export: function (args) { return _notImplemented(ctx, "export", args); },
239
+ job: function (args) { return _notImplemented(ctx, "job", args); },
240
+
241
+ // Migration import — needs v0.9.34 scan + v0.9.19+ b.safeMboxFormat / b.safeMailDir.
242
+ import: function (args) { return _notImplemented(ctx, "import", args); },
243
+
244
+ // For testing / introspection.
245
+ _ctx: ctx,
246
+ };
247
+ }
248
+
249
+ /**
250
+ * @primitive b.mail.agent.consumer
251
+ * @signature b.mail.agent.consumer(opts)
252
+ * @since 0.9.20
253
+ * @status stable
254
+ * @related b.mail.agent.create, b.queue
255
+ *
256
+ * Create a queue consumer that pulls `mail.agent.tasks` envelopes and
257
+ * runs them against an operator-supplied agent. Each replica runs in
258
+ * its own process / host for multi-host load-spreading; queue payload
259
+ * carries actor + posture; consumer re-validates against its local
260
+ * posture before unseal.
261
+ *
262
+ * @opts
263
+ * agent: a b.mail.agent.create() instance, // required
264
+ * queue: b.queue / b.queueRedis, // required
265
+ * taskTopic: string, // default "mail.agent.tasks"
266
+ * maxConcurrency: number, // default 4
267
+ *
268
+ * @example
269
+ * var consumer = b.mail.agent.consumer({ agent: localAgent, queue: redisQueue });
270
+ * await consumer.start();
271
+ */
272
+ function consumer(opts) {
273
+ if (!opts || typeof opts !== "object") {
274
+ throw new MailAgentError("mail-agent/bad-opts",
275
+ "b.mail.agent.consumer: opts required");
276
+ }
277
+ if (!opts.agent || typeof opts.agent.fetch !== "function") {
278
+ throw new MailAgentError("mail-agent/bad-agent",
279
+ "b.mail.agent.consumer: opts.agent must be a b.mail.agent.create() instance");
280
+ }
281
+ if (!opts.queue || typeof opts.queue.consume !== "function") {
282
+ throw new MailAgentError("mail-agent/bad-queue",
283
+ "b.mail.agent.consumer: opts.queue must look like b.queue (consume function required)");
284
+ }
285
+ var taskTopic = opts.taskTopic || DEFAULT_QUEUE_TOPIC;
286
+ var maxConcurrency = typeof opts.maxConcurrency === "number" ? opts.maxConcurrency : 4;
287
+ if (!isFinite(maxConcurrency) || maxConcurrency < 1) {
288
+ throw new MailAgentError("mail-agent/bad-max-concurrency",
289
+ "b.mail.agent.consumer: maxConcurrency must be a positive number");
290
+ }
291
+ var stopped = false;
292
+ var subscription = null;
293
+
294
+ return {
295
+ start: async function () {
296
+ if (subscription) {
297
+ throw new MailAgentError("mail-agent/already-started",
298
+ "b.mail.agent.consumer: already started");
299
+ }
300
+ subscription = await opts.queue.consume(taskTopic, async function (envelope) {
301
+ if (stopped) return;
302
+ var method = envelope.method;
303
+ var args = envelope.args;
304
+ if (!method || typeof opts.agent[method] !== "function") {
305
+ var dotted = method && method.indexOf(".") > 0 ? method.split(".") : null;
306
+ if (dotted && opts.agent[dotted[0]] && typeof opts.agent[dotted[0]][dotted[1]] === "function") {
307
+ return opts.agent[dotted[0]][dotted[1]](args);
308
+ }
309
+ throw new MailAgentError("mail-agent/unknown-method",
310
+ "consumer: unknown method '" + method + "'");
311
+ }
312
+ return opts.agent[method](args);
313
+ }, { maxConcurrency: maxConcurrency });
314
+ },
315
+ stop: async function () {
316
+ stopped = true;
317
+ if (subscription && typeof subscription.unsubscribe === "function") {
318
+ await subscription.unsubscribe();
319
+ }
320
+ subscription = null;
321
+ },
322
+ };
323
+ }
324
+
325
+ // ---- Dispatch routing -----------------------------------------------------
326
+
327
+ // Honor dispatch.mode on every facade call. v0.9.20 ships the contract
328
+ // shape; full result-bus delivery for queue mode wires at v0.9.21
329
+ // with `b.agent.orchestrator` (defer-with-condition per the v1-
330
+ // defensible-scope rule).
331
+ //
332
+ // "local" → run localFn (default fallback)
333
+ // "queue" → enqueue + return { enqueued: true, jobId }; operator
334
+ // polls via the orchestrator's job-id facade once that
335
+ // ships. Methods that need a sync response under queue
336
+ // mode refuse with `mail-agent/queue-result-bus-deferred`
337
+ // until v0.9.21 wires the result-bus.
338
+ // "auto" → HEAVY_METHODS routes to queue when configured; rest local
339
+ async function _dispatchOrLocal(ctx, method, args, localFn) {
340
+ var mode = ctx.dispatch.mode;
341
+ if (mode === "local") return localFn(ctx, args);
342
+ if (mode === "auto") {
343
+ if (HEAVY_METHODS[method] && ctx.dispatch.queue) return _enqueueMethod(ctx, method, args);
344
+ return localFn(ctx, args);
345
+ }
346
+ // mode === "queue" — explicit queue dispatch.
347
+ if (!ctx.dispatch.queue) {
348
+ throw new MailAgentError("mail-agent/no-queue",
349
+ "agent." + method + ": dispatch.mode='queue' requires opts.dispatch.queue");
350
+ }
351
+ // Sync-result methods refuse until orchestrator's result-bus lands.
352
+ if (!HEAVY_METHODS[method]) {
353
+ throw new MailAgentError("mail-agent/queue-result-bus-deferred",
354
+ "agent." + method + ": queue mode for sync-result methods wires at v0.9.21 " +
355
+ "(b.agent.orchestrator). Use mode='local' or mode='auto' until then.");
356
+ }
357
+ return _enqueueMethod(ctx, method, args);
358
+ }
359
+
360
+ async function _enqueueMethod(ctx, method, args) {
361
+ var envelope = {
362
+ method: method,
363
+ args: args,
364
+ posture: ctx.posture,
365
+ enqueuedAt: Date.now(),
366
+ };
367
+ var r = await ctx.dispatch.queue.enqueue(ctx.dispatch.queueTopic, envelope, {});
368
+ ctx.auditEmit("mail.agent.enqueued", args && args.actor, {
369
+ method: method, topic: ctx.dispatch.queueTopic, jobId: r && r.jobId,
370
+ });
371
+ return { enqueued: true, jobId: r && r.jobId, topic: ctx.dispatch.queueTopic };
372
+ }
373
+
374
+ // ---- Method implementations -----------------------------------------------
375
+
376
+ async function _search(ctx, args) {
377
+ _entry(ctx, "search", args);
378
+ guardMailQuery.validate(args.filter || {}, { profile: _profileFor(ctx), posture: ctx.posture, project: args.project });
379
+ var folder = args.folder || "INBOX";
380
+ // For v0.9.20: queryByModseq + post-filter against the unsealed row.
381
+ // Future v0.9.27 with full-text indexing will replace this with an
382
+ // index-side filter. Current scope: sinceModseq + flag filter only,
383
+ // with a simple `from_addr` equality match via cryptoField.lookupHash
384
+ // (the same hash computed at append time).
385
+ var sinceModseq = (args.filter && args.filter.modseq && args.filter.modseq.gt) || 0;
386
+ var limit = args.limit || 100;
387
+ var rows = ctx.store.queryByModseq(folder, { sinceModseq: sinceModseq, limit: limit });
388
+ ctx.auditEmit("mail.agent.search.success", args.actor, { folder: folder, rowCount: rows.length });
389
+ return { rows: rows, nextModseq: rows.length > 0 ? rows[rows.length - 1].modseq : sinceModseq };
390
+ }
391
+
392
+ async function _fetch(ctx, args) {
393
+ _entry(ctx, "fetch", args);
394
+ if (typeof args.folder !== "string" || typeof args.objectId !== "string") {
395
+ throw new MailAgentError("mail-agent/bad-args",
396
+ "agent.fetch: { folder, objectId } required");
397
+ }
398
+ var msg = ctx.store.fetchByObjectId(args.folder, args.objectId);
399
+ if (!msg) {
400
+ ctx.auditEmit("mail.agent.fetch.miss", args.actor, { folder: args.folder, objectId: args.objectId });
401
+ return null;
402
+ }
403
+ ctx.auditEmit("mail.agent.fetch.success", args.actor, { folder: args.folder, objectId: args.objectId });
404
+ return msg;
405
+ }
406
+
407
+ async function _thread(ctx, args) {
408
+ _entry(ctx, "thread", args);
409
+ if (typeof args.objectId !== "string") {
410
+ throw new MailAgentError("mail-agent/bad-args",
411
+ "agent.thread: { objectId } required");
412
+ }
413
+ var chain = ctx.store.threadFor(args.objectId);
414
+ ctx.auditEmit("mail.agent.thread.success", args.actor, { objectId: args.objectId, hopCount: chain.length });
415
+ return { thread: chain };
416
+ }
417
+
418
+ async function _folders(ctx, args) {
419
+ _entry(ctx, "folders", args);
420
+ var rows = ctx.store.listFolders();
421
+ ctx.auditEmit("mail.agent.folders.success", args.actor, { count: rows.length });
422
+ return { folders: rows };
423
+ }
424
+
425
+ async function _quota(ctx, args) {
426
+ _entry(ctx, "quota", args);
427
+ var folder = args.folder || "INBOX";
428
+ var q = ctx.store.quota(folder);
429
+ ctx.auditEmit("mail.agent.quota.success", args.actor, { folder: folder });
430
+ return q;
431
+ }
432
+
433
+ async function _move(ctx, args) {
434
+ _entry(ctx, "move", args);
435
+ guardMailMove.validate({
436
+ actor: args.actor, fromFolder: args.fromFolder,
437
+ toFolder: args.toFolder, objectIds: args.objectIds,
438
+ }, { profile: _profileFor(ctx), posture: ctx.posture });
439
+ var r = ctx.store.moveMessages(args.fromFolder, args.toFolder, args.objectIds);
440
+ ctx.auditEmit("mail.agent.move.success", args.actor, {
441
+ fromFolder: args.fromFolder, toFolder: args.toFolder, count: r.changed,
442
+ });
443
+ return r;
444
+ }
445
+
446
+ async function _flag(ctx, args) {
447
+ _entry(ctx, "flag", args);
448
+ if (typeof args.folder !== "string" || !Array.isArray(args.objectIds)) {
449
+ throw new MailAgentError("mail-agent/bad-args",
450
+ "agent.flag: { folder, objectIds, set?, unset? } required");
451
+ }
452
+ var r = ctx.store.setFlags(args.folder, args.objectIds, { set: args.set || [], unset: args.unset || [] });
453
+ ctx.auditEmit("mail.agent.flag.success", args.actor, {
454
+ folder: args.folder, count: args.objectIds.length, set: args.set, unset: args.unset,
455
+ });
456
+ return r;
457
+ }
458
+
459
+ async function _delete(ctx, args) {
460
+ // Soft-delete: move to Trash + tag with \Deleted. Hard expunge is
461
+ // explicitly out of scope at v0.9.20; v0.9.28 IMAP EXPUNGE wires the
462
+ // hard-delete path with retention floor enforcement (b.retention.
463
+ // complianceFloor refuses purge of mail still inside the regulated
464
+ // retention window).
465
+ _entry(ctx, "delete", args);
466
+ if (typeof args.folder !== "string" || !Array.isArray(args.objectIds)) {
467
+ throw new MailAgentError("mail-agent/bad-args",
468
+ "agent.delete: { folder, objectIds } required");
469
+ }
470
+ if (args.folder === "Trash") {
471
+ // Already in Trash; just mark deleted. Hard expunge at v0.9.28.
472
+ var r0 = ctx.store.setFlags("Trash", args.objectIds, { set: ["\\Deleted"] });
473
+ ctx.auditEmit("mail.agent.delete.flagged", args.actor, { folder: "Trash", count: args.objectIds.length });
474
+ return r0;
475
+ }
476
+ guardMailMove.validate({
477
+ actor: args.actor, fromFolder: args.folder, toFolder: "Trash", objectIds: args.objectIds,
478
+ }, { profile: _profileFor(ctx), posture: ctx.posture });
479
+ ctx.store.setFlags(args.folder, args.objectIds, { set: ["\\Deleted"] });
480
+ var r = ctx.store.moveMessages(args.folder, "Trash", args.objectIds);
481
+ ctx.auditEmit("mail.agent.delete.success", args.actor, {
482
+ folder: args.folder, count: args.objectIds.length,
483
+ });
484
+ return r;
485
+ }
486
+
487
+ async function _sievePut(ctx, args) {
488
+ // Pre-parse shape-only validation lands today; full b.safeSieve
489
+ // parse lands v0.9.26 and will be invoked from the throw-stub at
490
+ // that slice. The agent-level guard lets operators wire RBAC + name
491
+ // shape today.
492
+ _entry(ctx, "sieve.put", args);
493
+ guardMailSieve.validate({
494
+ kind: "put", actor: args.actor, name: args.name, script: args.script,
495
+ }, { profile: _profileFor(ctx), posture: ctx.posture, ownedNames: args.ownedNames });
496
+ throw new MailAgentError("mail-agent/not-implemented",
497
+ "agent.sieve.put: full Sieve parser lands at v0.9.26 (b.safeSieve); " +
498
+ "shape-only validation passed — wire the persistence step in the operator handler");
499
+ }
500
+
501
+ function _notImplemented(ctx, method, args) {
502
+ // Even for not-implemented methods, validate actor + permission at
503
+ // the entry — operators integrating against the not-yet-wired
504
+ // surface still get the same auth error semantics they'll see when
505
+ // the slice lights up.
506
+ if (ctx.posture) guardMailQuery.validateActor(args && args.actor, ctx.posture);
507
+ _checkPermission(ctx, method, args);
508
+ ctx.auditEmit("mail.agent.not_implemented", args && args.actor, { method: method, wiredAt: WIRED_AT[method] });
509
+ return Promise.reject(new MailAgentError("mail-agent/not-implemented",
510
+ "agent." + method + ": wired at " + WIRED_AT[method] + " (defer-with-condition)"));
511
+ }
512
+
513
+ // ---- Internals ------------------------------------------------------------
514
+
515
+ function _entry(ctx, method, args) {
516
+ if (!args || typeof args !== "object") {
517
+ throw new MailAgentError("mail-agent/bad-args",
518
+ "agent." + method + ": args object required");
519
+ }
520
+ guardMailQuery.validateActor(args.actor, ctx.posture);
521
+ _checkPermission(ctx, method, args);
522
+ }
523
+
524
+ function _checkPermission(ctx, method, args) {
525
+ if (!ctx.permissions) return;
526
+ var scope = SCOPE_FOR_METHOD[method];
527
+ if (!scope) return;
528
+ if (!args || !args.actor) {
529
+ throw new MailAgentError("mail-agent/no-actor",
530
+ "agent." + method + ": actor required");
531
+ }
532
+ if (!ctx.permissions.check(args.actor, scope)) {
533
+ ctx.auditEmit("mail.agent.permission_denied", args.actor, { method: method, scope: scope });
534
+ throw new MailAgentError("mail-agent/permission-denied",
535
+ "agent." + method + ": actor lacks scope '" + scope + "'");
536
+ }
537
+ }
538
+
539
+ function _profileFor(ctx) {
540
+ // Posture pins strict; otherwise default to strict (the framework's
541
+ // security-defaults-on rule applies here too — operators opt down
542
+ // explicitly when needed).
543
+ return ctx.posture ? "strict" : "strict";
544
+ }
545
+
546
+ function _validateDispatch(d) {
547
+ var mode = d.mode || "auto";
548
+ if (mode !== "local" && mode !== "queue" && mode !== "auto") {
549
+ throw new MailAgentError("mail-agent/bad-dispatch-mode",
550
+ "b.mail.agent.create: dispatch.mode must be 'local' | 'queue' | 'auto'");
551
+ }
552
+ if (mode === "queue" && (!d.queue || typeof d.queue.enqueue !== "function")) {
553
+ throw new MailAgentError("mail-agent/no-queue",
554
+ "b.mail.agent.create: dispatch.mode='queue' requires opts.dispatch.queue with .enqueue()");
555
+ }
556
+ if (d.workerPool && typeof d.workerPool.run !== "function") {
557
+ throw new MailAgentError("mail-agent/bad-worker-pool",
558
+ "b.mail.agent.create: dispatch.workerPool must expose .run()");
559
+ }
560
+ var topic = d.queueTopic || DEFAULT_QUEUE_TOPIC;
561
+ var taskTimeoutMs = typeof d.taskTimeoutMs === "number" ? d.taskTimeoutMs : DEFAULT_TASK_TIMEOUT_MS;
562
+ if (!isFinite(taskTimeoutMs) || taskTimeoutMs <= 0) {
563
+ throw new MailAgentError("mail-agent/bad-task-timeout",
564
+ "b.mail.agent.create: dispatch.taskTimeoutMs must be a positive finite number");
565
+ }
566
+ var queueDepthCap = typeof d.queueDepthCap === "number" ? d.queueDepthCap : DEFAULT_QUEUE_DEPTH_CAP;
567
+ if (!isFinite(queueDepthCap) || queueDepthCap < 0) {
568
+ throw new MailAgentError("mail-agent/bad-queue-depth-cap",
569
+ "b.mail.agent.create: dispatch.queueDepthCap must be a non-negative finite number");
570
+ }
571
+ var vaultKeyDelivery = d.vaultKeyDelivery || "in-worker";
572
+ if (vaultKeyDelivery !== "in-worker" && vaultKeyDelivery !== "main-only") {
573
+ throw new MailAgentError("mail-agent/bad-vault-key-delivery",
574
+ "b.mail.agent.create: dispatch.vaultKeyDelivery must be 'in-worker' | 'main-only'");
575
+ }
576
+ return {
577
+ mode: mode,
578
+ queue: d.queue || null,
579
+ workerPool: d.workerPool || null,
580
+ queueTopic: topic,
581
+ taskTimeoutMs: taskTimeoutMs,
582
+ queueDepthCap: queueDepthCap,
583
+ vaultKeyDelivery: vaultKeyDelivery,
584
+ };
585
+ }
586
+
587
+ function _identityResolver(spec) {
588
+ if (typeof spec === "function") return spec;
589
+ if (spec && typeof spec === "object") {
590
+ return function (actorId) { return spec[actorId] || null; };
591
+ }
592
+ return function () { return null; };
593
+ }
594
+
595
+ function _auditEmitter(auditOverride) {
596
+ if (auditOverride && typeof auditOverride.safeEmit === "function") {
597
+ return function (event, actor, metadata) {
598
+ auditOverride.safeEmit({
599
+ action: event,
600
+ actor: _actorShape(actor),
601
+ outcome: event.indexOf("denied") >= 0 || event.indexOf("not_implemented") >= 0 ? "failure" : "success",
602
+ metadata: metadata || {},
603
+ });
604
+ };
605
+ }
606
+ return function (event, actor, metadata) {
607
+ audit().safeEmit({
608
+ action: event,
609
+ actor: _actorShape(actor),
610
+ outcome: event.indexOf("denied") >= 0 || event.indexOf("not_implemented") >= 0 ? "failure" : "success",
611
+ metadata: metadata || {},
612
+ });
613
+ };
614
+ }
615
+
616
+ function _actorShape(actor) {
617
+ if (!actor || typeof actor !== "object") return { id: "<unknown>" };
618
+ return { id: actor.id, roles: actor.roles || [] };
619
+ }
620
+
621
+ module.exports = {
622
+ create: create,
623
+ consumer: consumer,
624
+ MailAgentError: MailAgentError,
625
+ SCOPE_FOR_METHOD: SCOPE_FOR_METHOD,
626
+ WIRED_AT: WIRED_AT,
627
+ HEAVY_METHODS: HEAVY_METHODS,
628
+ // Re-export the guard family so callers can introspect without
629
+ // separate requires.
630
+ guards: {
631
+ query: guardMailQuery,
632
+ compose: guardMailCompose,
633
+ reply: guardMailReply,
634
+ move: guardMailMove,
635
+ sieve: guardMailSieve,
636
+ messageId: guardMessageId,
637
+ },
638
+ };