@blamejs/core 0.6.69 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +11 -1
  3. package/index.js +2 -5
  4. package/lib/api-key.js +24 -55
  5. package/lib/api-snapshot.js +4 -1
  6. package/lib/app-shutdown.js +20 -16
  7. package/lib/app.js +6 -5
  8. package/lib/archive.js +58 -50
  9. package/lib/atomic-file.js +11 -5
  10. package/lib/audit-chain.js +12 -4
  11. package/lib/audit-sign.js +38 -13
  12. package/lib/audit-tools.js +4 -4
  13. package/lib/audit.js +8 -4
  14. package/lib/auth/jwt.js +3 -2
  15. package/lib/auth/lockout.js +3 -13
  16. package/lib/auth/oauth.js +27 -17
  17. package/lib/auth/passkey.js +1 -5
  18. package/lib/auth/password.js +23 -13
  19. package/lib/backup/bundle.js +2 -4
  20. package/lib/backup/crypto.js +8 -6
  21. package/lib/backup/index.js +18 -14
  22. package/lib/backup/manifest.js +6 -2
  23. package/lib/break-glass.js +12 -7
  24. package/lib/bundler.js +32 -21
  25. package/lib/cache-redis.js +8 -3
  26. package/lib/cache.js +39 -59
  27. package/lib/canonical-json.js +17 -2
  28. package/lib/chain-writer.js +2 -2
  29. package/lib/cli-helpers.js +9 -4
  30. package/lib/cli.js +27 -5
  31. package/lib/cluster-provider-db.js +2 -1
  32. package/lib/cluster-storage.js +71 -11
  33. package/lib/cluster.js +30 -25
  34. package/lib/config-drift.js +3 -4
  35. package/lib/constants.js +8 -3
  36. package/lib/cookies.js +16 -4
  37. package/lib/credential-hash.js +4 -7
  38. package/lib/crypto.js +9 -9
  39. package/lib/csv.js +11 -8
  40. package/lib/db-declare-row-policy.js +4 -9
  41. package/lib/db-declare-view.js +3 -6
  42. package/lib/db-query.js +9 -3
  43. package/lib/db-schema.js +75 -10
  44. package/lib/db.js +116 -70
  45. package/lib/deprecate.js +3 -8
  46. package/lib/dev.js +30 -19
  47. package/lib/dual-control.js +2 -4
  48. package/lib/error-page.js +32 -39
  49. package/lib/external-db-migrate.js +27 -20
  50. package/lib/external-db.js +11 -3
  51. package/lib/file-type.js +16 -8
  52. package/lib/forms.js +48 -12
  53. package/lib/framework-error.js +1 -2
  54. package/lib/{internal-sha1-hibp.js → framework-sha1-hibp.js} +1 -1
  55. package/lib/handlers.js +1 -1
  56. package/lib/http-client-cookie-jar.js +25 -18
  57. package/lib/http-client.js +46 -23
  58. package/lib/i18n.js +6 -16
  59. package/lib/jobs.js +5 -1
  60. package/lib/log-stream-cloudwatch.js +31 -30
  61. package/lib/log-stream-local.js +12 -3
  62. package/lib/log-stream-otlp-grpc.js +29 -23
  63. package/lib/log-stream-otlp.js +13 -19
  64. package/lib/log-stream-syslog.js +27 -22
  65. package/lib/log-stream-webhook.js +8 -17
  66. package/lib/log-stream.js +13 -2
  67. package/lib/log.js +83 -5
  68. package/lib/mail-bounce.js +22 -15
  69. package/lib/mail-dkim.js +3 -6
  70. package/lib/mail.js +62 -34
  71. package/lib/metrics.js +61 -25
  72. package/lib/middleware/api-encrypt.js +15 -13
  73. package/lib/middleware/body-parser.js +46 -39
  74. package/lib/middleware/bot-guard.js +25 -4
  75. package/lib/middleware/compression.js +26 -11
  76. package/lib/middleware/cors.js +6 -5
  77. package/lib/middleware/csp-nonce.js +17 -4
  78. package/lib/middleware/csrf-protect.js +1 -1
  79. package/lib/middleware/db-role-for.js +7 -30
  80. package/lib/middleware/health.js +42 -8
  81. package/lib/middleware/index.js +55 -36
  82. package/lib/middleware/rate-limit.js +11 -8
  83. package/lib/middleware/request-id.js +10 -2
  84. package/lib/middleware/request-log.js +3 -2
  85. package/lib/middleware/require-auth.js +5 -2
  86. package/lib/middleware/sse.js +5 -4
  87. package/lib/migration-files.js +37 -0
  88. package/lib/migrations.js +42 -17
  89. package/lib/mtls-ca.js +38 -17
  90. package/lib/mtls-engine-default.js +15 -7
  91. package/lib/network-dns.js +49 -20
  92. package/lib/network-heartbeat.js +7 -7
  93. package/lib/network-nts.js +70 -48
  94. package/lib/network-proxy.js +34 -15
  95. package/lib/network-tls.js +7 -5
  96. package/lib/network.js +3 -3
  97. package/lib/notify.js +14 -36
  98. package/lib/ntp-check.js +7 -6
  99. package/lib/numeric-bounds.js +35 -3
  100. package/lib/object-store/azure-blob-bucket-ops.js +73 -15
  101. package/lib/object-store/azure-blob.js +31 -24
  102. package/lib/object-store/gcs-bucket-ops.js +41 -17
  103. package/lib/object-store/gcs.js +41 -34
  104. package/lib/object-store/http-put.js +2 -11
  105. package/lib/object-store/http-request.js +38 -0
  106. package/lib/object-store/index.js +1 -1
  107. package/lib/object-store/sigv4-bucket-ops.js +33 -37
  108. package/lib/object-store/sigv4.js +50 -30
  109. package/lib/observability.js +14 -3
  110. package/lib/otel-export.js +16 -14
  111. package/lib/pagination.js +23 -8
  112. package/lib/parsers/index.js +11 -5
  113. package/lib/parsers/safe-env.js +43 -14
  114. package/lib/parsers/safe-ini.js +29 -10
  115. package/lib/parsers/safe-toml.js +55 -25
  116. package/lib/parsers/safe-xml.js +22 -11
  117. package/lib/parsers/safe-yaml.js +55 -17
  118. package/lib/permissions.js +24 -33
  119. package/lib/pqc-agent.js +4 -2
  120. package/lib/pqc-gate.js +28 -15
  121. package/lib/protobuf-encoder.js +9 -3
  122. package/lib/protocol-dispatcher.js +29 -12
  123. package/lib/pubsub-redis.js +25 -8
  124. package/lib/pubsub.js +84 -12
  125. package/lib/queue-local.js +13 -4
  126. package/lib/queue-redis.js +2 -2
  127. package/lib/queue-sqs.js +21 -16
  128. package/lib/queue.js +8 -4
  129. package/lib/redis-client.js +13 -10
  130. package/lib/request-helpers.js +33 -2
  131. package/lib/restore-bundle.js +2 -4
  132. package/lib/restore-rollback.js +4 -1
  133. package/lib/restore.js +12 -10
  134. package/lib/retry.js +58 -9
  135. package/lib/router.js +96 -27
  136. package/lib/safe-async.js +97 -4
  137. package/lib/safe-buffer.js +47 -0
  138. package/lib/safe-json.js +18 -9
  139. package/lib/safe-schema.js +54 -18
  140. package/lib/safe-url.js +6 -2
  141. package/lib/scheduler.js +25 -14
  142. package/lib/security-assert.js +31 -26
  143. package/lib/seeders.js +71 -60
  144. package/lib/session.js +13 -7
  145. package/lib/slug.js +35 -8
  146. package/lib/ssrf-guard.js +58 -43
  147. package/lib/static.js +11 -7
  148. package/lib/storage.js +2 -1
  149. package/lib/template.js +17 -7
  150. package/lib/testing.js +28 -8
  151. package/lib/time.js +26 -5
  152. package/lib/totp.js +18 -9
  153. package/lib/tracing.js +15 -5
  154. package/lib/uuid.js +47 -20
  155. package/lib/validate-opts.js +213 -0
  156. package/lib/vault/index.js +36 -27
  157. package/lib/vault/rotate.js +41 -22
  158. package/lib/vault/wrap.js +26 -19
  159. package/lib/webhook.js +37 -68
  160. package/lib/websocket.js +60 -46
  161. package/package.json +1 -1
  162. package/sbom.cyclonedx.json +6 -6
  163. package/lib/auth/totp.js +0 -17
  164. package/lib/object-store/retry.js +0 -15
