@blamejs/core 0.9.49 → 0.10.2

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 (82) hide show
  1. package/CHANGELOG.md +952 -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 +78 -5
  23. package/lib/circuit-breaker.js +10 -2
  24. package/lib/cli.js +13 -0
  25. package/lib/compliance.js +176 -8
  26. package/lib/crypto-field.js +114 -14
  27. package/lib/crypto.js +216 -20
  28. package/lib/db.js +1 -0
  29. package/lib/guard-graphql.js +37 -0
  30. package/lib/guard-jmap.js +321 -0
  31. package/lib/guard-managesieve-command.js +566 -0
  32. package/lib/guard-pop3-command.js +317 -0
  33. package/lib/guard-regex.js +138 -1
  34. package/lib/guard-smtp-command.js +58 -3
  35. package/lib/guard-xml.js +39 -1
  36. package/lib/mail-agent.js +20 -7
  37. package/lib/mail-arc-sign.js +12 -8
  38. package/lib/mail-auth.js +323 -34
  39. package/lib/mail-crypto-pgp.js +934 -0
  40. package/lib/mail-crypto-smime.js +340 -0
  41. package/lib/mail-crypto.js +108 -0
  42. package/lib/mail-dav.js +1224 -0
  43. package/lib/mail-deploy.js +492 -0
  44. package/lib/mail-dkim.js +431 -26
  45. package/lib/mail-journal.js +435 -0
  46. package/lib/mail-scan.js +502 -0
  47. package/lib/mail-server-imap.js +64 -26
  48. package/lib/mail-server-jmap.js +488 -0
  49. package/lib/mail-server-managesieve.js +853 -0
  50. package/lib/mail-server-mx.js +40 -30
  51. package/lib/mail-server-pop3.js +836 -0
  52. package/lib/mail-server-rate-limit.js +13 -0
  53. package/lib/mail-server-submission.js +70 -24
  54. package/lib/mail-server-tls.js +445 -0
  55. package/lib/mail-sieve.js +557 -0
  56. package/lib/mail-spam-score.js +284 -0
  57. package/lib/mail.js +99 -0
  58. package/lib/metrics.js +80 -3
  59. package/lib/middleware/dpop.js +58 -3
  60. package/lib/middleware/idempotency-key.js +255 -42
  61. package/lib/middleware/protected-resource-metadata.js +114 -2
  62. package/lib/network-dns-resolver.js +33 -0
  63. package/lib/network-tls.js +46 -0
  64. package/lib/otel-export.js +13 -4
  65. package/lib/outbox.js +62 -12
  66. package/lib/pqc-agent.js +13 -5
  67. package/lib/retry.js +23 -9
  68. package/lib/router.js +23 -1
  69. package/lib/safe-ical.js +634 -0
  70. package/lib/safe-icap.js +502 -0
  71. package/lib/safe-mime.js +15 -0
  72. package/lib/safe-sieve.js +684 -0
  73. package/lib/safe-smtp.js +57 -0
  74. package/lib/safe-url.js +37 -0
  75. package/lib/safe-vcard.js +473 -0
  76. package/lib/self-update-standalone-verifier.js +32 -3
  77. package/lib/self-update.js +153 -33
  78. package/lib/vendor/MANIFEST.json +161 -156
  79. package/lib/vendor-data.js +127 -9
  80. package/lib/vex.js +324 -59
  81. package/package.json +1 -1
  82. package/sbom.cdx.json +6 -6
