@blamejs/core 0.9.46 → 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 (78) hide show
  1. package/CHANGELOG.md +951 -893
  2. package/index.js +30 -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-imap-command.js +335 -0
  29. package/lib/guard-jmap.js +321 -0
  30. package/lib/guard-managesieve-command.js +566 -0
  31. package/lib/guard-pop3-command.js +317 -0
  32. package/lib/guard-smtp-command.js +58 -3
  33. package/lib/mail-agent.js +20 -7
  34. package/lib/mail-arc-sign.js +12 -8
  35. package/lib/mail-auth.js +323 -34
  36. package/lib/mail-crypto-pgp.js +934 -0
  37. package/lib/mail-crypto-smime.js +340 -0
  38. package/lib/mail-crypto.js +108 -0
  39. package/lib/mail-dav.js +1224 -0
  40. package/lib/mail-deploy.js +492 -0
  41. package/lib/mail-dkim.js +431 -26
  42. package/lib/mail-journal.js +435 -0
  43. package/lib/mail-scan.js +502 -0
  44. package/lib/mail-server-imap.js +1102 -0
  45. package/lib/mail-server-jmap.js +488 -0
  46. package/lib/mail-server-managesieve.js +853 -0
  47. package/lib/mail-server-mx.js +164 -34
  48. package/lib/mail-server-pop3.js +836 -0
  49. package/lib/mail-server-rate-limit.js +269 -0
  50. package/lib/mail-server-submission.js +1032 -0
  51. package/lib/mail-server-tls.js +445 -0
  52. package/lib/mail-sieve.js +557 -0
  53. package/lib/mail-spam-score.js +284 -0
  54. package/lib/mail.js +99 -0
  55. package/lib/metrics.js +130 -10
  56. package/lib/middleware/dpop.js +58 -3
  57. package/lib/middleware/idempotency-key.js +255 -42
  58. package/lib/middleware/protected-resource-metadata.js +114 -2
  59. package/lib/network-dns-resolver.js +33 -0
  60. package/lib/network-tls.js +46 -0
  61. package/lib/outbox.js +62 -12
  62. package/lib/pqc-agent.js +13 -5
  63. package/lib/retry.js +23 -9
  64. package/lib/router.js +23 -1
  65. package/lib/safe-ical.js +634 -0
  66. package/lib/safe-icap.js +502 -0
  67. package/lib/safe-mime.js +15 -0
  68. package/lib/safe-sieve.js +684 -0
  69. package/lib/safe-smtp.js +57 -0
  70. package/lib/safe-url.js +37 -0
  71. package/lib/safe-vcard.js +473 -0
  72. package/lib/self-update-standalone-verifier.js +32 -3
  73. package/lib/self-update.js +168 -17
  74. package/lib/vendor/MANIFEST.json +161 -156
  75. package/lib/vendor-data.js +127 -9
  76. package/lib/vex.js +324 -59
  77. package/package.json +1 -1
  78. package/sbom.cdx.json +6 -6
