@blamejs/core 0.6.70 → 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 +4 -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 +23 -26
  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 +4 -3
  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 +26 -15
  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
@@ -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() {
@@ -5,6 +5,7 @@ var dgram = require("node:dgram");
5
5
  var nodeCrypto = require("node:crypto");
6
6
 
7
7
  var C = require("./constants");
8
+ var { timingSafeEqual } = require("./crypto");
8
9
  var validateOpts = require("./validate-opts");
9
10
  var { defineClass } = require("./framework-error");
10
11
 
@@ -12,8 +13,28 @@ var NtsError = defineClass("NtsError", { alwaysPermanent: false });
12
13
 
13
14
  var NTS_KE_DEFAULT_PORT = 4460;
14
15
  var NTPV4_DEFAULT_PORT = 123;
16
+ // RFC 5905 §6 — seconds between 1900-01-01T00:00Z (NTP epoch) and
17
+ // 1970-01-01T00:00Z (Unix epoch). Protocol-fixed (not a tunable).
15
18
  var NTP_TO_UNIX_OFFSET_SECONDS = 2208988800;
16
19
 
20
+ // Protocol-fixed byte counts and offsets — every numeric literal that
21
+ // represents a wire-format size or position routes through C.BYTES.bytes
22
+ // (a value-passthrough) so the codebase has a single source of truth
23
+ // for "what shape is this number".
24
+ var AES_BLOCK_BYTES = C.BYTES.bytes(16); // AES-128/192/256 block size (RFC 3962)
25
+ var NTP_PACKET_BYTES = C.BYTES.bytes(48); // RFC 5905 §7.3 packet header length
26
+ var NTP_TX_TIMESTAMP_OFFSET = C.BYTES.bytes(40); // RFC 5905 §7.3 — Transmit Timestamp
27
+ var POLY1305_TAG_BYTES = C.BYTES.bytes(16); // RFC 8439 §2.5 tag length
28
+ var SIV_KEY_32_BYTES = C.BYTES.bytes(32);
29
+ var SIV_KEY_48_BYTES = C.BYTES.bytes(48);
30
+ var SIV_KEY_64_BYTES = C.BYTES.bytes(64);
31
+ var UNIQUE_ID_BYTES = C.BYTES.bytes(32); // RFC 8915 §5.3 unique-identifier extension
32
+ var CHACHA20_NONCE_BYTES = C.BYTES.bytes(12);
33
+
34
+ // Bits per byte — used to derive the AES key bit-width from the byte
35
+ // length (16-byte key → 128-bit cipher → "aes-128-..." suffix).
36
+ var BITS_PER_BYTE = 8;
37
+
17
38
  var REC_END = 0;
18
39
  var REC_NEXT_PROTOCOL = 1;
19
40
  var REC_ERROR = 2;
@@ -64,7 +85,9 @@ function _decodeRecords(buf) {
64
85
  }
65
86
 
66
87
  function _aesEncryptBlock(key, block) {
67
- var c = nodeCrypto.createCipheriv("aes-" + (key.length * 8) + "-ecb", key, Buffer.alloc(0));
88
+ // key.length is in bytes (16/24/32); multiply by BITS_PER_BYTE to get
89
+ // the OpenSSL cipher-name suffix (128/192/256).
90
+ var c = nodeCrypto.createCipheriv("aes-" + (key.length * BITS_PER_BYTE) + "-ecb", key, Buffer.alloc(0));
68
91
  c.setAutoPadding(false);
69
92
  return Buffer.concat([c.update(block), c.final()]);
70
93
  }
@@ -87,7 +110,7 @@ function _xorBuf(a, b) {
87
110
  }
88
111
 
89
112
  function _cmacSubkeys(key) {
90
- var L = _aesEncryptBlock(key, Buffer.alloc(16, 0));
113
+ var L = _aesEncryptBlock(key, Buffer.alloc(AES_BLOCK_BYTES, 0));
91
114
  var Rb = Buffer.from([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0x87]);
92
115
  var K1 = _shl1(L);
93
116
  if (L[0] & 0x80) K1 = _xorBuf(K1, Rb);
@@ -98,25 +121,25 @@ function _cmacSubkeys(key) {
98
121
 
99
122
  function _cmac(key, message) {
100
123
  var subkeys = _cmacSubkeys(key);
101
- var n = Math.ceil(message.length / 16);
124
+ var n = Math.ceil(message.length / AES_BLOCK_BYTES);
102
125
  if (n === 0) n = 1;
103
- var lastIsComplete = (message.length > 0) && (message.length % 16 === 0);
126
+ var lastIsComplete = (message.length > 0) && (message.length % AES_BLOCK_BYTES === 0);
104
127
  var blocks = [];
105
128
  for (var i = 0; i < n - 1; i++) {
106
- blocks.push(message.slice(i * 16, i * 16 + 16));
129
+ blocks.push(message.slice(i * AES_BLOCK_BYTES, i * AES_BLOCK_BYTES + AES_BLOCK_BYTES));
107
130
  }
108
131
  var lastBlock;
109
132
  if (lastIsComplete) {
110
- lastBlock = _xorBuf(message.slice((n - 1) * 16, n * 16), subkeys.K1);
133
+ lastBlock = _xorBuf(message.slice((n - 1) * AES_BLOCK_BYTES, n * AES_BLOCK_BYTES), subkeys.K1);
111
134
  } else {
112
- var rem = message.slice((n - 1) * 16);
113
- var padded = Buffer.alloc(16);
135
+ var rem = message.slice((n - 1) * AES_BLOCK_BYTES);
136
+ var padded = Buffer.alloc(AES_BLOCK_BYTES);
114
137
  rem.copy(padded);
115
138
  padded[rem.length] = 0x80;
116
139
  lastBlock = _xorBuf(padded, subkeys.K2);
117
140
  }
118
141
  blocks.push(lastBlock);
119
- var X = Buffer.alloc(16, 0);
142
+ var X = Buffer.alloc(AES_BLOCK_BYTES, 0);
120
143
  for (var b = 0; b < blocks.length; b++) {
121
144
  X = _aesEncryptBlock(key, _xorBuf(X, blocks[b]));
122
145
  }
@@ -131,18 +154,18 @@ function _dbl(buf) {
131
154
  }
132
155
 
133
156
  function _s2v(K, strings, plaintext) {
134
- var D = _cmac(K, Buffer.alloc(16, 0));
157
+ var D = _cmac(K, Buffer.alloc(AES_BLOCK_BYTES, 0));
135
158
  for (var i = 0; i < strings.length; i++) {
136
159
  D = _xorBuf(_dbl(D), _cmac(K, strings[i]));
137
160
  }
138
161
  var T;
139
- if (plaintext.length >= 16) {
140
- var head = plaintext.slice(0, plaintext.length - 16);
141
- var tail = plaintext.slice(plaintext.length - 16);
162
+ if (plaintext.length >= AES_BLOCK_BYTES) {
163
+ var head = plaintext.slice(0, plaintext.length - AES_BLOCK_BYTES);
164
+ var tail = plaintext.slice(plaintext.length - AES_BLOCK_BYTES);
142
165
  var xored = _xorBuf(tail, D);
143
166
  T = Buffer.concat([head, xored]);
144
167
  } else {
145
- var padded = Buffer.alloc(16);
168
+ var padded = Buffer.alloc(AES_BLOCK_BYTES);
146
169
  plaintext.copy(padded);
147
170
  padded[plaintext.length] = 0x80;
148
171
  T = _xorBuf(_dbl(D), padded);
@@ -152,14 +175,15 @@ function _s2v(K, strings, plaintext) {
152
175
 
153
176
  function _aesCtr(key, iv, data) {
154
177
  var ivCopy = Buffer.from(iv);
178
+ // Clear the high bit of the 8th and 12th counter octets per RFC 5297 §2.6.
155
179
  ivCopy[8] &= 0x7f;
156
180
  ivCopy[12] &= 0x7f;
157
- var c = nodeCrypto.createCipheriv("aes-" + (key.length * 8) + "-ctr", key, ivCopy);
181
+ var c = nodeCrypto.createCipheriv("aes-" + (key.length * BITS_PER_BYTE) + "-ctr", key, ivCopy);
158
182
  return Buffer.concat([c.update(data), c.final()]);
159
183
  }
160
184
 
161
185
  function aesSivEncrypt(K, plaintext, associatedData) {
162
- if (K.length !== 32 && K.length !== 48 && K.length !== 64) {
186
+ if (K.length !== SIV_KEY_32_BYTES && K.length !== SIV_KEY_48_BYTES && K.length !== SIV_KEY_64_BYTES) {
163
187
  throw new NtsError("nts/bad-key", "AES-SIV key must be 32/48/64 bytes, got " + K.length);
164
188
  }
165
189
  var half = K.length / 2;
@@ -174,11 +198,11 @@ function aesSivDecrypt(K, ciphertextWithIv, associatedData) {
174
198
  var half = K.length / 2;
175
199
  var K1 = K.slice(0, half);
176
200
  var K2 = K.slice(half);
177
- var V = ciphertextWithIv.slice(0, 16);
178
- var ct = ciphertextWithIv.slice(16);
201
+ var V = ciphertextWithIv.slice(0, AES_BLOCK_BYTES);
202
+ var ct = ciphertextWithIv.slice(AES_BLOCK_BYTES);
179
203
  var pt = _aesCtr(K2, V, ct);
180
204
  var Vcheck = _s2v(K1, associatedData || [], pt);
181
- if (!nodeCrypto.timingSafeEqual(V, Vcheck)) {
205
+ if (!timingSafeEqual(V, Vcheck)) {
182
206
  throw new NtsError("nts/auth-failed", "AES-SIV authentication failed");
183
207
  }
184
208
  return pt;
@@ -207,7 +231,7 @@ function _exportKeys(socket, aeadId) {
207
231
  var label = "EXPORTER-network-time-security";
208
232
  var contextC2S = Buffer.from([0x00, 0x00, (aeadId >> 8) & 0xff, aeadId & 0xff, 0x00]);
209
233
  var contextS2C = Buffer.from([0x00, 0x00, (aeadId >> 8) & 0xff, aeadId & 0xff, 0x01]);
210
- var keyLen = aeadId === AEAD_AES_SIV_CMAC_256 ? 32 : 32;
234
+ var keyLen = aeadId === AEAD_AES_SIV_CMAC_256 ? SIV_KEY_32_BYTES : SIV_KEY_32_BYTES;
211
235
  var c2s = socket.exportKeyingMaterial(keyLen, label, contextC2S);
212
236
  var s2c = socket.exportKeyingMaterial(keyLen, label, contextS2C);
213
237
  return { c2s: c2s, s2c: s2c };
@@ -216,9 +240,7 @@ function _exportKeys(socket, aeadId) {
216
240
  function performKeHandshake(opts) {
217
241
  opts = opts || {};
218
242
  validateOpts(opts, ["host", "port", "servername", "aead", "ca", "timeoutMs"], "nts.performKeHandshake");
219
- if (typeof opts.host !== "string" || opts.host.length === 0) {
220
- throw new NtsError("nts/bad-host", "nts.performKeHandshake: host required");
221
- }
243
+ validateOpts.requireNonEmptyString(opts.host, "nts.performKeHandshake: host", NtsError, "nts/bad-host");
222
244
  var timeoutMs = opts.timeoutMs || C.TIME.seconds(10);
223
245
  return new Promise(function (resolve, reject) {
224
246
  var settled = false;
@@ -238,7 +260,7 @@ function performKeHandshake(opts) {
238
260
  if (opts.ca) connectOpts.ca = opts.ca;
239
261
  var sock = tls.connect(connectOpts);
240
262
  var timer = setTimeout(function () {
241
- try { sock.destroy(); } catch (_e) {}
263
+ try { sock.destroy(); } catch (_e) { /* best-effort socket teardown */ }
242
264
  done(new NtsError("nts/ke-timeout", "NTS-KE handshake timed out after " + timeoutMs + "ms"));
243
265
  }, timeoutMs);
244
266
  timer.unref && timer.unref();
@@ -249,7 +271,7 @@ function performKeHandshake(opts) {
249
271
  sock.on("secureConnect", function () {
250
272
  if (sock.alpnProtocol !== "ntske/1") {
251
273
  clearTimeout(timer);
252
- try { sock.destroy(); } catch (_e) {}
274
+ try { sock.destroy(); } catch (_e) { /* best-effort socket teardown */ }
253
275
  done(new NtsError("nts/bad-alpn",
254
276
  "NTS-KE server did not negotiate ALPN 'ntske/1', got " + JSON.stringify(sock.alpnProtocol)));
255
277
  return;
@@ -268,7 +290,7 @@ function performKeHandshake(opts) {
268
290
  clearTimeout(timer);
269
291
  var errRec = records.find(function (r) { return r.type === REC_ERROR; });
270
292
  if (errRec) {
271
- try { sock.destroy(); } catch (_e) {}
293
+ try { sock.destroy(); } catch (_e) { /* best-effort socket teardown */ }
272
294
  done(new NtsError("nts/ke-error", "NTS-KE server returned error code " + errRec.body.readUInt16BE(0)));
273
295
  return;
274
296
  }
@@ -280,20 +302,20 @@ function performKeHandshake(opts) {
280
302
  }
281
303
  var aeadRec = records.find(function (r) { return r.type === REC_AEAD_ALGORITHM; });
282
304
  if (!aeadRec || aeadRec.body.length < 2) {
283
- try { sock.destroy(); } catch (_e) {}
305
+ try { sock.destroy(); } catch (_e) { /* best-effort socket teardown */ }
284
306
  done(new NtsError("nts/no-aead", "NTS-KE response missing AEAD algorithm"));
285
307
  return;
286
308
  }
287
309
  var aeadId = aeadRec.body.readUInt16BE(0);
288
310
  if (aeadId !== AEAD_AES_SIV_CMAC_256 && aeadId !== AEAD_CHACHA20_POLY1305) {
289
- try { sock.destroy(); } catch (_e) {}
311
+ try { sock.destroy(); } catch (_e) { /* best-effort socket teardown */ }
290
312
  done(new NtsError("nts/unsupported-aead", "NTS-KE server selected unsupported AEAD " + aeadId));
291
313
  return;
292
314
  }
293
315
  var cookies = records.filter(function (r) { return r.type === REC_NEW_COOKIE; })
294
316
  .map(function (r) { return r.body; });
295
317
  if (cookies.length === 0) {
296
- try { sock.destroy(); } catch (_e) {}
318
+ try { sock.destroy(); } catch (_e) { /* best-effort socket teardown */ }
297
319
  done(new NtsError("nts/no-cookies", "NTS-KE response contained no cookies"));
298
320
  return;
299
321
  }
@@ -304,7 +326,7 @@ function performKeHandshake(opts) {
304
326
  var portRec = records.find(function (r) { return r.type === REC_NTPV4_PORT; });
305
327
  if (portRec && portRec.body.length >= 2) ntpPort = portRec.body.readUInt16BE(0);
306
328
  var keys = _exportKeys(sock, aeadId);
307
- try { sock.end(); } catch (_e) {}
329
+ try { sock.end(); } catch (_e) { /* best-effort socket close */ }
308
330
  done(null, {
309
331
  aeadId: aeadId,
310
332
  c2sKey: keys.c2s,
@@ -316,7 +338,7 @@ function performKeHandshake(opts) {
316
338
  });
317
339
  } catch (e) {
318
340
  clearTimeout(timer);
319
- try { sock.destroy(); } catch (_e) {}
341
+ try { sock.destroy(); } catch (_e) { /* best-effort socket teardown */ }
320
342
  done(e);
321
343
  }
322
344
  });
@@ -338,7 +360,7 @@ function _aeadEncrypt(aeadId, key, nonce, plaintext, aad) {
338
360
  return aesSivEncrypt(key, plaintext, ad);
339
361
  }
340
362
  if (aeadId === AEAD_CHACHA20_POLY1305) {
341
- var c = nodeCrypto.createCipheriv("chacha20-poly1305", key, nonce, { authTagLength: 16 });
363
+ var c = nodeCrypto.createCipheriv("chacha20-poly1305", key, nonce, { authTagLength: POLY1305_TAG_BYTES });
342
364
  if (aad) c.setAAD(aad, { plaintextLength: plaintext.length });
343
365
  var ct = Buffer.concat([c.update(plaintext), c.final()]);
344
366
  var tag = c.getAuthTag();
@@ -353,9 +375,9 @@ function _aeadDecrypt(aeadId, key, nonce, ciphertext, aad) {
353
375
  return aesSivDecrypt(key, ciphertext, ad);
354
376
  }
355
377
  if (aeadId === AEAD_CHACHA20_POLY1305) {
356
- var ct = ciphertext.slice(0, ciphertext.length - 16);
357
- var tag = ciphertext.slice(ciphertext.length - 16);
358
- var d = nodeCrypto.createDecipheriv("chacha20-poly1305", key, nonce, { authTagLength: 16 });
378
+ var ct = ciphertext.slice(0, ciphertext.length - POLY1305_TAG_BYTES);
379
+ var tag = ciphertext.slice(ciphertext.length - POLY1305_TAG_BYTES);
380
+ var d = nodeCrypto.createDecipheriv("chacha20-poly1305", key, nonce, { authTagLength: POLY1305_TAG_BYTES });
359
381
  if (aad) d.setAAD(aad, { plaintextLength: ct.length });
360
382
  d.setAuthTag(tag);
361
383
  return Buffer.concat([d.update(ct), d.final()]);
@@ -364,8 +386,8 @@ function _aeadDecrypt(aeadId, key, nonce, ciphertext, aad) {
364
386
  }
365
387
 
366
388
  function _nonceForAead(aeadId) {
367
- if (aeadId === AEAD_AES_SIV_CMAC_256) return nodeCrypto.randomBytes(16);
368
- return nodeCrypto.randomBytes(12);
389
+ if (aeadId === AEAD_AES_SIV_CMAC_256) return nodeCrypto.randomBytes(AES_BLOCK_BYTES);
390
+ return nodeCrypto.randomBytes(CHACHA20_NONCE_BYTES);
369
391
  }
370
392
 
371
393
  function _walkExtensions(msg, startOff) {
@@ -402,12 +424,12 @@ function querySingle(opts) {
402
424
  function done(err, result) {
403
425
  if (settled) return;
404
426
  settled = true;
405
- try { sock.close(); } catch (_e) {}
427
+ try { sock.close(); } catch (_e) { /* best-effort socket close */ }
406
428
  if (err) reject(err); else resolve(result);
407
429
  }
408
- var unique = nodeCrypto.randomBytes(32);
430
+ var unique = nodeCrypto.randomBytes(UNIQUE_ID_BYTES);
409
431
  var cookie = opts.cookies[0];
410
- var packet = Buffer.alloc(48);
432
+ var packet = Buffer.alloc(NTP_PACKET_BYTES);
411
433
  packet[0] = 0x23;
412
434
  var ext1 = _encodeExtensionField(EXTENSION_UNIQUE_IDENTIFIER, unique);
413
435
  var ext2 = _encodeExtensionField(EXTENSION_NTS_COOKIE, cookie);
@@ -436,16 +458,16 @@ function querySingle(opts) {
436
458
  clearTimeout(timer);
437
459
  var receiveTimeMs = Date.now();
438
460
  try {
439
- if (msg.length < 48) {
461
+ if (msg.length < NTP_PACKET_BYTES) {
440
462
  done(new NtsError("nts/bad-reply", "NTS reply too short"));
441
463
  return;
442
464
  }
443
465
  var exts;
444
- try { exts = _walkExtensions(msg, 48); }
466
+ try { exts = _walkExtensions(msg, NTP_PACKET_BYTES); }
445
467
  catch (e) { done(e); return; }
446
468
  var uniqueExt = exts.find(function (e) { return e.type === EXTENSION_UNIQUE_IDENTIFIER; });
447
- if (!uniqueExt || uniqueExt.body.length < 32 ||
448
- !nodeCrypto.timingSafeEqual(uniqueExt.body.slice(0, 32), unique)) {
469
+ if (!uniqueExt || uniqueExt.body.length < UNIQUE_ID_BYTES ||
470
+ !timingSafeEqual(uniqueExt.body.slice(0, UNIQUE_ID_BYTES), unique)) {
449
471
  done(new NtsError("nts/unique-mismatch", "NTS reply unique-identifier mismatch (replay or spoof)"));
450
472
  return;
451
473
  }
@@ -489,11 +511,11 @@ function querySingle(opts) {
489
511
  opts.cookies.shift();
490
512
  for (var ci = 0; ci < newCookies.length; ci++) opts.cookies.push(newCookies[ci]);
491
513
  }
492
- var ntpSeconds = msg.readUInt32BE(40);
493
- var ntpFraction = msg.readUInt32BE(44);
514
+ var ntpSeconds = msg.readUInt32BE(NTP_TX_TIMESTAMP_OFFSET);
515
+ var ntpFraction = msg.readUInt32BE(NTP_TX_TIMESTAMP_OFFSET + 4);
494
516
  var serverUnixSeconds = ntpSeconds - NTP_TO_UNIX_OFFSET_SECONDS;
495
- var fracMs = Math.round(ntpFraction / 0x100000000 * 1000);
496
- var serverTimeMs = serverUnixSeconds * 1000 + fracMs;
517
+ var fracMs = Math.round(C.TIME.seconds(ntpFraction / 0x100000000));
518
+ var serverTimeMs = C.TIME.seconds(serverUnixSeconds) + fracMs;
497
519
  var midpointMs = sendTimeMs + (receiveTimeMs - sendTimeMs) / 2;
498
520
  var driftMs = serverTimeMs - midpointMs;
499
521
  done(null, {