@blamejs/core 0.9.24 → 0.9.38

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,218 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.agent.trace
4
+ * @nav Agent
5
+ * @title Agent Trace
6
+ * @order 85
7
+ *
8
+ * @intro
9
+ * Distributed tracing through every agent boundary. Composes the
10
+ * existing `b.tracing` (W3C trace context) so operators get a full
11
+ * request waterfall across the agent stack without wiring spans
12
+ * per-handler.
13
+ *
14
+ * The substrate at v0.9.29 ships the integration surface:
15
+ *
16
+ * - `startSpan(name, opts)` — wrap an agent method call in a span
17
+ * - `injectIntoEnvelope(envelope, currentSpan)` — inject W3C
18
+ * `traceparent` + `tracestate` into queue / event-bus / sub-
19
+ * agent envelopes so the consumer can continue the trace
20
+ * - `extractFromEnvelope(envelope)` — parse the envelope's
21
+ * trace context (refused via `b.guardTraceContext` if
22
+ * malformed)
23
+ * - `recordResult(span, result, error?)` — close span with
24
+ * success / error status
25
+ * - `shouldSample(method)` — sampling decision (global +
26
+ * per-method override)
27
+ *
28
+ * Span shape (per method call):
29
+ *
30
+ * name: "<agent-kind>.<method>" // e.g. "mail.agent.search"
31
+ * attributes:
32
+ * agent.method: method name
33
+ * agent.dispatch_mode: "local" | "queue" | "auto"
34
+ * agent.tenant_id: from v0.9.26 tenant scope (if present)
35
+ * agent.posture: JSON-array of v0.9.28 posture set
36
+ * agent.shard: from v0.9.21 shard routing
37
+ * agent.result_status: "success" | "error" | "not_implemented"
38
+ * agent.elapsed_ms: integer
39
+ *
40
+ * ```js
41
+ * var trace = b.agent.trace.create({
42
+ * tracing: b.tracing.create({ instrumentationName: "mail-agent" }),
43
+ * sampleRate: 1.0,
44
+ * perMethod: { fetch: 0.1, search: 0.5, send: 1.0 },
45
+ * });
46
+ *
47
+ * var span = trace.startSpan("mail.agent.fetch", { actor, method: "fetch" });
48
+ * try {
49
+ * var result = await agent.fetch(args);
50
+ * trace.recordResult(span, result);
51
+ * } catch (e) {
52
+ * trace.recordResult(span, null, e);
53
+ * throw e;
54
+ * }
55
+ * ```
56
+ *
57
+ * @card
58
+ * Distributed tracing through every agent boundary. W3C trace
59
+ * context injection at queue / event-bus / sub-agent envelopes;
60
+ * per-method sampling; integrated with existing b.tracing.
61
+ */
62
+
63
+ var lazyRequire = require("./lazy-require");
64
+ var { defineClass } = require("./framework-error");
65
+ var guardTraceContext = require("./guard-trace-context");
66
+
67
+ var audit = lazyRequire(function () { return require("./audit"); });
68
+
69
+ var AgentTraceError = defineClass("AgentTraceError", { alwaysPermanent: true });
70
+
71
+ /**
72
+ * @primitive b.agent.trace.create
73
+ * @signature b.agent.trace.create(opts)
74
+ * @since 0.9.29
75
+ * @status stable
76
+ * @related b.tracing.create, b.agent.orchestrator.create
77
+ *
78
+ * Create the trace facade. Composes operator-supplied `b.tracing`
79
+ * instance (or stub if absent — spans become no-ops).
80
+ *
81
+ * @opts
82
+ * tracing: b.tracing instance, // required for live spans
83
+ * audit: b.audit namespace, // optional
84
+ * sampleRate: number in [0..1], // default 1.0
85
+ * perMethod: { <method>: number }, // override per-method
86
+ *
87
+ * @example
88
+ * var trace = b.agent.trace.create({ tracing: myTracing, sampleRate: 0.5 });
89
+ * var span = trace.startSpan("mail.agent.fetch", { actor });
90
+ */
91
+ function create(opts) {
92
+ opts = opts || {};
93
+ if (!opts.tracing || typeof opts.tracing !== "object") {
94
+ throw new AgentTraceError("agent-trace/bad-tracing",
95
+ "create: opts.tracing is required (b.tracing.create() result)");
96
+ }
97
+ var sampleRate = typeof opts.sampleRate === "number" ? opts.sampleRate : 1.0;
98
+ if (!isFinite(sampleRate) || sampleRate < 0 || sampleRate > 1) {
99
+ throw new AgentTraceError("agent-trace/bad-sample-rate",
100
+ "create: sampleRate must be in [0, 1]");
101
+ }
102
+ var perMethod = opts.perMethod || {};
103
+ var auditImpl = opts.audit || audit();
104
+
105
+ return {
106
+ startSpan: function (name, sopts) { return _startSpan(opts.tracing, name, sopts || {}); },
107
+ injectIntoEnvelope: function (envelope, span) { return _injectIntoEnvelope(opts.tracing, envelope, span); },
108
+ extractFromEnvelope: function (envelope) { return _extractFromEnvelope(envelope); },
109
+ recordResult: function (span, result, error) { return _recordResult(span, result, error); },
110
+ shouldSample: function (method) { return _shouldSample(sampleRate, perMethod, method); },
111
+ formatAttributes: function (info) { return _formatAttributes(info); },
112
+ AgentTraceError: AgentTraceError,
113
+ _ctx: { sampleRate: sampleRate, perMethod: perMethod, audit: auditImpl },
114
+ };
115
+ }
116
+
117
+ function _startSpan(tracing, name, sopts) {
118
+ if (typeof name !== "string" || name.length === 0) {
119
+ throw new AgentTraceError("agent-trace/bad-span-name",
120
+ "startSpan: name required");
121
+ }
122
+ // Compose b.tracing's manual-lifetime span — sets the span as active
123
+ // on the registry stack so tracing.contextHeaders() / currentSpan()
124
+ // see it, then exposes end() so the agent boundary controls
125
+ // lifetime across publish → consume.
126
+ if (typeof tracing.manualSpan === "function") {
127
+ return tracing.manualSpan(name, sopts);
128
+ }
129
+ // Operator passed a non-b.tracing object (operator-supplied OTel
130
+ // tracer directly) — try its native startSpan. Refuse if neither.
131
+ if (typeof tracing.startSpan === "function") {
132
+ return tracing.startSpan(name, sopts);
133
+ }
134
+ throw new AgentTraceError("agent-trace/bad-tracing",
135
+ "startSpan: opts.tracing must expose manualSpan() (b.tracing.create()) " +
136
+ "or startSpan() (raw OTel tracer); neither found");
137
+ }
138
+
139
+ function _injectIntoEnvelope(tracing, envelope, span) {
140
+ if (!envelope || typeof envelope !== "object") {
141
+ throw new AgentTraceError("agent-trace/bad-envelope",
142
+ "injectIntoEnvelope: envelope required");
143
+ }
144
+ // tracing.contextHeaders() returns { traceparent, tracestate? } when
145
+ // a span is active. We pass through whatever's current.
146
+ var headers = (typeof tracing.contextHeaders === "function") ? tracing.contextHeaders() : null;
147
+ if (!headers || typeof headers.traceparent !== "string") return envelope;
148
+ envelope._trace = {
149
+ traceparent: headers.traceparent,
150
+ tracestate: headers.tracestate || "",
151
+ };
152
+ return envelope;
153
+ }
154
+
155
+ function _extractFromEnvelope(envelope) {
156
+ if (!envelope || typeof envelope !== "object" || !envelope._trace) return null;
157
+ // Validate via guardTraceContext — refuses malformed traceparent
158
+ // strings before the consumer side picks them up as a parent span.
159
+ try {
160
+ guardTraceContext.validate(envelope._trace);
161
+ } catch (e) {
162
+ throw new AgentTraceError("agent-trace/bad-envelope-trace",
163
+ "extractFromEnvelope: " + ((e && e.message) || String(e)));
164
+ }
165
+ return {
166
+ traceparent: envelope._trace.traceparent,
167
+ tracestate: envelope._trace.tracestate || "",
168
+ };
169
+ }
170
+
171
+ function _recordResult(span, result, error) {
172
+ if (!span || typeof span !== "object") return;
173
+ if (error) {
174
+ if (typeof span.recordException === "function") {
175
+ try { span.recordException(error); } catch (_e) { /* best-effort */ }
176
+ }
177
+ if (typeof span.setStatus === "function") {
178
+ try { span.setStatus({ code: 2, message: error.message || String(error) }); }
179
+ catch (_e) { /* best-effort */ }
180
+ }
181
+ } else if (typeof span.setStatus === "function") {
182
+ try { span.setStatus({ code: 1 }); } catch (_e) { /* best-effort */ }
183
+ }
184
+ if (typeof span.end === "function") {
185
+ try { span.end(); } catch (_e) { /* best-effort */ }
186
+ }
187
+ }
188
+
189
+ function _shouldSample(globalRate, perMethod, method) {
190
+ if (typeof method === "string" && Object.prototype.hasOwnProperty.call(perMethod, method)) {
191
+ var r = perMethod[method];
192
+ if (typeof r === "number" && isFinite(r) && r >= 0 && r <= 1) {
193
+ return Math.random() < r; // allow:math-random-noncrypto — sampling is statistical, not security-sensitive
194
+ }
195
+ }
196
+ return Math.random() < globalRate; // allow:math-random-noncrypto — sampling is statistical, not security-sensitive
197
+ }
198
+
199
+ function _formatAttributes(info) {
200
+ if (!info || typeof info !== "object") return {};
201
+ var attrs = {};
202
+ if (info.method) attrs["agent.method"] = info.method;
203
+ if (info.dispatchMode) attrs["agent.dispatch_mode"] = info.dispatchMode;
204
+ if (info.tenantId) attrs["agent.tenant_id"] = info.tenantId;
205
+ if (Array.isArray(info.postureSet)) attrs["agent.posture"] = JSON.stringify(info.postureSet);
206
+ if (typeof info.shard === "number") attrs["agent.shard"] = info.shard;
207
+ if (info.resultStatus) attrs["agent.result_status"] = info.resultStatus;
208
+ if (typeof info.elapsedMs === "number") attrs["agent.elapsed_ms"] = info.elapsedMs;
209
+ return attrs;
210
+ }
211
+
212
+ module.exports = {
213
+ create: create,
214
+ AgentTraceError: AgentTraceError,
215
+ guards: {
216
+ context: guardTraceContext,
217
+ },
218
+ };
package/lib/guard-all.js CHANGED
@@ -83,6 +83,7 @@ var STANDALONE_GUARDS = [
83
83
  require("./guard-image"),
84
84
  require("./guard-pdf"),
85
85
  require("./guard-auth"),
86
+ require("./guard-smtp-command"),
86
87
  ];