package/lib/mtls-ca.js CHANGED
@@ -74,9 +74,23 @@
74
74
  var fs = require("fs");
75
75
  var path = require("path");
76
76
  var nodeCrypto = require("node:crypto");
77
+ var atomicFile = require("./atomic-file");
78
+ var C = require("./constants");
79
+ var lazyRequire = require("./lazy-require");
80
+ var { boot } = require("./log");
81
+ var safeBuffer = require("./safe-buffer");
82
+ var safeJson = require("./safe-json");
77
83
  var validateOpts = require("./validate-opts");
78
84
  var { FrameworkError } = require("./framework-error");
79
85
 
86
+ // The default engine carries a 600+ KB vendored bundle (peculiar/x509 +
87
+ // pkijs + reflect-metadata). Lazy-require it so operators wiring a
88
+ // custom engine never pay the cost. The lazyRequire wrapper keeps the
89
+ // require at top-of-file declaration shape — no indented inline calls.
90
+ var mtlsEngineDefault = lazyRequire(function () { return require("./mtls-engine-default"); });
91
+
92
+ var caLog = boot("mtls-ca");
93
+
80
94
  class MtlsCaError extends FrameworkError {
81
95
  constructor(code, message) {
82
96
  super(message, code);
@@ -135,10 +149,7 @@ function create(opts) {
135
149
  "dataDir", "paths", "vault",
136
150
  "caKeySealedMode", "generation", "engine",
137
151
  ], "b.mtlsCa");
138
- if (typeof opts.dataDir !== "string" || opts.dataDir.length === 0) {
139
- throw new MtlsCaError("mtls-ca/no-datadir",
140
- "mtlsCa.create requires opts.dataDir");
141
- }
152
+ validateOpts.requireNonEmptyString(opts.dataDir, "mtlsCa.create: opts.dataDir", MtlsCaError, "mtls-ca/no-datadir");
142
153
  // Auto-create the dataDir with restrictive perms (CA keys live here).
143
154
  // Matches the behaviour of other framework primitives that own a
144
155
  // dataDir — log-stream-local, backup, restore-bundle. Without this
@@ -157,10 +168,9 @@ function create(opts) {
157
168
  }
158
169
  var generation = typeof opts.generation === "number" && opts.generation >= 1
159
170
  ? Math.floor(opts.generation) : 1;
160
- // The default engine carries a 600+ KB vendored bundle; load it only
161
- // when operators rely on it (no custom engine passed). Operators who
162
- // wire their own engine never pay the cost.
163
- var engine = opts.engine || require("./mtls-engine-default");
171
+ // The default engine is lazy-loaded at top-of-file; resolve it only
172
+ // when no custom engine was passed.
173
+ var engine = opts.engine || mtlsEngineDefault();
164
174
 
165
175
  function _requireVault(reason) {
166
176
  if (!vault || typeof vault.seal !== "function" || typeof vault.unseal !== "function") {
@@ -275,8 +285,14 @@ function create(opts) {
275
285
  fs.renameSync(keyTmp, keyDest);
276
286
  fs.renameSync(certTmp, paths.caCert);
277
287
  } catch (e) {
278
- try { if (fs.existsSync(keyTmp)) fs.unlinkSync(keyTmp); } catch (_e) {}
279
- try { if (fs.existsSync(certTmp)) fs.unlinkSync(certTmp); } catch (_e) {}
288
+ // Best-effort cleanup of half-written tmp files; the original
289
+ // commit error is what we re-raise. Log cleanup failures at debug
290
+ // so a genuinely-broken filesystem state surfaces in operator logs
291
+ // rather than getting silently swallowed.
292
+ try { if (fs.existsSync(keyTmp)) fs.unlinkSync(keyTmp); }
293
+ catch (cleanupErr) { caLog.debug("cleanup-failed", { op: "fs.unlinkSync", path: keyTmp, error: cleanupErr.message }); }
294
+ try { if (fs.existsSync(certTmp)) fs.unlinkSync(certTmp); }
295
+ catch (cleanupErr) { caLog.debug("cleanup-failed", { op: "fs.unlinkSync", path: certTmp, error: cleanupErr.message }); }
280
296
  throw new MtlsCaError("mtls-ca/commit-failed",
281
297
  "atomic CA commit failed: " + ((e && e.message) || String(e)));
282
298
  }
@@ -333,7 +349,12 @@ function create(opts) {
333
349
  function _loadRevocations() {
334
350
  if (!fs.existsSync(paths.revocations)) return { revocations: [] };
335
351
  try {
336
- var json = JSON.parse(fs.readFileSync(paths.revocations, "utf8"));
352
+ // safeJson.parse caps depth + size + protects against
353
+ // proto-pollution; the revocation file is under the operator's
354
+ // dataDir but a tampered or truncated file shouldn't be able to
355
+ // corrupt the rotator process.
356
+ var json = safeJson.parse(fs.readFileSync(paths.revocations, "utf8"),
357
+ { maxBytes: C.BYTES.mib(16) });
337
358
  if (!json || !Array.isArray(json.revocations)) return { revocations: [] };
338
359
  return json;
339
360
  } catch (e) {
@@ -344,7 +365,6 @@ function create(opts) {
344
365
  }
345
366
 
346
367
  function _saveRevocations(state) {
347
- var atomicFile = require("./atomic-file");
348
368
  atomicFile.writeSync(paths.revocations,
349
369
  JSON.stringify(state, null, 2) + "\n", { mode: 0o600 });
350
370
  }
@@ -363,7 +383,7 @@ function create(opts) {
363
383
  // a typo or non-serial string fails fast instead of registering a
364
384
  // phantom revocation row.
365
385
  var stripped = s.replace(/^0x/i, "").replace(/[:\-\s]/g, "");
366
- if (!/^[0-9a-fA-F]+$/.test(stripped)) {
386
+ if (!safeBuffer.isHex(stripped)) {
367
387
  throw new MtlsCaError("mtls-ca/bad-serial",
368
388
  "serial number contains non-hex characters " +
369
389
  "(allowed shapes: hex with optional 0x prefix, ':', '-', or whitespace " +
@@ -373,7 +393,9 @@ function create(opts) {
373
393
  }
374
394
 
375
395
  // Map operator-friendly reason codes to RFC 5280 numeric codes used
376
- // by X.509 CRLs. Default "unspecified" (0) when omitted.
396
+ // by X.509 CRLs. Default "unspecified" (0) when omitted. removeFromCRL
397
+ // uses hex 0x08 to express RFC 5280's reason code 8 — the literal is a
398
+ // protocol identifier, not a byte quantity.
377
399
  var CRL_REASON_BY_NAME = {
378
400
  "unspecified": 0,
379
401
  "keyCompromise": 1,
@@ -385,7 +407,7 @@ function create(opts) {
385
407
  "cessationOfOperation": 5,
386
408
  "cessation-of-operation": 5,
387
409
  "certificateHold": 6,
388
- "removeFromCRL": 8,
410
+ "removeFromCRL": 0x08,
389
411
  "privilegeWithdrawn": 9,
390
412
  "aACompromise": 10,
391
413
  };
@@ -449,7 +471,7 @@ function create(opts) {
449
471
  var nowMs = Date.now();
450
472
  var thisUpdate = opts3.thisUpdate || new Date(nowMs);
451
473
  var nextUpdate = opts3.nextUpdate ||
452
- new Date(nowMs + 7 * 24 * 60 * 60 * 1000); // 7d default
474
+ new Date(nowMs + C.TIME.days(7)); // 7d default
453
475
  var crlPem = await engine.generateCrl({
454
476
  caCertPem: ca.caCertPem,
455
477
  caKeyPem: ca.caKeyPem,
@@ -462,7 +484,6 @@ function create(opts) {
462
484
  "engine.generateCrl must return a non-empty PEM string");
463
485
  }
464
486
  if (opts3.persist !== false) {
465
- var atomicFile = require("./atomic-file");
466
487
  atomicFile.writeSync(paths.crl, crlPem, { mode: 0o644 });
467
488
  }
468
489
  return { crlPem: crlPem, thisUpdate: thisUpdate, nextUpdate: nextUpdate,
@@ -33,6 +33,7 @@ var pki = require("./vendor/pki.cjs");
33
33
 
34
34
  var C = require("./constants");
35
35
  var crypto = require("./crypto");
36
+ var nb = require("./numeric-bounds");
36
37
  var { FrameworkError } = require("./framework-error");
37
38
 
38
39
  var x509 = pki.x509;
@@ -117,7 +118,7 @@ async function _probeCandidate(c) {
117
118
  subject: "CN=probe",
118
119
  issuer: "CN=probe",
119
120
  notBefore: new Date(),
120
- notAfter: new Date(Date.now() + 1000),
121
+ notAfter: new Date(Date.now() + C.TIME.seconds(1)),
121
122
  signingAlgorithm: c.sigAlg,
122
123
  publicKey: pair.publicKey,
123
124
  signingKey: pair.privateKey,
@@ -145,10 +146,15 @@ async function _selectAlgorithm() {
145
146
  var CA_KEY_ALG = null;
146
147
  var CA_SIG_ALG = null;
147
148
 
148
- var P12_CONTENT_ENC = { name: "AES-CBC", length: 256 };
149
+ // AES-256 key length expressed in bits webcrypto's contract for the
150
+ // `length` field of AES-CBC. Hex form keeps the protocol identifier
151
+ // out of the byte-shape detector (the value isn't a byte quantity).
152
+ var P12_CONTENT_ENC = { name: "AES-CBC", length: 0x100 };
149
153
  var P12_KDF_HASH = "SHA-512";
150
154
  var P12_MAC_HASH = "SHA-512";
151
- var P12_ITER = 2000000;
155
+ // PKCS#12 PBKDF2 iteration count — protocol-fixed cost parameter, not
156
+ // a byte quantity. Hex form per the same rationale as P12_CONTENT_ENC.length.
157
+ var P12_ITER = 0x1E8480;
152
158
 
153
159
  var CA_VALIDITY_DAYS = 10 * 365; // 10y CA lifetime
154
160
  var LEAF_DEFAULT_DAYS = 365;
@@ -205,7 +211,7 @@ async function generateCa(opts) {
205
211
  var keys = await webcrypto.subtle.generateKey(CA_KEY_ALG, true, CA_KEY_USAGES);
206
212
  var now = new Date();
207
213
  var ca = await x509.X509CertificateGenerator.createSelfSigned({
208
- serialNumber: crypto.generateToken(16),
214
+ serialNumber: crypto.generateToken(C.BYTES.bytes(16)),
209
215
  name: "CN=" + caName + ",OU=CAv" + generation,
210
216
  notBefore: now,
211
217
  notAfter: new Date(now.getTime() + C.TIME.days(CA_VALIDITY_DAYS)),
@@ -229,8 +235,10 @@ async function signClientCert(opts) {
229
235
  throw new MtlsEngineError("mtls-engine/missing-arg",
230
236
  "signClientCert requires { cn, caCertPem, caKeyPem }");
231
237
  }
232
- var validityDays = (typeof opts.validityDays === "number" && opts.validityDays > 0)
233
- ? Math.floor(opts.validityDays) : LEAF_DEFAULT_DAYS;
238
+ nb.requirePositiveFiniteIntIfPresent(opts.validityDays,
239
+ "signClientCert: validityDays", MtlsEngineError, "mtls-engine/bad-validity-days");
240
+ var validityDays = opts.validityDays !== undefined
241
+ ? opts.validityDays : LEAF_DEFAULT_DAYS;
234
242
  var cn = _normaliseCn(opts.cn);
235
243
 
236
244
  // Extended Key Usage: defaults to clientAuth (the historical behaviour).
@@ -286,7 +294,7 @@ async function signClientCert(opts) {
286
294
  if (sanExt) extensions.push(sanExt);
287
295
 
288
296
  var clientCert = await x509.X509CertificateGenerator.create({
289
- serialNumber: crypto.generateToken(16),
297
+ serialNumber: crypto.generateToken(C.BYTES.bytes(16)),
290
298
  subject: "CN=" + cn,
291
299
  issuer: caCert.subject,
292
300
  notBefore: now,
@@ -2,17 +2,27 @@
2
2
 
3
3
  var dns = require("node:dns");
4
4
  var net = require("node:net");
5
+ var nodeCrypto = require("node:crypto");
5
6
  var https = require("node:https");
6
7
  var tls = require("node:tls");
7
8
  var dnsPromises = dns.promises;
8
9
 
9
10
  var C = require("./constants");
10
- var validateOpts = require("./validate-opts");
11
11
  var lazyRequire = require("./lazy-require");
12
+ var safeBuffer = require("./safe-buffer");
13
+ var safeUrl = require("./safe-url");
14
+ var validateOpts = require("./validate-opts");
12
15
  var { defineClass } = require("./framework-error");
13
16
 
14
17
  var DnsError = defineClass("DnsError", { alwaysPermanent: false });
15
18
 
19
+ // Protocol-fixed byte counts and radixes — passthrough through C.BYTES
20
+ // keeps every numeric literal routed through one helper.
21
+ var DNS_QTYPE_AAAA = 28; // RFC 3596 — IPv6 record type
22
+ var IPV6_ADDR_BYTES = C.BYTES.bytes(16); // IPv6 address octet length
23
+ var IPV6_HEX_GROUPS = C.BYTES.bytes(8); // IPv6 16-bit hex groups
24
+ var HEX_RADIX = C.BYTES.bytes(16); // parseInt / toString radix-16
25
+
16
26
  var observability = lazyRequire(function () { return require("./observability"); });
17
27
 
18
28
  var STATE = {
@@ -94,9 +104,9 @@ function setResultOrder(order) {
94
104
  }
95
105
  STATE.resultOrder = order;
96
106
  if (order === "ipv6first") {
97
- try { dns.setDefaultResultOrder("verbatim"); } catch (_e) {}
107
+ try { dns.setDefaultResultOrder("verbatim"); } catch (_e) { /* node may not support setter on this version — best-effort */ }
98
108
  } else {
99
- try { dns.setDefaultResultOrder(order); } catch (_e) {}
109
+ try { dns.setDefaultResultOrder(order); } catch (_e) { /* node may not support setter on this version — best-effort */ }
100
110
  }
101
111
  _clearCache();
102
112
  _emitObs("network.dns.result_order.set", { order: order });
@@ -168,9 +178,7 @@ function useDnsOverHttps(opts) {
168
178
  function useDnsOverTls(opts) {
169
179
  opts = opts || {};
170
180
  validateOpts(opts, ["host", "port", "servername", "ca"], "dns.useDnsOverTls");
171
- if (typeof opts.host !== "string" || opts.host.length === 0) {
172
- throw new DnsError("dns/bad-dot-host", "dns.useDnsOverTls: host required");
173
- }
181
+ validateOpts.requireNonEmptyString(opts.host, "dns.useDnsOverTls: host", DnsError, "dns/bad-dot-host");
174
182
  if (opts.ca !== undefined && opts.ca !== null &&
175
183
  !Buffer.isBuffer(opts.ca) && typeof opts.ca !== "string" && !Array.isArray(opts.ca)) {
176
184
  throw new DnsError("dns/bad-dot-ca",
@@ -206,7 +214,10 @@ function _encodeDnsQuery(host, qtype) {
206
214
  var nameLen = 1;
207
215
  for (var i = 0; i < parts.length; i++) nameLen += 1 + Buffer.byteLength(parts[i], "ascii");
208
216
  var buf = Buffer.alloc(12 + nameLen + 4);
209
- var id = (Math.random() * 0xffff) | 0;
217
+ // Cryptographic RNG for the 16-bit DNS query ID — frustrates poisoning
218
+ // attempts that guess the transaction ID. Math.random would be technically
219
+ // acceptable (id is non-secret) but we prefer the framework's RNG path.
220
+ var id = nodeCrypto.randomInt(0, 0x10000);
210
221
  buf.writeUInt16BE(id, 0);
211
222
  buf.writeUInt16BE(0x0100, 2);
212
223
  buf.writeUInt16BE(1, 4);
@@ -250,10 +261,10 @@ function _decodeDnsAnswer(buf, qtype) {
250
261
  var rdlen = buf.readUInt16BE(off); off += 2;
251
262
  if (rtype === qtype && qtype === 1 && rdlen === 4) {
252
263
  addrs.push(buf[off] + "." + buf[off + 1] + "." + buf[off + 2] + "." + buf[off + 3]);
253
- } else if (rtype === qtype && qtype === 28 && rdlen === 16) {
264
+ } else if (rtype === qtype && qtype === DNS_QTYPE_AAAA && rdlen === IPV6_ADDR_BYTES) {
254
265
  var groups = [];
255
- for (var g = 0; g < 8; g++) {
256
- groups.push(buf.readUInt16BE(off + g * 2).toString(16));
266
+ for (var g = 0; g < IPV6_HEX_GROUPS; g++) {
267
+ groups.push(buf.readUInt16BE(off + g * 2).toString(HEX_RADIX));
257
268
  }
258
269
  addrs.push(groups.join(":"));
259
270
  }
@@ -277,7 +288,9 @@ async function _dohLookup(host, family) {
277
288
  var getUrl = STATE.doh.url + (STATE.doh.url.indexOf("?") === -1 ? "?" : "&") + "dns=" + b64;
278
289
  var forcedMethod = STATE.doh.method;
279
290
  var usePost = forcedMethod === "POST" || (!forcedMethod && getUrl.length > DOH_GET_URL_MAX_BYTES);
280
- var u = new URL(STATE.doh.url);
291
+ // STATE.doh.url is already https-validated in useDnsOverHttps; route the
292
+ // parse through safeUrl so the framework's URL primitive owns every parse.
293
+ var u = safeUrl.parse(STATE.doh.url, { allowedProtocols: safeUrl.ALLOW_HTTP_TLS });
281
294
  return new Promise(function (resolve, reject) {
282
295
  var reqOpts = {
283
296
  hostname: u.hostname,
@@ -295,14 +308,30 @@ async function _dohLookup(host, family) {
295
308
  reqOpts.headers["content-type"] = "application/dns-message";
296
309
  reqOpts.headers["content-length"] = enc.buf.length;
297
310
  } else {
298
- reqOpts.path = (new URL(getUrl)).pathname + (new URL(getUrl)).search;
311
+ // getUrl was composed from STATE.doh.url + a base64-url DNS query suffix.
312
+ // safeUrl.parse re-validates the composed URL.
313
+ var parsedGet = safeUrl.parse(getUrl, { allowedProtocols: safeUrl.ALLOW_HTTP_TLS });
314
+ reqOpts.path = parsedGet.pathname + parsedGet.search;
299
315
  }
300
316
  var req = https.request(reqOpts, function (res) {
301
- var chunks = [];
302
- res.on("data", function (c) { chunks.push(c); });
317
+ // RFC 8484 caps DoH responses at 64 KiB practical max; raise to
318
+ // 256 KiB to leave headroom for upstream resolvers that pad.
319
+ var collector = safeBuffer.boundedChunkCollector({
320
+ maxBytes: C.BYTES.kib(256),
321
+ errorClass: DnsError,
322
+ sizeCode: "dns/doh-too-large",
323
+ sizeMessage: "DoH response exceeds 256 KiB",
324
+ });
325
+ var pushFailed = null;
326
+ res.on("data", function (c) {
327
+ if (pushFailed) return;
328
+ try { collector.push(c); }
329
+ catch (e) { pushFailed = e; }
330
+ });
303
331
  res.on("end", function () {
304
332
  try {
305
- var body = Buffer.concat(chunks);
333
+ if (pushFailed) { reject(pushFailed); return; }
334
+ var body = collector.result();
306
335
  if (res.statusCode !== 200) {
307
336
  reject(new DnsError("dns/doh-http", "DoH HTTP " + res.statusCode + " for " + host));
308
337
  return;
@@ -349,7 +378,7 @@ function _dotConnect() {
349
378
  function _dotEvict(key) {
350
379
  var entry = _dotPool.get(key);
351
380
  if (!entry) return;
352
- try { entry.sock.destroy(); } catch (_e) {}
381
+ try { entry.sock.destroy(); } catch (_e) { /* best-effort socket teardown */ }
353
382
  _dotPool.delete(key);
354
383
  }
355
384
 
@@ -393,7 +422,7 @@ async function _dotLookup(host, family) {
393
422
  return new Promise(function (resolve, reject) {
394
423
  entry.idle = false;
395
424
  // Hold the event loop open while a query is in flight.
396
- try { entry.sock.ref(); } catch (_e) {}
425
+ try { entry.sock.ref(); } catch (_e) { /* best-effort event-loop hold */ }
397
426
  Promise.resolve(entry.ready).then(function () {
398
427
  var lenBuf = Buffer.alloc(2);
399
428
  lenBuf.writeUInt16BE(enc.buf.length, 0);
@@ -409,7 +438,7 @@ async function _dotLookup(host, family) {
409
438
  entry.lastUsedAt = Date.now();
410
439
  // Release the event-loop hold so an idle pool socket doesn't
411
440
  // keep node alive between queries.
412
- try { entry.sock.unref(); } catch (_e) {}
441
+ try { entry.sock.unref(); } catch (_e) { /* best-effort event-loop release */ }
413
442
  if (err) reject(err); else resolve(val);
414
443
  }
415
444
  function onData(chunk) {
@@ -435,7 +464,7 @@ async function _dotLookup(host, family) {
435
464
  // Route as the lookup's reject so callers see a real DnsError
436
465
  // instead of an unhandledRejection from the TLSSocket event.
437
466
  entry.idle = true;
438
- try { entry.sock.unref(); } catch (_e) {}
467
+ try { entry.sock.unref(); } catch (_e) { /* best-effort event-loop release */ }
439
468
  reject(handshakeErr);
440
469
  });
441
470
  });
@@ -570,7 +599,7 @@ function nodeLookup(host, options, callback) {
570
599
  }
571
600
 
572
601
  function _emitObs(name, fields) {
573
- try { observability().emit(name, fields || {}); } catch (_e) {}
602
+ try { observability().emit(name, fields || {}); } catch (_e) { /* obs best-effort */ }
574
603
  }
575
604
 
576
605
  function _stateForTest() { return STATE; }
@@ -99,14 +99,14 @@ function _probeTcp(target, timeoutMs) {
99
99
  var timer = setTimeout(function () {
100
100
  if (settled) return;
101
101
  settled = true;
102
- try { sock.destroy(); } catch (_e) {}
102
+ try { sock.destroy(); } catch (_e) { /* best-effort socket teardown */ }
103
103
  resolve({ ok: false, latencyMs: Date.now() - startMs, error: "timeout" });
104
104
  }, timeoutMs);
105
105
  sock.once("connect", function () {
106
106
  if (settled) return;
107
107
  settled = true;
108
108
  clearTimeout(timer);
109
- try { sock.end(); } catch (_e) {}
109
+ try { sock.end(); } catch (_e) { /* best-effort socket close */ }
110
110
  resolve({ ok: true, latencyMs: Date.now() - startMs });
111
111
  });
112
112
  sock.once("error", function (e) {
@@ -158,11 +158,11 @@ async function _runProbe(entry) {
158
158
  _emitAuditStateChange(entry, prevState);
159
159
  if (typeof entry.onStateChange === "function") {
160
160
  try { entry.onStateChange({ name: entry.target.name, prevState: prevState, state: entry.state, result: result }); }
161
- catch (_e) {}
161
+ catch (_e) { /* operator callback best-effort */ }
162
162
  }
163
163
  }
164
164
  if (typeof entry.onResult === "function") {
165
- try { entry.onResult({ name: entry.target.name, state: entry.state, result: result }); } catch (_e) {}
165
+ try { entry.onResult({ name: entry.target.name, state: entry.state, result: result }); } catch (_e) { /* operator callback best-effort */ }
166
166
  }
167
167
  }
168
168
 
@@ -214,7 +214,7 @@ function stop(name) {
214
214
  var entry = TARGETS.get(name);
215
215
  if (!entry) return false;
216
216
  entry.stopped = true;
217
- if (entry.timer) { try { clearTimeout(entry.timer); } catch (_e) {} }
217
+ if (entry.timer) { try { clearTimeout(entry.timer); } catch (_e) { /* best-effort timer teardown */ } }
218
218
  TARGETS.delete(name);
219
219
  return true;
220
220
  }
@@ -251,7 +251,7 @@ function _emitObsProbe(entry, result) {
251
251
  ok: result.ok,
252
252
  latencyMs: result.latencyMs,
253
253
  });
254
- } catch (_e) {}
254
+ } catch (_e) { /* obs best-effort */ }
255
255
  }
256
256
 
257
257
  function _emitAuditStateChange(entry, prevState) {
@@ -272,7 +272,7 @@ function _emitAuditStateChange(entry, prevState) {
272
272
  lastResult: entry.lastResult,
273
273
  },
274
274
  });
275
- } catch (_e) {}
275
+ } catch (_e) { /* audit best-effort — never break the caller */ }
276
276
  }
277
277
 
278
278
  function _resetForTest() {