@blamejs/blamejs-shop 0.5.19 → 0.5.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 (194) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +1 -0
  3. package/SECURITY.md +17 -0
  4. package/lib/addresses.js +2 -2
  5. package/lib/admin.js +49 -45
  6. package/lib/affiliates.js +7 -23
  7. package/lib/announcement-bar.js +5 -22
  8. package/lib/api-keys.js +4 -13
  9. package/lib/asset-manifest.json +1 -1
  10. package/lib/banner-ab-tests.js +5 -9
  11. package/lib/blog-articles.js +8 -26
  12. package/lib/captcha-gate.js +7 -17
  13. package/lib/carrier-accounts.js +3 -10
  14. package/lib/catalog-drafts.js +5 -12
  15. package/lib/category-navigation.js +4 -20
  16. package/lib/cms-blocks.js +4 -7
  17. package/lib/compliance-export.js +4 -11
  18. package/lib/consent-ledger.js +2 -4
  19. package/lib/cost-layers.js +56 -21
  20. package/lib/customer-import.js +2 -2
  21. package/lib/customer-notes.js +7 -23
  22. package/lib/customer-roles.js +3 -5
  23. package/lib/customer-segments.js +17 -14
  24. package/lib/customer-surveys.js +7 -14
  25. package/lib/customers.js +2 -4
  26. package/lib/dispute-resolution.js +5 -13
  27. package/lib/dropship-forwarding.js +3 -5
  28. package/lib/dunning.js +2 -7
  29. package/lib/email-ab-tests.js +4 -8
  30. package/lib/email-campaigns.js +25 -14
  31. package/lib/email-warmup.js +2 -7
  32. package/lib/experiments.js +6 -10
  33. package/lib/externaldb-d1.js +78 -44
  34. package/lib/geolocation.js +18 -28
  35. package/lib/gift-options.js +5 -23
  36. package/lib/knowledge-base.js +7 -22
  37. package/lib/line-gift-wrap.js +3 -17
  38. package/lib/live-chat.js +5 -18
  39. package/lib/metered-usage.js +3 -8
  40. package/lib/notifications.js +3 -7
  41. package/lib/operator-accounts.js +3 -5
  42. package/lib/operator-approvals.js +6 -8
  43. package/lib/operator-audit-log.js +2 -7
  44. package/lib/operator-help-center.js +5 -21
  45. package/lib/operator-inbox.js +4 -11
  46. package/lib/operator-roles.js +5 -7
  47. package/lib/order-escalation.js +2 -2
  48. package/lib/order-export.js +24 -20
  49. package/lib/order-notes.js +4 -14
  50. package/lib/order-ratings.js +4 -10
  51. package/lib/payment-methods.js +5 -13
  52. package/lib/payment-retries.js +2 -7
  53. package/lib/payment.js +103 -58
  54. package/lib/pixel-events.js +8 -23
  55. package/lib/plan-changes.js +2 -7
  56. package/lib/product-qa.js +4 -8
  57. package/lib/promo-banners.js +7 -15
  58. package/lib/purchase-orders.js +3 -6
  59. package/lib/push-notifications.js +7 -32
  60. package/lib/pwa-manifest.js +6 -11
  61. package/lib/quotes.js +5 -9
  62. package/lib/referrals.js +10 -6
  63. package/lib/reviews.js +4 -10
  64. package/lib/robots-config.js +6 -14
  65. package/lib/sales-tax-filings.js +2 -2
  66. package/lib/search-facets.js +5 -5
  67. package/lib/search-ranking.js +4 -7
  68. package/lib/search-synonyms.js +7 -21
  69. package/lib/security-middleware.js +125 -70
  70. package/lib/seller-signup.js +10 -26
  71. package/lib/sidebar-widgets.js +6 -15
  72. package/lib/site-redirects.js +49 -137
  73. package/lib/sitemap-generator.js +3 -7
  74. package/lib/sms-dispatcher.js +7 -23
  75. package/lib/stock-receipts.js +4 -10
  76. package/lib/storefront-dashboards.js +2 -10
  77. package/lib/storefront-forms.js +9 -22
  78. package/lib/storefront-pages.js +6 -20
  79. package/lib/storefront.js +21 -23
  80. package/lib/subscription-billing.js +3 -8
  81. package/lib/subscription-controls.js +2 -7
  82. package/lib/suggestion-box.js +9 -26
  83. package/lib/support-tickets.js +6 -24
  84. package/lib/tax-remittance.js +2 -2
  85. package/lib/tenants.js +2 -7
  86. package/lib/text-guard.js +137 -35
  87. package/lib/theme-assets.js +4 -16
  88. package/lib/translations.js +3 -6
  89. package/lib/trust-badges.js +4 -7
  90. package/lib/vendor/MANIFEST.json +107 -97
  91. package/lib/vendor/blamejs/CHANGELOG.md +180 -0
  92. package/lib/vendor/blamejs/NOTICE +14 -10
  93. package/lib/vendor/blamejs/README.md +17 -5
  94. package/lib/vendor/blamejs/index.js +2 -0
  95. package/lib/vendor/blamejs/lib/acme.js +9 -9
  96. package/lib/vendor/blamejs/lib/api-snapshot.js +35 -16
  97. package/lib/vendor/blamejs/lib/app-shutdown.js +1 -1
  98. package/lib/vendor/blamejs/lib/archive-adapters.js +1 -1
  99. package/lib/vendor/blamejs/lib/archive.js +13 -24
  100. package/lib/vendor/blamejs/lib/auth/access-lock.js +2 -1
  101. package/lib/vendor/blamejs/lib/auth/bot-challenge.js +3 -6
  102. package/lib/vendor/blamejs/lib/auth/ciba.js +32 -3
  103. package/lib/vendor/blamejs/lib/auth/oauth.js +65 -8
  104. package/lib/vendor/blamejs/lib/auth/password.js +1 -1
  105. package/lib/vendor/blamejs/lib/auth/saml.js +21 -3
  106. package/lib/vendor/blamejs/lib/auth-bot-challenge.js +3 -3
  107. package/lib/vendor/blamejs/lib/backup/index.js +5 -5
  108. package/lib/vendor/blamejs/lib/case-fold-classes.js +491 -0
  109. package/lib/vendor/blamejs/lib/codepoint-class.js +131 -0
  110. package/lib/vendor/blamejs/lib/constants.js +79 -0
  111. package/lib/vendor/blamejs/lib/db-declare-view.js +8 -2
  112. package/lib/vendor/blamejs/lib/error-page.js +20 -1
  113. package/lib/vendor/blamejs/lib/external-db.js +5 -1
  114. package/lib/vendor/blamejs/lib/flag-evaluation-context.js +24 -6
  115. package/lib/vendor/blamejs/lib/flag.js +28 -2
  116. package/lib/vendor/blamejs/lib/framework-schema.js +14 -3
  117. package/lib/vendor/blamejs/lib/guard-regex.js +2345 -89
  118. package/lib/vendor/blamejs/lib/http-client.js +217 -40
  119. package/lib/vendor/blamejs/lib/mail-bimi.js +1 -1
  120. package/lib/vendor/blamejs/lib/mail-crypto-pgp.js +3 -2
  121. package/lib/vendor/blamejs/lib/mail-dav.js +26 -26
  122. package/lib/vendor/blamejs/lib/mail-deploy.js +10 -10
  123. package/lib/vendor/blamejs/lib/mail-scan.js +3 -0
  124. package/lib/vendor/blamejs/lib/mail-store.js +7 -1
  125. package/lib/vendor/blamejs/lib/mail.js +5 -1
  126. package/lib/vendor/blamejs/lib/middleware/age-gate.js +3 -2
  127. package/lib/vendor/blamejs/lib/middleware/api-encrypt.js +1 -1
  128. package/lib/vendor/blamejs/lib/middleware/assetlinks.js +2 -1
  129. package/lib/vendor/blamejs/lib/middleware/asyncapi-serve.js +3 -2
  130. package/lib/vendor/blamejs/lib/middleware/bearer-auth.js +2 -1
  131. package/lib/vendor/blamejs/lib/middleware/compression.js +1 -1
  132. package/lib/vendor/blamejs/lib/middleware/cors.js +1 -1
  133. package/lib/vendor/blamejs/lib/middleware/csp-report.js +5 -5
  134. package/lib/vendor/blamejs/lib/middleware/csrf-protect.js +2 -1
  135. package/lib/vendor/blamejs/lib/middleware/daily-byte-quota.js +2 -2
  136. package/lib/vendor/blamejs/lib/middleware/deny-response.js +2 -1
  137. package/lib/vendor/blamejs/lib/middleware/dpop.js +2 -2
  138. package/lib/vendor/blamejs/lib/middleware/fetch-metadata.js +2 -1
  139. package/lib/vendor/blamejs/lib/middleware/flag-context.js +17 -0
  140. package/lib/vendor/blamejs/lib/middleware/idempotency-key.js +10 -8
  141. package/lib/vendor/blamejs/lib/middleware/openapi-serve.js +3 -2
  142. package/lib/vendor/blamejs/lib/middleware/require-aal.js +3 -2
  143. package/lib/vendor/blamejs/lib/middleware/require-auth.js +4 -3
  144. package/lib/vendor/blamejs/lib/middleware/require-bound-key.js +4 -3
  145. package/lib/vendor/blamejs/lib/middleware/require-content-type.js +3 -2
  146. package/lib/vendor/blamejs/lib/middleware/require-methods.js +3 -2
  147. package/lib/vendor/blamejs/lib/middleware/require-mtls.js +3 -2
  148. package/lib/vendor/blamejs/lib/middleware/security-txt.js +2 -1
  149. package/lib/vendor/blamejs/lib/middleware/web-app-manifest.js +2 -1
  150. package/lib/vendor/blamejs/lib/network-dns-resolver.js +1 -1
  151. package/lib/vendor/blamejs/lib/network-dns.js +3 -3
  152. package/lib/vendor/blamejs/lib/network-nts.js +35 -14
  153. package/lib/vendor/blamejs/lib/network-proxy.js +42 -4
  154. package/lib/vendor/blamejs/lib/network-smtp-policy.js +1 -1
  155. package/lib/vendor/blamejs/lib/network-tls.js +341 -11
  156. package/lib/vendor/blamejs/lib/nist-crosswalk.js +2 -2
  157. package/lib/vendor/blamejs/lib/ntp-check.js +30 -19
  158. package/lib/vendor/blamejs/lib/object-store/azure-blob.js +1 -1
  159. package/lib/vendor/blamejs/lib/object-store/gcs.js +1 -1
  160. package/lib/vendor/blamejs/lib/object-store/http-put.js +2 -1
  161. package/lib/vendor/blamejs/lib/object-store/sigv4-bucket-ops.js +5 -5
  162. package/lib/vendor/blamejs/lib/object-store/sigv4.js +2 -2
  163. package/lib/vendor/blamejs/lib/observability-otlp-exporter.js +4 -2
  164. package/lib/vendor/blamejs/lib/openapi.js +1 -1
  165. package/lib/vendor/blamejs/lib/pqc-agent.js +4 -7
  166. package/lib/vendor/blamejs/lib/problem-details.js +2 -1
  167. package/lib/vendor/blamejs/lib/redis-client.js +12 -1
  168. package/lib/vendor/blamejs/lib/regex-linear.js +1349 -0
  169. package/lib/vendor/blamejs/lib/render.js +531 -7
  170. package/lib/vendor/blamejs/lib/request-helpers.js +181 -5
  171. package/lib/vendor/blamejs/lib/router.js +37 -13
  172. package/lib/vendor/blamejs/lib/safe-async.js +142 -0
  173. package/lib/vendor/blamejs/lib/safe-icap.js +3 -0
  174. package/lib/vendor/blamejs/lib/safe-sql.js +15 -3
  175. package/lib/vendor/blamejs/lib/self-update.js +2 -2
  176. package/lib/vendor/blamejs/lib/testing.js +54 -7
  177. package/lib/vendor/blamejs/lib/validate-opts.js +56 -0
  178. package/lib/vendor/blamejs/lib/vendor/MANIFEST.json +62 -25
  179. package/lib/vendor/blamejs/lib/vendor/blamejs-pki.cjs +2004 -142
  180. package/lib/vendor/blamejs/lib/vendor/browser/noble-ciphers.mjs +727 -0
  181. package/lib/vendor/blamejs/lib/vendor/browser/noble-hashes.mjs +1070 -0
  182. package/lib/vendor/blamejs/lib/vendor/browser/noble-post-quantum.mjs +1090 -0
  183. package/lib/vendor/blamejs/lib/vendor/noble-post-quantum.cjs +510 -227
  184. package/lib/vendor/blamejs/lib/webhook-hmac.js +19 -2
  185. package/lib/vendor/blamejs/lib/webhook.js +1 -1
  186. package/lib/vendor/blamejs/lib/ws-client.js +24 -2
  187. package/lib/vendor/blamejs/package.json +1 -1
  188. package/lib/vendor/blamejs/sbom.cdx.json +6 -6
  189. package/lib/vendor-invoices.js +3 -6
  190. package/lib/vendors.js +5 -19
  191. package/lib/webhook-receiver.js +46 -83
  192. package/lib/webhook-subscriptions.js +3 -13
  193. package/lib/wishlist-sharing.js +5 -10
  194. package/package.json +1 -1