@@ -0,0 +1,557 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * @module b.mail.sieve
5
+ * @nav Mail
6
+ * @title Sieve interpreter
7
+ * @order 240
8
+ * @since 0.9.55
9
+ *
10
+ * @intro
11
+ * RFC 5228 Sieve interpreter that walks the AST produced by
12
+ * `b.safeSieve.parse(script)` and emits an ordered action list the
13
+ * delivery agent applies to the inbound message. Runs under a gas
14
+ * counter (default 10 000 ops) so a hostile or runaway script can't
15
+ * stall the delivery thread. The interpreter is synchronous and
16
+ * pure — every test reads from the operator-supplied `env` object;
17
+ * the interpreter never touches the mail store, never opens a
18
+ * socket, never executes operator-supplied code. Side-effects (file-
19
+ * into / redirect / discard) materialize as result entries the
20
+ * caller dispatches against the store.
21
+ *
22
+ * Tests implemented at v0.9.55:
23
+ * - `address` — header-address-list test with `:all` / `:localpart`
24
+ * / `:domain` address-parts and `:is` / `:contains` / `:matches`
25
+ * match-types
26
+ * - `header` — header-value test with the same match-types
27
+ * - `envelope` — RFC 5228 §5.4; reads `env.envelope.{from,to}`
28
+ * - `exists` — header-presence test
29
+ * - `size` — `:over N` / `:under N` byte-count test
30
+ * - `not` / `allof` / `anyof` / `true` / `false`
31
+ *
32
+ * Actions implemented at v0.9.55:
33
+ * - `keep` — implicit default per §2.10.2
34
+ * - `fileinto "Folder"` — RFC 5228 §4.1
35
+ * - `discard` — RFC 5228 §4.4
36
+ * - `redirect "addr"` — RFC 5228 §4.2; tagged with the address
37
+ * - `stop` — RFC 5228 §3.2; halts further command execution
38
+ *
39
+ * Comparators: `i;octet` (default, exact byte) + `i;ascii-casemap`
40
+ * (case-insensitive ASCII). Other comparators refused at script
41
+ * parse time (require 'comparator-NAME' not in KNOWN_CAPABILITIES).
42
+ *
43
+ * Match-type wildcards: `:matches` uses `*` (any sequence) and `?`
44
+ * (one byte), per RFC 5228 §2.7.1. Both wildcards are converted to
45
+ * a bounded RegExp built from escaped literal byte segments — no
46
+ * user-controlled backtracking surface.
47
+ *
48
+ * The interpreter does NOT execute multi-script chains, sieve
49
+ * `include`s (RFC 6609), `notify` actions (RFC 5435), or `vacation`
50
+ * responses (RFC 5230); each of those will land with the
51
+ * corresponding extension RFC slice. Until then, scripts declaring
52
+ * them via `require` are refused at parse time.
53
+ *
54
+ * @card
55
+ * Sieve (RFC 5228) AST walker. Pure-functional, gas-bounded,
56
+ * side-effect-free — emits an action list the delivery agent
57
+ * dispatches.
58
+ */
59
+
60
+ var safeSieve = require("./safe-sieve");
61
+ var { defineClass } = require("./framework-error");
62
+ var numericBounds = require("./numeric-bounds");
63
+ var validateOpts = require("./validate-opts");
64
+
65
+ var MailSieveError = defineClass("MailSieveError", { alwaysPermanent: true });
66
+
67
+ var DEFAULT_GAS_UNITS = 10000; // allow:raw-byte-literal — operation cap
68
+ var MAX_GAS_UNITS = 1_000_000; // allow:raw-byte-literal — operator opt-up cap
69
+
70
+ // ---- env helpers ---------------------------------------------------------
71
+
72
+ function _headerValues(env, name) {
73
+ if (!env || !env.headers) return [];
74
+ var lc = String(name).toLowerCase();
75
+ var out = [];
76
+ for (var i = 0; i < env.headers.length; i++) {
77
+ var h = env.headers[i];
78
+ if (h && typeof h.name === "string" && h.name.toLowerCase() === lc) {
79
+ out.push(String(h.value != null ? h.value : ""));
80
+ }
81
+ }
82
+ return out;
83
+ }
84
+
85
+ function _addressesOf(values, part) {
86
+ // Parse "Name <local@domain>" address-list, return the requested
87
+ // part: :all / :localpart / :domain. RFC 5228 §5.1.
88
+ var out = [];
89
+ for (var i = 0; i < values.length; i++) {
90
+ var v = values[i];
91
+ var pieces = v.split(","); // crude but bounded
92
+ for (var j = 0; j < pieces.length; j++) {
93
+ var p = pieces[j].trim();
94
+ if (!p) continue;
95
+ var ltIdx = p.indexOf("<");
96
+ var gtIdx = p.lastIndexOf(">");
97
+ var addr = (ltIdx !== -1 && gtIdx > ltIdx) ? p.slice(ltIdx + 1, gtIdx).trim() : p;
98
+ if (part === "localpart" || part === "domain") {
99
+ var at = addr.lastIndexOf("@");
100
+ if (at === -1) {
101
+ if (part === "localpart") out.push(addr);
102
+ else out.push("");
103
+ continue;
104
+ }
105
+ out.push(part === "localpart" ? addr.slice(0, at) : addr.slice(at + 1));
106
+ continue;
107
+ }
108
+ out.push(addr); // :all
109
+ }
110
+ }
111
+ return out;
112
+ }
113
+
114
+ function _envelopeAddresses(env, key) {
115
+ if (!env || !env.envelope) return [];
116
+ var v = env.envelope[key];
117
+ if (v == null) return [];
118
+ if (Array.isArray(v)) return v.map(String);
119
+ return [String(v)];
120
+ }
121
+
122
+ // ---- match-type ---------------------------------------------------------
123
+
124
+ function _escapeRe(s) {
125
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
126
+ }
127
+
128
+ function _wildcardToRe(pattern, caseInsensitive) {
129
+ // RFC 5228 §2.7.1 — `*` matches any sequence, `?` matches one. Escape
130
+ // every other regex meta. Anchored both ends.
131
+ var out = "^";
132
+ for (var i = 0; i < pattern.length; i++) {
133
+ var c = pattern[i];
134
+ if (c === "*") out += ".*";
135
+ else if (c === "?") out += ".";
136
+ else out += _escapeRe(c);
137
+ }
138
+ out += "$";
139
+ return new RegExp(out, caseInsensitive ? "i" : ""); // allow:dynamic-regex — built from operator Sieve `:matches` pattern; every meta-char except `*`/`?` is regex-escaped, so the resulting NFA is linear in input length (no polynomial-backtrack surface)
140
+ }
141
+
142
+ function _matches(haystack, needle, matchType, comparator) {
143
+ var ci = (comparator === "i;ascii-casemap");
144
+ if (matchType === "is") {
145
+ return ci
146
+ ? haystack.toLowerCase() === needle.toLowerCase()
147
+ : haystack === needle;
148
+ }
149
+ if (matchType === "contains") {
150
+ return ci
151
+ ? haystack.toLowerCase().indexOf(needle.toLowerCase()) !== -1
152
+ : haystack.indexOf(needle) !== -1;
153
+ }
154
+ if (matchType === "matches") {
155
+ return _wildcardToRe(needle, ci).test(haystack);
156
+ }
157
+ throw new MailSieveError("mail-sieve/bad-match-type",
158
+ "unknown match-type: " + matchType);
159
+ }
160
+
161
+ function _anyOfMatches(haystackList, needleList, matchType, comparator) {
162
+ for (var i = 0; i < haystackList.length; i++) {
163
+ for (var j = 0; j < needleList.length; j++) {
164
+ if (_matches(haystackList[i], needleList[j], matchType, comparator)) return true;
165
+ }
166
+ }
167
+ return false;
168
+ }
169
+
170
+ // ---- tag helpers --------------------------------------------------------
171
+
172
+ function _tagAmong(tags, names, fallback) {
173
+ if (!tags || !tags.length) return fallback;
174
+ for (var i = 0; i < tags.length; i++) {
175
+ if (names.indexOf(tags[i].name) !== -1) return tags[i].name;
176
+ }
177
+ return fallback;
178
+ }
179
+
180
+ function _tagComparator(tags) {
181
+ // `:comparator "i;octet"` — comparator name is the FOLLOWING positional
182
+ // string per §2.7.3. We don't have positional-vs-tag separation in args
183
+ // yet; for v0.9.55 only the tag presence is honored, comparator name
184
+ // pulled from positional[0] only when `:comparator` is present.
185
+ for (var i = 0; i < tags.length; i++) {
186
+ if (tags[i].name === "comparator" && tags[i].val) return tags[i].val;
187
+ }
188
+ return null;
189
+ }
190
+
191
+ function _strArg(positional, idx) {
192
+ var p = positional[idx];
193
+ if (!p || (p.kind !== "str" && p.kind !== "list")) return null;
194
+ return p.kind === "str" ? p.v : (p.v[0] || null);
195
+ }
196
+
197
+ function _listArg(positional, idx) {
198
+ var p = positional[idx];
199
+ if (!p) return null;
200
+ if (p.kind === "list") return p.v;
201
+ if (p.kind === "str") return [p.v];
202
+ return null;
203
+ }
204
+
205
+ // ---- test evaluation ----------------------------------------------------
206
+
207
+ function _evalTest(test, env, ctx) {
208
+ ctx.gas++;
209
+ if (ctx.gas > ctx.maxGas) {
210
+ throw new MailSieveError("mail-sieve/gas-exhausted",
211
+ "Sieve gas exhausted (cap " + ctx.maxGas + ")");
212
+ }
213
+ var name = test.name;
214
+
215
+ if (name === "true") return true;
216
+ if (name === "false") return false;
217
+
218
+ if (name === "not") {
219
+ return !_evalTest(test.subs[0], env, ctx);
220
+ }
221
+ if (name === "allof") {
222
+ for (var i = 0; i < test.subs.length; i++) {
223
+ if (!_evalTest(test.subs[i], env, ctx)) return false;
224
+ }
225
+ return true;
226
+ }
227
+ if (name === "anyof") {
228
+ for (var j = 0; j < test.subs.length; j++) {
229
+ if (_evalTest(test.subs[j], env, ctx)) return true;
230
+ }
231
+ return false;
232
+ }
233
+
234
+ var args = test.args || { tags: [], positional: [] };
235
+
236
+ if (name === "exists") {
237
+ var headerNames = _listArg(args.positional, 0);
238
+ if (!headerNames) return false;
239
+ for (var h = 0; h < headerNames.length; h++) {
240
+ if (_headerValues(env, headerNames[h]).length === 0) return false;
241
+ }
242
+ return true;
243
+ }
244
+
245
+ if (name === "size") {
246
+ var bytes = (env && typeof env.sizeBytes === "number") ? env.sizeBytes :
247
+ (env && env.bodyBytes ? env.bodyBytes.length : 0);
248
+ var num = args.positional[0] && args.positional[0].kind === "num" ?
249
+ args.positional[0].v : 0;
250
+ var mode = _tagAmong(args.tags, ["over", "under"], "over");
251
+ return mode === "over" ? bytes > num : bytes < num;
252
+ }
253
+
254
+ if (name === "header") {
255
+ var matchType = _tagAmong(args.tags, ["is", "contains", "matches"], "is");
256
+ var comparator = _tagComparator(args.tags) || "i;ascii-casemap";
257
+ var hdrNames = _listArg(args.positional, 0) || [];
258
+ var keys = _listArg(args.positional, 1) || [];
259
+ var allValues = [];
260
+ for (var hh = 0; hh < hdrNames.length; hh++) {
261
+ var vs = _headerValues(env, hdrNames[hh]);
262
+ for (var vv = 0; vv < vs.length; vv++) allValues.push(vs[vv]);
263
+ }
264
+ return _anyOfMatches(allValues, keys, matchType, comparator);
265
+ }
266
+
267
+ if (name === "address" || name === "envelope") {
268
+ var matchType2 = _tagAmong(args.tags, ["is", "contains", "matches"], "is");
269
+ var comparator2 = _tagComparator(args.tags) || "i;ascii-casemap";
270
+ var addrPart = _tagAmong(args.tags, ["all", "localpart", "domain"], "all");
271
+ var addrFields = _listArg(args.positional, 0) || [];
272
+ var keys2 = _listArg(args.positional, 1) || [];
273
+ var allAddrs = [];
274
+ for (var af = 0; af < addrFields.length; af++) {
275
+ var fieldName = addrFields[af];
276
+ var values;
277
+ if (name === "envelope") {
278
+ // RFC 5228 §5.4 — only "from" and "to" defined for envelope test.
279
+ var lc = String(fieldName).toLowerCase();
280
+ if (lc !== "from" && lc !== "to") continue;
281
+ values = _envelopeAddresses(env, lc);
282
+ } else {
283
+ values = _headerValues(env, fieldName);
284
+ }
285
+ var parts = _addressesOf(values, addrPart);
286
+ for (var pp = 0; pp < parts.length; pp++) allAddrs.push(parts[pp]);
287
+ }
288
+ return _anyOfMatches(allAddrs, keys2, matchType2, comparator2);
289
+ }
290
+
291
+ // Unknown test — refuse rather than evaluate. The parser already
292
+ // refused any `require` that didn't resolve to KNOWN_CAPABILITIES, so
293
+ // reaching here means a known-capability test we forgot to wire.
294
+ throw new MailSieveError("mail-sieve/unknown-test",
295
+ "Sieve test '" + name + "' is RFC-defined but not wired in v0.9.55");
296
+ }
297
+
298
+ // ---- action evaluation --------------------------------------------------
299
+
300
+ function _runCommand(cmd, env, ctx) {
301
+ ctx.gas++;
302
+ if (ctx.gas > ctx.maxGas) {
303
+ throw new MailSieveError("mail-sieve/gas-exhausted",
304
+ "Sieve gas exhausted (cap " + ctx.maxGas + ")");
305
+ }
306
+ if (ctx.stopped) return;
307
+
308
+ if (cmd.kind === "require") return; // parser handled it
309
+
310
+ if (cmd.kind === "if") {
311
+ if (_evalTest(cmd.test, env, ctx)) {
312
+ _runBlock(cmd.thenBody, env, ctx);
313
+ return;
314
+ }
315
+ for (var i = 0; i < cmd.elif.length; i++) {
316
+ if (_evalTest(cmd.elif[i].test, env, ctx)) {
317
+ _runBlock(cmd.elif[i].body, env, ctx);
318
+ return;
319
+ }
320
+ }
321
+ if (cmd.elseBody) {
322
+ _runBlock(cmd.elseBody, env, ctx);
323
+ }
324
+ return;
325
+ }
326
+
327
+ if (cmd.kind === "action") {
328
+ var n = cmd.name;
329
+ var pos = cmd.args.positional;
330
+ if (n === "keep") {
331
+ ctx.implicitKeepCancelled = false;
332
+ ctx.actions.push({ kind: "keep" });
333
+ return;
334
+ }
335
+ if (n === "fileinto") {
336
+ var folder = _strArg(pos, 0);
337
+ if (folder == null) {
338
+ throw new MailSieveError("mail-sieve/bad-fileinto",
339
+ "fileinto requires a folder name");
340
+ }
341
+ ctx.implicitKeepCancelled = true;
342
+ ctx.actions.push({ kind: "fileinto", folder: folder });
343
+ return;
344
+ }
345
+ if (n === "discard") {
346
+ ctx.implicitKeepCancelled = true;
347
+ ctx.actions.push({ kind: "discard" });
348
+ return;
349
+ }
350
+ if (n === "redirect") {
351
+ var addr = _strArg(pos, 0);
352
+ if (addr == null) {
353
+ throw new MailSieveError("mail-sieve/bad-redirect",
354
+ "redirect requires an address");
355
+ }
356
+ ctx.implicitKeepCancelled = true;
357
+ ctx.actions.push({ kind: "redirect", address: addr });
358
+ return;
359
+ }
360
+ if (n === "stop") {
361
+ ctx.stopped = true;
362
+ return;
363
+ }
364
+ throw new MailSieveError("mail-sieve/unknown-action",
365
+ "Sieve action '" + n + "' is not wired in v0.9.55");
366
+ }
367
+
368
+ throw new MailSieveError("mail-sieve/bad-command",
369
+ "unknown command kind '" + cmd.kind + "'");
370
+ }
371
+
372
+ function _runBlock(commands, env, ctx) {
373
+ for (var i = 0; i < commands.length; i++) {
374
+ if (ctx.stopped) return;
375
+ _runCommand(commands[i], env, ctx);
376
+ }
377
+ }
378
+
379
+ /**
380
+ * @primitive b.mail.sieve.run
381
+ * @signature b.mail.sieve.run(ast, env, opts?)
382
+ * @since 0.9.55
383
+ * @status stable
384
+ * @related b.safeSieve.parse, b.mail.agent.create
385
+ *
386
+ * Walk a parsed Sieve AST against the message environment + return the
387
+ * ordered action list. The interpreter is pure — it reads only from
388
+ * `env` and never mutates it; every action surfaces as an entry in
389
+ * the returned list for the caller to dispatch.
390
+ *
391
+ * @opts
392
+ * maxGas: number, // default 10000; cap 1_000_000
393
+ *
394
+ * @example
395
+ * var ast = b.safeSieve.parse('if header :contains "X-Spam" "yes" { fileinto "Junk"; }');
396
+ * var rv = b.mail.sieve.run(ast, {
397
+ * headers: [{ name: "X-Spam", value: "yes" }],
398
+ * envelope: { from: "sender@example.com", to: "rcpt@example.com" },
399
+ * sizeBytes: 1024,
400
+ * });
401
+ * // → { actions: [{ kind: "fileinto", folder: "Junk" }, { kind: "keep" }], gas: 3, stopped: false }
402
+ */
403
+ function run(ast, env, opts) {
404
+ if (!ast || ast.kind !== "script") {
405
+ throw new MailSieveError("mail-sieve/bad-ast",
406
+ "mail.sieve.run: ast must be a parsed Sieve script (b.safeSieve.parse output)");
407
+ }
408
+ opts = opts || {};
409
+ var maxGas = opts.maxGas === undefined ? DEFAULT_GAS_UNITS : opts.maxGas;
410
+ if (!numericBounds.isPositiveFiniteInt(maxGas)) {
411
+ throw new MailSieveError("mail-sieve/bad-opt",
412
+ "maxGas must be a positive finite integer; got " + numericBounds.shape(maxGas));
413
+ }
414
+ if (maxGas > MAX_GAS_UNITS) {
415
+ throw new MailSieveError("mail-sieve/bad-opt",
416
+ "mail.sieve.run: maxGas " + maxGas + " exceeds cap " + MAX_GAS_UNITS);
417
+ }
418
+ var ctx = {
419
+ gas: 0,
420
+ maxGas: maxGas,
421
+ actions: [],
422
+ stopped: false,
423
+ implicitKeepCancelled: false,
424
+ };
425
+ _runBlock(ast.commands, env || {}, ctx);
426
+ // RFC 5228 §2.10.2 — if no explicit `keep` / `fileinto` / `discard` /
427
+ // `redirect` fired, implicit keep applies.
428
+ if (!ctx.implicitKeepCancelled) {
429
+ var sawExplicitKeep = false;
430
+ for (var i = 0; i < ctx.actions.length; i++) {
431
+ if (ctx.actions[i].kind === "keep") { sawExplicitKeep = true; break; }
432
+ }
433
+ if (!sawExplicitKeep) ctx.actions.push({ kind: "keep", implicit: true });
434
+ }
435
+ return { actions: ctx.actions, gas: ctx.gas, stopped: ctx.stopped };
436
+ }
437
+
438
+ /**
439
+ * @primitive b.mail.sieve.runScript
440
+ * @signature b.mail.sieve.runScript(script, env, opts?)
441
+ * @since 0.9.55
442
+ * @status stable
443
+ * @related b.mail.sieve.run, b.safeSieve.parse
444
+ *
445
+ * Parse + run in one call. Most call sites — JMAP `SieveScript/validate`,
446
+ * MX delivery hook — want this shape.
447
+ *
448
+ * @opts
449
+ * profile: "strict" | "balanced" | "permissive",
450
+ * compliancePosture: "hipaa" | "pci-dss" | "gdpr" | "soc2",
451
+ * maxGas: number,
452
+ *
453
+ * @example
454
+ * var rv = b.mail.sieve.runScript(
455
+ * 'require ["fileinto"];\nif header :is "From" "boss@x.com" { fileinto "Important"; }',
456
+ * { headers: [{ name: "From", value: "boss@x.com" }] }
457
+ * );
458
+ * rv.actions[0].folder; // → "Important"
459
+ */
460
+ function runScript(script, env, opts) {
461
+ var ast = safeSieve.parse(script, opts);
462
+ return run(ast, env, opts);
463
+ }
464
+
465
+ /**
466
+ * @primitive b.mail.sieve.create
467
+ * @signature b.mail.sieve.create(opts?)
468
+ * @since 0.9.55
469
+ * @status stable
470
+ * @related b.mail.sieve.run, b.mail.agent.create
471
+ *
472
+ * Returns a stateful Sieve handle the delivery agent + JMAP
473
+ * `SieveScript/validate` method compose. Distinct from the bare
474
+ * `b.mail.sieve.run(ast, env)` entry — the handle carries operator-
475
+ * supplied opts (`maxGas`, `profile`, `compliancePosture`, `audit`)
476
+ * so every invocation runs with the same posture.
477
+ *
478
+ * @opts
479
+ * maxGas: number,
480
+ * profile: "strict" | "balanced" | "permissive",
481
+ * compliancePosture: "hipaa" | "pci-dss" | "gdpr" | "soc2",
482
+ * audit: { safeEmit: function },
483
+ *
484
+ * @example
485
+ * var sieve = b.mail.sieve.create({ profile: "strict", audit: b.audit });
486
+ * sieve.validateScript(operatorScript);
487
+ * var rv = await sieve.runScript(operatorScript, mailEnv);
488
+ */
489
+ function create(opts) {
490
+ opts = validateOpts.requireObject(opts || {}, "mail.sieve.create",
491
+ MailSieveError, "mail-sieve/bad-opt");
492
+ var maxGas = opts.maxGas === undefined ? DEFAULT_GAS_UNITS : opts.maxGas;
493
+ if (!numericBounds.isPositiveFiniteInt(maxGas)) {
494
+ throw new MailSieveError("mail-sieve/bad-opt",
495
+ "maxGas must be a positive finite integer; got " + numericBounds.shape(maxGas));
496
+ }
497
+ if (maxGas > MAX_GAS_UNITS) {
498
+ throw new MailSieveError("mail-sieve/bad-opt",
499
+ "mail.sieve.create: maxGas " + maxGas + " exceeds cap " + MAX_GAS_UNITS);
500
+ }
501
+ var profile = opts.profile;
502
+ var posture = opts.compliancePosture;
503
+ var audit = opts.audit;
504
+
505
+ function _emit(action, outcome, metadata) {
506
+ if (!audit || typeof audit.safeEmit !== "function") return;
507
+ try {
508
+ audit.safeEmit({
509
+ action: action,
510
+ outcome: outcome || "success",
511
+ metadata: metadata || {},
512
+ });
513
+ } catch (_e) { /* drop-silent */ }
514
+ }
515
+
516
+ function validateScript(script) {
517
+ var rv = safeSieve.validate(script, { profile: profile, compliancePosture: posture });
518
+ if (rv.ok) {
519
+ _emit("mail.sieve.validate", "success", { requiredCaps: rv.requiredCaps });
520
+ } else {
521
+ _emit("mail.sieve.validate", "failure", { issues: rv.issues });
522
+ }
523
+ return rv;
524
+ }
525
+
526
+ function runScript_(script, env) {
527
+ var ast = safeSieve.parse(script, { profile: profile, compliancePosture: posture });
528
+ var rv = run(ast, env, { maxGas: maxGas });
529
+ _emit("mail.sieve.run", "success", {
530
+ gas: rv.gas, actionCount: rv.actions.length, stopped: rv.stopped,
531
+ });
532
+ return rv;
533
+ }
534
+
535
+ function runAst(ast, env) {
536
+ var rv = run(ast, env, { maxGas: maxGas });
537
+ _emit("mail.sieve.run", "success", {
538
+ gas: rv.gas, actionCount: rv.actions.length, stopped: rv.stopped,
539
+ });
540
+ return rv;
541
+ }
542
+
543
+ return {
544
+ validateScript: validateScript,
545
+ runScript: runScript_,
546
+ run: runAst,
547
+ };
548
+ }
549
+
550
+ module.exports = {
551
+ create: create,
552
+ run: run,
553
+ runScript: runScript,
554
+ MailSieveError: MailSieveError,
555
+ DEFAULT_GAS_UNITS: DEFAULT_GAS_UNITS,
556
+ MAX_GAS_UNITS: MAX_GAS_UNITS,
557
+ };