@blamejs/core 0.8.52 → 0.8.57

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 (41) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/index.js +8 -0
  3. package/lib/audit.js +4 -0
  4. package/lib/auth/fido-mds3.js +624 -0
  5. package/lib/auth/passkey.js +214 -2
  6. package/lib/auth-bot-challenge.js +1 -1
  7. package/lib/credential-hash.js +2 -2
  8. package/lib/framework-error.js +55 -0
  9. package/lib/guard-cidr.js +2 -1
  10. package/lib/guard-jwt.js +2 -2
  11. package/lib/guard-oauth.js +2 -2
  12. package/lib/http-client-cache.js +916 -0
  13. package/lib/http-client.js +242 -0
  14. package/lib/mail-arf.js +343 -0
  15. package/lib/mail-auth.js +265 -40
  16. package/lib/mail-bimi.js +948 -33
  17. package/lib/mail-bounce.js +386 -4
  18. package/lib/mail-mdn.js +424 -0
  19. package/lib/mail-unsubscribe.js +265 -25
  20. package/lib/mail.js +403 -21
  21. package/lib/middleware/bearer-auth.js +1 -1
  22. package/lib/middleware/clear-site-data.js +122 -0
  23. package/lib/middleware/dpop.js +1 -1
  24. package/lib/middleware/index.js +9 -0
  25. package/lib/middleware/nel.js +214 -0
  26. package/lib/middleware/security-headers.js +56 -4
  27. package/lib/middleware/speculation-rules.js +323 -0
  28. package/lib/mime-parse.js +198 -0
  29. package/lib/network-dns.js +890 -27
  30. package/lib/network-tls.js +745 -0
  31. package/lib/object-store/sigv4.js +54 -0
  32. package/lib/public-suffix.js +414 -0
  33. package/lib/safe-buffer.js +7 -0
  34. package/lib/safe-json.js +1 -1
  35. package/lib/static.js +120 -0
  36. package/lib/storage.js +11 -0
  37. package/lib/vendor/MANIFEST.json +33 -0
  38. package/lib/vendor/bimi-trust-anchors.pem +33 -0
  39. package/lib/vendor/public-suffix-list.dat +16376 -0
  40. package/package.json +1 -1
  41. package/sbom.cyclonedx.json +6 -6