@@ -55,6 +55,7 @@ var safeAsync = require("./safe-async");
55
55
  var safeBuffer = require("./safe-buffer");
56
56
  var safeUrl = require("./safe-url");
57
57
  var ssrfGuard = require("./ssrf-guard");
58
+ var net = require("node:net");
58
59
  var networkProxy = require("./network-proxy");
59
60
  var numericBounds = require("./numeric-bounds");
60
61
  var validateOpts = require("./validate-opts");
@@ -319,6 +320,14 @@ function _connectHttpsWithAlpn(u, ips) {
319
320
  _done(_makeH1Transport(u, ips));
320
321
  return;
321
322
  }
323
+ // A refused handshake arrives as a bare OpenSSL alert naming neither the
324
+ // peer nor the posture clause that caused it. Diagnose against the
325
+ // options this session actually used, not the shared posture.
326
+ networkTls().annotateOutboundFailure(err, {
327
+ host: u.hostname,
328
+ port: Number(u.port) || 443,
329
+ tlsOpts: connectOpts,
330
+ });
322
331
  _fail(err);
323
332
  });
324
333
  });
@@ -514,17 +523,18 @@ function _makeError(errorClass, code, message, permanent, statusCode) {
514
523
  return new Cls(code, message, permanent, statusCode);
515
524
  }
