@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
@@ -442,14 +442,17 @@ function querySingle(opts) {
442
442
  throw new NtsError("nts/no-cookies", "nts.querySingle: cookies array required");
443
443
  }
444
444
  return new Promise(function (resolve, reject) {
445
- var sock = dgram.createSocket("udp4");
446
- var settled = false;
447
- function done(err, result) {
448
- if (settled) return;
449
- settled = true;
450
- try { sock.close(); } catch (_e) { /* best-effort socket close */ }
451
- if (err) reject(err); else resolve(result);
452
- }
445
+ // Build the request BEFORE opening the socket. Encoding it can fail — an
446
+ // aeadId the encoder does not recognise is the reachable case, and
447
+ // refusing it is the point — and a socket opened first has nobody left to
448
+ // close it once that throw unwinds. Holding the acquisition until every
449
+ // step that can throw has succeeded is what makes the leak impossible,
450
+ // rather than something each new failure path has to remember to undo.
451
+ //
452
+ // Inside the executor, not above it: a throw here still reaches the
453
+ // caller as a REJECTION, so `querySingle(opts).catch(...)` keeps seeing
454
+ // the documented refusal. Hoisting this out would leak the throw
455
+ // synchronously past a caller that only attached a .catch.
453
456
  var unique = nodeCrypto.randomBytes(UNIQUE_ID_BYTES);
454
457
  var cookie = opts.cookies[0];
455
458
  var packet = Buffer.alloc(NTP_PACKET_BYTES);
@@ -468,6 +471,15 @@ function querySingle(opts) {
468
471
  encrypted.copy(authBody, 4 + nonceLen);
469
472
  var ext3 = _encodeExtensionField(EXTENSION_NTS_AUTHENTICATOR_AND_ENC, authBody);
470
473
  var fullPacket = Buffer.concat([packet, ext1, ext2, ext3]);
474
+
475
+ var sock = dgram.createSocket("udp4");
476
+ var settled = false;
477
+ function done(err, result) {
478
+ if (settled) return;
479
+ settled = true;
480
+ try { sock.close(); } catch (_e) { /* best-effort socket close */ }
481
+ if (err) reject(err); else resolve(result);
482
+ }
471
483
  var sendTimeMs = Date.now();
472
484
  var timer = setTimeout(function () {
473
485
  done(new NtsError("nts/timeout", "NTS query timed out after " + timeoutMs + "ms"));
@@ -553,12 +565,21 @@ function querySingle(opts) {
553
565
  done(new NtsError("nts/bad-reply", "NTS reply processing failed: " + e.message));
554
566
  }
555
567
  });
556
- sock.send(fullPacket, 0, fullPacket.length, opts.port || NTPV4_DEFAULT_PORT, opts.host, function (err) {
557
- if (err) {
558
- clearTimeout(timer);
559
- done(new NtsError("nts/send", "NTS send failed: " + err.message));
560
- }
561
- });
568
+ // send() rejects a malformed destination synchronously. Routing that
569
+ // throw through done() closes the socket; letting it escape the executor
570
+ // would reject the caller and strand the handle, which is the failure
571
+ // holding the request build above the acquisition.
572
+ try {
573
+ sock.send(fullPacket, 0, fullPacket.length, opts.port || NTPV4_DEFAULT_PORT, opts.host, function (err) {
574
+ if (err) {
575
+ clearTimeout(timer);
576
+ done(new NtsError("nts/send", "NTS send failed: " + err.message));
577
+ }
578
+ });
579
+ } catch (e) {
580
+ clearTimeout(timer);
581
+ done(new NtsError("nts/send", "NTS send failed: " + e.message));
582
+ }
562
583
  });
563
584
  }
564
585
 
@@ -183,12 +183,16 @@ function _proxyAuthHeader(proxyUrl) {
183
183
 
184
184
  function _connectThroughTunnel(proxyUrl, targetHost, targetPort, callback) {
185
185
  var proxyPort = proxyUrl.port || (proxyUrl.protocol === "https:" ? DEFAULT_HTTPS_PORT : DEFAULT_HTTP_PORT);
186
+ // Read ONCE, so the diagnosis below describes the handshake that happened.
187
+ // A preferredGroups.set(...) landing while this handshake is in flight would
188
+ // otherwise have the error handler read a posture this dial never offered.
189
+ var proxyPosture = proxyUrl.protocol === "https:" ? networkTls().outboundPosture() : null;
186
190
  var proxySocket = proxyUrl.protocol === "https:"
187
191
  ? nodeTls.connect(Object.assign({
188
192
  host: proxyUrl.hostname,
189
193
  port: proxyPort,
190
194
  servername: proxyUrl.hostname,
191
- }, networkTls().outboundPosture()))
195
+ }, proxyPosture))
192
196
  : net.connect({ host: proxyUrl.hostname, port: proxyPort });