@@ -0,0 +1,424 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.mailMdn
4
+ * @nav Communication
5
+ * @title MDN
6
+ *
7
+ * @intro
8
+ * RFC 3798 / RFC 8098 Message Disposition Notification builder +
9
+ * parser. An MDN is the "I read your message" return-receipt — a
10
+ * multipart/report MIME body with a `message/disposition-notification`
11
+ * segment that names what the user-agent did with the original
12
+ * message (displayed / deleted / dispatched / processed / failed).
13
+ *
14
+ * Auto-generation discipline: the framework refuses to auto-build an
15
+ * MDN unless the operator explicitly opts in via
16
+ * `requireUserConfirmation: false`. RFC 3798 §2.1 plus RFC 8098
17
+ * require user opt-in for MDN delivery — accidental automatic
18
+ * generation leaks behavioural metadata and is a known privacy
19
+ * regression in mail clients. The framework defaults to refusal so
20
+ * the operator codepath has to actively choose to send.
21
+ *
22
+ * Parser tolerates both bare RFC 3798 reports and the RFC 8098
23
+ * updated shape (action / sending / disposition modes), surfaces
24
+ * the original-message-id binding, the reporting user-agent string,
25
+ * and the optional original-message attachment.
26
+ *
27
+ * @card
28
+ * RFC 3798 / RFC 8098 Message Disposition Notification builder + parser — generate "message read" return-receipts and parse inbound MDNs into a normalized event shape. Auto-generation refuses without explicit operator opt-in to prevent accidental privacy leaks.
29
+ */
30
+
31
+ var crypto = require("./crypto");
32
+ var lazyRequire = require("./lazy-require");
33
+ var mimeParse = require("./mime-parse");
34
+ var audit = lazyRequire(function () { return require("./audit"); });
35
+ var C = require("./constants");
36
+ var validateOpts = require("./validate-opts");
37
+ var { MailMdnError } = require("./framework-error");
38
+
39
+ // Body cap for the MDN parser — same rationale as the DSN cap. RFC
40
+ // 3798 doesn't specify a maximum, but real-world MDNs are tiny;
41
+ // anything above 1 MiB is pathological and will pin the regex
42
+ // scanner if accepted.
43
+ var MDN_MAX_BYTES = C.BYTES.mib(1);
44
+
45
+ // RFC 3798 §3.2.6 disposition-types.
46
+ var DISPOSITION_TYPES = {
47
+ "displayed": true,
48
+ "deleted": true,
49
+ "dispatched": true,
50
+ "processed": true,
51
+ "failed": true,
52
+ "denied": true,
53
+ };
54
+
55
+ // RFC 3798 §3.2.6.1 action-modes.
56
+ var ACTION_MODES = {
57
+ "manual-action": true,
58
+ "automatic-action": true,
59
+ };
60
+
61
+ // RFC 3798 §3.2.6.2 sending-modes.
62
+ var SENDING_MODES = {
63
+ "mdn-sent-manually": true,
64
+ "mdn-sent-automatically": true,
65
+ };
66
+
67
+ function _err(code, message) {
68
+ return new MailMdnError(code, message);
69
+ }
70
+
71
+ function _parseDisposition(value) {
72
+ // RFC 3798 §3.2.6 — `disposition-mode; disposition-type/<modifier>`
73
+ // examples:
74
+ // manual-action/MDN-sent-manually; displayed
75
+ // automatic-action/MDN-sent-automatically; processed/error
76
+ if (typeof value !== "string") return null;
77
+ var semi = value.indexOf(";");
78
+ if (semi === -1) {
79
+ return {
80
+ actionMode: null,
81
+ sendingMode: null,
82
+ type: value.trim().toLowerCase(),
83
+ };
84
+ }
85
+ var modePart = value.slice(0, semi).trim();
86
+ var typePart = value.slice(semi + 1).trim();
87
+ var slash = modePart.indexOf("/");
88
+ var actionMode = slash === -1 ? modePart.toLowerCase() : modePart.slice(0, slash).trim().toLowerCase();
89
+ var sendingMode = slash === -1 ? null : modePart.slice(slash + 1).trim().toLowerCase();
90
+ var type = typePart.toLowerCase();
91
+ // Strip /modifier off the type token.
92
+ var typeSlash = type.indexOf("/");
93
+ if (typeSlash !== -1) type = type.slice(0, typeSlash).trim();
94
+ return {
95
+ actionMode: actionMode,
96
+ sendingMode: sendingMode,
97
+ type: type,
98
+ };
99
+ }
100
+
101
+ function _generateBoundary() {
102
+ return "blamejs-mdn-" + crypto.generateToken(C.BYTES.bytes(12));
103
+ }
104
+
105
+ /**
106
+ * @primitive b.mailMdn.build
107
+ * @signature b.mailMdn.build(opts)
108
+ * @since 0.8.53
109
+ * @status stable
110
+ * @related b.mailMdn.parse, b.mailBounce.parse
111
+ *
112
+ * Build an RFC 3798 / RFC 8098 multipart/report message body carrying
113
+ * a `message/disposition-notification` segment. The result is a raw
114
+ * RFC 5322 message body ready for SMTP relay back to the sender.
115
+ *
116
+ * The framework refuses to auto-generate an MDN (emits the audit row
117
+ * `mailmdn.suppressed` instead) when:
118
+ *
119
+ * - The original message's `Disposition-Notification-Options`
120
+ * header asserted `important=required` AND
121
+ * - `opts.requireUserConfirmation` is not explicitly `false`
122
+ *
123
+ * RFC 3798 §2.1 requires user opt-in for MDN delivery; the default is
124
+ * refusal so accidental automatic generation by an unattended mail
125
+ * processor cannot leak behavioural metadata. Operators with an
126
+ * explicit "the user clicked send-receipt" code path pass
127
+ * `requireUserConfirmation: false` to skip the gate.
128
+ *
129
+ * @opts
130
+ * originalMessageId: string, // required — Message-Id of the message being acknowledged
131
+ * originalRecipient: string, // optional — RFC 5322 address of the original recipient
132
+ * finalRecipient: string, // required — RFC 5322 address of the final-recipient (may differ after forwarding)
133
+ * disposition: "displayed" | "deleted" | "dispatched" | "processed" | "failed" | "denied",
134
+ * actionMode: "manual-action" | "automatic-action", // default: manual-action
135
+ * sendingMode: "MDN-sent-manually" | "MDN-sent-automatically", // default: MDN-sent-manually
136
+ * reportingUserAgent: string, // optional — RFC 3798 §3.2.1 reporting agent name/version
137
+ * originalMessage: string, // optional — raw RFC 5322 message body to attach as message/rfc822
138
+ * from: string, // optional — From: header for the MDN envelope
139
+ * to: string, // optional — To: header for the MDN envelope (typically the original sender)
140
+ * subject: string, // optional — Subject: header
141
+ * dispositionNotificationOptions: string, // RFC 3798 Disposition-Notification-Options value from the inbound message
142
+ * requireUserConfirmation: boolean, // default: true — refuse to auto-build unless the operator explicitly opts out
143
+ *
144
+ * @example
145
+ * var b = require("@blamejs/core");
146
+ * var mdn = b.mailMdn.build({
147
+ * originalMessageId: "<orig-1@sender.example>",
148
+ * finalRecipient: "user@example.com",
149
+ * disposition: "displayed",
150
+ * reportingUserAgent: "blamejs/0.8.53",
151
+ * requireUserConfirmation: false,
152
+ * });
153
+ * typeof mdn; // -> "string"
154
+ * /multipart\/report/.test(mdn); // -> true
155
+ * /message\/disposition-notification/.test(mdn); // -> true
156
+ */
157
+ function build(opts) {
158
+ validateOpts.requireObject(opts, "mailMdn.build", MailMdnError, "mdn/missing-required-field");
159
+ validateOpts.requireNonEmptyString(opts.originalMessageId,
160
+ "mailMdn.build: opts.originalMessageId", MailMdnError, "mdn/missing-required-field");
161
+ validateOpts.requireNonEmptyString(opts.finalRecipient,
162
+ "mailMdn.build: opts.finalRecipient", MailMdnError, "mdn/missing-required-field");
163
+ var disposition = String(opts.disposition || "").toLowerCase();
164
+ if (!DISPOSITION_TYPES[disposition]) {
165
+ throw _err("mdn/missing-required-field",
166
+ "mailMdn.build: opts.disposition must be one of " +
167
+ Object.keys(DISPOSITION_TYPES).join(" / ") +
168
+ "; got '" + String(opts.disposition) + "'");
169
+ }
170
+ var actionMode = String(opts.actionMode || "manual-action").toLowerCase();
171
+ if (!ACTION_MODES[actionMode]) {
172
+ throw _err("mdn/missing-required-field",
173
+ "mailMdn.build: opts.actionMode must be one of " +
174
+ Object.keys(ACTION_MODES).join(" / ") +
175
+ "; got '" + String(opts.actionMode) + "'");
176
+ }
177
+ var sendingMode = String(opts.sendingMode || "mdn-sent-manually").toLowerCase();
178
+ // Accept the canonical mixed-case form too — RFC 3798 §3.2.6.2 uses
179
+ // `MDN-sent-manually` / `MDN-sent-automatically`. Compare lower-case
180
+ // for robustness; emit canonical mixed-case in the output.
181
+ if (!SENDING_MODES[sendingMode]) {
182
+ throw _err("mdn/missing-required-field",
183
+ "mailMdn.build: opts.sendingMode must be one of " +
184
+ "MDN-sent-manually / MDN-sent-automatically; got '" +
185
+ String(opts.sendingMode) + "'");
186
+ }
187
+
188
+ // Auto-generation gate. RFC 3798 §2.1 — when the inbound message's
189
+ // Disposition-Notification-Options header asserts important=required,
190
+ // an auto-processor must not emit an MDN without explicit user
191
+ // confirmation. The framework defaults to requiring opt-in
192
+ // (requireUserConfirmation defaults to true) so accidental
193
+ // automatic generation is a typed refusal.
194
+ var requireConfirmation = opts.requireUserConfirmation !== false;
195
+ var dnOpts = String(opts.dispositionNotificationOptions || "").toLowerCase();
196
+ var requestRequiresConfirmation = /important\s*=\s*required/.test(dnOpts);
197
+ if (requestRequiresConfirmation && requireConfirmation) {
198
+ audit().safeEmit({
199
+ action: "mailmdn.suppressed",
200
+ outcome: "denied",
201
+ metadata: {
202
+ originalMessageId: opts.originalMessageId,
203
+ finalRecipient: opts.finalRecipient,
204
+ reason: "auto-generation refused: Disposition-Notification-Options demands user confirmation",
205
+ },
206
+ });
207
+ throw _err("mdn/auto-generation-refused",
208
+ "mailMdn.build: inbound Disposition-Notification-Options asserts important=required " +
209
+ "and opts.requireUserConfirmation is not explicitly false (RFC 3798 §2.1)");
210
+ }
211
+
212
+ var boundary = _generateBoundary();
213
+ var recipType = mimeParse.addressType(opts.finalRecipient);
214
+ var origRecipType = opts.originalRecipient ? mimeParse.addressType(opts.originalRecipient) : recipType;
215
+
216
+ // Canonical sendingMode casing for the output (RFC 3798 §3.2.6.2).
217
+ var sendingModeOut = sendingMode === "mdn-sent-automatically"
218
+ ? "MDN-sent-automatically"
219
+ : "MDN-sent-manually";
220
+
221
+ var lines = [];
222
+ lines.push("MIME-Version: 1.0");
223
+ lines.push('Content-Type: multipart/report; report-type=disposition-notification; boundary="' + boundary + '"');
224
+ if (opts.from) lines.push("From: " + opts.from);
225
+ if (opts.to) lines.push("To: " + opts.to);
226
+ if (opts.subject) lines.push("Subject: " + opts.subject);
227
+ lines.push("");
228
+
229
+ // Part 1 — human-readable description.
230
+ lines.push("--" + boundary);
231
+ lines.push("Content-Type: text/plain; charset=utf-8");
232
+ lines.push("Content-Transfer-Encoding: 8bit");
233
+ lines.push("");
234
+ lines.push("This is a Message Disposition Notification.");
235
+ lines.push("");
236
+ lines.push("The message sent on " + new Date().toUTCString());
237
+ lines.push("to " + opts.finalRecipient);
238
+ lines.push("with subject of (none) was " + disposition + ".");
239
+ lines.push("");
240
+
241
+ // Part 2 — message/disposition-notification.
242
+ lines.push("--" + boundary);
243
+ lines.push("Content-Type: message/disposition-notification");
244
+ lines.push("");
245
+ if (opts.reportingUserAgent) {
246
+ lines.push("Reporting-UA: " + opts.reportingUserAgent);
247
+ }
248
+ if (opts.originalRecipient) {
249
+ lines.push("Original-Recipient: " + origRecipType + ";" + opts.originalRecipient);
250
+ }
251
+ lines.push("Final-Recipient: " + recipType + ";" + opts.finalRecipient);
252
+ lines.push("Original-Message-ID: " + opts.originalMessageId);
253
+ lines.push("Disposition: " + actionMode + "/" + sendingModeOut + "; " + disposition);
254
+ lines.push("");
255
+
256
+ // Part 3 (optional) — original message attached as message/rfc822.
257
+ if (opts.originalMessage && typeof opts.originalMessage === "string") {
258
+ lines.push("--" + boundary);
259
+ lines.push("Content-Type: message/rfc822");
260
+ lines.push("");
261
+ lines.push(opts.originalMessage);
262
+ lines.push("");
263
+ }
264
+
265
+ lines.push("--" + boundary + "--");
266
+ lines.push("");
267
+
268
+ audit().safeEmit({
269
+ action: "mailmdn.generated",
270
+ outcome: "success",
271
+ metadata: {
272
+ originalMessageId: opts.originalMessageId,
273
+ finalRecipient: opts.finalRecipient,
274
+ disposition: disposition,
275
+ actionMode: actionMode,
276
+ sendingMode: sendingModeOut,
277
+ },
278
+ });
279
+
280
+ return lines.join("\r\n");
281
+ }
282
+
283
+ /**
284
+ * @primitive b.mailMdn.parse
285
+ * @signature b.mailMdn.parse(rawMessage)
286
+ * @since 0.8.53
287
+ * @status stable
288
+ * @related b.mailMdn.build, b.mailBounce.parse
289
+ *
290
+ * Parse a raw RFC 3798 / RFC 8098 multipart/report message into a
291
+ * normalized event shape:
292
+ *
293
+ * {
294
+ * messageId: string | null, // outer Message-ID of the MDN itself
295
+ * originalMessageId: string, // Original-Message-ID field
296
+ * originalRecipient: string | null, // Original-Recipient field
297
+ * finalRecipient: string, // Final-Recipient field (required)
298
+ * disposition: {
299
+ * actionMode: "manual-action" | "automatic-action",
300
+ * sendingMode: "mdn-sent-manually" | "mdn-sent-automatically" | null,
301
+ * type: "displayed" | "deleted" | "dispatched" | "processed" | "failed" | "denied",
302
+ * },
303
+ * reportingUserAgent: string | null,
304
+ * originalMessage: string | null, // attached message/rfc822 body, when present
305
+ * }
306
+ *
307
+ * Throws `MailMdnError` on missing top-level Content-Type, non-
308
+ * multipart/report content type, missing message/disposition-
309
+ * notification segment, missing Final-Recipient, or oversized payload.
310
+ *
311
+ * @example
312
+ * var b = require("@blamejs/core");
313
+ * var mdn = b.mailMdn.build({
314
+ * originalMessageId: "<orig-1@sender.example>",
315
+ * finalRecipient: "user@example.com",
316
+ * disposition: "displayed",
317
+ * reportingUserAgent: "blamejs/0.8.53",
318
+ * requireUserConfirmation: false,
319
+ * });
320
+ * var parsed = b.mailMdn.parse(mdn);
321
+ * parsed.disposition.type; // -> "displayed"
322
+ * parsed.finalRecipient; // -> "user@example.com"
323
+ * parsed.originalMessageId; // -> "<orig-1@sender.example>"
324
+ */
325
+ function parse(rawMessage) {
326
+ if (typeof rawMessage !== "string" || rawMessage.length === 0) {
327
+ throw _err("mdn/parse-failed",
328
+ "mailMdn.parse: rawMessage must be a non-empty string");
329
+ }
330
+ if (rawMessage.length > MDN_MAX_BYTES) {
331
+ throw _err("mdn/parse-failed",
332
+ "mailMdn.parse: message exceeds " + MDN_MAX_BYTES + " bytes");
333
+ }
334
+
335
+ var top = mimeParse.splitHeadersAndBody(rawMessage);
336
+ var ctRaw = mimeParse.findHeader(top.headers, "Content-Type");
337
+ if (!ctRaw) {
338
+ throw _err("mdn/parse-failed",
339
+ "mailMdn.parse: missing top-level Content-Type");
340
+ }
341
+ var ct = mimeParse.parseContentType(ctRaw);
342
+ if (ct.type !== "multipart/report") {
343
+ throw _err("mdn/parse-failed",
344
+ "mailMdn.parse: top-level Content-Type must be multipart/report; got " + ct.type);
345
+ }
346
+ if (ct.params["report-type"] && ct.params["report-type"].toLowerCase() !== "disposition-notification") {
347
+ throw _err("mdn/parse-failed",
348
+ "mailMdn.parse: report-type must be disposition-notification; got " + ct.params["report-type"]);
349
+ }
350
+ var boundary = ct.params.boundary;
351
+ if (!boundary) {
352
+ throw _err("mdn/parse-failed",
353
+ "mailMdn.parse: multipart/report missing boundary parameter");
354
+ }
355
+
356
+ var parts = mimeParse.splitMimeParts(top.body, boundary);
357
+ if (parts.length < 2) {
358
+ throw _err("mdn/parse-failed",
359
+ "mailMdn.parse: multipart/report needs at least 2 parts; got " + parts.length);
360
+ }
361
+
362
+ var notification = null;
363
+ var originalMessage = null;
364
+ for (var i = 0; i < parts.length; i += 1) {
365
+ var partSplit = mimeParse.splitHeadersAndBody(parts[i].replace(/^\r?\n/, ""));
366
+ var partCtRaw = mimeParse.findHeader(partSplit.headers, "Content-Type") || "text/plain";
367
+ var partCt = mimeParse.parseContentType(partCtRaw);
368
+ if (partCt.type === "message/disposition-notification") {
369
+ notification = mimeParse.parseHeaderBlock(partSplit.body);
370
+ } else if (partCt.type === "message/rfc822" || partCt.type === "text/rfc822-headers") {
371
+ originalMessage = partSplit.body;
372
+ }
373
+ }
374
+
375
+ if (!notification) {
376
+ throw _err("mdn/parse-failed",
377
+ "mailMdn.parse: no message/disposition-notification part found");
378
+ }
379
+
380
+ // Index notification fields by lowercase name.
381
+ var fields = {};
382
+ for (var j = 0; j < notification.length; j += 1) {
383
+ fields[notification[j].name.toLowerCase()] = notification[j].value;
384
+ }
385
+
386
+ var finalRecipient = mimeParse.stripAddressType(fields["final-recipient"]);
387
+ if (!finalRecipient) {
388
+ throw _err("mdn/missing-required-field",
389
+ "mailMdn.parse: message/disposition-notification missing Final-Recipient");
390
+ }
391
+ var dispositionField = fields["disposition"];
392
+ if (!dispositionField) {
393
+ throw _err("mdn/missing-required-field",
394
+ "mailMdn.parse: message/disposition-notification missing Disposition");
395
+ }
396
+ var disposition = _parseDisposition(dispositionField);
397
+ if (!disposition || !DISPOSITION_TYPES[disposition.type]) {
398
+ throw _err("mdn/parse-failed",
399
+ "mailMdn.parse: Disposition type token not in RFC 3798 §3.2.6 vocabulary; got '" +
400
+ (disposition && disposition.type) + "'");
401
+ }
402
+
403
+ return {
404
+ messageId: mimeParse.findHeader(top.headers, "Message-ID"),
405
+ originalMessageId: fields["original-message-id"] || null,
406
+ originalRecipient: mimeParse.stripAddressType(fields["original-recipient"]),
407
+ finalRecipient: finalRecipient,
408
+ disposition: disposition,
409
+ reportingUserAgent: fields["reporting-ua"] || null,
410
+ originalMessage: originalMessage,
411
+ };
412
+ }
413
+
414
+ module.exports = {
415
+ build: build,
416
+ parse: parse,
417
+ MailMdnError: MailMdnError,
418
+ // Vocabulary tables surfaced for tests + advanced operator code
419
+ // (e.g. operators wiring an inbound MDN router that switches on
420
+ // disposition.type).
421
+ DISPOSITION_TYPES: Object.keys(DISPOSITION_TYPES),
422
+ ACTION_MODES: Object.keys(ACTION_MODES),
423
+ SENDING_MODES: Object.keys(SENDING_MODES),
424
+ };