516
525
 
517
- // RFC 9110 §15.5 4xx codes that are NOT permanent (request-timeout,
518
- // too-early, too-many-requestsoperator should retry).
519
- var STATUS_REQUEST_TIMEOUT = C.BYTES.bytes(408);
520
- var STATUS_TOO_EARLY = 425;
521
- var STATUS_TOO_MANY_REQUESTS = 429;
522
-
526
+ // The three 4xx codes that are NOT permanent — the request timed out, arrived
527
+ // too early, or arrived too often so the caller should retry rather than give
528
+ // up. RFC 9110 §15.5.
529
+ //
530
+ // These were local copies, and one of them was laundered through `C.BYTES.bytes`
531
+ // to get a status past a detector that only knew about byte counts. Naming them
532
+ // where they belong removes both the copies and the reason for the disguise.
523
533
  function _isPermanentStatus(statusCode) {
524
- if (statusCode >= 400 && statusCode < 500) {
525
- return statusCode !== STATUS_REQUEST_TIMEOUT &&
526
- statusCode !== STATUS_TOO_EARLY &&
527
- statusCode !== STATUS_TOO_MANY_REQUESTS;
534
+ if (C.HTTP.clientError(statusCode)) {
535
+ return statusCode !== C.HTTP.STATUS.REQUEST_TIMEOUT &&
536
+ statusCode !== C.HTTP.STATUS.TOO_EARLY &&
537
+ statusCode !== C.HTTP.STATUS.TOO_MANY_REQUESTS;
528
538
  }
529
539
  return false;
530
540
  }
@@ -599,6 +609,187 @@ function _fromH2Headers(h2Headers) {
599
609
  return out;
600
610
  }
601
611
 
612
+ // hostAllowed — the allowedHosts membership test, extracted so anything
613
+ // enforcing the same pin agrees with `request()` on what an entry means
614
+ // rather than re-deriving suffix and method semantics.
615
+ //
616
+ // Entry forms (each entry is a string OR an object):
617
+ // "api.partner.com" exact host match
618
+ // ".partner.com" suffix match: "api.partner.com" yes, "evilpartner.com" no
619
+ // "*.partner.com" same as ".partner.com" (the DNS-glob shape operators
620
+ // expect from firewall configs)
621
+ // { host: "api.x.com", methods: ["GET","HEAD"] }
622
+ // method-restricted; methods omitted = any method
623
+ function hostAllowed(host, allowedHosts, method) {
624
+ if (!Array.isArray(allowedHosts) || allowedHosts.length === 0) return true;
625
+ var wanted = String(host || "").toLowerCase();
626
+ var verb = String(method || "GET").toUpperCase();
627
+ for (var ai = 0; ai < allowedHosts.length; ai++) {
628
+ var entry = allowedHosts[ai];
629
+ var allow, allowedMethods = null;
630
+ if (typeof entry === "object" && entry !== null) {
631
+ allow = String(entry.host || "").toLowerCase();
632
+ if (Array.isArray(entry.methods) && entry.methods.length > 0) {
633
+ allowedMethods = entry.methods.map(function (m) { return String(m).toUpperCase(); });
634
+ }
635
+ } else {
636
+ allow = String(entry || "").toLowerCase();
637
+ }
638
+ if (allow.length === 0) continue;
639
+ // Normalise "*.x.com" to ".x.com" for the suffix match path.
640
+ if (allow.charAt(0) === "*" && allow.charAt(1) === ".") allow = allow.slice(1);
641
+ var matched = false;
642
+ if (allow.charAt(0) === ".") {
643
+ if (wanted === allow.slice(1) || wanted.endsWith(allow)) matched = true;
644
+ } else if (wanted === allow) {
645
+ matched = true;
646
+ }
647
+ if (!matched) continue;
648
+ if (allowedMethods !== null && allowedMethods.indexOf(verb) === -1) continue;
649
+ return true;
650
+ }
651
+ return false;
652
+ }
653
+
654
+ // Does the caller's `allowInternal` waive this destination? The option is
655
+ // documented and implemented as `true | CIDR[]`, so reading only the boolean
656
+ // would silently drop the array form the moment a caller supplies their own
657
+ // client — a documented waiver that stops working is as much a defect as a
658
+ // gate that stops gating. Matched with the same cidrContains ssrfGuard uses,
659
+ // so an entry means the same thing on either path. A name has no address to
660
+ // match a CIDR against, so only the blanket form waives one.
661
+ function _internalWaived(host, isLiteral, allowInternal) {
662
+ if (allowInternal === true) return true;
663
+ if (!isLiteral || !Array.isArray(allowInternal)) return false;
664
+ for (var i = 0; i < allowInternal.length; i += 1) {
665
+ try { if (ssrfGuard.cidrContains(allowInternal[i], host)) return true; }
666
+ catch (_e) { /* a malformed entry waives nothing */ }
667
+ }
668
+ return false;
669
+ }
670
+
671
+ /**
672
+ * @primitive b.httpClient.pinnedClient
673
+ * @signature b.httpClient.pinnedClient(client, allowedHosts)
674
+ * @since 0.18.18
675
+ * @status stable
676
+ * @related b.httpClient.request, b.ssrfGuard.checkUrl
677
+ *
678
+ * Wrap an HTTP client so an <code>allowedHosts</code> pin is ENFORCED rather
679
+ * than merely advertised, and return the wrapper.
680
+ *
681
+ * The contract for a caller-supplied client is a <code>request</code> method
682
+ * and nothing more, so it need not know what <code>allowedHosts</code> on a
683
+ * request object means. Passing the pin through as a request field therefore
684
+ * leaves it advisory: a client that ignores the field fetches a disallowed
685
+ * host while the operator believes the pin is in force. This wrapper checks
686
+ * the destination itself — using the same membership test
687
+ * <code>request()</code> applies, so an entry means the same thing either way
688
+ * — and refuses before delegating. A destination that cannot be parsed is
689
+ * refused rather than passed through unchecked.
690
+ *
691
+ * Redirects are the pin's blind spot: an allowed host can answer with a
692
+ * redirect to a disallowed or private one, and a client that follows it
693
+ * internally would fetch that without ever returning here. The framework's own
694
+ * client re-checks every hop. The wrapper therefore asks for redirect
695
+ * following to be OFF on each request it forwards — <code>maxRedirects: 0</code>,
696
+ * <code>followRedirects: false</code> and <code>redirect: "manual"</code>,
697
+ * covering the spellings in common use — and a redirect then comes back as an
698
+ * ordinary 3xx response for the caller to act on. A client that follows
699
+ * redirects regardless of all three is outside what this can enforce: it must
700
+ * apply the pin per hop itself, exactly as it must apply anything else its own
701
+ * transport decides.
702
+ *
703
+ * The pin is optional; the redirect control is not. Every caller that hands a
704
+ * client here validated ONE url — its scheme, its host, or both — so an absent
705
+ * pin still returns a wrapper, or a 307 from https to http would be free to
706
+ * resend the credentials that were checked onto the first hop.
707
+ *
708
+ * The framework primitives that accept an injected client
709
+ * (<code>b.auth.oauth.create</code>, <code>b.auth.ciba.client.create</code>,
710
+ * <code>b.auth.saml.fetchMdq</code>) route it through here.
711
+ *
712
+ * @example
713
+ * var pinned = b.httpClient.pinnedClient(myClient, ["api.partner.com"]);
714
+ * pinned.request({ url: "https://elsewhere.example/x" });
715
+ * // → rejects HOST_DISALLOWED; myClient is never called
716
+ */
717
+ function pinnedClient(client, allowedHosts) {
718
+ // The pin is optional; the redirect control is not. Every caller that hands
719
+ // this a client validated ONE url — the scheme, the host, or both — and a
720
+ // redirect the wrapper never sees is a hop none of that applied to. Returning
721
+ // the client untouched when no pin was named would leave a 307 from https to
722
+ // http free to resend the credentials that were checked onto the first hop.
723
+ var pin = (Array.isArray(allowedHosts) && allowedHosts.length > 0) ? allowedHosts : null;
724
+ return {
725
+ request: function (opts) {
726
+ var target = opts && opts.url;
727
+ // Through safeUrl so the destination is read the way the rest of the
728
+ // framework reads one — obfuscated host spellings collapse, credentials
729
+ // in the URL are refused, and a non-http scheme is refused outright.
730
+ // Both http schemes are permitted here because this is not the scheme
731
+ // policy: each caller has its own (b.auth.oauth's allowHttp, and so on)
732
+ // and applies it before handing the request over.
733
+ var parsed = null;
734
+ try { parsed = safeUrl.parse(String(target), { allowedProtocols: safeUrl.ALLOW_HTTP_ALL }); }
735
+ catch (_e) { parsed = null; }
736
+ if (parsed === null) {
737
+ return Promise.reject(_makeError(opts && opts.errorClass, "HOST_DISALLOWED",
738
+ "destination '" + String(target) + "' is not a usable http(s) URL", true));
739
+ }
740
+ if (pin !== null && !hostAllowed(parsed.hostname, pin, opts && opts.method)) {
741
+ return Promise.reject(_makeError(opts && opts.errorClass, "HOST_DISALLOWED",
742
+ "host '" + parsed.hostname + "' not in allowedHosts (method=" +
743
+ String((opts && opts.method) || "GET").toUpperCase() + ")", true));
744
+ }
745
+ // Redirect following is turned off on the way through, pin or no pin: a
746
+ // hop this wrapper never sees is a hop nothing checked. Set on a copy so
747
+ // a caller's own request object is not mutated.
748
+ var forwarded = Object.assign({}, opts, {
749
+ maxRedirects: 0,
750
+ followRedirects: false,
751
+ redirect: "manual",
752
+ });
753
+ // The SSRF gate is the last guarantee that came free while b.httpClient
754
+ // was the only dialer, and a supplied client makes no such promise — a
755
+ // mangled or attacker-influenced endpoint would otherwise reach the
756
+ // metadata service straight through it.
757
+ //
758
+ // What is enforceable for a transport this does not own is the TEXTUAL
759
+ // half: a cloud-metadata address is refused unconditionally, and an
760
+ // IP-literal destination in a private, loopback, link-local or reserved
761
+ // range is refused unless the caller waived it with `allowInternal`. The
762
+ // resolve-and-pin half is deliberately not attempted: a name checked
763
+ // here would be resolved AGAIN by the client, so the rebinding window
764
+ // belongs to whichever transport actually connects and only that
765
+ // transport can close it. This is the same reasoning b.httpClient
766
+ // applies when a proxy resolves on its behalf.
767
+ try {
768
+ ssrfGuard.checkUrlTextual(parsed, { errorClass: opts && opts.errorClass });
769
+ // canonicalizeHost strips the IPv6 brackets and folds an IPv4-mapped
770
+ // address to its dotted form, so a non-canonical spelling classifies
771
+ // the same as the plain one.
772
+ var literal = ssrfGuard.canonicalizeHost(String(parsed.hostname));
773
+ var isLiteral = net.isIP(literal) !== 0;
774
+ // A loopback NAME is a loopback destination. Checking only IP literals
775
+ // let `localhost` — and anything under it, since the whole TLD resolves
776
+ // there — past a gate that refuses 127.0.0.1, which is the same
777
+ // destination spelled differently.
778
+ var internal = isLiteral
779
+ ? (ssrfGuard.isPrivate(literal) || ssrfGuard.isLoopback(literal) ||
780
+ ssrfGuard.isLinkLocal(literal) || ssrfGuard.isReserved(literal))
781
+ : ssrfGuard.isLoopbackHost(literal);
782
+ if (internal && !_internalWaived(literal, isLiteral, opts && opts.allowInternal)) {
783
+ return Promise.reject(_makeError(opts && opts.errorClass, "HOST_DISALLOWED",
784
+ "destination '" + literal + "' is an internal address — pass " +
785
+ "allowInternal to waive this deliberately", true));
786
+ }
787
+ } catch (e) { return Promise.reject(e); }
788
+ return client.request(forwarded);
789
+ },
790
+ };
791
+ }
792
+
602
793
  // ---- request() ----
603
794
 
604
795
  var REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
@@ -1376,7 +1567,7 @@ function _revalidate(cache, method, opts, entry, requestHeaders) {
1376
1567
 
1377
1568
  return p.then(function (boxed) {
1378
1569
  var res = boxed.res;
1379
- if (res.statusCode === 304) { // HTTP 304 Not Modified status code, not bytes
1570
+ if (res.statusCode === C.HTTP.STATUS.NOT_MODIFIED) { // HTTP 304 Not Modified status code, not bytes
1380
1571
  // Merge 304 headers into the stored entry.
1381
1572
  var refreshed;
1382
1573
  try { refreshed = cache._refreshFrom304(entry, res.headers); }
@@ -1464,8 +1655,8 @@ function _requestWithRedirects(opts, hopsLeft) {
1464
1655
  // → preserve method + body.
1465
1656
  var nextMethod = current.method || "GET";
1466
1657
  var nextBody = current.body;
1467
- if (res.statusCode === 303 ||
1468
- ((res.statusCode === 301 || res.statusCode === 302) &&
1658
+ if (res.statusCode === C.HTTP.STATUS.SEE_OTHER ||
1659
+ ((res.statusCode === C.HTTP.STATUS.MOVED_PERMANENTLY || res.statusCode === C.HTTP.STATUS.FOUND) &&
1469
1660
  nextMethod !== "GET" && nextMethod !== "HEAD")) {
1470
1661
  nextMethod = "GET";
1471
1662
  nextBody = undefined;
@@ -1559,32 +1750,7 @@ function _requestSingle(opts) {
1559
1750
  if (Array.isArray(opts.allowedHosts) && opts.allowedHosts.length > 0) {
1560
1751
  var host = u.hostname.toLowerCase();
1561
1752
  var method = (opts.method || "GET").toUpperCase();
1562
- var ok = false;
1563
- for (var ai = 0; ai < opts.allowedHosts.length; ai++) {
1564
- var entry = opts.allowedHosts[ai];
1565
- var allow, allowedMethods = null;
1566
- if (typeof entry === "object" && entry !== null) {
1567
- allow = String(entry.host || "").toLowerCase();
1568
- if (Array.isArray(entry.methods) && entry.methods.length > 0) {
1569
- allowedMethods = entry.methods.map(function (m) { return String(m).toUpperCase(); });
1570
- }
1571
- } else {
1572
- allow = String(entry || "").toLowerCase();
1573
- }
1574
- if (allow.length === 0) continue;
1575
- // Normalise "*.x.com" to ".x.com" for the suffix match path.
1576
- if (allow.charAt(0) === "*" && allow.charAt(1) === ".") allow = allow.slice(1);
1577
- var matched = false;
1578
- if (allow.charAt(0) === ".") {
1579
- if (host === allow.slice(1) || host.endsWith(allow)) matched = true;
1580
- } else if (host === allow) {
1581
- matched = true;
1582
- }
1583
- if (!matched) continue;
1584
- if (allowedMethods !== null && allowedMethods.indexOf(method) === -1) continue;
1585
- ok = true;
1586
- break;
1587
- }
1753
+ var ok = hostAllowed(host, opts.allowedHosts, method);
1588
1754
  if (!ok) {
1589
1755
  if (opts.audit && typeof opts.audit.safeEmit === "function") {
1590
1756
  try {
@@ -1865,6 +2031,16 @@ function _requestH1(transport, u, opts) {
1865
2031
  });
1866
2032
 
1867
2033
  req.on("error", function (e) {
2034
+ // The agent carrying this request owns its TLS settings, and it may be
2035
+ // one the caller supplied via opts.agent rather than the framework's.
2036
+ // Naming them explicitly means an agent that pins neither is diagnosed
2037
+ // as pinning neither, instead of being blamed on the shared posture.
2038
+ var agentTls = (reqOpts.agent && reqOpts.agent.options) || {};
2039
+ networkTls().annotateOutboundFailure(e, {
2040
+ host: reqOpts.hostname,
2041
+ port: Number(reqOpts.port),
2042
+ tlsOpts: agentTls,
2043
+ });
1868
2044
  if (observer) observer("error", { phase: "request", message: e.message });
1869
2045
  _reject(_makeError(opts.errorClass, e.code || "REQ_ERROR", e.message, false));
1870
2046
  });
@@ -2054,7 +2230,7 @@ function _requestH2(transport, u, opts) {
2054
2230
  durationMs: Date.now() - startedAt,
2055
2231
  bytes: buf.length,
2056
2232
  });
2057
- if (statusCode >= 200 && statusCode < 300) {
2233
+ if (C.HTTP.success(statusCode)) {
2058
2234
  _resolve({ statusCode: statusCode, headers: responseHeaders, body: buf });
2059
2235
  } else if (responseMode === "always-resolve") {
2060
2236
  _resolve({ statusCode: statusCode, headers: responseHeaders, body: buf });
@@ -2525,6 +2701,7 @@ function _getCachedTransportKind(url) {
2525
2701
 
2526
2702
  module.exports = {
2527
2703
  request: request,
2704
+ pinnedClient: pinnedClient,
2528
2705
  downloadStream: downloadStream,
2529
2706
  uploadMultipartStream: uploadMultipartStream,
2530
2707
  configurePool: configurePool,
@@ -676,7 +676,7 @@ async function fetchAndVerifyMark(opts) {
676
676
  throw new MailBimiError("bimi/vmc-fetch-failed",
677
677
  "bimi.fetchAndVerifyMark: GET " + url + " failed: " + ((e && e.message) || String(e)));
678
678
  }
679
- if (rsp.statusCode !== 200) {
679
+ if (rsp.statusCode !== C.HTTP.STATUS.OK) {
680
680
  _emitAudit(opts, "mail.bimi.vmc.fetched", "failure",
681
681
  { url: url, domain: opts.domain, status: rsp.statusCode });
682
682
  throw new MailBimiError("bimi/vmc-fetch-failed",
@@ -114,6 +114,7 @@
114
114
  * - CVE-2019-13050 (PGP keyserver flood — not in scope here; out-of-
115
115
  * band fingerprint pinning is the operator's responsibility)
116
116
  */
117
+ var C = require("./constants");
117
118
  var lazyRequire = require("./lazy-require");
118
119
  var safeBuffer = require("./safe-buffer");
119
120
  var audit = lazyRequire(function () { return require("./audit"); });
@@ -1217,7 +1218,7 @@ function wkdFetch(email, opts) {
1217
1218
  var maxBytes = typeof opts.maxKeyBytes === "number" ? opts.maxKeyBytes : (256 * 1024); // allow:raw-byte-literal — 256 KiB default key cap
1218
1219
  var urls = wkdComputeUrl(email, { advancedHost: opts.advancedHost });
1219
1220
  return Promise.resolve(opts.httpsGet(urls.direct)).then(function (resp) {
1220
- if (resp && resp.status === 200 && Buffer.isBuffer(resp.body) && resp.body.length > 0) { // HTTP 200
1221
+ if (resp && resp.status === C.HTTP.STATUS.OK && Buffer.isBuffer(resp.body) && resp.body.length > 0) { // HTTP 200
1221
1222
  if (safeBuffer.byteLengthOf(resp.body) > maxBytes) {
1222
1223
  throw new MailCryptoError("mail-crypto/pgp/wkd-too-large",
1223
1224
  "wkd.fetch: key bytes " + resp.body.length + " exceed maxKeyBytes=" + maxBytes);
@@ -1225,7 +1226,7 @@ function wkdFetch(email, opts) {
1225
1226
  return { keyBytes: resp.body, source: "direct", url: urls.direct };
1226
1227
  }
1227
1228
  return Promise.resolve(opts.httpsGet(urls.advanced)).then(function (resp2) {
1228
- if (resp2 && resp2.status === 200 && Buffer.isBuffer(resp2.body) && resp2.body.length > 0) { // HTTP 200
1229
+ if (resp2 && resp2.status === C.HTTP.STATUS.OK && Buffer.isBuffer(resp2.body) && resp2.body.length > 0) { // HTTP 200
1229
1230
  if (safeBuffer.byteLengthOf(resp2.body) > maxBytes) {
1230
1231
  throw new MailCryptoError("mail-crypto/pgp/wkd-too-large",
1231
1232
  "wkd.fetch: key bytes " + resp2.body.length + " exceed maxKeyBytes=" + maxBytes);
@@ -582,7 +582,7 @@ function create(opts) {
582
582
  responses.push({
583
583
  href: "/caldav/" + principalId + "/",
584
584
  propstat: [{
585
- status: 200,
585
+ status: C.HTTP.STATUS.OK,
586
586
  propXml: _renderProps({
587
587
  displayName: principalId,
588
588
  resourcetype: "<D:collection/>",
@@ -593,7 +593,7 @@ function create(opts) {
593
593
  var cals = await calStorage.listCalendars(principalId);
594
594
  responses.push({
595
595
  href: "/caldav/" + principalId + "/",
596
- propstat: [{ status: 200,
596
+ propstat: [{ status: C.HTTP.STATUS.OK,
597
597
  propXml: _renderProps({ displayName: principalId,
598
598
  resourcetype: "<D:collection/>" }, body.props) }],
599
599
  });
@@ -601,7 +601,7 @@ function create(opts) {
601
601
  var cal = cals[i];
602
602
  responses.push({
603
603
  href: "/caldav/" + principalId + "/" + cal.id + "/",
604
- propstat: [{ status: 200,
604
+ propstat: [{ status: C.HTTP.STATUS.OK,
605
605
  propXml: _renderProps({
606
606
  displayName: cal.displayName || cal.id,
607
607
  resourcetype: "<D:collection/><C:calendar/>",
@@ -615,7 +615,7 @@ function create(opts) {
615
615
  var components = await calStorage.listComponents(principalId, calendarId, null);
616
616
  responses.push({
617
617
  href: "/caldav/" + principalId + "/" + calendarId + "/",
618
- propstat: [{ status: 200,
618
+ propstat: [{ status: C.HTTP.STATUS.OK,
619
619
  propXml: _renderProps({ displayName: calendarId,
620
620
  resourcetype: "<D:collection/><C:calendar/>" }, body.props) }],
621
621
  });
@@ -624,7 +624,7 @@ function create(opts) {
624
624
  var c = components[j];
625
625
  responses.push({
626
626
  href: "/caldav/" + principalId + "/" + calendarId + "/" + c.id,
627
- propstat: [{ status: 200,
627
+ propstat: [{ status: C.HTTP.STATUS.OK,
628
628
  propXml: _renderProps({
629
629
  displayName: c.id,
630
630
  resourcetype: "",
@@ -640,11 +640,11 @@ function create(opts) {
640
640
  // Single component.
641
641
  var comp = await calStorage.getComponent(principalId, calendarId, componentId);
642
642
  if (!comp) {
643
- responses.push({ href: req.url, status: 404 });
643
+ responses.push({ href: req.url, status: C.HTTP.STATUS.NOT_FOUND });
644
644
  } else {
645
645
  responses.push({
646
646
  href: req.url,
647
- propstat: [{ status: 200,
647
+ propstat: [{ status: C.HTTP.STATUS.OK,
648
648
  propXml: _renderProps({
649
649
  displayName: comp.id,
650
650
  etag: comp.etag,
@@ -673,18 +673,18 @@ function create(opts) {
673
673
  var href = report.hrefs[i];
674
674
  var hrefParsed = _parsePath(href);
675
675
  if (hrefParsed.rejected || hrefParsed.principalId !== principalId) {
676
- responses.push({ href: href, status: 403 });
676
+ responses.push({ href: href, status: C.HTTP.STATUS.FORBIDDEN });
677
677
  continue;
678
678
  }
679
679
  var hrefCalId = hrefParsed.parts[0];
680
680
  var hrefCompId = hrefParsed.parts[1];
681
681
  var comp = await calStorage.getComponent(principalId, hrefCalId, hrefCompId);
682
682
  if (!comp) {
683
- responses.push({ href: href, status: 404 });
683
+ responses.push({ href: href, status: C.HTTP.STATUS.NOT_FOUND });
684
684
  } else {
685
685
  responses.push({
686
686
  href: href,
687
- propstat: [{ status: 200,
687
+ propstat: [{ status: C.HTTP.STATUS.OK,
688
688
  propXml: _renderProps({
689
689
  etag: comp.etag,
690
690
  contentType: "text/calendar; charset=utf-8",
@@ -700,7 +700,7 @@ function create(opts) {
700
700
  var r = rows[j];
701
701
  responses.push({
702
702
  href: "/caldav/" + principalId + "/" + calendarId + "/" + r.id,
703
- propstat: [{ status: 200,
703
+ propstat: [{ status: C.HTTP.STATUS.OK,
704
704
  propXml: _renderProps({
705
705
  etag: r.etag,
706
706
  contentType: "text/calendar; charset=utf-8",
@@ -761,7 +761,7 @@ function create(opts) {
761
761
  result = await calStorage.putComponent(principalId, calendarId, componentId,
762
762
  bodyBuf, { ifMatch: ifMatch, ifNoneMatch: ifNoneMatch });
763
763
  } catch (e) {
764
- if (e && (e.code === "etag-mismatch" || e.statusCode === 412)) {
764
+ if (e && (e.code === "etag-mismatch" || e.statusCode === C.HTTP.STATUS.PRECONDITION_FAILED)) {
765
765
  return _refuseStatus(res, 412, "ETag precondition failed");
766
766
  }
767
767
  throw e;
@@ -784,7 +784,7 @@ function create(opts) {
784
784
  { ifMatch: ifMatch });
785
785
  if (!r || r.notFound) return _refuseStatus(res, 404, "Component not found");
786
786
  } catch (e) {
787
- if (e && (e.code === "etag-mismatch" || e.statusCode === 412)) {
787
+ if (e && (e.code === "etag-mismatch" || e.statusCode === C.HTTP.STATUS.PRECONDITION_FAILED)) {
788
788
  return _refuseStatus(res, 412, "ETag precondition failed");
789
789
  }
790
790
  throw e;
@@ -922,7 +922,7 @@ function create(opts) {
922
922
  if (depth === "0") {
923
923
  responses.push({
924
924
  href: "/carddav/" + principalId + "/",
925
- propstat: [{ status: 200,
925
+ propstat: [{ status: C.HTTP.STATUS.OK,
926
926
  propXml: _renderProps({ displayName: principalId,
927
927
  resourcetype: "<D:collection/>" }, body.props) }],
928
928
  });
@@ -930,7 +930,7 @@ function create(opts) {
930
930
  var books = await cardStorage.listAddressbooks(principalId);
931
931
  responses.push({
932
932
  href: "/carddav/" + principalId + "/",
933
- propstat: [{ status: 200,
933
+ propstat: [{ status: C.HTTP.STATUS.OK,
934
934
  propXml: _renderProps({ displayName: principalId,
935
935
  resourcetype: "<D:collection/>" }, body.props) }],
936
936
  });
@@ -938,7 +938,7 @@ function create(opts) {
938
938
  var bk = books[i];
939
939
  responses.push({
940
940
  href: "/carddav/" + principalId + "/" + bk.id + "/",
941
- propstat: [{ status: 200,
941
+ propstat: [{ status: C.HTTP.STATUS.OK,
942
942
  propXml: _renderProps({
943
943
  displayName: bk.displayName || bk.id,
944
944
  resourcetype: "<D:collection/><A:addressbook/>",
@@ -951,7 +951,7 @@ function create(opts) {
951
951
  var cards = await cardStorage.listCards(principalId, addressbookId, null);
952
952
  responses.push({
953
953
  href: "/carddav/" + principalId + "/" + addressbookId + "/",
954
- propstat: [{ status: 200,
954
+ propstat: [{ status: C.HTTP.STATUS.OK,
955
955
  propXml: _renderProps({ displayName: addressbookId,
956
956
  resourcetype: "<D:collection/><A:addressbook/>" }, body.props) }],
957
957
  });
@@ -960,7 +960,7 @@ function create(opts) {
960
960
  var card = cards[j];
961
961
  responses.push({
962
962
  href: "/carddav/" + principalId + "/" + addressbookId + "/" + card.id,
963
- propstat: [{ status: 200,
963
+ propstat: [{ status: C.HTTP.STATUS.OK,
964
964
  propXml: _renderProps({
965
965
  displayName: card.id,
966
966
  etag: card.etag,
@@ -974,11 +974,11 @@ function create(opts) {
974
974
  } else {
975
975
  var single = await cardStorage.getCard(principalId, addressbookId, cardId);
976
976
  if (!single) {
977
- responses.push({ href: req.url, status: 404 });
977
+ responses.push({ href: req.url, status: C.HTTP.STATUS.NOT_FOUND });
978
978
  } else {
979
979
  responses.push({
980
980
  href: req.url,
981
- propstat: [{ status: 200,
981
+ propstat: [{ status: C.HTTP.STATUS.OK,
982
982
  propXml: _renderProps({
983
983
  displayName: single.id,
984
984
  etag: single.etag,
@@ -1008,18 +1008,18 @@ function create(opts) {
1008
1008
  var href = report.hrefs[i];
1009
1009
  var hp = _parsePath(href);
1010
1010
  if (hp.rejected || hp.principalId !== principalId) {
1011
- responses.push({ href: href, status: 403 });
1011
+ responses.push({ href: href, status: C.HTTP.STATUS.FORBIDDEN });
1012
1012
  continue;
1013
1013
  }
1014
1014
  var hpBookId = hp.parts[0];
1015
1015
  var hpCardId = hp.parts[1];
1016
1016
  var card = await cardStorage.getCard(principalId, hpBookId, hpCardId);
1017
1017
  if (!card) {
1018
- responses.push({ href: href, status: 404 });
1018
+ responses.push({ href: href, status: C.HTTP.STATUS.NOT_FOUND });
1019
1019
  } else {
1020
1020
  responses.push({
1021
1021
  href: href,
1022
- propstat: [{ status: 200,
1022
+ propstat: [{ status: C.HTTP.STATUS.OK,
1023
1023
  propXml: _renderProps({
1024
1024
  etag: card.etag,
1025
1025
  contentType: "text/vcard; charset=utf-8",
@@ -1035,7 +1035,7 @@ function create(opts) {
1035
1035
  var r = rows[j];
1036
1036
  responses.push({
1037
1037
  href: "/carddav/" + principalId + "/" + addressbookId + "/" + r.id,
1038
- propstat: [{ status: 200,
1038
+ propstat: [{ status: C.HTTP.STATUS.OK,
1039
1039
  propXml: _renderProps({
1040
1040
  etag: r.etag,
1041
1041
  contentType: "text/vcard; charset=utf-8",
@@ -1090,7 +1090,7 @@ function create(opts) {
1090
1090
  result = await cardStorage.putCard(principalId, addressbookId, cardId,
1091
1091
  bodyBuf, { ifMatch: ifMatch, ifNoneMatch: ifNoneMatch });
1092
1092
  } catch (e) {
1093
- if (e && (e.code === "etag-mismatch" || e.statusCode === 412)) {
1093
+ if (e && (e.code === "etag-mismatch" || e.statusCode === C.HTTP.STATUS.PRECONDITION_FAILED)) {
1094
1094
  return _refuseStatus(res, 412, "ETag precondition failed");
1095
1095
  }
1096
1096
  throw e;
@@ -1112,7 +1112,7 @@ function create(opts) {
1112
1112
  { ifMatch: ifMatch });
1113
1113
  if (!r || r.notFound) return _refuseStatus(res, 404, "Card not found");
1114
1114
  } catch (e) {
1115
- if (e && (e.code === "etag-mismatch" || e.statusCode === 412)) {
1115
+ if (e && (e.code === "etag-mismatch" || e.statusCode === C.HTTP.STATUS.PRECONDITION_FAILED)) {
1116
1116
  return _refuseStatus(res, 412, "ETag precondition failed");
1117
1117
  }
1118
1118
  throw e;
@@ -949,7 +949,7 @@ function tlsRptIngestHttp(opts) {
949
949
 
950
950
  return function tlsRptHandler(req, res) {
951
951
  if (req.method !== "POST") {
952
- res.writeHead(405, { "Allow": "POST", "Content-Type": "text/plain" }); // allow:raw-time-literal — RFC 8460 §5.4 status code
952
+ res.writeHead(C.HTTP.STATUS.METHOD_NOT_ALLOWED, { "Allow": "POST", "Content-Type": "text/plain" }); // allow:raw-time-literal — RFC 8460 §5.4 status code
953
953
  res.end("RFC 8460 §5.4 requires POST\n");
954
954
  return;
955
955
  }
@@ -961,7 +961,7 @@ function tlsRptIngestHttp(opts) {
961
961
  });
962
962
  if (onRefuse) try { onRefuse("mail-tlsrpt/bad-content-type", "unexpected content-type " + ctRoot, req); }
963
963
  catch (_e) { /* drop-silent */ }
964
- res.writeHead(415, { "Content-Type": "text/plain", "Accept": "application/tlsrpt+json, application/tlsrpt+gzip" }); // allow:raw-time-literal — RFC 8460 §5.4 status code
964
+ res.writeHead(C.HTTP.STATUS.UNSUPPORTED_MEDIA_TYPE, { "Content-Type": "text/plain", "Accept": "application/tlsrpt+json, application/tlsrpt+gzip" }); // allow:raw-time-literal — RFC 8460 §5.4 status code
965
965
  res.end("RFC 8460 §6.4-6.5 media types required\n");
966
966
  return;
967
967
  }
@@ -978,7 +978,7 @@ function tlsRptIngestHttp(opts) {
978
978
  _safeAuditEmit(opts.audit, "mail.tlsrpt.ingest_http", "denied", { reason: "unauthenticated" });
979
979
  if (onRefuse) try { onRefuse("mail-tlsrpt/unauthenticated", "authenticate(req) returned falsy", req); }
980
980
  catch (_e) { /* drop-silent */ }
981
- res.writeHead(401, { "Content-Type": "text/plain", "Error-Type": "mail-tlsrpt/unauthenticated" }); // allow:raw-time-literal — RFC 8460 §5.4 status code
981
+ res.writeHead(C.HTTP.STATUS.UNAUTHORIZED, { "Content-Type": "text/plain", "Error-Type": "mail-tlsrpt/unauthenticated" }); // allow:raw-time-literal — RFC 8460 §5.4 status code
982
982
  res.end("authentication required\n");
983
983
  return;
984
984
  }
@@ -989,7 +989,7 @@ function tlsRptIngestHttp(opts) {
989
989
  });
990
990
  if (onRefuse) try { onRefuse("mail-tlsrpt/auth-error", (err && err.message) || String(err), req); }
991
991
  catch (_e) { /* drop-silent */ }
992
- res.writeHead(500, { "Content-Type": "text/plain", "Error-Type": "mail-tlsrpt/auth-error" }); // allow:raw-time-literal — RFC 8460 §5.4 status code
992
+ res.writeHead(C.HTTP.STATUS.INTERNAL_SERVER_ERROR, { "Content-Type": "text/plain", "Error-Type": "mail-tlsrpt/auth-error" }); // allow:raw-time-literal — RFC 8460 §5.4 status code
993
993
  res.end("authenticate hook threw\n");
994
994
  });
995
995
  return;
@@ -1015,7 +1015,7 @@ function tlsRptIngestHttp(opts) {
1015
1015
  if (onRefuse) try { onRefuse("mail-tlsrpt/oversize-compressed", "body exceeded " + maxCompressed + " bytes", req); }
1016
1016
  catch (_e) { /* drop-silent */ }
1017
1017
  if (!res.headersSent) {
1018
- res.writeHead(413, { "Content-Type": "text/plain" }); // allow:raw-time-literal — RFC 8460 §5.4 status code
1018
+ res.writeHead(C.HTTP.STATUS.CONTENT_TOO_LARGE, { "Content-Type": "text/plain" }); // allow:raw-time-literal — RFC 8460 §5.4 status code
1019
1019
  res.end("RFC 8460 §5.4 — body exceeds " + maxCompressed + " bytes\n");
1020
1020
  }
1021
1021
  void e; // _e shadowed by lower scope; mark intent
@@ -1051,7 +1051,7 @@ function tlsRptIngestHttp(opts) {
1051
1051
  if (onRefuse) try { onRefuse("mail-tlsrpt/untrusted-reporter",
1052
1052
  "reporter '" + report["organization-name"] + "' not in trustedReporters", req); }
1053
1053
  catch (_e) { /* drop-silent */ }
1054
- res.writeHead(403, { "Content-Type": "text/plain", "Error-Type": "mail-tlsrpt/untrusted-reporter" }); // allow:raw-time-literal — RFC 8460 §5.4 status code
1054
+ res.writeHead(C.HTTP.STATUS.FORBIDDEN, { "Content-Type": "text/plain", "Error-Type": "mail-tlsrpt/untrusted-reporter" }); // allow:raw-time-literal — RFC 8460 §5.4 status code
1055
1055
  res.end("RFC 8460 §5.3-class: untrusted reporter\n");
1056
1056
  return;
1057
1057
  }
@@ -1072,12 +1072,12 @@ function tlsRptIngestHttp(opts) {
1072
1072
  if (ret && typeof ret.then === "function") {
1073
1073
  ret.then(function () {
1074
1074
  if (!res.headersSent) {
1075
- res.writeHead(201, { "Content-Type": "text/plain" }); // allow:raw-time-literal — RFC 8460 §5.4 status code
1075
+ res.writeHead(C.HTTP.STATUS.CREATED, { "Content-Type": "text/plain" }); // allow:raw-time-literal — RFC 8460 §5.4 status code
1076
1076
  res.end("RFC 8460 §5.4 — accepted\n");
1077
1077
  }
1078
1078
  }, function (_e) {
1079
1079
  if (!res.headersSent) {
1080
- res.writeHead(500, { "Content-Type": "text/plain" }); // internal-error status
1080
+ res.writeHead(C.HTTP.STATUS.INTERNAL_SERVER_ERROR, { "Content-Type": "text/plain" }); // internal-error status
1081
1081
  res.end("internal error processing report\n");
1082
1082
  }
1083
1083
  });
@@ -1085,7 +1085,7 @@ function tlsRptIngestHttp(opts) {
1085
1085
  }
1086
1086
  } catch (_e) { /* fall through to 201 — operator hook is best-effort */ }
1087
1087
  }
1088
- res.writeHead(201, { "Content-Type": "text/plain" }); // allow:raw-time-literal — RFC 8460 §5.4 status code
1088
+ res.writeHead(C.HTTP.STATUS.CREATED, { "Content-Type": "text/plain" }); // allow:raw-time-literal — RFC 8460 §5.4 status code
1089
1089
  res.end("RFC 8460 §5.4 — accepted\n");
1090
1090
  });
1091
1091
  req.on("error", function () {
@@ -1093,7 +1093,7 @@ function tlsRptIngestHttp(opts) {
1093
1093
  aborted = true;
1094
1094
  _safeAuditEmit(opts.audit, "mail.tlsrpt.ingest_http", "denied", { reason: "req-error" });
1095
1095
  if (!res.headersSent) {
1096
- res.writeHead(400, { "Content-Type": "text/plain" }); // allow:raw-time-literal — RFC 8460 §5.4 status code
1096
+ res.writeHead(C.HTTP.STATUS.BAD_REQUEST, { "Content-Type": "text/plain" }); // allow:raw-time-literal — RFC 8460 §5.4 status code
1097
1097
  res.end("malformed request\n");
1098
1098
  }
1099
1099
  });