@blamejs/core 0.14.19 → 0.14.21

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 +4 -0
  2. package/README.md +1 -1
  3. package/lib/auth/oauth.js +736 -1
  4. package/lib/auth/oid4vci.js +124 -5
  5. package/lib/auth/oid4vp.js +14 -4
  6. package/lib/auth/sd-jwt-vc-holder.js +46 -1
  7. package/lib/break-glass.js +1 -2
  8. package/lib/config.js +28 -31
  9. package/lib/crypto-field.js +274 -17
  10. package/lib/dora.js +8 -5
  11. package/lib/dsr.js +2 -2
  12. package/lib/flag-evaluation-context.js +7 -0
  13. package/lib/guard-html-wcag-aria.js +4 -2
  14. package/lib/guard-html-wcag-forms.js +4 -2
  15. package/lib/guard-html-wcag-tables.js +4 -2
  16. package/lib/guard-html-wcag-tagwalk.js +20 -0
  17. package/lib/guard-html-wcag.js +1 -1
  18. package/lib/honeytoken.js +27 -20
  19. package/lib/mail-auth.js +333 -0
  20. package/lib/mail-deploy.js +1 -1
  21. package/lib/mail-send-deliver.js +13 -4
  22. package/lib/middleware/api-encrypt.js +140 -13
  23. package/lib/middleware/asyncapi-serve.js +3 -0
  24. package/lib/middleware/csp-report.js +13 -9
  25. package/lib/middleware/fetch-metadata.js +115 -14
  26. package/lib/middleware/openapi-serve.js +3 -0
  27. package/lib/middleware/scim-server.js +297 -19
  28. package/lib/middleware/security-headers.js +47 -0
  29. package/lib/middleware/security-txt.js +1 -2
  30. package/lib/middleware/trace-log-correlation.js +1 -2
  31. package/lib/network-smtp-policy.js +4 -4
  32. package/lib/object-store/sigv4-bucket-ops.js +11 -2
  33. package/lib/observability-tracer.js +1 -1
  34. package/lib/observability.js +39 -1
  35. package/lib/problem-details.js +56 -11
  36. package/lib/pubsub-cluster.js +16 -3
  37. package/lib/queue-sqs.js +20 -2
  38. package/lib/redis-client.js +32 -4
  39. package/lib/safe-redirect.js +16 -2
  40. package/package.json +1 -1
  41. package/sbom.cdx.json +6 -6
package/lib/mail-auth.js CHANGED
@@ -50,6 +50,7 @@ var validateOpts = require("./validate-opts");
50
50
  var bCrypto = require("./crypto");
51
51
  var C = require("./constants");
52
52
  var dkim = require("./mail-dkim");
53
+ var mimeParse = require("./mime-parse");
53
54
  var safeXml = require("./parsers/safe-xml");
54
55
  var ipUtils = require("./ip-utils");
55
56
  var publicSuffix = require("./public-suffix");
@@ -2615,6 +2616,337 @@ async function iprevVerify(ip) {
2615
2616
  };
2616
2617
  }
2617
2618
 