@@ -0,0 +1,684 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * @module b.safeSieve
5
+ * @nav Mail
6
+ * @title Sieve parser
7
+ * @order 230
8
+ * @since 0.9.55
9
+ *
10
+ * @intro
11
+ * Bounded RFC 5228 Sieve parser. Produces an AST that
12
+ * `b.mail.sieve.run` walks at delivery time + at `agent.sieve.put`
13
+ * pre-validation. Caps script bytes / nesting depth / string-list
14
+ * length / per-string bytes per profile so a hostile script can't
15
+ * exhaust the parser. Refuses C0 / DEL / NUL controls outside
16
+ * string literals, refuses bare LF / bare CR (Sieve uses CRLF line
17
+ * terminators per RFC 5228 §2.1), and refuses oversized scripts at
18
+ * the byte level before tokenization.
19
+ *
20
+ * Grammar coverage:
21
+ * - `require ["module" ...]`
22
+ * - control: `if` / `elsif` / `else` with block-body
23
+ * - tests: `address` / `header` / `exists` / `size` / `envelope`
24
+ * (when `envelope` capability declared), plus `not` / `allof` /
25
+ * `anyof` / `true` / `false`
26
+ * - actions: `keep` / `fileinto` / `discard` / `redirect` / `stop`
27
+ * - match-types: `:is` (default) / `:contains` / `:matches`
28
+ * - comparators: `i;octet` (default) / `i;ascii-casemap`
29
+ * - address-parts: `:all` (default) / `:localpart` / `:domain`
30
+ * - string lists, quoted strings (`"..."` with backslash escapes),
31
+ * multi-line strings (`text:\r\n...\r\n.\r\n`)
32
+ * - comments: `#` line and `/* block * /`
33
+ *
34
+ * Extensions deferred (RFC 5229 variables, 5230 vacation, 5231
35
+ * relational, 5232 imap4flags, 5233 subaddress, 5235 spamtest /
36
+ * virustest, 5260 date / index, 5293 editheader, 5429 reject /
37
+ * extlists, 5435 enotify, 5703 mime / replace / enclose /
38
+ * extracttext, 6009 ihave, 6131 mailboxid, 6134 extlists, 6558
39
+ * mailbox, 6609 include, 6785 imapsieve, 8580 fcc) — refused at
40
+ * `require` time so scripts depending on them fail fast rather than
41
+ * silently mis-execute. The framework will light these incrementally
42
+ * as the operator-roadmap calls for them; until then, ship the base
43
+ * grammar that covers ~80% of operator-written scripts.
44
+ *
45
+ * @card
46
+ * Bounded Sieve (RFC 5228) parser. Produces an AST the interpreter
47
+ * walks under a gas counter; the 17 extension RFCs are refused at
48
+ * `require` time until each lights up.
49
+ */
50
+
51
+ var { defineClass } = require("./framework-error");
52
+
53
+ var SafeSieveError = defineClass("SafeSieveError", { alwaysPermanent: true });
54
+
55
+ var DEFAULTS = Object.freeze({
56
+ maxScriptBytes: 65536, // allow:raw-byte-literal — 64 KiB
57
+ maxDepth: 32, // allow:raw-byte-literal — block nesting cap
58
+ maxIfChainLen: 32, // allow:raw-byte-literal — elsif/elsif... cap
59
+ maxStringListLen: 256, // allow:raw-byte-literal
60
+ maxStringBytes: 4096, // allow:raw-byte-literal — per-string cap
61
+ maxArgsPerCmd: 32, // allow:raw-byte-literal — per-command arg cap
62
+ maxRequiredCaps: 32, // allow:raw-byte-literal
63
+ });
64
+
65
+ var PROFILES = Object.freeze({
66
+ strict: Object.assign({}, DEFAULTS),
67
+ balanced: Object.assign({}, DEFAULTS, {
68
+ maxScriptBytes: 262144, // allow:raw-byte-literal — 256 KiB
69
+ maxDepth: 64, // allow:raw-byte-literal
70
+ maxIfChainLen: 64, // allow:raw-byte-literal
71
+ maxStringListLen: 1024, // allow:raw-byte-literal
72
+ maxStringBytes: 16384, // allow:raw-byte-literal
73
+ maxArgsPerCmd: 64, // allow:raw-byte-literal
74
+ }),
75
+ permissive: Object.assign({}, DEFAULTS, {
76
+ maxScriptBytes: 1048576, // allow:raw-byte-literal — 1 MiB
77
+ maxDepth: 128, // allow:raw-byte-literal
78
+ maxIfChainLen: 128, // allow:raw-byte-literal
79
+ maxStringListLen: 4096, // allow:raw-byte-literal
80
+ maxStringBytes: 65536, // allow:raw-byte-literal
81
+ maxArgsPerCmd: 128, // allow:raw-byte-literal
82
+ }),
83
+ });
84
+
85
+ var COMPLIANCE_POSTURES = Object.freeze({
86
+ hipaa: "strict",
87
+ "pci-dss": "strict",
88
+ gdpr: "strict",
89
+ soc2: "strict",
90
+ });
91
+
92
+ // RFC 5228 §1.2 capability identifiers. Each entry lists whether the
93
+ // framework's v0.9.55 interpreter implements the capability. Unknown
94
+ // or not-yet-implemented capabilities surface a typed parse error at
95
+ // `require` time per §3.2 — "If a script does not contain a require
96
+ // statement for a feature it uses, an implementation MUST NOT execute
97
+ // the script."
98
+ var KNOWN_CAPABILITIES = Object.freeze({
99
+ // RFC 5228 base implementation provides these implicitly; declaring
100
+ // them in `require` is allowed.
101
+ "fileinto": true, // §4.1 — implemented (action)
102
+ "envelope": true, // §5.4 — implemented (test)
103
+ "encoded-character": true, // §2.4.2.4 — implemented (string escape)
104
+ "comparator-i;octet": true,
105
+ "comparator-i;ascii-casemap": true,
106
+ // Deferred — scripts MUST declare via `require` and the framework
107
+ // refuses to load them until the corresponding slice ships. Listed
108
+ // here so the parser distinguishes "spec'd but not yet ours" from
109
+ // "unknown / typo".
110
+ "variables": false, // RFC 5229
111
+ "vacation": false, // RFC 5230
112
+ "relational": false, // RFC 5231
113
+ "imap4flags": false, // RFC 5232 // allow:raw-byte-literal — RFC number
114
+ "subaddress": false, // RFC 5233
115
+ "spamtest": false, // RFC 5235
116
+ "virustest": false, // RFC 5235
117
+ "date": false, // RFC 5260
118
+ "index": false, // RFC 5260
119
+ "editheader": false, // RFC 5293
120
+ "reject": false, // RFC 5429
121
+ "ereject": false, // RFC 5429
122
+ "enotify": false, // RFC 5435
123
+ "mime": false, // RFC 5703
124
+ "replace": false, // RFC 5703
125
+ "enclose": false, // RFC 5703
126
+ "extracttext": false, // RFC 5703
127
+ "ihave": false, // RFC 6009
128
+ "mailboxid": false, // RFC 6131
129
+ "extlists": false, // RFC 6134
130
+ "mailbox": false, // RFC 6558
131
+ "include": false, // RFC 6609
132
+ "imapsieve": false, // RFC 6785
133
+ "fcc": false, // RFC 8580 // allow:raw-time-literal — RFC number, not time
134
+ });
135
+
136
+ function _resolveProfile(opts) {
137
+ if (!opts) return "strict";
138
+ if (typeof opts.profile === "string") return opts.profile;
139
+ if (typeof opts.compliancePosture === "string") {
140
+ return COMPLIANCE_POSTURES[opts.compliancePosture] || "strict";
141
+ }
142
+ return "strict";
143
+ }
144
+
145
+ function _resolveCaps(opts) {
146
+ var name = _resolveProfile(opts);
147
+ var caps = PROFILES[name];
148
+ if (!caps) {
149
+ throw new SafeSieveError("safe-sieve/bad-profile",
150
+ "safeSieve: unknown profile '" + name + "' (expected strict|balanced|permissive)");
151
+ }
152
+ return caps;
153
+ }
154
+
155
+ // ---- Tokenizer -----------------------------------------------------------
156
+
157
+ // Token kinds:
158
+ // "id" — identifier (require / if / address / ...)
159
+ // "tag" — `:name`
160
+ // "str" — string literal (quoted or multi-line)
161
+ // "num" — number (optionally suffixed K/M/G)
162
+ // "lbr"/"rbr" — `{` / `}`
163
+ // "lsb"/"rsb" — `[` / `]`
164
+ // "lp"/"rp" — `(` / `)` (rare; not in base grammar but tolerated)
165
+ // "comma"
166
+ // "semi"
167
+ // "eof"
168
+
169
+ function _isIdStart(c) {
170
+ return (c >= 0x41 && c <= 0x5A) || // A-Z
171
+ (c >= 0x61 && c <= 0x7A) || // a-z
172
+ c === 0x5F; // _
173
+ }
174
+ function _isIdCont(c) {
175
+ return _isIdStart(c) ||
176
+ (c >= 0x30 && c <= 0x39) || // 0-9
177
+ c === 0x2D; // -
178
+ }
179
+ function _isDigit(c) { return c >= 0x30 && c <= 0x39; }
180
+
181
+ function _tokenize(script, caps) {
182
+ var tokens = [];
183
+ var i = 0;
184
+ var n = script.length;
185
+ var line = 1;
186
+ var col = 1;
187
+
188
+ function _error(msg, atI) {
189
+ var l = line, c = col;
190
+ if (atI !== undefined && atI !== i) {
191
+ // Recompute line/col at atI (cheap — only on error path)
192
+ l = 1; c = 1;
193
+ for (var k = 0; k < atI && k < n; k++) {
194
+ if (script.charCodeAt(k) === 0x0A) { l++; c = 1; } else { c++; }
195
+ }
196
+ }
197
+ throw new SafeSieveError("safe-sieve/parse-error",
198
+ "safeSieve.parse: " + msg + " at line " + l + ":" + c);
199
+ }
200
+
201
+ function _advance(ch) {
202
+ if (ch === 0x0A) { line++; col = 1; } else { col++; }
203
+ i++;
204
+ }
205
+
206
+ while (i < n) {
207
+ var c = script.charCodeAt(i);
208
+
209
+ // Whitespace + line counter.
210
+ if (c === 0x20 || c === 0x09) { _advance(c); continue; }
211
+ if (c === 0x0D) {
212
+ // CRLF expected; bare CR refused per RFC 5228 §2.1.
213
+ if (i + 1 < n && script.charCodeAt(i + 1) === 0x0A) {
214
+ i += 2; line++; col = 1; continue;
215
+ }
216
+ _error("bare CR (RFC 5228 §2.1 requires CRLF)");
217
+ }
218
+ if (c === 0x0A) { _advance(c); continue; }
219
+
220
+ // Control bytes outside strings refused (NUL / C0 except TAB/LF/CR).
221
+ if (c < 0x20 && c !== 0x09 && c !== 0x0A && c !== 0x0D) {
222
+ _error("control byte 0x" + c.toString(16) + " refused outside string literal"); // allow:raw-byte-literal — base-16 toString radix
223
+ }
224
+ if (c === 0x7F) _error("DEL byte refused outside string literal");
225
+
226
+ // Comments: `#` to end of line; `/* ... */` block.
227
+ if (c === 0x23) { // #
228
+ while (i < n && script.charCodeAt(i) !== 0x0A) _advance(script.charCodeAt(i));
229
+ continue;
230
+ }
231
+ if (c === 0x2F && i + 1 < n && script.charCodeAt(i + 1) === 0x2A) { // /*
232
+ i += 2; col += 2;
233
+ while (i + 1 < n && !(script.charCodeAt(i) === 0x2A && script.charCodeAt(i + 1) === 0x2F)) {
234
+ _advance(script.charCodeAt(i));
235
+ }
236
+ if (i + 1 >= n) _error("unterminated block comment");
237
+ i += 2; col += 2;
238
+ continue;
239
+ }
240
+
241
+ // Punctuation.
242
+ if (c === 0x7B) { tokens.push({ k: "lbr", line: line, col: col }); _advance(c); continue; }
243
+ if (c === 0x7D) { tokens.push({ k: "rbr", line: line, col: col }); _advance(c); continue; }
244
+ if (c === 0x5B) { tokens.push({ k: "lsb", line: line, col: col }); _advance(c); continue; }
245
+ if (c === 0x5D) { tokens.push({ k: "rsb", line: line, col: col }); _advance(c); continue; }
246
+ if (c === 0x28) { tokens.push({ k: "lp", line: line, col: col }); _advance(c); continue; }
247
+ if (c === 0x29) { tokens.push({ k: "rp", line: line, col: col }); _advance(c); continue; }
248
+ if (c === 0x2C) { tokens.push({ k: "comma", line: line, col: col }); _advance(c); continue; }
249
+ if (c === 0x3B) { tokens.push({ k: "semi", line: line, col: col }); _advance(c); continue; }
250
+
251
+ // Tag `:name`.
252
+ if (c === 0x3A) {
253
+ _advance(c);
254
+ if (i >= n || !_isIdStart(script.charCodeAt(i))) _error("`:` not followed by identifier");
255
+ var tagStart = i;
256
+ while (i < n && _isIdCont(script.charCodeAt(i))) _advance(script.charCodeAt(i));
257
+ tokens.push({ k: "tag", v: script.slice(tagStart, i), line: line, col: col });
258
+ continue;
259
+ }
260
+
261
+ // Number with optional K/M/G suffix.
262
+ if (_isDigit(c)) {
263
+ var nStart = i;
264
+ while (i < n && _isDigit(script.charCodeAt(i))) _advance(script.charCodeAt(i));
265
+ var num = parseInt(script.slice(nStart, i), 10);
266
+ if (i < n) {
267
+ var suf = script.charCodeAt(i);
268
+ if (suf === 0x4B || suf === 0x6B) { num *= 1024; _advance(suf); } // allow:raw-byte-literal — K
269
+ else if (suf === 0x4D || suf === 0x6D) { num *= 1024 * 1024; _advance(suf); } // allow:raw-byte-literal — M
270
+ else if (suf === 0x47 || suf === 0x67) { num *= 1024 * 1024 * 1024; _advance(suf); } // allow:raw-byte-literal — G
271
+ }
272
+ if (!Number.isFinite(num)) _error("number overflowed");
273
+ tokens.push({ k: "num", v: num, line: line, col: col });
274
+ continue;
275
+ }
276
+
277
+ // Identifier.
278
+ if (_isIdStart(c)) {
279
+ var idStart = i;
280
+ while (i < n && _isIdCont(script.charCodeAt(i))) _advance(script.charCodeAt(i));
281
+ var id = script.slice(idStart, i);
282
+ // `text:` introduces a multi-line string per RFC 5228 §2.4.2.
283
+ if (id === "text" && i < n && script.charCodeAt(i) === 0x3A) {
284
+ _advance(0x3A);
285
+ // Optional hash-comment after `text:` per §2.4.2.
286
+ if (i < n && script.charCodeAt(i) === 0x23) {
287
+ while (i < n && script.charCodeAt(i) !== 0x0A) _advance(script.charCodeAt(i));
288
+ }
289
+ // CRLF expected.
290
+ if (i + 1 >= n || script.charCodeAt(i) !== 0x0D || script.charCodeAt(i + 1) !== 0x0A) {
291
+ _error("`text:` must be followed by CRLF");
292
+ }
293
+ i += 2; line++; col = 1;
294
+ var bodyStart = i;
295
+ // Multi-line content terminated by CRLF . CRLF; lines starting
296
+ // with `.` are dot-stuffed per §2.4.2.
297
+ while (i + 2 < n) {
298
+ if (script.charCodeAt(i) === 0x0D &&
299
+ script.charCodeAt(i + 1) === 0x0A &&
300
+ script.charCodeAt(i + 2) === 0x2E &&
301
+ i + 4 < n &&
302
+ script.charCodeAt(i + 3) === 0x0D &&
303
+ script.charCodeAt(i + 4) === 0x0A) {
304
+ break;
305
+ }
306
+ if (script.charCodeAt(i) === 0x0A) { line++; col = 1; }
307
+ i++;
308
+ }
309
+ if (i + 4 >= n) _error("unterminated multi-line string (missing CRLF.CRLF)");
310
+ var raw = script.slice(bodyStart, i);
311
+ i += 5; line++; col = 1; // skip `CRLF.CRLF`
312
+ // Dot-unstuff: lines starting with `..` collapse to `.`.
313
+ var body = raw.replace(/\r\n\.\./g, "\r\n.");
314
+ if (Buffer.byteLength(body, "utf8") > caps.maxStringBytes) {
315
+ _error("multi-line string " + Buffer.byteLength(body, "utf8") +
316
+ " bytes exceeds maxStringBytes=" + caps.maxStringBytes);
317
+ }
318
+ tokens.push({ k: "str", v: body, line: line, col: col });
319
+ continue;
320
+ }
321
+ tokens.push({ k: "id", v: id, line: line, col: col });
322
+ continue;
323
+ }
324
+
325
+ // Quoted string.
326
+ if (c === 0x22) {
327
+ _advance(c);
328
+ var sStart = i;
329
+ var out = "";
330
+ while (i < n) {
331
+ var ch = script.charCodeAt(i);
332
+ if (ch === 0x22) {
333
+ var lit = out + script.slice(sStart, i);
334
+ _advance(ch);
335
+ if (Buffer.byteLength(lit, "utf8") > caps.maxStringBytes) {
336
+ _error("string literal " + Buffer.byteLength(lit, "utf8") +
337
+ " bytes exceeds maxStringBytes=" + caps.maxStringBytes);
338
+ }
339
+ tokens.push({ k: "str", v: lit, line: line, col: col });
340
+ break;
341
+ }
342
+ if (ch === 0x5C) { // backslash
343
+ out += script.slice(sStart, i);
344
+ _advance(ch);
345
+ if (i >= n) _error("unterminated string escape");
346
+ var esc = script.charCodeAt(i);
347
+ // RFC 5228 §2.4.2 — only `\\` and `\"` defined; other
348
+ // escapes pass through the backslash and the byte.
349
+ if (esc === 0x22) { out += '"'; _advance(esc); }
350
+ else if (esc === 0x5C) { out += "\\"; _advance(esc); }
351
+ else { out += "\\" + script[i]; _advance(esc); }
352
+ sStart = i;
353
+ continue;
354
+ }
355
+ if (ch === 0x00) _error("NUL byte inside string literal");
356
+ if (ch === 0x0A) { line++; col = 1; }
357
+ _advance(ch);
358
+ }
359
+ if (i > n) _error("unterminated string literal");
360
+ continue;
361
+ }
362
+
363
+ _error("unexpected byte 0x" + c.toString(16)); // allow:raw-byte-literal — base-16 toString radix
364
+ }
365
+
366
+ tokens.push({ k: "eof", line: line, col: col });
367
+ return tokens;
368
+ }
369
+
370
+ // ---- Parser --------------------------------------------------------------
371
+
372
+ function _parseScript(tokens, caps, requiredCaps) {
373
+ var pos = 0;
374
+ var depth = 0;
375
+
376
+ function peek(ahead) { return tokens[pos + (ahead || 0)]; }
377
+ function consume(kind) {
378
+ var t = tokens[pos];
379
+ if (t.k !== kind) {
380
+ throw new SafeSieveError("safe-sieve/parse-error",
381
+ "safeSieve.parse: expected " + kind + " but got " + t.k +
382
+ (t.v ? " '" + t.v + "'" : "") + " at line " + t.line + ":" + t.col);
383
+ }
384
+ pos++;
385
+ return t;
386
+ }
387
+ function match(kind, v) {
388
+ var t = tokens[pos];
389
+ if (!t || t.k !== kind) return false;
390
+ if (v !== undefined && t.v !== v) return false;
391
+ return true;
392
+ }
393
+
394
+ function _parseStringList() {
395
+ if (match("str")) {
396
+ var t = consume("str");
397
+ return [t.v];
398
+ }
399
+ consume("lsb");
400
+ var out = [];
401
+ if (!match("rsb")) {
402
+ out.push(consume("str").v);
403
+ while (match("comma")) {
404
+ consume("comma");
405
+ if (out.length >= caps.maxStringListLen) {
406
+ throw new SafeSieveError("safe-sieve/parse-error",
407
+ "safeSieve.parse: string list exceeds maxStringListLen=" + caps.maxStringListLen);
408
+ }
409
+ out.push(consume("str").v);
410
+ }
411
+ }
412
+ consume("rsb");
413
+ return out;
414
+ }
415
+
416
+ function _parseArgs() {
417
+ // Reads tagged-args + positional-args + an optional embedded test.
418
+ // For the v0.9.55 base grammar, args are: tag* (number | string |
419
+ // string-list)* [test]?. The grammar disambiguates tests from
420
+ // positional args by identifier: a known TEST name introduces a
421
+ // test, otherwise we treat the identifier as the start of the next
422
+ // command (return).
423
+ var tags = [];
424
+ var positional = [];
425
+ var argCount = 0;
426
+ while (true) {
427
+ var t = peek();
428
+ if (argCount++ > caps.maxArgsPerCmd) {
429
+ throw new SafeSieveError("safe-sieve/parse-error",
430
+ "safeSieve.parse: too many args (cap " + caps.maxArgsPerCmd + ")");
431
+ }
432
+ if (t.k === "tag") {
433
+ consume("tag");
434
+ tags.push({ name: t.v });
435
+ continue;
436
+ }
437
+ if (t.k === "num") {
438
+ consume("num");
439
+ positional.push({ kind: "num", v: t.v });
440
+ continue;
441
+ }
442
+ if (t.k === "str") {
443
+ consume("str");
444
+ positional.push({ kind: "str", v: t.v });
445
+ continue;
446
+ }
447
+ if (t.k === "lsb") {
448
+ var list = _parseStringList();
449
+ positional.push({ kind: "list", v: list });
450
+ continue;
451
+ }
452
+ break;
453
+ }
454
+ return { tags: tags, positional: positional };
455
+ }
456
+
457
+ function _parseTest() {
458
+ var t = consume("id");
459
+ var name = t.v;
460
+ if (name === "anyof" || name === "allof") {
461
+ consume("lp");
462
+ var subs = [_parseTest()];
463
+ while (match("comma")) {
464
+ consume("comma");
465
+ if (subs.length >= caps.maxArgsPerCmd) {
466
+ throw new SafeSieveError("safe-sieve/parse-error",
467
+ "safeSieve.parse: too many sub-tests in " + name);
468
+ }
469
+ subs.push(_parseTest());
470
+ }
471
+ consume("rp");
472
+ return { kind: "test", name: name, subs: subs };
473
+ }
474
+ if (name === "not") {
475
+ var inner = _parseTest();
476
+ return { kind: "test", name: "not", subs: [inner] };
477
+ }
478
+ if (name === "true" || name === "false") {
479
+ return { kind: "test", name: name };
480
+ }
481
+ var args = _parseArgs();
482
+ return { kind: "test", name: name, args: args };
483
+ }
484
+
485
+ function _parseBlock() {
486
+ consume("lbr");
487
+ depth++;
488
+ if (depth > caps.maxDepth) {
489
+ throw new SafeSieveError("safe-sieve/parse-error",
490
+ "safeSieve.parse: block nesting exceeds maxDepth=" + caps.maxDepth);
491
+ }
492
+ var cmds = [];
493
+ while (!match("rbr") && !match("eof")) {
494
+ cmds.push(_parseCommand());
495
+ }
496
+ consume("rbr");
497
+ depth--;
498
+ return cmds;
499
+ }
500
+
501
+ function _parseCommand() {
502
+ var t = consume("id");
503
+ var name = t.v;
504
+
505
+ if (name === "require") {
506
+ var caps2 = _parseStringList();
507
+ consume("semi");
508
+ if (caps2.length + requiredCaps.length > caps.maxRequiredCaps) {
509
+ throw new SafeSieveError("safe-sieve/parse-error",
510
+ "safeSieve.parse: too many required capabilities (cap " +
511
+ caps.maxRequiredCaps + ")");
512
+ }
513
+ for (var i = 0; i < caps2.length; i++) {
514
+ var capName = caps2[i];
515
+ if (!Object.prototype.hasOwnProperty.call(KNOWN_CAPABILITIES, capName)) {
516
+ throw new SafeSieveError("safe-sieve/unknown-capability",
517
+ "safeSieve.parse: unknown capability '" + capName + "' at require");
518
+ }
519
+ if (KNOWN_CAPABILITIES[capName] === false) {
520
+ throw new SafeSieveError("safe-sieve/unimplemented-capability",
521
+ "safeSieve.parse: capability '" + capName + "' is RFC-defined but " +
522
+ "not implemented in v0.9.55 — script refused per RFC 5228 §3.2");
523
+ }
524
+ requiredCaps.push(capName);
525
+ }
526
+ return { kind: "require", caps: caps2 };
527
+ }
528
+
529
+ if (name === "if") {
530
+ var test = _parseTest();
531
+ var thenBlock = _parseBlock();
532
+ var elif = [];
533
+ var elseBlock = null;
534
+ while (match("id", "elsif")) {
535
+ if (elif.length >= caps.maxIfChainLen) {
536
+ throw new SafeSieveError("safe-sieve/parse-error",
537
+ "safeSieve.parse: elsif chain exceeds maxIfChainLen=" + caps.maxIfChainLen);
538
+ }
539
+ consume("id");
540
+ var elifTest = _parseTest();
541
+ var elifBlock = _parseBlock();
542
+ elif.push({ test: elifTest, body: elifBlock });
543
+ }
544
+ if (match("id", "else")) {
545
+ consume("id");
546
+ elseBlock = _parseBlock();
547
+ }
548
+ return { kind: "if", test: test, thenBody: thenBlock, elif: elif, elseBody: elseBlock };
549
+ }
550
+
551
+ // Action / unknown command — consume args then `;`.
552
+ var args = _parseArgs();
553
+ consume("semi");
554
+ return { kind: "action", name: name, args: args };
555
+ }
556
+
557
+ var commands = [];
558
+ while (!match("eof")) {
559
+ commands.push(_parseCommand());
560
+ }
561
+ return { kind: "script", commands: commands, requiredCaps: requiredCaps.slice() };
562
+ }
563
+
564
+ // ---- Public surface ------------------------------------------------------
565
+
566
+ /**
567
+ * @primitive b.safeSieve.parse
568
+ * @signature b.safeSieve.parse(script, opts?)
569
+ * @since 0.9.55
570
+ * @status stable
571
+ * @related b.safeSieve.validate, b.mail.sieve.run, b.guardMailSieve.validate
572
+ *
573
+ * Parse a Sieve script (RFC 5228) and return an AST. Refuses oversized
574
+ * scripts, control bytes, unknown capabilities, and RFC-defined-but-
575
+ * not-implemented capabilities at `require` time. The returned AST is
576
+ * the input to `b.mail.sieve.run(ast, env)`.
577
+ *
578
+ * @opts
579
+ * profile: "strict" | "balanced" | "permissive",
580
+ * compliancePosture: "hipaa" | "pci-dss" | "gdpr" | "soc2",
581
+ *
582
+ * @example
583
+ * var ast = b.safeSieve.parse('require ["fileinto"];\r\n' +
584
+ * 'if header :contains "Subject" "[bug]" {\r\n' +
585
+ * ' fileinto "bugs";\r\n' +
586
+ * '}\r\n');
587
+ * // → { kind: "script", commands: [...], requiredCaps: ["fileinto"] }
588
+ */
589
+ function parse(script, opts) {
590
+ if (typeof script !== "string") {
591
+ throw new SafeSieveError("safe-sieve/bad-input",
592
+ "safeSieve.parse: script must be a string");
593
+ }
594
+ var caps = _resolveCaps(opts);
595
+ var byteLen = Buffer.byteLength(script, "utf8");
596
+ if (byteLen > caps.maxScriptBytes) {
597
+ throw new SafeSieveError("safe-sieve/script-too-large",
598
+ "safeSieve.parse: script " + byteLen + " bytes exceeds maxScriptBytes=" +
599
+ caps.maxScriptBytes);
600
+ }
601
+ // Sieve scripts MUST use CRLF (RFC 5228 §2.1). Normalize input that
602
+ // arrives over a HTTP boundary where LF-only is common.
603
+ var norm = script;
604
+ if (script.indexOf("\r") === -1) {
605
+ norm = script.replace(/\n/g, "\r\n");
606
+ }
607
+ var tokens = _tokenize(norm, caps);
608
+ var requiredCaps = [];
609
+ return _parseScript(tokens, caps, requiredCaps);
610
+ }
611
+
612
+ /**
613
+ * @primitive b.safeSieve.validate
614
+ * @signature b.safeSieve.validate(script, opts?)
615
+ * @since 0.9.55
616
+ * @status stable
617
+ * @related b.safeSieve.parse
618
+ *
619
+ * Parse-only validation — returns `{ ok, requiredCaps, issues }`
620
+ * shape mirroring the rest of the guard family. Operator-facing
621
+ * primitives that want a JMAP-style `SieveScript/validate` response
622
+ * (RFC 9404) compose this and surface `issues` directly.
623
+ *
624
+ * @opts
625
+ * profile: "strict" | "balanced" | "permissive",
626
+ * compliancePosture: "hipaa" | "pci-dss" | "gdpr" | "soc2",
627
+ *
628
+ * @example
629
+ * var v = b.safeSieve.validate('require ["fileinto"];\r\nkeep;\r\n');
630
+ * v.ok; // → true
631
+ * v.requiredCaps; // → ["fileinto"]
632
+ */
633
+ function validate(script, opts) {
634
+ try {
635
+ var ast = parse(script, opts);
636
+ return { ok: true, requiredCaps: ast.requiredCaps, issues: [] };
637
+ } catch (e) {
638
+ return {
639
+ ok: false,
640
+ requiredCaps: [],
641
+ issues: [{
642
+ kind: "parse-error",
643
+ severity: "high",
644
+ ruleId: e.code || "safe-sieve/parse-error",
645
+ snippet: e.message,
646
+ }],
647
+ };
648
+ }
649
+ }
650
+
651
+ /**
652
+ * @primitive b.safeSieve.compliancePosture
653
+ * @signature b.safeSieve.compliancePosture(name)
654
+ * @since 0.9.55
655
+ * @status stable
656
+ * @related b.safeSieve.parse, b.safeSieve.validate
657
+ *
658
+ * Look up the recommended profile name for a compliance posture
659
+ * (`hipaa` / `pci-dss` / `gdpr` / `soc2`). Returns `"strict"` for any
660
+ * known posture, `null` for unknown names. Operator-facing primitives
661
+ * that thread `compliancePosture` opt through to safeSieve compose
662
+ * this for the explicit-cast pattern when they need the name string
663
+ * (rather than relying on `_resolveOpts` to do the lookup).
664
+ *
665
+ * @example
666
+ * b.safeSieve.compliancePosture("hipaa"); // → "strict"
667
+ * b.safeSieve.compliancePosture("loose"); // → null
668
+ */
669
+ function compliancePosture(name) {
670
+ return COMPLIANCE_POSTURES[name] || null;
671
+ }
672
+
673
+ module.exports = {
674
+ parse: parse,
675
+ validate: validate,
676
+ compliancePosture: compliancePosture,
677
+ KNOWN_CAPABILITIES: KNOWN_CAPABILITIES,
678
+ PROFILES: PROFILES,
679
+ COMPLIANCE_POSTURES: COMPLIANCE_POSTURES,
680
+ SafeSieveError: SafeSieveError,
681
+ // Internal exports for the interpreter at lib/mail-sieve.js.
682
+ _tokenize: _tokenize,
683
+ _resolveCaps: _resolveCaps,
684
+ };