@bumail/mta-sts 0.0.0-stage → 0.1.0

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.
package/dist/index.js ADDED
@@ -0,0 +1,535 @@
1
+ // src/fetch-error.ts
2
+ class Failure extends Error {
3
+ detail;
4
+ constructor(detail) {
5
+ super(detail.message);
6
+ this.detail = detail;
7
+ }
8
+ }
9
+ function failure(code, message) {
10
+ const tlsResult = code === "CERTIFICATE" ? "sts-webpki-invalid" : code === "INVALID_POLICY" || code === "NOT_UTF8" ? "sts-policy-invalid" : "sts-policy-fetch-error";
11
+ return new Failure({ code, message, tlsResult });
12
+ }
13
+ var TLS_CODES = new Set([
14
+ "CERT_HAS_EXPIRED",
15
+ "CERT_NOT_YET_VALID",
16
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
17
+ "SELF_SIGNED_CERT_IN_CHAIN",
18
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
19
+ "UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
20
+ "ERR_TLS_CERT_ALTNAME_INVALID",
21
+ "CERT_UNTRUSTED",
22
+ "UNKNOWN_CA"
23
+ ]);
24
+ function isCertificateError(error) {
25
+ for (let e = error, i = 0;e && i < 4; i++) {
26
+ if (typeof e.code === "string" && TLS_CODES.has(e.code))
27
+ return true;
28
+ if (typeof e.message === "string" && /certificate/i.test(e.message))
29
+ return true;
30
+ e = e.cause;
31
+ }
32
+ return false;
33
+ }
34
+
35
+ // src/body.ts
36
+ var POLICY_MAX_BYTES = 64 * 1024;
37
+ async function readBody(response) {
38
+ const declared = Number(response.headers.get("content-length"));
39
+ if (Number.isFinite(declared) && declared > POLICY_MAX_BYTES) {
40
+ await response.body?.cancel();
41
+ throw failure("TOO_LARGE", `The policy is larger than ${POLICY_MAX_BYTES} bytes`);
42
+ }
43
+ const reader = response.body?.getReader();
44
+ const chunks = [];
45
+ let size = 0;
46
+ while (reader) {
47
+ let chunk;
48
+ try {
49
+ chunk = await reader.read();
50
+ } catch (error) {
51
+ throw failure("NETWORK", `The policy could not be read: ${error?.message ?? "the stream failed"}`);
52
+ }
53
+ const { done, value } = chunk;
54
+ if (done)
55
+ break;
56
+ size += value.byteLength;
57
+ if (size > POLICY_MAX_BYTES) {
58
+ await reader.cancel();
59
+ throw failure("TOO_LARGE", `The policy is larger than ${POLICY_MAX_BYTES} bytes`);
60
+ }
61
+ chunks.push(value);
62
+ }
63
+ const bytes = new Uint8Array(size);
64
+ let at = 0;
65
+ for (const chunk of chunks) {
66
+ bytes.set(chunk, at);
67
+ at += chunk.byteLength;
68
+ }
69
+ try {
70
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
71
+ } catch {
72
+ throw failure("NOT_UTF8", "The policy is not UTF-8 text");
73
+ }
74
+ }
75
+ // src/errors.ts
76
+ class MtaStsError extends Error {
77
+ name = "MtaStsError";
78
+ code;
79
+ constructor(code, message) {
80
+ super(message);
81
+ this.code = code;
82
+ }
83
+ }
84
+ function fail(code, message) {
85
+ return { ok: false, error: { code, message } };
86
+ }
87
+ // src/deadline.ts
88
+ function aborted() {
89
+ return failure("NETWORK", "The policy fetch was aborted by the caller");
90
+ }
91
+ function withDeadline(outer, timeoutMs) {
92
+ const controller = new AbortController;
93
+ let timer;
94
+ let onAbort;
95
+ const stopped = new Promise((_, reject) => {
96
+ timer = setTimeout(() => {
97
+ controller.abort();
98
+ reject(failure("TIMEOUT", `The policy was not had within ${timeoutMs} ms`));
99
+ }, timeoutMs);
100
+ timer.unref?.();
101
+ onAbort = () => {
102
+ controller.abort();
103
+ reject(aborted());
104
+ };
105
+ outer?.addEventListener("abort", onAbort, { once: true });
106
+ });
107
+ stopped.catch(() => {});
108
+ return {
109
+ signal: controller.signal,
110
+ stopped,
111
+ dispose() {
112
+ clearTimeout(timer);
113
+ if (onAbort)
114
+ outer?.removeEventListener("abort", onAbort);
115
+ }
116
+ };
117
+ }
118
+
119
+ // src/policy.ts
120
+ var MAX_AGE_LIMIT = 31557600;
121
+ var MODES = ["enforce", "testing", "none"];
122
+ var MX_PATTERN = /^(\*\.)?([a-z0-9_]([a-z0-9_-]{0,61}[a-z0-9_])?\.)*[a-z0-9_]([a-z0-9_-]{0,61}[a-z0-9_])?$/;
123
+ function isMxPattern(pattern) {
124
+ return pattern.length <= 253 && MX_PATTERN.test(pattern);
125
+ }
126
+ function parsePolicy(text) {
127
+ const fields = new Map;
128
+ const mx = [];
129
+ for (const raw of text.split(/\r?\n/)) {
130
+ const line = raw.trim();
131
+ if (line === "")
132
+ continue;
133
+ const colon = line.indexOf(":");
134
+ if (colon <= 0) {
135
+ return fail("BAD_LINE", `"${line.slice(0, 40)}" is not a key: value line`);
136
+ }
137
+ const key = line.slice(0, colon).trim();
138
+ const value = line.slice(colon + 1).trim();
139
+ if (key === "mx")
140
+ mx.push(value);
141
+ else if (!fields.has(key))
142
+ fields.set(key, value);
143
+ }
144
+ const version = fields.get("version");
145
+ if (version === undefined)
146
+ return fail("MISSING_VERSION", "The policy has no version line");
147
+ if (version !== "STSv1") {
148
+ return fail("BAD_VERSION", `version "${version}" is not STSv1`);
149
+ }
150
+ const mode = fields.get("mode");
151
+ if (mode === undefined)
152
+ return fail("MISSING_MODE", "The policy has no mode line");
153
+ if (!MODES.includes(mode)) {
154
+ return fail("BAD_MODE", `mode "${mode}" is not enforce, testing or none`);
155
+ }
156
+ const age = fields.get("max_age");
157
+ if (age === undefined)
158
+ return fail("MISSING_MAX_AGE", "The policy has no max_age line");
159
+ if (!/^[0-9]{1,10}$/.test(age) || Number(age) > MAX_AGE_LIMIT) {
160
+ return fail("BAD_MAX_AGE", `max_age "${age}" is not a whole number of seconds from 0 to ${MAX_AGE_LIMIT}`);
161
+ }
162
+ const patterns = mx.map((pattern) => pattern.toLowerCase());
163
+ const bad = patterns.find((pattern) => !isMxPattern(pattern));
164
+ if (bad !== undefined) {
165
+ return fail("BAD_MX", `mx "${bad}" is not a host name or a *. wildcard`);
166
+ }
167
+ if (patterns.length === 0 && mode !== "none") {
168
+ return fail("MISSING_MX", `A policy in mode ${mode} needs at least one mx line`);
169
+ }
170
+ return {
171
+ ok: true,
172
+ value: {
173
+ version: "STSv1",
174
+ mode,
175
+ mx: patterns,
176
+ maxAge: Number(age)
177
+ }
178
+ };
179
+ }
180
+ function formatPolicy(policy) {
181
+ const refuse = (why) => {
182
+ throw new MtaStsError("INVALID_POLICY", `formatPolicy(): ${why}`);
183
+ };
184
+ if (policy?.version !== undefined && policy.version !== "STSv1")
185
+ refuse("version must be STSv1");
186
+ if (!MODES.includes(policy?.mode))
187
+ refuse("mode must be enforce, testing or none");
188
+ if (!Number.isSafeInteger(policy.maxAge) || policy.maxAge < 0 || policy.maxAge > MAX_AGE_LIMIT) {
189
+ refuse(`maxAge must be a whole number of seconds from 0 to ${MAX_AGE_LIMIT}`);
190
+ }
191
+ if (!Array.isArray(policy.mx))
192
+ refuse("mx must be a list");
193
+ for (const pattern of policy.mx) {
194
+ if (typeof pattern !== "string" || !isMxPattern(pattern.toLowerCase())) {
195
+ refuse(`mx "${String(pattern)}" is not a host name or a *. wildcard`);
196
+ }
197
+ }
198
+ if (policy.mx.length === 0 && policy.mode !== "none") {
199
+ refuse(`mode ${policy.mode} needs at least one mx`);
200
+ }
201
+ return [
202
+ "version: STSv1",
203
+ `mode: ${policy.mode}`,
204
+ ...policy.mx.map((pattern) => `mx: ${pattern.toLowerCase()}`),
205
+ `max_age: ${policy.maxAge}`,
206
+ ""
207
+ ].join(`\r
208
+ `);
209
+ }
210
+
211
+ // src/fetch.ts
212
+ var POLICY_TIMEOUT_MS = 60000;
213
+ var DOMAIN = /^([a-z0-9_]([a-z0-9_-]{0,61}[a-z0-9_])?\.)+[a-z0-9_]([a-z0-9_-]{0,61}[a-z0-9_])?$/;
214
+ async function get(url, fetcher, signal) {
215
+ let response;
216
+ try {
217
+ response = await fetcher(url, {
218
+ redirect: "manual",
219
+ signal,
220
+ headers: { accept: "text/plain" }
221
+ });
222
+ } catch (error) {
223
+ if (isCertificateError(error)) {
224
+ throw failure("CERTIFICATE", `The policy host's certificate was refused: ${error?.message}`);
225
+ }
226
+ throw failure("NETWORK", `The policy could not be fetched: ${error?.message ?? "no answer"}`);
227
+ }
228
+ if (response.type === "opaqueredirect" || response.status >= 300 && response.status < 400) {
229
+ await response.body?.cancel();
230
+ throw failure("REDIRECT", "The policy host redirected; RFC 8461 §3.3 does not follow redirects");
231
+ }
232
+ if (response.status !== 200) {
233
+ await response.body?.cancel();
234
+ throw failure("STATUS", `The policy host answered ${response.status}, not 200`);
235
+ }
236
+ const type = (response.headers.get("content-type") ?? "").split(";", 1)[0]?.trim().toLowerCase();
237
+ if (type !== "text/plain") {
238
+ await response.body?.cancel();
239
+ throw failure("CONTENT_TYPE", `The policy was served as "${type || "no content type"}", not text/plain`);
240
+ }
241
+ return readBody(response);
242
+ }
243
+ var MAX_TIMEOUT_MS = 2147483647;
244
+ function checkOptions(options) {
245
+ const { timeoutMs, maxAgeCap } = options;
246
+ if (timeoutMs !== undefined && (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > MAX_TIMEOUT_MS)) {
247
+ throw new MtaStsError("INVALID_OPTION", `fetchPolicy(): timeoutMs must be a number of milliseconds from 1 to ${MAX_TIMEOUT_MS}`);
248
+ }
249
+ if (maxAgeCap !== undefined && (!Number.isSafeInteger(maxAgeCap) || maxAgeCap < 0 || maxAgeCap > MAX_AGE_LIMIT)) {
250
+ throw new MtaStsError("INVALID_OPTION", `fetchPolicy(): maxAgeCap must be a whole number of seconds from 0 to ${MAX_AGE_LIMIT}`);
251
+ }
252
+ }
253
+ async function fetchPolicy(domain, options = {}) {
254
+ checkOptions(options);
255
+ const name = typeof domain === "string" ? domain.toLowerCase().replace(/\.$/, "") : "";
256
+ if (!DOMAIN.test(name) || name.length > 253) {
257
+ const shown = typeof domain === "string" ? domain.slice(0, 80) : String(domain);
258
+ return {
259
+ ok: false,
260
+ error: failure("INVALID_DOMAIN", `"${shown}" is not a domain to fetch a policy for`).detail
261
+ };
262
+ }
263
+ const outer = options.signal;
264
+ if (outer?.aborted) {
265
+ return { ok: false, error: aborted().detail };
266
+ }
267
+ const fetcher = options.fetch ?? ((url, init) => globalThis.fetch(url, init));
268
+ const timeoutMs = options.timeoutMs ?? POLICY_TIMEOUT_MS;
269
+ const cap = options.maxAgeCap ?? MAX_AGE_LIMIT;
270
+ const deadline = withDeadline(outer, timeoutMs);
271
+ try {
272
+ const url = `https://mta-sts.${name}/.well-known/mta-sts.txt`;
273
+ const text = await Promise.race([
274
+ get(url, fetcher, deadline.signal),
275
+ deadline.stopped
276
+ ]);
277
+ const parsed = parsePolicy(text);
278
+ if (!parsed.ok)
279
+ throw failure("INVALID_POLICY", `The policy is invalid: ${parsed.error.message}`);
280
+ const policy = {
281
+ ...parsed.value,
282
+ maxAge: Math.min(parsed.value.maxAge, cap)
283
+ };
284
+ return { ok: true, policy, text };
285
+ } catch (error) {
286
+ if (error instanceof Failure)
287
+ return { ok: false, error: error.detail };
288
+ return {
289
+ ok: false,
290
+ error: failure("NETWORK", `The policy could not be fetched: ${error?.message ?? "no answer"}`).detail
291
+ };
292
+ } finally {
293
+ deadline.dispose();
294
+ }
295
+ }
296
+ // src/tls-result.ts
297
+ var TLS_RESULT_TYPES = [
298
+ "starttls-not-supported",
299
+ "certificate-host-mismatch",
300
+ "certificate-expired",
301
+ "certificate-not-trusted",
302
+ "validation-failure",
303
+ "tlsa-invalid",
304
+ "dnssec-invalid",
305
+ "dane-required",
306
+ "sts-policy-fetch-error",
307
+ "sts-policy-invalid",
308
+ "sts-webpki-invalid"
309
+ ];
310
+
311
+ // src/report.ts
312
+ var DATETIME = /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\dZ$/;
313
+ function refuse(why) {
314
+ throw new MtaStsError("INVALID_REPORT", `tlsReport(): ${why}`);
315
+ }
316
+ function text(value, name) {
317
+ if (typeof value !== "string" || value === "")
318
+ refuse(`${name} must be a non-empty string`);
319
+ return value;
320
+ }
321
+ function count(value, name) {
322
+ if (!Number.isSafeInteger(value) || value < 0) {
323
+ refuse(`${name} must be a whole number, 0 or more`);
324
+ }
325
+ return value;
326
+ }
327
+ function datetime(value, name) {
328
+ if (value instanceof Date) {
329
+ if (Number.isNaN(value.getTime()))
330
+ refuse(`${name} is an invalid Date`);
331
+ return `${value.toISOString().slice(0, 19)}Z`;
332
+ }
333
+ if (typeof value !== "string" || !DATETIME.test(value)) {
334
+ refuse(`${name} must be a Date or YYYY-MM-DDTHH:MM:SSZ`);
335
+ }
336
+ return value;
337
+ }
338
+ function lines(value) {
339
+ const all = typeof value === "string" ? value.split(/\r?\n/) : [...value];
340
+ while (all.length > 0 && all[all.length - 1] === "")
341
+ all.pop();
342
+ return all;
343
+ }
344
+ function detail(d, at) {
345
+ if (!TLS_RESULT_TYPES.includes(d?.resultType)) {
346
+ refuse(`${at}.resultType "${String(d?.resultType)}" is not a result type of RFC 8460 §4.3`);
347
+ }
348
+ const out = { "result-type": d.resultType };
349
+ const optional = [
350
+ ["sendingMtaIp", "sending-mta-ip"],
351
+ ["receivingMxHostname", "receiving-mx-hostname"],
352
+ ["receivingMxHelo", "receiving-mx-helo"],
353
+ ["receivingIp", "receiving-ip"]
354
+ ];
355
+ for (const [key, wire] of optional) {
356
+ if (d[key] !== undefined)
357
+ out[wire] = text(d[key], `${at}.${key}`);
358
+ }
359
+ out["failed-session-count"] = count(d.failedSessionCount, `${at}.failedSessionCount`);
360
+ if (d.additionalInformation !== undefined) {
361
+ out["additional-information"] = text(d.additionalInformation, `${at}.additionalInformation`);
362
+ }
363
+ if (d.failureReasonCode !== undefined) {
364
+ out["failure-reason-code"] = text(d.failureReasonCode, `${at}.failureReasonCode`);
365
+ }
366
+ return out;
367
+ }
368
+ function entry(p, index) {
369
+ const at = `policies[${index}]`;
370
+ const type = p?.policy?.type;
371
+ if (type !== "sts" && type !== "no-policy-found") {
372
+ refuse(`${at}.policy.type must be "sts" or "no-policy-found"`);
373
+ }
374
+ const policy = { "policy-type": type };
375
+ if (type === "sts") {
376
+ if (p.policy.string === undefined)
377
+ refuse(`${at}.policy.string is required for an sts policy`);
378
+ policy["policy-string"] = lines(p.policy.string);
379
+ }
380
+ policy["policy-domain"] = text(p.policy.domain, `${at}.policy.domain`);
381
+ if (p.policy.mxHost !== undefined) {
382
+ policy["mx-host"] = p.policy.mxHost.map((h, i) => text(h, `${at}.policy.mxHost[${i}]`));
383
+ }
384
+ const out = {
385
+ policy,
386
+ summary: {
387
+ "total-successful-session-count": count(p.summary?.success, `${at}.summary.success`),
388
+ "total-failure-session-count": count(p.summary?.failure, `${at}.summary.failure`)
389
+ }
390
+ };
391
+ if (p.failureDetails !== undefined && p.failureDetails.length > 0) {
392
+ out["failure-details"] = p.failureDetails.map((d, i) => detail(d, `${at}.failureDetails[${i}]`));
393
+ }
394
+ return out;
395
+ }
396
+ function tlsReport(input) {
397
+ if (!Array.isArray(input?.policies) || input.policies.length === 0) {
398
+ refuse("policies must hold at least one policy");
399
+ }
400
+ return {
401
+ "organization-name": text(input.organizationName, "organizationName"),
402
+ "date-range": {
403
+ "start-datetime": datetime(input.dateRange?.start, "dateRange.start"),
404
+ "end-datetime": datetime(input.dateRange?.end, "dateRange.end")
405
+ },
406
+ "contact-info": text(input.contactInfo, "contactInfo"),
407
+ "report-id": text(input.reportId, "reportId"),
408
+ policies: input.policies.map(entry)
409
+ };
410
+ }
411
+ function serialiseTlsReport(report) {
412
+ return JSON.stringify(report);
413
+ }
414
+ // src/txt.ts
415
+ function texts(answer) {
416
+ const items = typeof answer === "string" ? [answer] : answer;
417
+ return items.map((item) => typeof item === "string" ? item : item.text);
418
+ }
419
+
420
+ // src/sts-record.ts
421
+ var ID = /^[A-Za-z0-9]{1,32}$/;
422
+ function isStsText(text) {
423
+ const first = text.split(";", 1)[0] ?? "";
424
+ const equals = first.indexOf("=");
425
+ return equals >= 0 && first.slice(0, equals).trim() === "v" && first.slice(equals + 1).trim() === "STSv1";
426
+ }
427
+ function parseStsRecord(txt) {
428
+ const records = texts(txt).filter(isStsText);
429
+ if (records.length === 0) {
430
+ return fail("NO_RECORD", "No TXT record starts with v=STSv1");
431
+ }
432
+ if (records.length > 1) {
433
+ return fail("MULTIPLE_RECORDS", `${records.length} TXT records start with v=STSv1; RFC 8461 §3.1 treats that as no policy`);
434
+ }
435
+ let id;
436
+ for (const field of records[0].split(";").slice(1)) {
437
+ const equals = field.indexOf("=");
438
+ if (equals < 0)
439
+ continue;
440
+ if (field.slice(0, equals).trim() === "id" && id === undefined) {
441
+ id = field.slice(equals + 1).trim();
442
+ }
443
+ }
444
+ if (id === undefined)
445
+ return fail("MISSING_ID", "The record has no id field");
446
+ if (!ID.test(id)) {
447
+ return fail("BAD_ID", `id "${id}" is not 1 to 32 letters and digits`);
448
+ }
449
+ return { ok: true, value: { id } };
450
+ }
451
+ function formatStsRecord(record) {
452
+ if (typeof record?.id !== "string" || !ID.test(record.id)) {
453
+ throw new MtaStsError("INVALID_RECORD", "formatStsRecord(): id must be 1 to 32 letters and digits");
454
+ }
455
+ return `v=STSv1; id=${record.id}`;
456
+ }
457
+ // src/tls-rpt-record.ts
458
+ function isTlsRptText(text) {
459
+ const first = text.split(";", 1)[0] ?? "";
460
+ const equals = first.indexOf("=");
461
+ return equals >= 0 && first.slice(0, equals).trim() === "v" && first.slice(equals + 1).trim() === "TLSRPTv1";
462
+ }
463
+ function isReportUri(uri, strict = false) {
464
+ if (/[\s,;]/.test(uri) || strict && uri.includes("!"))
465
+ return false;
466
+ if (/^mailto:/i.test(uri)) {
467
+ const address = uri.slice("mailto:".length).split("?", 1)[0] ?? "";
468
+ return /^[^@]+@[^@]+$/.test(address);
469
+ }
470
+ if (/^https:/i.test(uri)) {
471
+ try {
472
+ return new URL(uri).hostname !== "";
473
+ } catch {
474
+ return false;
475
+ }
476
+ }
477
+ return false;
478
+ }
479
+ function parseTlsRptRecord(txt) {
480
+ const records = texts(txt).filter(isTlsRptText);
481
+ if (records.length === 0) {
482
+ return fail("NO_RECORD", "No TXT record starts with v=TLSRPTv1");
483
+ }
484
+ if (records.length > 1) {
485
+ return fail("MULTIPLE_RECORDS", `${records.length} TXT records start with v=TLSRPTv1; RFC 8460 §3 treats that as no record`);
486
+ }
487
+ let rua;
488
+ for (const field of records[0].split(";").slice(1)) {
489
+ const equals = field.indexOf("=");
490
+ if (equals < 0)
491
+ continue;
492
+ if (field.slice(0, equals).trim() === "rua" && rua === undefined) {
493
+ rua = field.slice(equals + 1).trim();
494
+ }
495
+ }
496
+ if (rua === undefined)
497
+ return fail("MISSING_RUA", "The record has no rua field");
498
+ const uris = rua.split(",").map((uri) => uri.trim());
499
+ const supported = uris.filter((uri) => isReportUri(uri));
500
+ if (supported.length === 0) {
501
+ return fail("BAD_RUA", `rua "${rua.slice(0, 80)}" holds no mailto: or https: URI`);
502
+ }
503
+ return { ok: true, value: { rua: supported } };
504
+ }
505
+ function formatTlsRptRecord(record) {
506
+ const rua = record?.rua;
507
+ if (!Array.isArray(rua) || rua.length === 0) {
508
+ throw new MtaStsError("INVALID_RECORD", "formatTlsRptRecord(): rua must hold at least one URI");
509
+ }
510
+ for (const uri of rua) {
511
+ if (typeof uri !== "string" || !isReportUri(uri, true)) {
512
+ throw new MtaStsError("INVALID_RECORD", `formatTlsRptRecord(): rua "${String(uri)}" is not a mailto: or https: URI without a raw ! or , (write them %21 and %2C)`);
513
+ }
514
+ }
515
+ return `v=TLSRPTv1; rua=${rua.join(",")}`;
516
+ }
517
+ export {
518
+ MAX_AGE_LIMIT,
519
+ MtaStsError,
520
+ POLICY_MAX_BYTES,
521
+ POLICY_TIMEOUT_MS,
522
+ TLS_RESULT_TYPES,
523
+ fetchPolicy,
524
+ formatPolicy,
525
+ formatStsRecord,
526
+ formatTlsRptRecord,
527
+ parsePolicy,
528
+ parseStsRecord,
529
+ parseTlsRptRecord,
530
+ serialiseTlsReport,
531
+ tlsReport
532
+ };
533
+
534
+ //# debugId=3988B28D53CE33DC64756E2164756E21
535
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,20 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/fetch-error.ts", "../src/body.ts", "../src/errors.ts", "../src/deadline.ts", "../src/policy.ts", "../src/fetch.ts", "../src/tls-result.ts", "../src/report.ts", "../src/txt.ts", "../src/sts-record.ts", "../src/tls-rpt-record.ts"],
4
+ "sourcesContent": [
5
+ "import type { TlsResultType } from './tls-result';\n\n/** Why a policy could not be had; each maps to a result type of RFC 8460 §4.3 through {@link FetchPolicyError.tlsResult}. */\nexport type FetchPolicyErrorCode =\n\t| 'INVALID_DOMAIN'\n\t| 'NETWORK'\n\t| 'TIMEOUT'\n\t| 'CERTIFICATE'\n\t| 'REDIRECT'\n\t| 'STATUS'\n\t| 'CONTENT_TYPE'\n\t| 'TOO_LARGE'\n\t| 'NOT_UTF8'\n\t| 'INVALID_POLICY';\n\nexport interface FetchPolicyError {\n\treadonly code: FetchPolicyErrorCode;\n\treadonly message: string;\n\t/** The `result-type` a TLS report names this failure with. */\n\treadonly tlsResult: Extract<\n\t\tTlsResultType,\n\t\t'sts-policy-fetch-error' | 'sts-policy-invalid' | 'sts-webpki-invalid'\n\t>;\n}\n\nexport class Failure extends Error {\n\tconstructor(readonly detail: FetchPolicyError) {\n\t\tsuper(detail.message);\n\t}\n}\n\nexport function failure(code: FetchPolicyErrorCode, message: string): Failure {\n\tconst tlsResult =\n\t\tcode === 'CERTIFICATE'\n\t\t\t? 'sts-webpki-invalid'\n\t\t\t: code === 'INVALID_POLICY' || code === 'NOT_UTF8'\n\t\t\t\t? 'sts-policy-invalid'\n\t\t\t\t: 'sts-policy-fetch-error';\n\treturn new Failure({ code, message, tlsResult });\n}\n\nconst TLS_CODES = new Set([\n\t'CERT_HAS_EXPIRED',\n\t'CERT_NOT_YET_VALID',\n\t'DEPTH_ZERO_SELF_SIGNED_CERT',\n\t'SELF_SIGNED_CERT_IN_CHAIN',\n\t'UNABLE_TO_VERIFY_LEAF_SIGNATURE',\n\t'UNABLE_TO_GET_ISSUER_CERT_LOCALLY',\n\t'ERR_TLS_CERT_ALTNAME_INVALID',\n\t'CERT_UNTRUSTED',\n\t'UNKNOWN_CA',\n]);\n\n/** Whether a fetch rejection is a certificate refusal (Bun and Node put the code on the error or its cause). */\nexport function isCertificateError(error: unknown): boolean {\n\tfor (\n\t\tlet e = error as\n\t\t\t\t| { code?: unknown; cause?: unknown; message?: unknown }\n\t\t\t\t| undefined,\n\t\t\ti = 0;\n\t\te && i < 4;\n\t\ti++\n\t) {\n\t\tif (typeof e.code === 'string' && TLS_CODES.has(e.code)) return true;\n\t\tif (typeof e.message === 'string' && /certificate/i.test(e.message))\n\t\t\treturn true;\n\t\te = e.cause as typeof e;\n\t}\n\treturn false;\n}\n",
6
+ "import { failure } from './fetch-error';\n\n/** A policy is a few lines; RFC 8461 §3.2 caps it at 64 KiB. */\nexport const POLICY_MAX_BYTES = 64 * 1024;\n\nexport async function readBody(response: Response): Promise<string> {\n\tconst declared = Number(response.headers.get('content-length'));\n\tif (Number.isFinite(declared) && declared > POLICY_MAX_BYTES) {\n\t\tawait response.body?.cancel();\n\t\tthrow failure(\n\t\t\t'TOO_LARGE',\n\t\t\t`The policy is larger than ${POLICY_MAX_BYTES} bytes`,\n\t\t);\n\t}\n\tconst reader = response.body?.getReader();\n\tconst chunks: Uint8Array[] = [];\n\tlet size = 0;\n\twhile (reader) {\n\t\tlet chunk: Awaited<ReturnType<typeof reader.read>>;\n\t\ttry {\n\t\t\tchunk = await reader.read();\n\t\t} catch (error) {\n\t\t\tthrow failure(\n\t\t\t\t'NETWORK',\n\t\t\t\t`The policy could not be read: ${(error as Error)?.message ?? 'the stream failed'}`,\n\t\t\t);\n\t\t}\n\t\tconst { done, value } = chunk;\n\t\tif (done) break;\n\t\tsize += value.byteLength;\n\t\tif (size > POLICY_MAX_BYTES) {\n\t\t\tawait reader.cancel();\n\t\t\tthrow failure(\n\t\t\t\t'TOO_LARGE',\n\t\t\t\t`The policy is larger than ${POLICY_MAX_BYTES} bytes`,\n\t\t\t);\n\t\t}\n\t\tchunks.push(value);\n\t}\n\tconst bytes = new Uint8Array(size);\n\tlet at = 0;\n\tfor (const chunk of chunks) {\n\t\tbytes.set(chunk, at);\n\t\tat += chunk.byteLength;\n\t}\n\ttry {\n\t\treturn new TextDecoder('utf-8', { fatal: true }).decode(bytes);\n\t} catch {\n\t\tthrow failure('NOT_UTF8', 'The policy is not UTF-8 text');\n\t}\n}\n",
7
+ "/**\n * Why a formatter or the report builder refused its input. A parser never\n * throws: it answers a {@link ParseResult}.\n *\n * - `INVALID_OPTION`: an option of `fetchPolicy` out of range;\n * - `INVALID_RECORD`: a record to write that RFC 8461 §3.1 or RFC 8460 §3 cannot hold;\n * - `INVALID_POLICY`: a policy to write that RFC 8461 §3.2 cannot hold;\n * - `INVALID_REPORT`: a TLS report that RFC 8460 §4 cannot hold.\n */\nexport type MtaStsErrorCode =\n\t| 'INVALID_OPTION'\n\t| 'INVALID_RECORD'\n\t| 'INVALID_POLICY'\n\t| 'INVALID_REPORT';\n\nexport class MtaStsError extends Error {\n\toverride readonly name = 'MtaStsError';\n\treadonly code: MtaStsErrorCode;\n\n\tconstructor(code: MtaStsErrorCode, message: string) {\n\t\tsuper(message);\n\t\tthis.code = code;\n\t}\n}\n\n/** A parse that failed: a stable `code` to branch on and a `message` to log. */\nexport interface ParseFailure<Code extends string> {\n\treadonly code: Code;\n\treadonly message: string;\n}\n\n/** What a parser answers: the value, or why the text is not one. */\nexport type ParseResult<T, Code extends string> =\n\t| { readonly ok: true; readonly value: T }\n\t| { readonly ok: false; readonly error: ParseFailure<Code> };\n\nexport function fail<Code extends string>(\n\tcode: Code,\n\tmessage: string,\n): { readonly ok: false; readonly error: ParseFailure<Code> } {\n\treturn { ok: false, error: { code, message } };\n}\n",
8
+ "import { type Failure, failure } from './fetch-error';\n\nexport function aborted(): Failure {\n\treturn failure('NETWORK', 'The policy fetch was aborted by the caller');\n}\n\n/**\n * A deadline over one fetch: `signal` to give the injected `fetch`,\n * `stopped`, which rejects with a `TIMEOUT` failure after `timeoutMs` or with\n * the abort of the caller's `outer` signal, and `dispose`, which clears the\n * timer and removes the listener. Race the work against `stopped`, so a\n * `fetch` that ignores its signal is still cut off.\n */\nexport function withDeadline(\n\touter: AbortSignal | undefined,\n\ttimeoutMs: number,\n): { signal: AbortSignal; stopped: Promise<never>; dispose: () => void } {\n\tconst controller = new AbortController();\n\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\tlet onAbort: (() => void) | undefined;\n\tconst stopped = new Promise<never>((_, reject) => {\n\t\ttimer = setTimeout(() => {\n\t\t\tcontroller.abort();\n\t\t\treject(\n\t\t\t\tfailure('TIMEOUT', `The policy was not had within ${timeoutMs} ms`),\n\t\t\t);\n\t\t}, timeoutMs);\n\t\t(timer as { unref?: () => void }).unref?.();\n\t\tonAbort = () => {\n\t\t\tcontroller.abort();\n\t\t\treject(aborted());\n\t\t};\n\t\touter?.addEventListener('abort', onAbort, { once: true });\n\t});\n\tstopped.catch(() => {});\n\treturn {\n\t\tsignal: controller.signal,\n\t\tstopped,\n\t\tdispose() {\n\t\t\tclearTimeout(timer);\n\t\t\tif (onAbort) outer?.removeEventListener('abort', onAbort);\n\t\t},\n\t};\n}\n",
9
+ "import { fail, MtaStsError, type ParseResult } from './errors';\n\n/** The policy file of RFC 8461 §3.2. */\nexport interface StsPolicy {\n\treadonly version: 'STSv1';\n\treadonly mode: 'enforce' | 'testing' | 'none';\n\t/** MX patterns, lowercase: a host name or `*.` and a suffix (§4.1). Empty only for `none`. */\n\treadonly mx: readonly string[];\n\t/** Seconds the policy may be cached, 0 to {@link MAX_AGE_LIMIT}. */\n\treadonly maxAge: number;\n}\n\nexport type PolicyErrorCode =\n\t| 'MISSING_VERSION'\n\t| 'BAD_VERSION'\n\t| 'MISSING_MODE'\n\t| 'BAD_MODE'\n\t| 'MISSING_MX'\n\t| 'BAD_MX'\n\t| 'MISSING_MAX_AGE'\n\t| 'BAD_MAX_AGE'\n\t| 'BAD_LINE';\n\n/** The largest `max_age` §3.2 allows: 31 557 600 seconds, about a year. */\nexport const MAX_AGE_LIMIT = 31_557_600;\n\nconst MODES: readonly string[] = ['enforce', 'testing', 'none'];\nconst MX_PATTERN =\n\t/^(\\*\\.)?([a-z0-9_]([a-z0-9_-]{0,61}[a-z0-9_])?\\.)*[a-z0-9_]([a-z0-9_-]{0,61}[a-z0-9_])?$/;\n\nfunction isMxPattern(pattern: string): boolean {\n\treturn pattern.length <= 253 && MX_PATTERN.test(pattern);\n}\n\n/**\n * Read a policy file: `key: value` lines, CRLF or LF, blank lines and\n * unknown keys ignored, `mx` given once per pattern. Keys are lowercase; a\n * key given twice keeps its first value, except `mx`. `mx` is required\n * unless the mode is `none`.\n */\nexport function parsePolicy(\n\ttext: string,\n): ParseResult<StsPolicy, PolicyErrorCode> {\n\tconst fields = new Map<string, string>();\n\tconst mx: string[] = [];\n\tfor (const raw of text.split(/\\r?\\n/)) {\n\t\tconst line = raw.trim();\n\t\tif (line === '') continue;\n\t\tconst colon = line.indexOf(':');\n\t\tif (colon <= 0) {\n\t\t\treturn fail(\n\t\t\t\t'BAD_LINE',\n\t\t\t\t`\"${line.slice(0, 40)}\" is not a key: value line`,\n\t\t\t);\n\t\t}\n\t\tconst key = line.slice(0, colon).trim();\n\t\tconst value = line.slice(colon + 1).trim();\n\t\tif (key === 'mx') mx.push(value);\n\t\telse if (!fields.has(key)) fields.set(key, value);\n\t}\n\tconst version = fields.get('version');\n\tif (version === undefined)\n\t\treturn fail('MISSING_VERSION', 'The policy has no version line');\n\tif (version !== 'STSv1') {\n\t\treturn fail('BAD_VERSION', `version \"${version}\" is not STSv1`);\n\t}\n\tconst mode = fields.get('mode');\n\tif (mode === undefined)\n\t\treturn fail('MISSING_MODE', 'The policy has no mode line');\n\tif (!MODES.includes(mode)) {\n\t\treturn fail('BAD_MODE', `mode \"${mode}\" is not enforce, testing or none`);\n\t}\n\tconst age = fields.get('max_age');\n\tif (age === undefined)\n\t\treturn fail('MISSING_MAX_AGE', 'The policy has no max_age line');\n\tif (!/^[0-9]{1,10}$/.test(age) || Number(age) > MAX_AGE_LIMIT) {\n\t\treturn fail(\n\t\t\t'BAD_MAX_AGE',\n\t\t\t`max_age \"${age}\" is not a whole number of seconds from 0 to ${MAX_AGE_LIMIT}`,\n\t\t);\n\t}\n\tconst patterns = mx.map((pattern) => pattern.toLowerCase());\n\tconst bad = patterns.find((pattern) => !isMxPattern(pattern));\n\tif (bad !== undefined) {\n\t\treturn fail('BAD_MX', `mx \"${bad}\" is not a host name or a *. wildcard`);\n\t}\n\tif (patterns.length === 0 && mode !== 'none') {\n\t\treturn fail(\n\t\t\t'MISSING_MX',\n\t\t\t`A policy in mode ${mode} needs at least one mx line`,\n\t\t);\n\t}\n\treturn {\n\t\tok: true,\n\t\tvalue: {\n\t\t\tversion: 'STSv1',\n\t\t\tmode: mode as StsPolicy['mode'],\n\t\t\tmx: patterns,\n\t\t\tmaxAge: Number(age),\n\t\t},\n\t};\n}\n\n/** The policy file, the inverse of {@link parsePolicy}: CRLF lines, the last ended too. */\nexport function formatPolicy(\n\tpolicy: Omit<StsPolicy, 'version'> & { readonly version?: 'STSv1' },\n): string {\n\tconst refuse = (why: string): never => {\n\t\tthrow new MtaStsError('INVALID_POLICY', `formatPolicy(): ${why}`);\n\t};\n\tif (policy?.version !== undefined && policy.version !== 'STSv1')\n\t\trefuse('version must be STSv1');\n\tif (!MODES.includes(policy?.mode))\n\t\trefuse('mode must be enforce, testing or none');\n\tif (\n\t\t!Number.isSafeInteger(policy.maxAge) ||\n\t\tpolicy.maxAge < 0 ||\n\t\tpolicy.maxAge > MAX_AGE_LIMIT\n\t) {\n\t\trefuse(\n\t\t\t`maxAge must be a whole number of seconds from 0 to ${MAX_AGE_LIMIT}`,\n\t\t);\n\t}\n\tif (!Array.isArray(policy.mx)) refuse('mx must be a list');\n\tfor (const pattern of policy.mx) {\n\t\tif (typeof pattern !== 'string' || !isMxPattern(pattern.toLowerCase())) {\n\t\t\trefuse(`mx \"${String(pattern)}\" is not a host name or a *. wildcard`);\n\t\t}\n\t}\n\tif (policy.mx.length === 0 && policy.mode !== 'none') {\n\t\trefuse(`mode ${policy.mode} needs at least one mx`);\n\t}\n\treturn [\n\t\t'version: STSv1',\n\t\t`mode: ${policy.mode}`,\n\t\t...policy.mx.map((pattern) => `mx: ${pattern.toLowerCase()}`),\n\t\t`max_age: ${policy.maxAge}`,\n\t\t'',\n\t].join('\\r\\n');\n}\n",
10
+ "import { readBody } from './body';\nimport { aborted, withDeadline } from './deadline';\nimport { MtaStsError } from './errors';\nimport {\n\tFailure,\n\ttype FetchPolicyError,\n\tfailure,\n\tisCertificateError,\n} from './fetch-error';\nimport { MAX_AGE_LIMIT, parsePolicy, type StsPolicy } from './policy';\n\nexport type FetchPolicyResult =\n\t| {\n\t\t\treadonly ok: true;\n\t\t\treadonly policy: StsPolicy;\n\t\t\t/** The policy file as served, for a report's `policy-string`. */\n\t\t\treadonly text: string;\n\t }\n\t| { readonly ok: false; readonly error: FetchPolicyError };\n\n/** The smallest `fetch` this package calls, so `globalThis.fetch` and a fake both fit. */\nexport type PolicyFetch = (\n\turl: string,\n\tinit: {\n\t\treadonly redirect: 'manual';\n\t\treadonly signal: AbortSignal;\n\t\treadonly headers: Record<string, string>;\n\t},\n) => Promise<Response>;\n\nexport interface FetchPolicyOptions {\n\t/** Defaults to `globalThis.fetch`. */\n\treadonly fetch?: PolicyFetch;\n\t/** The whole fetch, body included: a number of milliseconds, above 0 and at most 2147483647. Default {@link POLICY_TIMEOUT_MS}. */\n\treadonly timeoutMs?: number;\n\t/** The most `max_age` the returned policy keeps: a whole number of seconds, 0 to {@link MAX_AGE_LIMIT}. Default {@link MAX_AGE_LIMIT}. */\n\treadonly maxAgeCap?: number;\n\t/** Gives up when it aborts: the answer is a `NETWORK` failure that says \"aborted\". */\n\treadonly signal?: AbortSignal;\n}\n\n/** RFC 8461 §3.3 asks for a bounded fetch and fixes no figure; a 60-second default. */\nexport const POLICY_TIMEOUT_MS = 60_000;\n\nconst DOMAIN =\n\t/^([a-z0-9_]([a-z0-9_-]{0,61}[a-z0-9_])?\\.)+[a-z0-9_]([a-z0-9_-]{0,61}[a-z0-9_])?$/;\nasync function get(\n\turl: string,\n\tfetcher: PolicyFetch,\n\tsignal: AbortSignal,\n): Promise<string> {\n\tlet response: Response;\n\ttry {\n\t\tresponse = await fetcher(url, {\n\t\t\tredirect: 'manual',\n\t\t\tsignal,\n\t\t\theaders: { accept: 'text/plain' },\n\t\t});\n\t} catch (error) {\n\t\tif (isCertificateError(error)) {\n\t\t\tthrow failure(\n\t\t\t\t'CERTIFICATE',\n\t\t\t\t`The policy host's certificate was refused: ${(error as Error)?.message}`,\n\t\t\t);\n\t\t}\n\t\tthrow failure(\n\t\t\t'NETWORK',\n\t\t\t`The policy could not be fetched: ${(error as Error)?.message ?? 'no answer'}`,\n\t\t);\n\t}\n\tif (\n\t\tresponse.type === 'opaqueredirect' ||\n\t\t(response.status >= 300 && response.status < 400)\n\t) {\n\t\tawait response.body?.cancel();\n\t\tthrow failure(\n\t\t\t'REDIRECT',\n\t\t\t'The policy host redirected; RFC 8461 §3.3 does not follow redirects',\n\t\t);\n\t}\n\tif (response.status !== 200) {\n\t\tawait response.body?.cancel();\n\t\tthrow failure(\n\t\t\t'STATUS',\n\t\t\t`The policy host answered ${response.status}, not 200`,\n\t\t);\n\t}\n\tconst type = (response.headers.get('content-type') ?? '')\n\t\t.split(';', 1)[0]\n\t\t?.trim()\n\t\t.toLowerCase();\n\tif (type !== 'text/plain') {\n\t\tawait response.body?.cancel();\n\t\tthrow failure(\n\t\t\t'CONTENT_TYPE',\n\t\t\t`The policy was served as \"${type || 'no content type'}\", not text/plain`,\n\t\t);\n\t}\n\treturn readBody(response);\n}\n\n/** The longest delay a timer takes; Bun clamps a larger one to 1 ms. */\nconst MAX_TIMEOUT_MS = 2_147_483_647;\n\nfunction checkOptions(options: FetchPolicyOptions): void {\n\tconst { timeoutMs, maxAgeCap } = options;\n\tif (\n\t\ttimeoutMs !== undefined &&\n\t\t(typeof timeoutMs !== 'number' ||\n\t\t\t!Number.isFinite(timeoutMs) ||\n\t\t\ttimeoutMs <= 0 ||\n\t\t\ttimeoutMs > MAX_TIMEOUT_MS)\n\t) {\n\t\tthrow new MtaStsError(\n\t\t\t'INVALID_OPTION',\n\t\t\t`fetchPolicy(): timeoutMs must be a number of milliseconds from 1 to ${MAX_TIMEOUT_MS}`,\n\t\t);\n\t}\n\tif (\n\t\tmaxAgeCap !== undefined &&\n\t\t(!Number.isSafeInteger(maxAgeCap) ||\n\t\t\tmaxAgeCap < 0 ||\n\t\t\tmaxAgeCap > MAX_AGE_LIMIT)\n\t) {\n\t\tthrow new MtaStsError(\n\t\t\t'INVALID_OPTION',\n\t\t\t`fetchPolicy(): maxAgeCap must be a whole number of seconds from 0 to ${MAX_AGE_LIMIT}`,\n\t\t);\n\t}\n}\n\n/**\n * Fetch and read the policy of `domain` (RFC 8461 §3.3):\n * `GET https://mta-sts.<domain>/.well-known/mta-sts.txt` over HTTPS with a\n * valid certificate, no redirect followed, status 200, `text/plain`, at\n * most 64 KiB, within `timeoutMs`. Whatever the network, the host or the\n * injected `fetch` does, the answer is `{ ok: false, error }`, whose\n * `tlsResult` is the type to report; only an option out of range (a\n * programming error) throws `MtaStsError` `INVALID_OPTION`, before any fetch.\n */\nexport async function fetchPolicy(\n\tdomain: string,\n\toptions: FetchPolicyOptions = {},\n): Promise<FetchPolicyResult> {\n\tcheckOptions(options);\n\tconst name =\n\t\ttypeof domain === 'string' ? domain.toLowerCase().replace(/\\.$/, '') : '';\n\tif (!DOMAIN.test(name) || name.length > 253) {\n\t\tconst shown =\n\t\t\ttypeof domain === 'string' ? domain.slice(0, 80) : String(domain);\n\t\treturn {\n\t\t\tok: false,\n\t\t\terror: failure(\n\t\t\t\t'INVALID_DOMAIN',\n\t\t\t\t`\"${shown}\" is not a domain to fetch a policy for`,\n\t\t\t).detail,\n\t\t};\n\t}\n\tconst outer = options.signal;\n\tif (outer?.aborted) {\n\t\treturn { ok: false, error: aborted().detail };\n\t}\n\tconst fetcher: PolicyFetch =\n\t\toptions.fetch ?? ((url, init) => globalThis.fetch(url, init));\n\tconst timeoutMs = options.timeoutMs ?? POLICY_TIMEOUT_MS;\n\tconst cap = options.maxAgeCap ?? MAX_AGE_LIMIT;\n\tconst deadline = withDeadline(outer, timeoutMs);\n\ttry {\n\t\tconst url = `https://mta-sts.${name}/.well-known/mta-sts.txt`;\n\t\tconst text = await Promise.race([\n\t\t\tget(url, fetcher, deadline.signal),\n\t\t\tdeadline.stopped,\n\t\t]);\n\t\tconst parsed = parsePolicy(text);\n\t\tif (!parsed.ok)\n\t\t\tthrow failure(\n\t\t\t\t'INVALID_POLICY',\n\t\t\t\t`The policy is invalid: ${parsed.error.message}`,\n\t\t\t);\n\t\tconst policy = {\n\t\t\t...parsed.value,\n\t\t\tmaxAge: Math.min(parsed.value.maxAge, cap),\n\t\t};\n\t\treturn { ok: true, policy, text };\n\t} catch (error) {\n\t\tif (error instanceof Failure) return { ok: false, error: error.detail };\n\t\treturn {\n\t\t\tok: false,\n\t\t\terror: failure(\n\t\t\t\t'NETWORK',\n\t\t\t\t`The policy could not be fetched: ${(error as Error)?.message ?? 'no answer'}`,\n\t\t\t).detail,\n\t\t};\n\t} finally {\n\t\tdeadline.dispose();\n\t}\n}\n",
11
+ "/**\n * The result types of RFC 8460 §4.3. `@bumail/smtp` names the same strings\n * (`src/errors.ts`); this package peers on nothing, so it declares them\n * itself. A deliberate duplication, see AGENTS.md: change both together;\n * `tls-result.spec.ts` fails `typecheck` when they differ.\n */\nexport const TLS_RESULT_TYPES = [\n\t'starttls-not-supported',\n\t'certificate-host-mismatch',\n\t'certificate-expired',\n\t'certificate-not-trusted',\n\t'validation-failure',\n\t'tlsa-invalid',\n\t'dnssec-invalid',\n\t'dane-required',\n\t'sts-policy-fetch-error',\n\t'sts-policy-invalid',\n\t'sts-webpki-invalid',\n] as const;\n\nexport type TlsResultType = (typeof TLS_RESULT_TYPES)[number];\n",
12
+ "import { MtaStsError } from './errors';\nimport { TLS_RESULT_TYPES, type TlsResultType } from './tls-result';\n\n/** One failure line of a policy's `failure-details` (RFC 8460 §4.4). */\nexport interface TlsFailureDetail {\n\treadonly resultType: TlsResultType;\n\treadonly sendingMtaIp?: string;\n\treadonly receivingMxHostname?: string;\n\treadonly receivingMxHelo?: string;\n\treadonly receivingIp?: string;\n\treadonly failedSessionCount: number;\n\treadonly additionalInformation?: string;\n\treadonly failureReasonCode?: string;\n}\n\nexport interface TlsReportPolicy {\n\treadonly policy: {\n\t\treadonly type: 'sts' | 'no-policy-found';\n\t\t/** The policy file as fetched, text or its lines; for `sts`. */\n\t\treadonly string?: string | readonly string[];\n\t\treadonly domain: string;\n\t\treadonly mxHost?: readonly string[];\n\t};\n\treadonly summary: { readonly success: number; readonly failure: number };\n\treadonly failureDetails?: readonly TlsFailureDetail[];\n}\n\nexport interface TlsReportInput {\n\treadonly organizationName: string;\n\treadonly contactInfo: string;\n\treadonly reportId: string;\n\t/** A `Date`, or text such as `2026-11-02T00:00:00Z`. */\n\treadonly dateRange: {\n\t\treadonly start: Date | string;\n\t\treadonly end: Date | string;\n\t};\n\treadonly policies: readonly TlsReportPolicy[];\n}\n\n/** The report as RFC 8460 §4 writes it, with its hyphenated keys. */\nexport interface TlsReport {\n\treadonly 'organization-name': string;\n\treadonly 'date-range': {\n\t\treadonly 'start-datetime': string;\n\t\treadonly 'end-datetime': string;\n\t};\n\treadonly 'contact-info': string;\n\treadonly 'report-id': string;\n\treadonly policies: readonly Record<string, unknown>[];\n}\n\nconst DATETIME = /^\\d{4}-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\dZ$/;\n\nfunction refuse(why: string): never {\n\tthrow new MtaStsError('INVALID_REPORT', `tlsReport(): ${why}`);\n}\n\nfunction text(value: unknown, name: string): string {\n\tif (typeof value !== 'string' || value === '')\n\t\trefuse(`${name} must be a non-empty string`);\n\treturn value as string;\n}\n\nfunction count(value: unknown, name: string): number {\n\tif (!Number.isSafeInteger(value) || (value as number) < 0) {\n\t\trefuse(`${name} must be a whole number, 0 or more`);\n\t}\n\treturn value as number;\n}\n\nfunction datetime(value: Date | string, name: string): string {\n\tif (value instanceof Date) {\n\t\tif (Number.isNaN(value.getTime())) refuse(`${name} is an invalid Date`);\n\t\treturn `${value.toISOString().slice(0, 19)}Z`;\n\t}\n\tif (typeof value !== 'string' || !DATETIME.test(value)) {\n\t\trefuse(`${name} must be a Date or YYYY-MM-DDTHH:MM:SSZ`);\n\t}\n\treturn value;\n}\n\nfunction lines(value: string | readonly string[]): string[] {\n\tconst all = typeof value === 'string' ? value.split(/\\r?\\n/) : [...value];\n\twhile (all.length > 0 && all[all.length - 1] === '') all.pop();\n\treturn all;\n}\n\nfunction detail(d: TlsFailureDetail, at: string): Record<string, unknown> {\n\tif (!TLS_RESULT_TYPES.includes(d?.resultType)) {\n\t\trefuse(\n\t\t\t`${at}.resultType \"${String(d?.resultType)}\" is not a result type of RFC 8460 §4.3`,\n\t\t);\n\t}\n\tconst out: Record<string, unknown> = { 'result-type': d.resultType };\n\tconst optional: [keyof TlsFailureDetail, string][] = [\n\t\t['sendingMtaIp', 'sending-mta-ip'],\n\t\t['receivingMxHostname', 'receiving-mx-hostname'],\n\t\t['receivingMxHelo', 'receiving-mx-helo'],\n\t\t['receivingIp', 'receiving-ip'],\n\t];\n\tfor (const [key, wire] of optional) {\n\t\tif (d[key] !== undefined) out[wire] = text(d[key], `${at}.${key}`);\n\t}\n\tout['failed-session-count'] = count(\n\t\td.failedSessionCount,\n\t\t`${at}.failedSessionCount`,\n\t);\n\tif (d.additionalInformation !== undefined) {\n\t\tout['additional-information'] = text(\n\t\t\td.additionalInformation,\n\t\t\t`${at}.additionalInformation`,\n\t\t);\n\t}\n\tif (d.failureReasonCode !== undefined) {\n\t\tout['failure-reason-code'] = text(\n\t\t\td.failureReasonCode,\n\t\t\t`${at}.failureReasonCode`,\n\t\t);\n\t}\n\treturn out;\n}\n\nfunction entry(p: TlsReportPolicy, index: number): Record<string, unknown> {\n\tconst at = `policies[${index}]`;\n\tconst type = p?.policy?.type;\n\tif (type !== 'sts' && type !== 'no-policy-found') {\n\t\trefuse(`${at}.policy.type must be \"sts\" or \"no-policy-found\"`);\n\t}\n\tconst policy: Record<string, unknown> = { 'policy-type': type };\n\tif (type === 'sts') {\n\t\tif (p.policy.string === undefined)\n\t\t\trefuse(`${at}.policy.string is required for an sts policy`);\n\t\tpolicy['policy-string'] = lines(\n\t\t\tp.policy.string as string | readonly string[],\n\t\t);\n\t}\n\tpolicy['policy-domain'] = text(p.policy.domain, `${at}.policy.domain`);\n\tif (p.policy.mxHost !== undefined) {\n\t\tpolicy['mx-host'] = p.policy.mxHost.map((h, i) =>\n\t\t\ttext(h, `${at}.policy.mxHost[${i}]`),\n\t\t);\n\t}\n\tconst out: Record<string, unknown> = {\n\t\tpolicy,\n\t\tsummary: {\n\t\t\t'total-successful-session-count': count(\n\t\t\t\tp.summary?.success,\n\t\t\t\t`${at}.summary.success`,\n\t\t\t),\n\t\t\t'total-failure-session-count': count(\n\t\t\t\tp.summary?.failure,\n\t\t\t\t`${at}.summary.failure`,\n\t\t\t),\n\t\t},\n\t};\n\tif (p.failureDetails !== undefined && p.failureDetails.length > 0) {\n\t\tout['failure-details'] = p.failureDetails.map((d, i) =>\n\t\t\tdetail(d, `${at}.failureDetails[${i}]`),\n\t\t);\n\t}\n\treturn out;\n}\n\n/**\n * The TLS report of RFC 8460 §4 for the given counts, validated: strings\n * non-empty, counts whole numbers, dates as `YYYY-MM-DDTHH:MM:SSZ`, result\n * types from §4.3, an `sts` policy with its policy text. Throws\n * `MtaStsError` `INVALID_REPORT`, naming the field. Compressing it (§5.1)\n * and sending it are the caller's.\n */\nexport function tlsReport(input: TlsReportInput): TlsReport {\n\tif (!Array.isArray(input?.policies) || input.policies.length === 0) {\n\t\trefuse('policies must hold at least one policy');\n\t}\n\treturn {\n\t\t'organization-name': text(input.organizationName, 'organizationName'),\n\t\t'date-range': {\n\t\t\t'start-datetime': datetime(input.dateRange?.start, 'dateRange.start'),\n\t\t\t'end-datetime': datetime(input.dateRange?.end, 'dateRange.end'),\n\t\t},\n\t\t'contact-info': text(input.contactInfo, 'contactInfo'),\n\t\t'report-id': text(input.reportId, 'reportId'),\n\t\tpolicies: input.policies.map(entry),\n\t};\n}\n\n/** The report as JSON text (RFC 8460 §4: a JSON object, UTF-8). */\nexport function serialiseTlsReport(report: TlsReport): string {\n\treturn JSON.stringify(report);\n}\n",
13
+ "/**\n * The TXT answer of a name: its strings, or records with a `text` (what\n * `@bumail/dns`'s `Resolver.txt` answers, matched by shape).\n */\nexport type TxtAnswer =\n\t| string\n\t| readonly (string | { readonly text: string })[];\n\nexport function texts(answer: TxtAnswer): string[] {\n\tconst items = typeof answer === 'string' ? [answer] : answer;\n\treturn items.map((item) => (typeof item === 'string' ? item : item.text));\n}\n",
14
+ "import { fail, MtaStsError, type ParseResult } from './errors';\nimport { type TxtAnswer, texts } from './txt';\n\n/**\n * The `_mta-sts.<domain>` TXT record (RFC 8461 §3.1): `v=STSv1; id=<id>`.\n */\nexport interface StsRecord {\n\t/** 1 to 32 letters and digits; a change tells senders to refetch the policy. */\n\treadonly id: string;\n}\n\nexport type StsRecordErrorCode =\n\t| 'NO_RECORD'\n\t| 'MULTIPLE_RECORDS'\n\t| 'MISSING_ID'\n\t| 'BAD_ID';\n\nconst ID = /^[A-Za-z0-9]{1,32}$/;\n\n/** Whether a TXT string is an `STSv1` record: `v=STSv1` first (§3.1; the version is case-sensitive). */\nfunction isStsText(text: string): boolean {\n\tconst first = text.split(';', 1)[0] ?? '';\n\tconst equals = first.indexOf('=');\n\treturn (\n\t\tequals >= 0 &&\n\t\tfirst.slice(0, equals).trim() === 'v' &&\n\t\tfirst.slice(equals + 1).trim() === 'STSv1'\n\t);\n}\n\n/**\n * Read the `_mta-sts` TXT answer. Pass the strings of every TXT record of\n * the name (one record's chunks joined), as strings or as `@bumail/dns`\n * `TxtRecord`s; the ones that are not `v=STSv1`\n * are ignored, and more than one that is makes the answer invalid (§3.1).\n * Fields after the id are extensions and ignored.\n */\nexport function parseStsRecord(\n\ttxt: TxtAnswer,\n): ParseResult<StsRecord, StsRecordErrorCode> {\n\tconst records = texts(txt).filter(isStsText);\n\tif (records.length === 0) {\n\t\treturn fail('NO_RECORD', 'No TXT record starts with v=STSv1');\n\t}\n\tif (records.length > 1) {\n\t\treturn fail(\n\t\t\t'MULTIPLE_RECORDS',\n\t\t\t`${records.length} TXT records start with v=STSv1; RFC 8461 §3.1 treats that as no policy`,\n\t\t);\n\t}\n\tlet id: string | undefined;\n\tfor (const field of (records[0] as string).split(';').slice(1)) {\n\t\tconst equals = field.indexOf('=');\n\t\tif (equals < 0) continue;\n\t\tif (field.slice(0, equals).trim() === 'id' && id === undefined) {\n\t\t\tid = field.slice(equals + 1).trim();\n\t\t}\n\t}\n\tif (id === undefined) return fail('MISSING_ID', 'The record has no id field');\n\tif (!ID.test(id)) {\n\t\treturn fail('BAD_ID', `id \"${id}\" is not 1 to 32 letters and digits`);\n\t}\n\treturn { ok: true, value: { id } };\n}\n\n/** The record's text, the inverse of {@link parseStsRecord}: `v=STSv1; id=<id>`. */\nexport function formatStsRecord(record: StsRecord): string {\n\tif (typeof record?.id !== 'string' || !ID.test(record.id)) {\n\t\tthrow new MtaStsError(\n\t\t\t'INVALID_RECORD',\n\t\t\t'formatStsRecord(): id must be 1 to 32 letters and digits',\n\t\t);\n\t}\n\treturn `v=STSv1; id=${record.id}`;\n}\n",
15
+ "import { fail, MtaStsError, type ParseResult } from './errors';\nimport { type TxtAnswer, texts } from './txt';\n\n/**\n * The `_smtp._tls.<domain>` TXT record (RFC 8460 §3): where to send TLS\n * reports, `v=TLSRPTv1; rua=mailto:tls@example.com,https://example.com/r`.\n */\nexport interface TlsRptRecord {\n\t/** One or more `mailto:` or `https:` URIs, in the record's order. */\n\treadonly rua: readonly string[];\n}\n\nexport type TlsRptRecordErrorCode =\n\t| 'NO_RECORD'\n\t| 'MULTIPLE_RECORDS'\n\t| 'MISSING_RUA'\n\t| 'BAD_RUA';\n\nfunction isTlsRptText(text: string): boolean {\n\tconst first = text.split(';', 1)[0] ?? '';\n\tconst equals = first.indexOf('=');\n\treturn (\n\t\tequals >= 0 &&\n\t\tfirst.slice(0, equals).trim() === 'v' &&\n\t\tfirst.slice(equals + 1).trim() === 'TLSRPTv1'\n\t);\n}\n\n/**\n * Whether a `rua` entry is a `mailto:` address or an `https:` URL (§3). To\n * write one (`strict`), a raw `!` or `,` is refused: §3 has them\n * percent-encoded (`%21`, `%2C`) because they delimit the list.\n */\nfunction isReportUri(uri: string, strict = false): boolean {\n\tif (/[\\s,;]/.test(uri) || (strict && uri.includes('!'))) return false;\n\tif (/^mailto:/i.test(uri)) {\n\t\tconst address = uri.slice('mailto:'.length).split('?', 1)[0] ?? '';\n\t\treturn /^[^@]+@[^@]+$/.test(address);\n\t}\n\tif (/^https:/i.test(uri)) {\n\t\ttry {\n\t\t\treturn new URL(uri).hostname !== '';\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn false;\n}\n\n/**\n * Read the `_smtp._tls` TXT answer, like {@link parseStsRecord}: strings\n * that are not `v=TLSRPTv1` are ignored, more than one that is makes it\n * invalid, unknown fields are ignored. `rua` is a comma-separated list;\n * the entries that are not a `mailto:` address or an `https:` URL (another\n * scheme a sender may support, or a malformed one) are left out, and the\n * record is `BAD_RUA` only when none is left (§3). Pass the TXT strings or\n * `@bumail/dns` `TxtRecord`s.\n */\nexport function parseTlsRptRecord(\n\ttxt: TxtAnswer,\n): ParseResult<TlsRptRecord, TlsRptRecordErrorCode> {\n\tconst records = texts(txt).filter(isTlsRptText);\n\tif (records.length === 0) {\n\t\treturn fail('NO_RECORD', 'No TXT record starts with v=TLSRPTv1');\n\t}\n\tif (records.length > 1) {\n\t\treturn fail(\n\t\t\t'MULTIPLE_RECORDS',\n\t\t\t`${records.length} TXT records start with v=TLSRPTv1; RFC 8460 §3 treats that as no record`,\n\t\t);\n\t}\n\tlet rua: string | undefined;\n\tfor (const field of (records[0] as string).split(';').slice(1)) {\n\t\tconst equals = field.indexOf('=');\n\t\tif (equals < 0) continue;\n\t\tif (field.slice(0, equals).trim() === 'rua' && rua === undefined) {\n\t\t\trua = field.slice(equals + 1).trim();\n\t\t}\n\t}\n\tif (rua === undefined)\n\t\treturn fail('MISSING_RUA', 'The record has no rua field');\n\tconst uris = rua.split(',').map((uri) => uri.trim());\n\tconst supported = uris.filter((uri) => isReportUri(uri));\n\tif (supported.length === 0) {\n\t\treturn fail(\n\t\t\t'BAD_RUA',\n\t\t\t`rua \"${rua.slice(0, 80)}\" holds no mailto: or https: URI`,\n\t\t);\n\t}\n\treturn { ok: true, value: { rua: supported } };\n}\n\n/** The record's text, the inverse of {@link parseTlsRptRecord}. */\nexport function formatTlsRptRecord(record: TlsRptRecord): string {\n\tconst rua = record?.rua;\n\tif (!Array.isArray(rua) || rua.length === 0) {\n\t\tthrow new MtaStsError(\n\t\t\t'INVALID_RECORD',\n\t\t\t'formatTlsRptRecord(): rua must hold at least one URI',\n\t\t);\n\t}\n\tfor (const uri of rua) {\n\t\tif (typeof uri !== 'string' || !isReportUri(uri, true)) {\n\t\t\tthrow new MtaStsError(\n\t\t\t\t'INVALID_RECORD',\n\t\t\t\t`formatTlsRptRecord(): rua \"${String(uri)}\" is not a mailto: or https: URI without a raw ! or , (write them %21 and %2C)`,\n\t\t\t);\n\t\t}\n\t}\n\treturn `v=TLSRPTv1; rua=${rua.join(',')}`;\n}\n"
16
+ ],
17
+ "mappings": "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",
18
+ "debugId": "3988B28D53CE33DC64756E2164756E21",
19
+ "names": []
20
+ }