2619
+ // ---- DMARC forensic (RUF) failure-report parser (RFC 6591 + RFC 7489 §7.3) ----
2620
+ //
2621
+ // A domain publishing a DMARC `ruf=` policy receives per-message
2622
+ // failure reports when an authentication check fails. RFC 7489 §7.3
2623
+ // specifies the Authentication Failure Reporting Format (AFRF) of
2624
+ // RFC 6591 for these: a multipart/report (report-type=feedback-report)
2625
+ // carrying a `message/feedback-report` part whose header block adds the
2626
+ // DMARC-specific fields (Auth-Failure, Delivery-Result, Identity-
2627
+ // Alignment, DKIM-*/SPF-* result fields) on top of the RFC 5965 base
2628
+ // fields, plus a third part (message/rfc822 or text/rfc822-headers)
2629
+ // with the reported message (in full or headers-only).
2630
+ //
2631
+ // Composes the shared lib/mime-parse.js substrate (the same MIME walker
2632
+ // the RFC 5965 ARF ingest in lib/mail-arf.js uses) for the
2633
+ // multipart/report bisection + message/feedback-report extraction, then
2634
+ // shapes the full RFC 6591 §3.1 forensic field set (which the abuse-
2635
+ // report profile does not model) plus the reported message's headers.
2636
+ //
2637
+ // var rv = b.mail.dmarc.parseForensicReport(rawMessageBytes);
2638
+ // if (!rv.ok) { /* rv.error.code / rv.error.message */ }
2639
+ // else { /* rv.report.feedbackType / .authFailure / … */ }
2640
+ //
2641
+ // Validation tier: DEFENSIVE READER. The input is hostile-by-default
2642
+ // (a per-message failure report arrives at an operator endpoint from an
2643
+ // arbitrary reporting peer). The parser RETURNS a typed error object on
2644
+ // any malformed / over-cap / wrong-shape input — it does NOT throw in
2645
+ // the hot path, so a crafted report can't crash the request that
2646
+ // ingested it. Bytes + part-count + reported-header counts are bounded
2647
+ // (CWE-400 resource-exhaustion class) like the sibling aggregate-report
2648
+ // parser.
2649
+ //
2650
+ // { ok: true, report: { … } }
2651
+ // { ok: false, error: { code: "<slug>", message: "<reason>" } }
2652
+
2653
+ // RFC 6591 §3.2 — the report is small in practice; cap at 8 MiB to match
2654
+ // the sibling DMARC aggregate-report ceiling so operators have one
2655
+ // mental model for "what fits".
2656
+ var DMARC_RUF_MAX_REPORT_BYTES = C.BYTES.mib(8);
2657
+
2658
+ // RFC 2046 §5.1 — a multipart/report failure report has a handful of
2659
+ // parts (text/plain + message/feedback-report + the reported message);
2660
+ // bound the part count so a hostile report with thousands of empty
2661
+ // boundary delimiters can't force unbounded walk work.
2662
+ var DMARC_RUF_MAX_PARTS = 64; // resource-exhaustion bound (CWE-400)
2663
+
2664
+ // RFC 6591 §3.1 — required forensic fields. Feedback-Type and Auth-
2665
+ // Failure are the two that make an auth-failure report a DMARC forensic
2666
+ // report (RFC 7489 §7.3). User-Agent / Version are advisory in practice.
2667
+ var DMARC_RUF_REQUIRED_FIELDS = ["feedback-type", "auth-failure"];
2668
+
2669
+ // RFC 6591 §3.1 — Auth-Failure registry values. Unknown values pass
2670
+ // through (the IANA "Authentication Failure" registry grows); this set
2671
+ // documents the launch vocabulary so operators can route on it.
2672
+ var DMARC_RUF_AUTH_FAILURE_TYPES = Object.freeze({
2673
+ adsp: 1, // RFC 6591 §3.1 (historic ADSP)
2674
+ "bodyhash": 1, // DKIM body-hash mismatch
2675
+ dkim: 1, // DKIM signature failure
2676
+ dmarc: 1, // RFC 7489 §7.3 — DMARC evaluation failure
2677
+ revoked: 1, // signing key revoked
2678
+ signature: 1, // DKIM signature syntactically invalid
2679
+ spf: 1, // SPF check failure
2680
+ });
2681
+ void DMARC_RUF_AUTH_FAILURE_TYPES;
2682
+
2683
+ // RFC 6591 §3.2 — the reported message's header section can be large but
2684
+ // is bounded; cap the number of reported headers we normalize so a
2685
+ // crafted report can't force unbounded work. The full reported message
2686
+ // text is still surfaced verbatim under `reportedMessage` (bounded by
2687
+ // the overall byte cap), but the parsed `reportedHeaders` list is
2688
+ // header-count-capped.
2689
+ var DMARC_RUF_MAX_REPORTED_HEADERS = 256; // resource-exhaustion bound (CWE-400)
2690
+
2691
+ function _rufError(code, message) {
2692
+ return { ok: false, error: { code: code, message: message } };
2693
+ }
2694
+
2695
+ // Parse the reported message's headers (RFC 6591 §3.2 — the third part
2696
+ // of the report carries the message that failed authentication, in full
2697
+ // or headers-only). Returns an own-keys-only map (null-prototype) of
2698
+ // header-name → value (last-wins for duplicate single-valued lookups;
2699
+ // the full ordered list is also returned) so a header named
2700
+ // `__proto__` / `constructor` in a hostile report can't pollute the
2701
+ // prototype chain (prototype-pollution class).
2702
+ function _parseReportedHeaders(reportedMessage) {
2703
+ var ordered = [];
2704
+ var map = Object.create(null);
2705
+ if (typeof reportedMessage !== "string" || reportedMessage.length === 0) {
2706
+ return { headers: ordered, map: map, truncated: false };
2707
+ }
2708
+ var split;
2709
+ try { split = mimeParse.splitHeadersAndBody(reportedMessage); }
2710
+ catch (_e) { return { headers: ordered, map: map, truncated: false }; }
2711
+ var hdrs = Array.isArray(split.headers) ? split.headers : [];
2712
+ var truncated = false;
2713
+ for (var i = 0; i < hdrs.length; i += 1) {
2714
+ if (ordered.length >= DMARC_RUF_MAX_REPORTED_HEADERS) { truncated = true; break; }
2715
+ var h = hdrs[i];
2716
+ if (!h || typeof h.name !== "string") continue;
2717
+ var name = h.name;
2718
+ var value = typeof h.value === "string" ? h.value : "";
2719
+ ordered.push({ name: name, value: value });
2720
+ // Own-key assignment on a null-prototype object: a reported header
2721
+ // named __proto__ / constructor / prototype is stored as data, not
2722
+ // walked up the chain.
2723
+ map[name.toLowerCase()] = value;
2724
+ }
2725
+ return { headers: ordered, map: map, truncated: truncated };
2726
+ }
2727
+
2728
+ // Reassemble a MIME part's headers + body so a reported message that
2729
+ // ships as text/rfc822-headers (no separate body) still round-trips its
2730
+ // header bytes (RFC 6591 §3.2 permits headers-only).
2731
+ function _reassembleRufPart(part) {
2732
+ var hdrs = "";
2733
+ var ph = Array.isArray(part.headers) ? part.headers : [];
2734
+ for (var i = 0; i < ph.length; i += 1) {
2735
+ hdrs += ph[i].name + ": " + ph[i].value + "\r\n";
2736
+ }
2737
+ return hdrs + "\r\n" + (part.body || "");
2738
+ }
2739
+
2740
+ function dmarcParseForensicReport(input, opts) {
2741
+ opts = opts || {};
2742
+
2743
+ // ---- Coerce + byte-cap (defensive: typed error, never throw) ----
2744
+ var asString;
2745
+ if (typeof input === "string") asString = input;
2746
+ else if (Buffer.isBuffer(input)) asString = input.toString("utf8");
2747
+ else {
2748
+ return _rufError("mail-auth/dmarc-ruf-bad-input",
2749
+ "dmarc.parseForensicReport: input must be a string or Buffer");
2750
+ }
2751
+ var maxBytes = (typeof opts.maxBytes === "number" && isFinite(opts.maxBytes) && opts.maxBytes > 0)
2752
+ ? opts.maxBytes
2753
+ : DMARC_RUF_MAX_REPORT_BYTES;
2754
+ if (asString.length > maxBytes) {
2755
+ return _rufError("mail-auth/dmarc-ruf-too-large",
2756
+ "dmarc.parseForensicReport: report exceeds " + maxBytes + " bytes (got " + asString.length + ")");
2757
+ }
2758
+
2759
+ // ---- Bisect top-level headers / body; require multipart/report ----
2760
+ var top;
2761
+ try { top = mimeParse.splitHeadersAndBody(asString); }
2762
+ catch (e) {
2763
+ return _rufError("mail-auth/dmarc-ruf-bad-report",
2764
+ "dmarc.parseForensicReport: header/body split failed: " + ((e && e.message) || String(e)));
2765
+ }
2766
+ var ct = mimeParse.parseContentType(mimeParse.findHeader(top.headers, "Content-Type") || "");
2767
+ if (ct.type !== "multipart/report") {
2768
+ return _rufError("mail-auth/dmarc-ruf-bad-report",
2769
+ "dmarc.parseForensicReport: top-level Content-Type must be multipart/report (got '" + ct.type + "')");
2770
+ }
2771
+ // RFC 6591 §2 / RFC 5965 §2 — report-type=feedback-report. Tolerate an
2772
+ // omitted report-type (shipping reporters sometimes drop it); refuse a
2773
+ // mismatched value.
2774
+ if (ct.params["report-type"] && ct.params["report-type"].toLowerCase() !== "feedback-report") {
2775
+ return _rufError("mail-auth/dmarc-ruf-bad-report",
2776
+ "dmarc.parseForensicReport: report-type must be feedback-report (got '" +
2777
+ ct.params["report-type"] + "')");
2778
+ }
2779
+ if (!ct.params.boundary) {
2780
+ return _rufError("mail-auth/dmarc-ruf-bad-report",
2781
+ "dmarc.parseForensicReport: multipart/report Content-Type lacks boundary parameter");
2782
+ }
2783
+
2784
+ // ---- Walk the parts; find message/feedback-report + reported msg ----
2785
+ var parts = mimeParse.splitMimeParts(top.body, ct.params.boundary);
2786
+ if (parts.length === 0) {
2787
+ return _rufError("mail-auth/dmarc-ruf-bad-report",
2788
+ "dmarc.parseForensicReport: multipart/report body contains no parts");
2789
+ }
2790
+ if (parts.length > DMARC_RUF_MAX_PARTS) {
2791
+ return _rufError("mail-auth/dmarc-ruf-too-many-parts",
2792
+ "dmarc.parseForensicReport: report has " + parts.length + " parts (cap " +
2793
+ DMARC_RUF_MAX_PARTS + ")");
2794
+ }
2795
+
2796
+ var feedbackPart = null;
2797
+ var reportedPart = null;
2798
+ for (var pi = 0; pi < parts.length; pi += 1) {
2799
+ var split;
2800
+ try { split = mimeParse.splitHeadersAndBody(parts[pi]); }
2801
+ catch (_e) { continue; }
2802
+ var partCt = mimeParse.parseContentType(
2803
+ mimeParse.findHeader(split.headers, "Content-Type") || "");
2804
+ if (partCt.type === "message/feedback-report" && !feedbackPart) {
2805
+ feedbackPart = split;
2806
+ } else if ((partCt.type === "message/rfc822" ||
2807
+ partCt.type === "text/rfc822-headers") && !reportedPart) {
2808
+ reportedPart = split;
2809
+ }
2810
+ }
2811
+ if (!feedbackPart) {
2812
+ return _rufError("mail-auth/dmarc-ruf-no-feedback-report",
2813
+ "dmarc.parseForensicReport: missing message/feedback-report subpart (RFC 6591 §3)");
2814
+ }
2815
+
2816
+ // ---- Parse the feedback-report header block (RFC 6591 §3.1) ----
2817
+ // Field names are stored own-key on a null-prototype map so a hostile
2818
+ // field named __proto__ / constructor can't pollute the prototype.
2819
+ var fields;
2820
+ try { fields = mimeParse.parseHeaderBlock(feedbackPart.body); }
2821
+ catch (e) {
2822
+ return _rufError("mail-auth/dmarc-ruf-bad-report",
2823
+ "dmarc.parseForensicReport: feedback-report field parse failed: " + ((e && e.message) || String(e)));
2824
+ }
2825
+ var fieldMap = Object.create(null);
2826
+ var rcptToList = [];
2827
+ for (var fi = 0; fi < fields.length; fi += 1) {
2828
+ var f = fields[fi];
2829
+ if (!f || typeof f.name !== "string") continue;
2830
+ var lc = f.name.toLowerCase();
2831
+ var val = typeof f.value === "string" ? f.value : "";
2832
+ fieldMap[lc] = val;
2833
+ if (lc === "original-rcpt-to") rcptToList.push(val);
2834
+ }
2835
+ function _field(name) {
2836
+ return Object.prototype.hasOwnProperty.call(fieldMap, name) ? fieldMap[name] : null;
2837
+ }
2838
+
2839
+ // ---- Required fields (RFC 6591 §3.1 / RFC 7489 §7.3) ----
2840
+ for (var ri = 0; ri < DMARC_RUF_REQUIRED_FIELDS.length; ri += 1) {
2841
+ var req = DMARC_RUF_REQUIRED_FIELDS[ri];
2842
+ var rv = _field(req);
2843
+ if (typeof rv !== "string" || rv.length === 0) {
2844
+ if (req === "auth-failure") {
2845
+ return _rufError("mail-auth/dmarc-ruf-missing-auth-failure",
2846
+ "dmarc.parseForensicReport: required field 'Auth-Failure' is missing (RFC 6591 §3.1)");
2847
+ }
2848
+ return _rufError("mail-auth/dmarc-ruf-missing-field",
2849
+ "dmarc.parseForensicReport: required field '" + req + "' is missing (RFC 6591 §3.1)");
2850
+ }
2851
+ }
2852
+
2853
+ // RFC 7489 §7.3 — a DMARC forensic report carries Feedback-Type:
2854
+ // auth-failure (the AFRF profile of RFC 6591). A report whose Feedback-
2855
+ // Type is another ARF class (e.g. plain "abuse") is a valid feedback
2856
+ // report but NOT a DMARC forensic report; surface the mismatch rather
2857
+ // than mislabeling it. Field values are case-insensitive tokens.
2858
+ var feedbackType = String(_field("feedback-type")).toLowerCase();
2859
+ if (feedbackType !== "auth-failure") {
2860
+ return _rufError("mail-auth/dmarc-ruf-not-auth-failure",
2861
+ "dmarc.parseForensicReport: Feedback-Type must be 'auth-failure' for a " +
2862
+ "DMARC forensic report (RFC 7489 §7.3 / RFC 6591), got " +
2863
+ JSON.stringify(_field("feedback-type")));
2864
+ }
2865
+
2866
+ // ---- RFC 6591 §3.2 reported message ----
2867
+ var reportedMessage = null;
2868
+ if (reportedPart) {
2869
+ reportedMessage = (reportedPart.body && reportedPart.body.length > 0)
2870
+ ? reportedPart.body
2871
+ : _reassembleRufPart(reportedPart);
2872
+ }
2873
+ var reported = _parseReportedHeaders(reportedMessage);
2874
+
2875
+ // ---- Normalize Arrival-Date / Incidents (RFC 5965 §3.1) ----
2876
+ var arrivalRaw = _field("arrival-date") || _field("received-date") || null;
2877
+ var arrivalIso = null;
2878
+ if (arrivalRaw) {
2879
+ var d = new Date(arrivalRaw);
2880
+ if (!isNaN(d.getTime())) arrivalIso = d.toISOString();
2881
+ }
2882
+ var incidentsRaw = _field("incidents");
2883
+ var incidents = null;
2884
+ if (typeof incidentsRaw === "string") {
2885
+ var inc = parseInt(incidentsRaw, 10);
2886
+ if (isFinite(inc) && inc >= 0) incidents = inc;
2887
+ }
2888
+
2889
+ // Surface unmodeled fields under extraFields for operator visibility
2890
+ // (vendor X-* tags). Own-key copy off the null-prototype fieldMap onto
2891
+ // a null-prototype target so a field named __proto__ / constructor in a
2892
+ // hostile report is stored as data, not as a prototype mutation.
2893
+ var KNOWN = Object.create(null);
2894
+ ["feedback-type", "user-agent", "version", "auth-failure",
2895
+ "delivery-result", "identity-alignment", "dkim-domain",
2896
+ "dkim-identity", "dkim-selector", "dkim-canonicalized-header",
2897
+ "dkim-canonicalized-body", "spf-dns", "original-mail-from",
2898
+ "original-rcpt-to", "arrival-date", "received-date",
2899
+ "reported-domain", "source-ip", "authentication-results",
2900
+ "reported-uri", "incidents", "original-envelope-id"
2901
+ ].forEach(function (k) { KNOWN[k] = 1; });
2902
+ var extraFields = Object.create(null);
2903
+ Object.keys(fieldMap).forEach(function (k) {
2904
+ if (!Object.prototype.hasOwnProperty.call(KNOWN, k)) extraFields[k] = fieldMap[k];
2905
+ });
2906
+
2907
+ var report = {
2908
+ // ---- RFC 5965 base fields ----
2909
+ feedbackType: _field("feedback-type"),
2910
+ userAgent: _field("user-agent"),
2911
+ version: _field("version") || "1",
2912
+ arrivalDate: arrivalIso || arrivalRaw,
2913
+ reportedDomain: _field("reported-domain"),
2914
+ sourceIp: _field("source-ip"),
2915
+ originalFrom: _field("original-mail-from"),
2916
+ originalRcptTo: rcptToList,
2917
+ originalEnvelopeId: _field("original-envelope-id"),
2918
+ authenticationResults: _field("authentication-results"),
2919
+ incidents: incidents,
2920
+ reportedUri: _field("reported-uri"),
2921
+
2922
+ // ---- RFC 6591 §3.1 / RFC 7489 §7.3 forensic-specific fields ----
2923
+ authFailure: _field("auth-failure"), // RFC 6591 §3.1 — "dkim" | "spf" | "dmarc" | "bodyhash" | …
2924
+ deliveryResult: _field("delivery-result"), // RFC 6591 §3.1 — "delivered" | "spam" | "policy" | "reject" | "other"
2925
+ identityAlignment: _field("identity-alignment"), // RFC 7489 §7.3 — "none" | "spf" | "dkim" | "dkim spf"
2926
+ dkim: {
2927
+ domain: _field("dkim-domain"), // RFC 6591 §3.1
2928
+ identity: _field("dkim-identity"),
2929
+ selector: _field("dkim-selector"),
2930
+ canonicalizedHeader: _field("dkim-canonicalized-header"),
2931
+ canonicalizedBody: _field("dkim-canonicalized-body"),
2932
+ },
2933
+ spf: {
2934
+ dns: _field("spf-dns"), // RFC 6591 §3.1 — the SPF DNS record at evaluation time
2935
+ },
2936
+
2937
+ // ---- RFC 6591 §3.2 reported message ----
2938
+ reportedMessage: reportedMessage,
2939
+ reportedHeaders: reported.headers, // [ { name, value }, … ] — order preserved
2940
+ reportedHeaderMap: reported.map, // null-prototype lower-cased-name → value
2941
+ reportedHeadersTruncated: reported.truncated, // true when the §3.2 header cap clipped the list
2942
+
2943
+ // ---- operator-visible passthrough for unmodeled fields ----
2944
+ extraFields: extraFields,
2945
+ };
2946
+
2947
+ return { ok: true, report: report };
2948
+ }
2949
+
2618
2950
  module.exports = {
2619
2951
  spf: Object.freeze({
2620
2952
  verify: spfVerify,
@@ -2625,6 +2957,7 @@ module.exports = {
2625
2957
  parseRecord: _parseDmarcRecord,
2626
2958
  parseAggregateReport: dmarcParseAggregateReport,
2627
2959
  buildAggregateReport: dmarcBuildAggregateReport,
2960
+ parseForensicReport: dmarcParseForensicReport,
2628
2961
  }),
2629
2962
  arc: Object.freeze({
2630
2963
  verify: arcVerify,
@@ -919,7 +919,7 @@ function tlsRptIngestHttp(opts) {
919
919
  opts = opts || {};
920
920
  validateOpts(opts, ["authenticate", "trustedReporters", "maxCompressedBytes",
921
921
  "maxDecompressedBytes", "maxRatio", "onAccept", "onRefuse",
922
- "audit", "compliance"],
922
+ "audit"],
923
923
  "mail.deploy.tlsRptIngestHttp");
924
924
  validateOpts.optionalFunction(opts.authenticate, "tlsRptIngestHttp: opts.authenticate",
925
925
  MailDeployError, "mail-tlsrpt/bad-opts");
@@ -468,16 +468,25 @@ function create(opts) {
468
468
 
469
469
  var retryOpts = opts.retry || {};
470
470
  validateOpts(retryOpts, ["maxAttempts", "backoffMs"], "mail.send.deliver.create.retry");
471
- var maxAttempts = typeof retryOpts.maxAttempts === "number" && retryOpts.maxAttempts > 0
472
- ? Math.floor(retryOpts.maxAttempts) : DEFAULT_RETRY_BACKOFF_MS.length;
471
+ // Config-time entry-point opts: a typo (maxAttempts:"5", mxLookupMs:-1)
472
+ // must fail at create(), not silently fall back to the default. Absent
473
+ // keeps the default; present-but-bad throws. Matches opts.port above.
474
+ validateOpts.optionalPositiveInt(retryOpts.maxAttempts,
475
+ "mail.send.deliver.create.retry.maxAttempts", DeliverError, "deliver/bad-retry-maxAttempts");
476
+ var maxAttempts = retryOpts.maxAttempts !== undefined
477
+ ? retryOpts.maxAttempts : DEFAULT_RETRY_BACKOFF_MS.length;
473
478
  var backoffMs = Array.isArray(retryOpts.backoffMs) && retryOpts.backoffMs.length > 0
474
479
  ? retryOpts.backoffMs.slice() : DEFAULT_RETRY_BACKOFF_MS.slice();
475
480
 
476
481
  var timeouts = opts.timeouts || {};
477
482
  validateOpts(timeouts, ["mxLookupMs", "perHostMs"], "mail.send.deliver.create.timeouts");
478
- var mxLookupTimeoutMs = typeof timeouts.mxLookupMs === "number" && timeouts.mxLookupMs > 0
483
+ validateOpts.optionalPositiveInt(timeouts.mxLookupMs,
484
+ "mail.send.deliver.create.timeouts.mxLookupMs", DeliverError, "deliver/bad-timeout-mxLookupMs");
485
+ validateOpts.optionalPositiveInt(timeouts.perHostMs,
486
+ "mail.send.deliver.create.timeouts.perHostMs", DeliverError, "deliver/bad-timeout-perHostMs");
487
+ var mxLookupTimeoutMs = timeouts.mxLookupMs !== undefined
479
488
  ? timeouts.mxLookupMs : DEFAULT_MX_LOOKUP_TIMEOUT_MS;
480
- var perHostTimeoutMs = typeof timeouts.perHostMs === "number" && timeouts.perHostMs > 0
489
+ var perHostTimeoutMs = timeouts.perHostMs !== undefined
481
490
  ? timeouts.perHostMs : DEFAULT_PER_HOST_TIMEOUT_MS;
482
491
 
483
492
  var dsnOpts = opts.dsn || null;