87
88
 
88
89
  // Framework-wide profile + posture vocabulary that every guard MUST
@@ -0,0 +1,379 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.guardDsn
4
+ * @nav Guards
5
+ * @title Guard DSN
6
+ * @order 460
7
+ *
8
+ * @intro
9
+ * RFC 3464 Delivery Status Notification parser. Reads the
10
+ * `multipart/report; report-type=delivery-status` structure that
11
+ * bounces, delayed-delivery notices, and successful-delivery
12
+ * confirmations carry and surfaces the per-recipient action +
13
+ * enhanced status code so operator-side delivery-failure routing
14
+ * (`b.mail.bounce` retry curve, address-book invalidation, mailing-
15
+ * list cleanup, transactional-mail dead-letter handling) reads a
16
+ * stable shape regardless of MTA wording.
17
+ *
18
+ * ## RFC 3464 structure
19
+ *
20
+ * `multipart/report` per RFC 6522 §3:
21
+ *
22
+ * 1. `text/plain` (or text/html) — human-readable wording
23
+ * ("Your message could not be delivered to alice@example.com");
24
+ * the framework does NOT route on this prose.
25
+ * 2. **`message/delivery-status`** (RFC 3464 §2) — the
26
+ * machine-readable DSN body the framework parses.
27
+ * 3. Optional `message/rfc822` (or `text/rfc822-headers`) —
28
+ * the original message (or its headers) that bounced.
29
+ *
30
+ * ## Required fields the parser extracts
31
+ *
32
+ * **Per-message fields (RFC 3464 §2.2)**:
33
+ * - `Reporting-MTA` — MTA that issued the DSN. Mandatory.
34
+ * - `Original-Envelope-Id` (optional) — DSN-tied envelope id.
35
+ * - `Arrival-Date` (optional) — when the original message
36
+ * arrived at the reporting MTA.
37
+ *
38
+ * **Per-recipient fields (RFC 3464 §2.3)** — repeated, one block
39
+ * per recipient:
40
+ * - `Final-Recipient` — recipient address as the reporting MTA
41
+ * knows it. Mandatory.
42
+ * - `Action` — `failed` / `delayed` / `delivered` / `relayed` /
43
+ * `expanded`. Mandatory.
44
+ * - `Status` — RFC 3463 enhanced status code, format
45
+ * `D.D[D[D]].D[D[D]]` (e.g. `5.1.1` = bad address).
46
+ * - `Original-Recipient` (optional).
47
+ * - `Diagnostic-Code` (optional) — raw MTA error line.
48
+ *
49
+ * ## RFC 3463 status-class semantics
50
+ *
51
+ * The first digit classifies the verdict and drives the framework's
52
+ * downstream routing:
53
+ *
54
+ * - **`2.x.y`** — success (delivered / relayed / expanded). Used
55
+ * by mailing-list `verp` tracking + delivery-receipt auditing.
56
+ * - **`4.x.y`** — persistent transient failure. Operator's
57
+ * `b.outbox` retry curve applies; address stays valid.
58
+ * - **`5.x.y`** — permanent failure. Address-book invalidation
59
+ * trigger; mailing-list cleanup; no further retries.
60
+ *
61
+ * The framework surfaces `statusClass` (`success` / `temporary` /
62
+ * `permanent`) so operator routing reads one shape regardless of
63
+ * the exact subcode.
64
+ *
65
+ * ## Defenses
66
+ *
67
+ * - **Oversize DSN** — bounded body cap (default 256 KiB strict)
68
+ * per the profile; legitimate DSNs are KB-scale, multi-MB DSNs
69
+ * are pathological / DoS-shaped.
70
+ * - **Recipient-count cap** — per-DSN recipient cap (default 256
71
+ * strict). A DSN with thousands of recipients is forged or
72
+ * misconfigured; operator opts permissive for mailing-list
73
+ * blast-bounces.
74
+ * - **Header-line cap** — each field-line capped at 998 bytes
75
+ * per RFC 5322 §2.1.1.
76
+ * - **CRLF + control-char refusal** — header injection defense
77
+ * for fields that propagate to operator's audit log /
78
+ * monitoring dashboard.
79
+ *
80
+ * ## CVE / threat model
81
+ *
82
+ * - **Bounce-flood / backscatter** — operator's MX should refuse
83
+ * mail with envelope-from that doesn't pass SPF before
84
+ * generating a DSN (the existing `b.mail.bounce` primitive does
85
+ * this); this guard parses INBOUND DSNs and gates the parse
86
+ * surface bounds, not the bounce-generation policy.
87
+ * - **DSN header-injection class** (CVE-2026-32178 .NET
88
+ * System.Net.Mail at outbound; the inbound parse path here)
89
+ * — refuses CR/LF/NUL/C0 in header lines.
90
+ * - **CSAF / iSchedule prose tampering** — operator inspecting
91
+ * the prose part for the original recipient runs into the
92
+ * ambiguous wording that DSNs vary across MTAs (Postfix vs
93
+ * Exchange vs SES vs Gmail). The parser surfaces the
94
+ * STRUCTURED fields so operator routing doesn't have to
95
+ * regex MTA-specific prose.
96
+ *
97
+ * @card
98
+ * RFC 3464 DSN parser. Walks message/delivery-status per-message + per-recipient blocks, surfaces Action / Status / Final-Recipient + the RFC 3463 status-class verdict (success / temporary / permanent). Bounded recipient count + body size + header-line length; CRLF / NUL / C0 refusal. Operator delivery-failure routing reads one shape regardless of MTA wording.
99
+ */
100
+
101
+ var C = require("./constants");
102
+ var { defineClass } = require("./framework-error");
103
+
104
+ var GuardDsnError = defineClass("GuardDsnError", { alwaysPermanent: true });
105
+
106
+ var DEFAULT_PROFILE = "strict";
107
+
108
+ var PROFILES = Object.freeze({
109
+ strict: { maxBytes: C.BYTES.kib(256), maxRecipients: 256, maxHeaderLine: 998 }, // allow:raw-byte-literal — RFC 5322 §2.1.1 header line cap; RFC 3464 recipient count
110
+ balanced: { maxBytes: C.BYTES.mib(1), maxRecipients: 1024, maxHeaderLine: 998 }, // allow:raw-byte-literal — RFC 5322 §2.1.1 line cap; mailing-list blast bounces
111
+ permissive: { maxBytes: C.BYTES.mib(4), maxRecipients: 4096, maxHeaderLine: 998 }, // allow:raw-byte-literal — RFC 5322 §2.1.1 line cap; large-blast bounce class
112
+ });
113
+
114
+ var COMPLIANCE_POSTURES = Object.freeze({
115
+ hipaa: "strict",
116
+ "pci-dss": "strict",
117
+ gdpr: "strict",
118
+ soc2: "strict",
119
+ });
120
+
121
+ var KNOWN_ACTIONS = Object.freeze({
122
+ failed: true,
123
+ delayed: true,
124
+ delivered: true,
125
+ relayed: true,
126
+ expanded: true,
127
+ });
128
+
129
+ // RFC 3463 §3.1: status code is digit . digit{1,3} . digit{1,3}.
130
+ var STATUS_RE = /^([245])\.(\d{1,3})\.(\d{1,3})$/; // allow:regex-no-length-cap — anchored + per-component repeat cap
131
+
132
+ /**
133
+ * @primitive b.guardDsn.parse
134
+ * @signature b.guardDsn.parse(deliveryStatusBody, opts?)
135
+ * @since 0.9.37
136
+ * @status stable
137
+ * @related b.safeMime.parse, b.guardEnvelope.check
138
+ *
139
+ * Parse a `message/delivery-status` body (the MIME part body, not
140
+ * the entire RFC 3464 multipart/report — extract that via
141
+ * `b.safeMime.parse` first). Returns `{ perMessage, perRecipients,
142
+ * worstStatusClass, action }`.
143
+ *
144
+ * Throws `GuardDsnError` on oversize body / recipient count /
145
+ * header-line length / malformed status code / required-field
146
+ * missing / control-char in field value.
147
+ *
148
+ * @opts
149
+ * profile: "strict" | "balanced" | "permissive",
150
+ * posture: "hipaa" | "pci-dss" | "gdpr" | "soc2",
151
+ *
152
+ * @example
153
+ * var mime = b.safeMime.parse(rawBouncedMessage);
154
+ * var deliveryStatusPart = b.safeMime.findFirst(mime, function (p) {
155
+ * return p.leaf && p.leaf.contentType === "message/delivery-status";
156
+ * });
157
+ * var dsn = b.guardDsn.parse(deliveryStatusPart.leaf.body);
158
+ * if (dsn.worstStatusClass === "permanent") {
159
+ * dsn.perRecipients.forEach(function (r) { invalidateAddress(r.finalRecipient); });
160
+ * }
161
+ */
162
+ function parse(deliveryStatusBody, opts) {
163
+ opts = opts || {};
164
+ var caps = _resolveProfile(opts);
165
+ var bytes;
166
+ if (Buffer.isBuffer(deliveryStatusBody)) {
167
+ bytes = deliveryStatusBody;
168
+ } else if (typeof deliveryStatusBody === "string") {
169
+ bytes = Buffer.from(deliveryStatusBody, "utf8");
170
+ } else {
171
+ throw new GuardDsnError("guard-dsn/bad-input",
172
+ "parse: deliveryStatusBody must be a Buffer or string");
173
+ }
174
+ if (bytes.length > caps.maxBytes) {
175
+ throw new GuardDsnError("guard-dsn/oversize-body",
176
+ "parse: body " + bytes.length + " bytes exceeds maxBytes=" + caps.maxBytes);
177
+ }
178
+
179
+ // RFC 3464 §2.1: the delivery-status body is per-message fields,
180
+ // a blank line, then per-recipient field groups separated by
181
+ // blank lines.
182
+ var text = bytes.toString("utf8");
183
+ var blocks = _splitBlocks(text);
184
+ if (blocks.length === 0) {
185
+ throw new GuardDsnError("guard-dsn/empty",
186
+ "parse: delivery-status body has no field blocks");
187
+ }
188
+ var perMessageFields = _parseFieldBlock(blocks[0], caps.maxHeaderLine);
189
+
190
+ // Reporting-MTA is mandatory per RFC 3464 §2.2.2.
191
+ if (!perMessageFields["reporting-mta"]) {
192
+ throw new GuardDsnError("guard-dsn/missing-reporting-mta",
193
+ "parse: required per-message field Reporting-MTA missing (RFC 3464 §2.2.2)");
194
+ }
195
+
196
+ var perRecipients = [];
197
+ for (var i = 1; i < blocks.length; i += 1) {
198
+ if (perRecipients.length >= caps.maxRecipients) {
199
+ throw new GuardDsnError("guard-dsn/too-many-recipients",
200
+ "parse: per-recipient count exceeds maxRecipients=" + caps.maxRecipients);
201
+ }
202
+ var fields = _parseFieldBlock(blocks[i], caps.maxHeaderLine);
203
+ if (Object.keys(fields).length === 0) continue; // empty trailing block
204
+ if (!fields["final-recipient"]) {
205
+ throw new GuardDsnError("guard-dsn/missing-final-recipient",
206
+ "parse: per-recipient block missing Final-Recipient (RFC 3464 §2.3.2)");
207
+ }
208
+ if (!fields["action"]) {
209
+ throw new GuardDsnError("guard-dsn/missing-action",
210
+ "parse: per-recipient block missing Action (RFC 3464 §2.3.3)");
211
+ }
212
+ var action = fields["action"].toLowerCase();
213
+ if (!KNOWN_ACTIONS[action]) {
214
+ throw new GuardDsnError("guard-dsn/bad-action",
215
+ "parse: Action '" + action + "' not in RFC 3464 §2.3.3 vocabulary");
216
+ }
217
+ if (!fields["status"]) {
218
+ throw new GuardDsnError("guard-dsn/missing-status",
219
+ "parse: per-recipient block missing Status (RFC 3464 §2.3.4)");
220
+ }
221
+ var statusMatch = fields["status"].match(STATUS_RE);
222
+ if (!statusMatch) {
223
+ throw new GuardDsnError("guard-dsn/bad-status",
224
+ "parse: Status '" + fields["status"] + "' not RFC 3463 D.D.D form");
225
+ }
226
+ perRecipients.push({
227
+ finalRecipient: _stripRecipientType(fields["final-recipient"]),
228
+ originalRecipient: fields["original-recipient"] ? _stripRecipientType(fields["original-recipient"]) : null,
229
+ action: action,
230
+ status: fields["status"],
231
+ statusClass: _statusClass(statusMatch[1]),
232
+ diagnosticCode: fields["diagnostic-code"] || null,
233
+ remoteMta: fields["remote-mta"] || null,
234
+ lastAttemptDate: fields["last-attempt-date"] || null,
235
+ });
236
+ }
237
+
238
+ if (perRecipients.length === 0) {
239
+ throw new GuardDsnError("guard-dsn/no-recipients",
240
+ "parse: delivery-status has no per-recipient blocks (RFC 3464 §2.1 requires at least one)");
241
+ }
242
+
243
+ // Worst status class across recipients: permanent > temporary > success.
244
+ var worst = "success";
245
+ for (var r = 0; r < perRecipients.length; r += 1) {
246
+ if (perRecipients[r].statusClass === "permanent") { worst = "permanent"; break; }
247
+ if (perRecipients[r].statusClass === "temporary") worst = "temporary";
248
+ }
249
+
250
+ return {
251
+ perMessage: {
252
+ reportingMta: perMessageFields["reporting-mta"],
253
+ originalEnvelopeId: perMessageFields["original-envelope-id"] || null,
254
+ arrivalDate: perMessageFields["arrival-date"] || null,
255
+ receivedFromMta: perMessageFields["received-from-mta"] || null,
256
+ },
257
+ perRecipients: perRecipients,
258
+ worstStatusClass: worst,
259
+ action: worst === "permanent" ? "invalidate" :
260
+ worst === "temporary" ? "retry" :
261
+ "deliver",
262
+ };
263
+ }
264
+
265
+ /**
266
+ * @primitive b.guardDsn.compliancePosture
267
+ * @signature b.guardDsn.compliancePosture(posture)
268
+ * @since 0.9.37
269
+ * @status stable
270
+ *
271
+ * Return the effective profile name for a compliance posture, or
272
+ * `null` for unknown posture names.
273
+ *
274
+ * @example
275
+ * b.guardDsn.compliancePosture("hipaa"); // → "strict"
276
+ */
277
+ function compliancePosture(posture) {
278
+ return COMPLIANCE_POSTURES[posture] || null;
279
+ }
280
+
281
+ function _splitBlocks(text) {
282
+ // RFC 3464 §2.1: blank line separates per-message from
283
+ // per-recipient blocks; blank lines also separate consecutive
284
+ // per-recipient blocks.
285
+ // Normalize CRLF + bare-CR to LF for split.
286
+ var normalized = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); // allow:regex-no-length-cap — input length already capped
287
+ return normalized.split(/\n\s*\n/); // allow:regex-no-length-cap — input length already capped
288
+ }
289
+
290
+ function _parseFieldBlock(block, maxHeaderLine) {
291
+ // RFC 5322 §2.2: header field = name ":" value; continuation
292
+ // lines start with whitespace.
293
+ var lines = block.split("\n");
294
+ var fields = Object.create(null);
295
+ var current = null;
296
+ for (var i = 0; i < lines.length; i += 1) {
297
+ var raw = lines[i];
298
+ if (raw.length > maxHeaderLine) {
299
+ throw new GuardDsnError("guard-dsn/oversize-header-line",
300
+ "parse: header line " + raw.length + " bytes exceeds maxHeaderLine=" + maxHeaderLine + " (RFC 5322 §2.1.1)");
301
+ }
302
+ if (raw.length === 0) continue;
303
+ _checkControlChars(raw);
304
+ if (/^[ \t]/.test(raw) && current) { // allow:regex-no-length-cap — single-char check on capped line
305
+ // Continuation.
306
+ fields[current] += " " + raw.replace(/^[ \t]+/, ""); // allow:regex-no-length-cap — trim on capped line // allow:duplicate-regex — leading-WS-trim shape common to RFC 5322 header continuation parsers
307
+ continue;
308
+ }
309
+ var colon = raw.indexOf(":");
310
+ if (colon === -1) {
311
+ throw new GuardDsnError("guard-dsn/malformed-field",
312
+ "parse: line '" + raw + "' missing ':' field-name terminator");
313
+ }
314
+ var name = raw.slice(0, colon).trim().toLowerCase();
315
+ var value = raw.slice(colon + 1).trim();
316
+ if (name.length === 0) {
317
+ throw new GuardDsnError("guard-dsn/malformed-field",
318
+ "parse: empty field name on line '" + raw + "'");
319
+ }
320
+ fields[name] = value;
321
+ current = name;
322
+ }
323
+ return fields;
324
+ }
325
+
326
+ function _checkControlChars(line) {
327
+ // Refuse NUL, C0 controls (except TAB which is valid in
328
+ // continuation), DEL. Bare CR and LF can't appear because we
329
+ // already split on \n; this catches forms that survive the
330
+ // split (e.g. backslash + literal sequence).
331
+ for (var i = 0; i < line.length; i += 1) {
332
+ var c = line.charCodeAt(i);
333
+ if (c === 0x00 || c === 0x7f || (c < 0x20 && c !== 0x09)) { // allow:raw-byte-literal — RFC 5322 control char + TAB allow
334
+ throw new GuardDsnError("guard-dsn/control-char",
335
+ "parse: control char 0x" + c.toString(16) + " in field line refused (header-injection defense)");
336
+ }
337
+ }
338
+ }
339
+
340
+ function _stripRecipientType(value) {
341
+ // RFC 3464 §2.3.2: "rfc822;alice@example.com" — type prefix
342
+ // before semicolon classifies the address. Strip for the common
343
+ // case of rfc822, surface the raw value otherwise.
344
+ var semi = value.indexOf(";");
345
+ if (semi === -1) return value;
346
+ var type = value.slice(0, semi).trim().toLowerCase();
347
+ if (type === "rfc822") return value.slice(semi + 1).trim();
348
+ return value;
349
+ }
350
+
351
+ function _statusClass(firstDigit) {
352
+ if (firstDigit === "2") return "success";
353
+ if (firstDigit === "4") return "temporary";
354
+ if (firstDigit === "5") return "permanent";
355
+ return "unknown";
356
+ }
357
+
358
+ function _resolveProfile(opts) {
359
+ if (opts.posture && COMPLIANCE_POSTURES[opts.posture]) {
360
+ return PROFILES[COMPLIANCE_POSTURES[opts.posture]];
361
+ }
362
+ var p = opts.profile || DEFAULT_PROFILE;
363
+ if (!PROFILES[p]) {
364
+ throw new GuardDsnError("guard-dsn/bad-profile",
365
+ "guardDsn: unknown profile '" + p + "'");
366
+ }
367
+ return PROFILES[p];
368
+ }
369
+
370
+ module.exports = {
371
+ parse: parse,
372
+ compliancePosture: compliancePosture,
373
+ PROFILES: PROFILES,
374
+ COMPLIANCE_POSTURES: COMPLIANCE_POSTURES,
375
+ KNOWN_ACTIONS: KNOWN_ACTIONS,
376
+ GuardDsnError: GuardDsnError,
377
+ NAME: "dsn",
378
+ KIND: "delivery-status",
379
+ };