193
197
  var settled = false;
194
198
  var connectDeadline = null;
@@ -212,7 +216,25 @@ function _connectThroughTunnel(proxyUrl, targetHost, targetPort, callback) {
212
216
  try { proxySocket.destroy(); } catch (_e) { /* best-effort socket teardown */ }
213
217
  }, connectTimeoutMs);
214
218
  if (connectDeadline && typeof connectDeadline.unref === "function") connectDeadline.unref();
215
- proxySocket.on("error", function (e) { done(e); });
219
+ proxySocket.on("error", function (e) {
220
+ // The handshake that failed here is with the PROXY, not the destination:
221
+ // no tunnel exists yet. This error travels out through the agent callback
222
+ // of the request to the destination, so it is claimed here, with the
223
+ // proxy's identity and the options this leg dialled with. Otherwise it
224
+ // would be reported as the destination refusing a connection nothing ever
225
+ // attempted, sending an operator to troubleshoot the wrong peer.
226
+ if (proxyPosture) {
227
+ networkTls().annotateOutboundFailure(e, {
228
+ host: proxyUrl.hostname,
229
+ // `URL.port` is a string, and the explanation appends a port only when
230
+ // it is given a number — so an explicit `:8443` was dropped and the
231
+ // message named a host that may run several TLS listeners.
232
+ port: Number(proxyPort),
233
+ tlsOpts: proxyPosture,
234
+ });
235
+ }
236
+ done(e);
237
+ });
216
238
  proxySocket.on(proxyUrl.protocol === "https:" ? "secureConnect" : "connect", function () {
217
239
  if (proxyUrl.protocol === "https:") {
218
240
  // The CONNECT-tunnel leg to an https proxy is itself a TLS handshake;
@@ -275,11 +297,17 @@ function agentFor(targetUrl) {
275
297
  agent.createConnection = function (options, cb) {
276
298
  _connectThroughTunnel(proxy, options.host, options.port, function (err, tunnel) {
277
299
  if (err) return cb(err);
300
+ // Read ONCE, and diagnose against what was read. The agent above
301
+ // snapshotted a posture when it was built and is then cached, so a
302
+ // later preferredGroups.set(...) leaves agent.options describing
303
+ // groups this dial did not offer — the caller reading them back would
304
+ // be told about a handshake that never happened.
305
+ var livePosture = networkTls().outboundPosture();
278
306
  var secure = nodeTls.connect(Object.assign({
279
307
  socket: tunnel,
280
308
  servername: options.servername || options.host,
281
309
  ALPNProtocols: options.ALPNProtocols,
282
- }, networkTls().outboundPosture()), function () {
310
+ }, livePosture), function () {
283
311
  // Audit a classical-group fallback on the upstream (target) handshake
284
312
  // reached through the proxy tunnel, so the "every outbound TLS path
285
313
  // emits tls.classical_downgrade" guarantee holds for proxied requests
@@ -291,7 +319,17 @@ function agentFor(targetUrl) {
291
319
  });
292
320
  cb(null, secure);
293
321
  });
294
- secure.on("error", function (e) { cb(e); });
322
+ secure.on("error", function (e) {
323
+ // Annotated HERE, with the settings this leg actually used. The
324
+ // annotation is one-shot, so naming the destination leg first stops
325
+ // the outer handler describing it from the stale cached agent.
326
+ networkTls().annotateOutboundFailure(e, {
327
+ host: options.servername || options.host,
328
+ port: Number(options.port),
329
+ tlsOpts: livePosture,
330
+ });
331
+ cb(e);
332
+ });
295
333
  });
296
334
  };
297
335
  } else {
@@ -189,7 +189,7 @@ async function mtaStsFetch(domain, opts) {
189
189
  // this is a deliberate opportunistic fallback, not a silent swallow.
190
190
  return null;
191
191
  }
192
- if (res.statusCode === 404) return null; // HTTP 404
192
+ if (res.statusCode === C.HTTP.STATUS.NOT_FOUND) return null; // HTTP 404
193
193
  if (res.statusCode < 200 || res.statusCode >= 300) { // HTTP 2xx range
194
194
  throw new SmtpPolicyError("smtp/mta-sts-fetch-failed",
195
195
  "MTA-STS fetch returned " + res.statusCode + " for " + url);
@@ -476,6 +476,79 @@ function detectBaselineDrift() {
476
476
  return { added: added, removed: removed, drifted: added.length > 0 || removed.length > 0 };
477
477
  }
478
478
 
479
+ // Normalise an operator-supplied named-group preference to the colon-separated
480
+ // string node:tls reads. Accepts a non-empty string or a non-empty array of
481
+ // non-empty strings; anything else is a config-time mistake and throws, so it
482
+ // surfaces at boot rather than as a handshake that negotiated something other
483
+ // than what was configured.
484
+ function _groupPreferenceString(value, where) {
485
+ if (Array.isArray(value)) {
486
+ if (value.length === 0) {
487
+ throw new TlsTrustError("tls/bad-group-preference",
488
+ where + ": group preference must name at least one group");
489
+ }
490
+ for (var i = 0; i < value.length; i++) {
491
+ if (typeof value[i] !== "string" || value[i].length === 0) {
492
+ throw new TlsTrustError("tls/bad-group-preference",
493
+ where + "[" + i + "]: group preference entries must be non-empty strings, got " +
494
+ (typeof value[i] === "string" ? "empty string" : typeof value[i]));
495
+ }
496
+ }
497
+ return value.join(":");
498
+ }
499
+ if (typeof value !== "string" || value.length === 0) {
500
+ throw new TlsTrustError("tls/bad-group-preference",
501
+ where + ": group preference must be a non-empty string or array of group " +
502
+ "names, got " + (typeof value === "string" ? "empty string" : typeof value));
503
+ }
504
+ return value;
505
+ }
506
+
507
+ // A dial or context built from these options can reach TLS 1.3 unless the
508
+ // caller capped it below — through `maxVersion`, or a `secureProtocol` method
509
+ // that pins one version. Two things turn on the answer and both used to get
510
+ // it wrong: RFC 8879 certificate compression is a TLS 1.3 extension that Node
511
+ // refuses outright when the range excludes 1.3, and a handshake explanation
512
+ // must not send an operator after a TLS 1.3 cipher list their connection
513
+ // never offered. One predicate answers for both.
514
+ // The OpenSSL methods that pin one version. SSLv23_method is the trap: it
515
+ // reads as the version-flexible one and is not — Node maps it to TLS_method
516
+ // with the ceiling held at TLS 1.2. Matching whole names rather than a prefix
517
+ // keeps a hypothetical TLSv1_3_method out of the set, which a prefix on
518
+ // `TLSv1_` would swallow.
519
+ var VERSION_PINNED_METHOD_RE =
520
+ /^(?:SSLv2|SSLv23|SSLv3|TLSv1|TLSv1_1|TLSv1_2)_(?:client_|server_)?method$/;
521
+ var MAX_METHOD_NAME = 32;
522
+
523
+ function _reachesTls13(opts) {
524
+ if (opts === null || typeof opts !== "object") return true;
525
+ var cap = opts.maxVersion;
526
+ if (cap !== undefined && cap !== null && cap !== "TLSv1.3") return false;
527
+ // An OpenSSL method name is a short fixed identifier — the longest Node
528
+ // accepts is `TLSv1_2_client_method` — so bound the input before matching
529
+ // rather than scanning whatever a caller put there. Anything longer is not
530
+ // a method name and pins no version.
531
+ var method = opts.secureProtocol;
532
+ if (typeof method !== "string" || method.length > MAX_METHOD_NAME) return true;
533
+ return !VERSION_PINNED_METHOD_RE.test(method);
534
+ }
535
+
536
+ // Drop the certificate-compression advertisement the framework contributed
537
+ // when the merged options cannot reach TLS 1.3. Without this, capping a
538
+ // connection — a documented override on the WebSocket client and the mail
539
+ // sender — turns into ERR_INVALID_ARG_VALUE before any bytes move, over an
540
+ // option the caller never asked for. A caller who asked for it themselves
541
+ // keeps it, and keeps the error: they named two settings that contradict each
542
+ // other, and quietly dropping one would hide the contradiction.
543
+ function _stripUnreachableCertCompression(merged, caller) {
544
+ if (!merged || typeof merged !== "object") return merged;
545
+ if (_reachesTls13(merged)) return merged;
546
+ if (caller && typeof caller === "object" &&
547
+ caller.certificateCompression !== undefined) return merged;
548
+ delete merged.certificateCompression;
549
+ return merged;
550
+ }
551
+
479
552
  function applyToContext(opts) {
480
553
  opts = opts || {};
481
554
  validateOpts(opts, ["base"], "tls.applyToContext");
@@ -492,18 +565,38 @@ function applyToContext(opts) {
492
565
  // (the framework default is the three ML-KEM hybrids plus the classical
493
566
  // X25519 fallback) so https.Server / https.Agent
494
567
  // negotiate the hybrid KEM with peers that support it and fall back
495
- // to classical X25519 with peers that don't. Operators who explicitly
496
- // pass `groups` in their base config keep the override.
497
- if (base.groups === undefined && STATE.tlsKeyShares.length > 0) {
498
- base.groups = STATE.tlsKeyShares.join(":");
499
- }
568
+ // to classical X25519 with peers that don't.
569
+ //
570
+ // node:tls reads that preference as `ecdhCurve`. A `groups` key is accepted
571
+ // and silently ignored — an unimplemented option is not an error there,
572
+ // whereas a malformed `ecdhCurve` throws — so the list has to land under
573
+ // the name the TLS layer actually reads. A base config naming a preference
574
+ // under either spelling keeps its override; `groups` is carried over rather
575
+ // than dropped, since that is the name this function asked operators for.
576
+ //
577
+ // An override that is PRESENT but not a usable list is refused rather than
578
+ // replaced. node:tls rejects a non-string ecdhCurve, so before the
579
+ // preference was translated to that key a malformed one stopped the server
580
+ // at createSecureContext; quietly substituting the framework default here
581
+ // would instead start a listener on groups the operator did not choose,
582
+ // with nothing said. Only a genuinely absent override takes the default.
583
+ var overrideKey = base.ecdhCurve !== undefined ? "ecdhCurve"
584
+ : base.groups !== undefined ? "groups"
585
+ : null;
586
+ if (overrideKey !== null) {
587
+ base.ecdhCurve = _groupPreferenceString(base[overrideKey],
588
+ "tls.applyToContext: base." + overrideKey);
589
+ } else if (STATE.tlsKeyShares.length > 0) {
590
+ base.ecdhCurve = STATE.tlsKeyShares.join(":");
591
+ }
592
+ if (base.groups !== undefined) delete base.groups;
500
593
  // RFC 8879 certificate compression — advertise what this runtime can
501
594
  // decompress so a peer may send its Certificate message compressed. The
502
595
  // framework's own leaf certificates are ML-DSA-87, so the Certificate
503
596
  // message is the largest thing on the wire during a handshake. Operators
504
597
  // who set `certificateCompression` in their base config keep the override,
505
598
  // including `[]` to advertise none.
506
- if (base.certificateCompression === undefined) {
599
+ if (base.certificateCompression === undefined && _reachesTls13(base)) {
507
600
  var certAlgs = C.TLS_CERT_COMPRESSION();
508
601
  if (certAlgs.length > 0) base.certificateCompression = certAlgs.slice();
509
602
  }
@@ -636,9 +729,14 @@ function getCaPems() {
636
729
  //
637
730
  // buildOptions({ ecdhCurve, groups, cert, key, ca, minVersion, sni,
638
731
  // certificateCompression })
639
- // returns { minVersion, ecdhCurve, groups, cert, key, ca, servername,
732
+ // returns { minVersion, ecdhCurve, cert, key, ca, servername,
640
733
  // certificateCompression }
641
734
  //
735
+ // `groups` is accepted as a spelling of the preference and is NOT returned:
736
+ // node:tls reads the list as `ecdhCurve` and silently ignores `groups`, so
737
+ // emitting both would put the operator's preference under a name the TLS
738
+ // layer never looks at beside the one it does.
739
+ //
642
740
  // `ca` accepts a PEM string OR Buffer OR Array<string|Buffer>; arrays
643
741
  // are concatenated with `\n` so Node's TLS layer parses every block.
644
742
  function _normalizeCaInput(ca) {
@@ -726,9 +824,11 @@ function buildOptions(opts) {
726
824
  }
727
825
  resolved = requested;
728
826
  }
729
- var resolvedStr = resolved.join(":");
730
- out.ecdhCurve = resolvedStr;
731
- out.groups = resolvedStr;
827
+ // Emitted only as `ecdhCurve` — the one name node:tls reads. Returning a
828
+ // matching `groups` key looked like a second, equivalent handle on the
829
+ // preference and was inert, which is the shape that let a group list ship
830
+ // for several releases without ever reaching a handshake.
831
+ out.ecdhCurve = resolved.join(":");
732
832
 
733
833
  // cert / key — pass-through with light shape check. Both are
734
834
  // typically PEM strings or Buffers; arrays are valid for cert
@@ -885,6 +985,233 @@ function outboundPosture() {
885
985
  return posture;
886
986
  }
887
987
 
988
+ /**
989
+ * @primitive b.network.tls.explainOutboundFailure
990
+ * @signature b.network.tls.explainOutboundFailure(err, ctx?)
991
+ * @since 0.18.18
992
+ * @status stable
993
+ * @related b.network.tls.outboundPosture
994
+ *
995
+ * Turn a refused outbound handshake into a sentence naming the likely cause.
996
+ *
997
+ * OpenSSL reports a rejected ClientHello as a bare alert — `tlsv1 alert
998
+ * protocol version` — which names neither the peer nor anything an operator
999
+ * can act on, and reads as a problem with the request rather than with the
1000
+ * posture the client applied to it. Two of those alerts are routinely the
1001
+ * posture doing its job:
1002
+ *
1003
+ * A `protocol_version` alert against a client pinning `minVersion: "TLSv1.3"`
1004
+ * means the peer offers no TLS 1.3. That is a version refusal and has nothing
1005
+ * to do with the key-exchange groups, which is worth stating plainly: the
1006
+ * post-quantum posture is the framework's most visible property, so it draws
1007
+ * the blame for handshake failures it did not cause.
1008
+ *
1009
+ * A `handshake_failure` alert against a narrowed group list means the peer
1010
+ * shares none of the offered groups. Under the shipped preference this cannot
1011
+ * happen — the list ends in classical X25519, so a peer with no post-quantum
1012
+ * hybrid negotiates that — so it points at a list an operator has narrowed
1013
+ * past what the peer can do.
1014
+ *
1015
+ * Neither alert is exclusive to these causes, so the wording is hedged and
1016
+ * the original error text is carried through rather than replaced. Returns
1017
+ * null when the error is not one this can speak to, which callers use to fall
1018
+ * back to the original message.
1019
+ *
1020
+ * Pass `tlsOpts` — the options object the dial was made with — rather than
1021
+ * copying settings out of it. Handing the object over names every setting it
1022
+ * could carry, so the explanation stays right as the diagnosis learns to read
1023
+ * settings a caller was not thinking about when the error handler was written.
1024
+ *
1025
+ * @opts
1026
+ * host: string, // peer named in the message; default: omitted
1027
+ * port: number, // appended to host when both are present
1028
+ * tlsOpts: object, // the options the dial used; each setting below is read from it
1029
+ * minVersion: string, // the floor that was applied; default: the live posture
1030
+ * maxVersion: string, // the ceiling that was applied, when a caller capped the dial below TLS 1.3
1031
+ * secureProtocol: string, // an OpenSSL method name, when a caller pinned one version that way
1032
+ * ecdhCurve: string, // the group list that was offered; default: the live posture
1033
+ *
1034
+ * @example
1035
+ * var why = b.network.tls.explainOutboundFailure(err, { host: "registry.example.com", port: 443 });
1036
+ * // -> "TLS handshake refused by registry.example.com:443 - the peer most
1037
+ * // likely does not offer TLS 1.3, ..."
1038
+ */
1039
+ var EXPLAIN_PREFIX = "TLS handshake refused";
1040
+ var DIAGNOSED = "_blamejsTlsDiagnosed";
1041
+
1042
+ function _hasOwn(obj, key) {
1043
+ return Object.prototype.hasOwnProperty.call(obj, key);
1044
+ }
1045
+
1046
+ // Resolve one setting of the dial being diagnosed. A caller either names the
1047
+ // setting or hands over `tlsOpts` — the options object the dial was made with.
1048
+ // Handing that object over names every setting it could carry, so a value
1049
+ // missing from it means "not pinned for this dial", exactly as a named-but-
1050
+ // undefined field does. Copying fields out of it one at a time is how a
1051
+ // setting the explanation needs goes missing at a call site nobody remembered
1052
+ // to update, which is what kept every capped dial reading as a TLS 1.3 one;
1053
+ // the framework's own callers pass the object.
1054
+ function _dialSetting(ctx, key, fallback) {
1055
+ if (_hasOwn(ctx, key)) return ctx[key];
1056
+ var dialed = ctx.tlsOpts;
1057
+ if (dialed !== null && typeof dialed === "object") return dialed[key];
1058
+ return fallback;
1059
+ }
1060
+
1061
+ // A port the caller had as text is still a port. `URL.port` is a string, so
1062
+ // requiring a number here silently dropped the endpoint from the message for
1063
+ // any caller that passed one through — and a host running several TLS
1064
+ // listeners is exactly where the operator needs to know which one refused.
1065
+ function _portSuffix(port) {
1066
+ var n = typeof port === "string" && port !== "" ? Number(port) : port;
1067
+ if (typeof n !== "number" || !isFinite(n)) return "";
1068
+ return ":" + n;
1069
+ }
1070
+
1071
+ function explainOutboundFailure(err, ctx) {
1072
+ if (!err || typeof err !== "object") return null;
1073
+ ctx = ctx || {};
1074
+ var code = typeof err.code === "string" ? err.code : "";
1075
+ var text = typeof err.message === "string" ? err.message : "";
1076
+ // A caller that names a setting is reporting what THIS dial actually used,
1077
+ // and an absent value there means "not pinned for this dial" rather than
1078
+ // "ask the shared posture" -- a request handed a caller-supplied agent, or a
1079
+ // dial whose own TLS options overrode the posture, negotiates on settings
1080
+ // the shared object does not describe. Diagnosing those against the shared
1081
+ // posture can invert the answer: an agent capped at TLS 1.2 talking to a
1082
+ // 1.3-only peer would be reported as the peer lacking 1.3. Only a caller
1083
+ // that names nothing at all falls back to the live posture.
1084
+ var posture = outboundPosture();
1085
+ var minVersion = _dialSetting(ctx, "minVersion", posture.minVersion);
1086
+ var offered = _dialSetting(ctx, "ecdhCurve", posture.ecdhCurve);
1087
+
1088
+ var peer = "";
1089
+ if (typeof ctx.host === "string" && ctx.host.length > 0) {
1090
+ peer = " by " + ctx.host +
1091
+ _portSuffix(ctx.port);
1092
+ }
1093
+
1094
+ // Alert 70 (protocol_version) and the local equivalents Node raises when the
1095
+ // floor cannot be met. Matched on code first; the text is the fallback for
1096
+ // builds that surface the alert without a distinct code.
1097
+ var isVersion = code === "ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION" ||
1098
+ code === "ERR_SSL_UNSUPPORTED_PROTOCOL" ||
1099
+ code === "ERR_SSL_VERSION_TOO_LOW" ||
1100
+ /alert protocol version|unsupported protocol|version too low/i.test(text);
1101
+ if (isVersion && minVersion === "TLSv1.3") {
1102
+ return EXPLAIN_PREFIX + peer + " - the peer most likely does not " +
1103
+ "offer TLS 1.3, which this client requires (minVersion TLSv1.3). This is " +
1104
+ "a protocol-version refusal and is unrelated to the post-quantum group " +
1105
+ "preference. Underlying error: " + text;
1106
+ }
1107
+
1108
+ var list = typeof offered === "string" && offered.length > 0 ? offered.split(":") : [];
1109
+ if (list.length === 0) return null;
1110
+
1111
+ var hasClassicalFallback = false;
1112
+ for (var i = 0; i < list.length; i += 1) {
1113
+ if (list[i].length > 0 && list[i].indexOf("MLKEM") === -1) hasClassicalFallback = true;
1114
+ }
1115
+ // Only the hybrids are on offer, so a peer without one has nothing left --
1116
+ // worth saying wherever the group list is in the frame.
1117
+ var hybridOnlyNote = hasClassicalFallback ? ""
1118
+ : " The list names only post-quantum hybrids, so a peer without one has " +
1119
+ "nothing left to negotiate.";
1120
+
1121
+ // Codes that mean the group specifically. Carrying a classical group is NOT
1122
+ // proof the peer can pick one -- a TLS 1.3 peer restricted to secp256r1
1123
+ // shares nothing with the hybrids plus X25519 -- so a fallback in the list
1124
+ // does not suppress this.
1125
+ if (code === "ERR_SSL_NO_SHARED_GROUP" || code === "ERR_SSL_WRONG_CURVE" ||
1126
+ /no shared group|wrong curve/i.test(text)) {
1127
+ return EXPLAIN_PREFIX + peer + " - the peer supports none of the " +
1128
+ "key-exchange groups this client offers (" + offered + ")." + hybridOnlyNote +
1129
+ " Underlying error: " + text;
1130
+ }
1131
+
1132
+ // Alert 40 is generic: the peer says it could not proceed without saying
1133
+ // what it objected to. A disjoint TLS 1.3 cipher list produces exactly this
1134
+ // code with a group both sides support, so naming the group list as the
1135
+ // cause would send an operator after the wrong setting. Report what was
1136
+ // pinned and let them narrow it down.
1137
+ if (code === "ERR_SSL_SSL/TLS_ALERT_HANDSHAKE_FAILURE" ||
1138
+ /alert handshake failure/i.test(text)) {
1139
+ // Name TLS 1.3 only when this dial could have reached it. An operator who
1140
+ // capped the connection lower — through a WebSocket tlsOpts override,
1141
+ // their own agent, or a version-pinned secureProtocol — never attempted
1142
+ // 1.3, so pointing at its cipher list sends them after a setting that had
1143
+ // no part in the failure.
1144
+ var reached13 = _reachesTls13({
1145
+ maxVersion: _dialSetting(ctx, "maxVersion", undefined),
1146
+ secureProtocol: _dialSetting(ctx, "secureProtocol", undefined),
1147
+ });
1148
+ return EXPLAIN_PREFIX + peer + " - the peer rejected the handshake with a " +
1149
+ "generic failure alert, which does not say what it objected to. With " +
1150
+ "this client's posture the usual candidates are no mutually supported " +
1151
+ "key-exchange group (it offers " + offered + "), " +
1152
+ (reached13 ? "no shared TLS 1.3 cipher suite" : "no shared cipher suite") +
1153
+ ", and certificate selection." + hybridOnlyNote +
1154
+ " Underlying error: " + text;
1155
+ }
1156
+ return null;
1157
+ }
1158
+
1159
+ /**
1160
+ * @primitive b.network.tls.annotateOutboundFailure
1161
+ * @signature b.network.tls.annotateOutboundFailure(err, ctx?)
1162
+ * @since 0.18.18
1163
+ * @status stable
1164
+ * @related b.network.tls.explainOutboundFailure
1165
+ *
1166
+ * Rewrite a refused handshake's message in place to name the likely cause,
1167
+ * and hand the same error back.
1168
+ *
1169
+ * The error object is kept rather than replaced so `err.code`, the error
1170
+ * class and anything a caller already branches on survive untouched; only the
1171
+ * message changes, and only when there is something to say. The stack's
1172
+ * leading line is refreshed alongside it, since it embeds a copy of the
1173
+ * message captured at construction.
1174
+ *
1175
+ * Diagnosis never fails a request: any error raised while working out the
1176
+ * explanation is swallowed and the original message stands.
1177
+ *
1178
+ * @opts
1179
+ * host: string, // peer named in the message
1180
+ * port: number, // appended to host when both are present
1181
+ * tlsOpts: object, // the options the dial used; see explainOutboundFailure for the settings read from it
1182
+ *
1183
+ * @example
1184
+ * socket.on("error", function (err) {
1185
+ * b.network.tls.annotateOutboundFailure(err, { host: host, port: port });
1186
+ * handle(err);
1187
+ * });
1188
+ */
1189
+ function annotateOutboundFailure(err, ctx) {
1190
+ try {
1191
+ // An error can reach more than one handler on its way out: an h2 session
1192
+ // error retried on h1, or a proxy-leg failure surfacing through the agent
1193
+ // callback of the request to the destination. Whichever leg owns the peer
1194
+ // the handshake was with annotates first and claims the error, because a
1195
+ // later handler would attach a different peer's name to it -- reporting
1196
+ // that the destination refused a handshake that never reached it. The
1197
+ // claim is recorded even when there was nothing to say, since silence
1198
+ // from the owning leg does not make an outer leg's guess correct.
1199
+ if (!err || typeof err !== "object") return err;
1200
+ if (err[DIAGNOSED] === true) return err;
1201
+ Object.defineProperty(err, DIAGNOSED, {
1202
+ value: true, enumerable: false, writable: true, configurable: true,
1203
+ });
1204
+ var why = explainOutboundFailure(err, ctx);
1205
+ if (typeof why !== "string" || why.length === 0) return err;
1206
+ var previous = err.message;
1207
+ err.message = why;
1208
+ if (typeof err.stack === "string" && previous && err.stack.indexOf(previous) !== -1) {
1209
+ err.stack = err.stack.replace(previous, why);
1210
+ }
1211
+ } catch (_e) { /* best-effort: an unexplained error beats a masked one */ }
1212
+ return err;
1213
+ }
1214
+
888
1215
  function _emitAuditAdd(metaList, opts) {
889
1216
  if (opts.audit === false) return;
890
1217
  var sink;
@@ -1707,7 +2034,7 @@ async function fetchOcspResponse(opts) {
1707
2034
  throw new TlsTrustError("tls/ocsp-fetch-failed",
1708
2035
  "ocsp.fetch: responder request to " + responderUrl + " failed: " + ((e && e.message) || String(e)));
1709
2036
  }
1710
- if (res.status !== 200 || !Buffer.isBuffer(res.body) || res.body.length === 0) {
2037
+ if (res.status !== C.HTTP.STATUS.OK || !Buffer.isBuffer(res.body) || res.body.length === 0) {
1711
2038
  throw new TlsTrustError("tls/ocsp-fetch-bad-status",
1712
2039
  "ocsp.fetch: responder returned status " + res.status + " with an empty/non-buffer body");
1713
2040
  }
@@ -3707,6 +4034,8 @@ module.exports = {
3707
4034
  auditInsecureTls: auditInsecureTls,
3708
4035
  certificateCompressionAlgorithms: certificateCompressionAlgorithms,
3709
4036
  outboundPosture: outboundPosture,
4037
+ explainOutboundFailure: explainOutboundFailure,
4038
+ annotateOutboundFailure: annotateOutboundFailure,
3710
4039
  postureGeneration: postureGeneration,
3711
4040
  addCa: addCa,
3712
4041
  addCaBundle: addCaBundle,
@@ -3736,6 +4065,7 @@ module.exports = {
3736
4065
  TlsTrustError: TlsTrustError,
3737
4066
  NetworkTlsError: NetworkTlsError,
3738
4067
  _resetForTest: _resetForTest,
4068
+ _stripUnreachableCertCompression: _stripUnreachableCertCompression,
3739
4069
  _checkServerIdentityStrict: _checkServerIdentityStrict,
3740
4070
  _stripSctExtensionFromCert: _stripSctExtensionFromCert,
3741
4071
  _buildSctSignedEntry: _buildSctSignedEntry,
@@ -29,10 +29,10 @@
29
29
  * evidence-of-control at the SSP / ATO level.
30
30
  */
31
31
 
32
- var framework_error = require("./framework-error");
32
+ var frameworkError = require("./framework-error");
33
33
  var validateOpts = require("./validate-opts");
34
34
 
35
- var NistCrosswalkError = framework_error.defineClass(
35
+ var NistCrosswalkError = frameworkError.defineClass(
36
36
  "NistCrosswalkError",
37
37
  "nist-crosswalk"
38
38
  );
@@ -181,6 +181,23 @@ function querySingle(server, opts) {
181
181
  var timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
182
182
 
183
183
  return new Promise(function (resolve, reject) {
184
+ // SNTPv4 client request: NTP_PACKET_BYTES buffer, byte 0 = 0b00_100_011 = 0x23
185
+ // LI=0 (no warning), VN=4, Mode=3 (client). Other bytes zero.
186
+ //
187
+ // Built before the socket is opened, and inside this executor: entropy for
188
+ // the nonce can fail, a socket acquired first would have nobody left to
189
+ // close it, and a throw out here still reaches the caller as a rejection.
190
+ var req = Buffer.alloc(NTP_PACKET_BYTES);
191
+ req[0] = 0x23;
192
+ // RFC 5905 §8 client-cookie: put a random 64-bit nonce in the request's
193
+ // Transmit Timestamp (bytes 40-47). A conformant server copies it verbatim
194
+ // into the reply's Originate Timestamp (bytes 24-31). Verifying that echo
195
+ // rejects an off-path spoofed reply — without it ANY 48-byte UDP datagram
196
+ // reaching our ephemeral port becomes the authoritative time, letting a
197
+ // spoofer force a fatal-drift refuse-to-boot under BLAMEJS_NTP_STRICT.
198
+ var originCookie = nodeCrypto.randomBytes(8);
199
+ originCookie.copy(req, 40);
200
+
184
201
  // udp6 for IPv6 literals (`::1`, `fd00::…`), udp4 otherwise. Without
185
202
  // this branch a query to an IPv6 NTP host fails with EINVAL because
186
203
  // you can't send IPv6 packets through a udp4 socket.
@@ -199,19 +216,6 @@ function querySingle(server, opts) {
199
216
  done({ code: "ntp/timeout", message: "no reply from " + server + " within " + timeoutMs + "ms" });
200
217
  }, timeoutMs);
201
218
  timer.unref();
202
-
203
- // SNTPv4 client request: NTP_PACKET_BYTES buffer, byte 0 = 0b00_100_011 = 0x23
204
- // LI=0 (no warning), VN=4, Mode=3 (client). Other bytes zero.
205
- var req = Buffer.alloc(NTP_PACKET_BYTES);
206
- req[0] = 0x23;
207
- // RFC 5905 §8 client-cookie: put a random 64-bit nonce in the request's
208
- // Transmit Timestamp (bytes 40-47). A conformant server copies it verbatim
209
- // into the reply's Originate Timestamp (bytes 24-31). Verifying that echo
210
- // rejects an off-path spoofed reply — without it ANY 48-byte UDP datagram
211
- // reaching our ephemeral port becomes the authoritative time, letting a
212
- // spoofer force a fatal-drift refuse-to-boot under BLAMEJS_NTP_STRICT.
213
- var originCookie = nodeCrypto.randomBytes(8);
214
- originCookie.copy(req, 40);
215
219
  var sendTimeMs = Date.now();
216
220
 
217
221
  socket.on("error", function (e) {
@@ -270,12 +274,19 @@ function querySingle(server, opts) {
270
274
  done(null, { driftMs: driftMs, serverTimeMs: serverTimeMs, server: server });
271
275
  });
272
276
 
273
- socket.send(req, 0, req.length, port, server, function (err) {
274
- if (err) {
275
- clearTimeout(timer);
276
- done({ code: "ntp/refused", message: "send to " + server + ": " + err.message });
277
- }
278
- });
277
+ // send() rejects a malformed destination synchronously; routing that
278
+ // through done() closes the socket rather than stranding it.
279
+ try {
280
+ socket.send(req, 0, req.length, port, server, function (err) {
281
+ if (err) {
282
+ clearTimeout(timer);
283
+ done({ code: "ntp/refused", message: "send to " + server + ": " + err.message });
284
+ }
285
+ });
286
+ } catch (e) {
287
+ clearTimeout(timer);
288
+ done({ code: "ntp/refused", message: "send to " + server + ": " + e.message });
289
+ }
279
290
  });
280
291
  }
281
292