@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
@@ -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, {
@@ -6,12 +6,19 @@ var net = require("node:net");
6
6
  var tls = require("node:tls");
7
7
 
8
8
  var C = require("./constants");
9
- var validateOpts = require("./validate-opts");
10
9
  var lazyRequire = require("./lazy-require");
10
+ var safeUrl = require("./safe-url");
11
+ var validateOpts = require("./validate-opts");
11
12
  var { defineClass } = require("./framework-error");
12
13
 
13
14
  var ProxyError = defineClass("ProxyError", { alwaysPermanent: false });
14
15
 
16
+ // Protocol-fixed byte counts — passthrough through C.BYTES so the codebase
17
+ // has a single source of truth for "what shape is this number".
18
+ var IPV4_PREFIX_MAX_BITS = C.BYTES.bytes(32); // RFC 791 §3.1 IPv4 address bit-width
19
+ var DEFAULT_HTTPS_PORT = 443; // RFC 9110 §4.2.2
20
+ var DEFAULT_HTTP_PORT = C.BYTES.bytes(80); // RFC 9110 §4.2.1
21
+
15
22
  var observability = lazyRequire(function () { return require("./observability"); });
16
23
 
17
24
  var STATE = {
@@ -31,14 +38,18 @@ function _parseAuth(spec) {
31
38
 
32
39
  function _parseProxyUrl(value) {
33
40
  if (!value) return null;
34
- var u;
35
- try { u = new URL(value); } catch (_e) {
41
+ try {
42
+ return safeUrl.parse(value, {
43
+ allowedProtocols: safeUrl.ALLOW_HTTP_ALL,
44
+ allowUserinfo: true, // proxy auth is conventionally placed in the URL
45
+ errorClass: ProxyError,
46
+ });
47
+ } catch (e) {
48
+ if (e && e.isSafeUrlError && e.code === "safe-url/protocol-disallowed") {
49
+ throw new ProxyError("proxy/bad-protocol", "proxy URL must be http:// or https://, got " + value);
50
+ }
36
51
  throw new ProxyError("proxy/bad-url", "invalid proxy URL: " + value);
37
52
  }
38
- if (u.protocol !== "http:" && u.protocol !== "https:") {
39
- throw new ProxyError("proxy/bad-protocol", "proxy URL must be http:// or https://, got " + u.protocol);
40
- }
41
- return u;
42
53
  }
43
54
 
44
55
  function _parseNoProxy(spec) {
@@ -97,14 +108,22 @@ function _matchCidr(cidr, host) {
97
108
  var network = cidr.slice(0, slash);
98
109
  var prefix = parseInt(cidr.slice(slash + 1), 10);
99
110
  if (net.isIP(host) !== 4 || net.isIP(network) !== 4) return false;
100
- if (!isFinite(prefix) || prefix < 0 || prefix > 32) return false;
111
+ if (!isFinite(prefix) || prefix < 0 || prefix > IPV4_PREFIX_MAX_BITS) return false;
101
112
  if (prefix === 0) return true;
102
- var mask = (0xffffffff << (32 - prefix)) >>> 0;
113
+ var mask = (0xffffffff << (IPV4_PREFIX_MAX_BITS - prefix)) >>> 0;
103
114
  return (_ipv4ToInt(host) & mask) === (_ipv4ToInt(network) & mask);
104
115
  }
105
116
 
117
+ function _parseTarget(targetUrl) {
118
+ if (targetUrl instanceof URL) return targetUrl;
119
+ return safeUrl.parse(String(targetUrl), {
120
+ allowedProtocols: safeUrl.ALLOW_HTTP_ALL,
121
+ errorClass: ProxyError,
122
+ });
123
+ }
124
+
106
125
  function shouldProxy(targetUrl) {
107
- var u = targetUrl instanceof URL ? targetUrl : new URL(String(targetUrl));
126
+ var u = _parseTarget(targetUrl);
108
127
  var host = u.hostname;
109
128
  var rules = STATE.noProxy;
110
129
  for (var i = 0; i < rules.length; i++) {
@@ -121,7 +140,7 @@ function shouldProxy(targetUrl) {
121
140
  }
122
141
 
123
142
  function _proxyForUrl(targetUrl) {
124
- var u = targetUrl instanceof URL ? targetUrl : new URL(String(targetUrl));
143
+ var u = _parseTarget(targetUrl);
125
144
  if (!shouldProxy(u)) return null;
126
145
  return u.protocol === "https:" ? STATE.https : STATE.http;
127
146
  }
@@ -134,7 +153,7 @@ function _proxyAuthHeader(proxyUrl) {
134
153
  }
135
154
 
136
155
  function _connectThroughTunnel(proxyUrl, targetHost, targetPort, callback) {
137
- var proxyPort = proxyUrl.port || (proxyUrl.protocol === "https:" ? 443 : 80);
156
+ var proxyPort = proxyUrl.port || (proxyUrl.protocol === "https:" ? DEFAULT_HTTPS_PORT : DEFAULT_HTTP_PORT);
138
157
  var proxySocket = proxyUrl.protocol === "https:"
139
158
  ? tls.connect({
140
159
  host: proxyUrl.hostname,
@@ -167,7 +186,7 @@ function _connectThroughTunnel(proxyUrl, targetHost, targetPort, callback) {
167
186
  var match = /HTTP\/1\.[01] (\d{3})/.exec(status);
168
187
  if (!match || parseInt(match[1], 10) !== 200) {
169
188
  done(new ProxyError("proxy/connect-failed", "proxy CONNECT to " + targetHost + ":" + targetPort + " failed: " + status));
170
- try { proxySocket.destroy(); } catch (_e) {}
189
+ try { proxySocket.destroy(); } catch (_e) { /* best-effort socket teardown */ }
171
190
  return;
172
191
  }
173
192
  done(null, proxySocket);
@@ -179,7 +198,7 @@ function _connectThroughTunnel(proxyUrl, targetHost, targetPort, callback) {
179
198
  function agentFor(targetUrl) {
180
199
  var proxy = _proxyForUrl(targetUrl);
181
200
  if (!proxy) return null;
182
- var u = targetUrl instanceof URL ? targetUrl : new URL(String(targetUrl));
201
+ var u = _parseTarget(targetUrl);
183
202
  var key = proxy.toString() + "|" + u.protocol;
184
203
  if (STATE.agentCache.has(key)) return STATE.agentCache.get(key);
185
204
 
@@ -227,7 +246,7 @@ function snapshot() {
227
246
  }
228
247
 
229
248
  function _emitObs(name, fields) {
230
- try { observability().emit(name, fields || {}); } catch (_e) {}
249
+ try { observability().emit(name, fields || {}); } catch (_e) { /* obs best-effort */ }
231
250
  }
232
251
 
233
252
  function _resetForTest() {
@@ -5,6 +5,8 @@ var fs = require("node:fs");
5
5
  var path = require("node:path");
6
6
  var nodeCrypto = require("node:crypto");
7
7
 
8
+ var C = require("./constants");
9
+ var safeBuffer = require("./safe-buffer");
8
10
  var validateOpts = require("./validate-opts");
9
11
  var lazyRequire = require("./lazy-require");
10
12
  var { defineClass } = require("./framework-error");
@@ -66,8 +68,8 @@ function _certMetadata(pem) {
66
68
 
67
69
  function _isPathLike(s) {
68
70
  if (s.indexOf("-----BEGIN") !== -1) return false;
69
- if (s.length > 1024) return false;
70
- if (/[\r\n]/.test(s)) return false;
71
+ if (s.length > C.BYTES.kib(1)) return false;
72
+ if (safeBuffer.hasCrlf(s)) return false;
71
73
  return true;
72
74
  }
73
75
 
@@ -159,7 +161,7 @@ function _emitAuditRemove(metaList, reason) {
159
161
  reason: reason || "operator",
160
162
  },
161
163
  });
162
- } catch (_e) {}
164
+ } catch (_e) { /* audit best-effort — never break the caller */ }
163
165
  }
164
166
  }
165
167
 
@@ -292,12 +294,12 @@ function _emitAuditAdd(metaList, opts) {
292
294
  label: opts.label || null,
293
295
  },
294
296
  });
295
- } catch (_e) {}
297
+ } catch (_e) { /* audit best-effort — never break the caller */ }
296
298
  }
297
299
  }
298
300
 
299
301
  function _emitObs(name, fields) {
300
- try { observability().emit(name, fields || {}); } catch (_e) {}
302
+ try { observability().emit(name, fields || {}); } catch (_e) { /* obs best-effort */ }
301
303
  }
302
304
 
303
305
  function _resetForTest() {
package/lib/network.js CHANGED
@@ -77,7 +77,7 @@ function applyToSocket(socket) {
77
77
  if (typeof socket.setKeepAlive === "function") {
78
78
  socket.setKeepAlive(SOCKET_DEFAULTS.keepAlive, SOCKET_DEFAULTS.keepAliveInitialDelayMs || 0);
79
79
  }
80
- } catch (_e) {}
80
+ } catch (_e) { /* best-effort socket-tuning — apply or skip */ }
81
81
  return socket;
82
82
  }
83
83
 
@@ -172,7 +172,7 @@ function bootFromEnv(opts) {
172
172
  outcome: "success",
173
173
  metadata: applied,
174
174
  });
175
- } catch (_e) {}
175
+ } catch (_e) { /* audit best-effort — never break boot */ }
176
176
  }
177
177
  }
178
178
  _emitObs("network.boot.from_env", { source: "env" });
@@ -197,7 +197,7 @@ function snapshot() {
197
197
  }
198
198
 
199
199
  function _emitObs(name, fields) {
200
- try { observability().emit(name, fields || {}); } catch (_e) {}
200
+ try { observability().emit(name, fields || {}); } catch (_e) { /* obs best-effort */ }
201
201
  }
202
202
 
203
203
  function _resetForTest() {
package/lib/notify.js CHANGED
@@ -127,9 +127,7 @@ function _validateChannelEntry(name, entry) {
127
127
  }
128
128
 
129
129
  function _validateCreateOpts(opts) {
130
- if (!opts || typeof opts !== "object") {
131
- throw _err("BAD_OPT", "notify.create: opts must be an object");
132
- }
130
+ validateOpts.requireObject(opts, "notify.create", NotifyError);
133
131
  if (!opts.channels || typeof opts.channels !== "object") {
134
132
  throw _err("BAD_OPT", "notify.create: channels must be an object mapping channel names to transports");
135
133
  }
@@ -137,17 +135,9 @@ function _validateCreateOpts(opts) {
137
135
  if (keys.length === 0) {
138
136
  throw _err("BAD_OPT", "notify.create: channels object must have at least one channel");
139
137
  }
140
- if (opts.audit !== undefined && opts.audit !== null) {
141
- if (typeof opts.audit !== "object" || typeof opts.audit.safeEmit !== "function") {
142
- throw _err("BAD_OPT", "notify.create: audit must be a b.audit-shaped object (safeEmit fn)");
143
- }
144
- }
145
- if (opts.auditSuccess !== undefined && typeof opts.auditSuccess !== "boolean") {
146
- throw _err("BAD_OPT", "notify.create: auditSuccess must be a boolean");
147
- }
148
- if (opts.auditFailures !== undefined && typeof opts.auditFailures !== "boolean") {
149
- throw _err("BAD_OPT", "notify.create: auditFailures must be a boolean");
150
- }
138
+ validateOpts.auditShape(opts.audit, "notify.create", NotifyError);
139
+ validateOpts.optionalBoolean(opts.auditSuccess, "notify.create: auditSuccess", NotifyError);
140
+ validateOpts.optionalBoolean(opts.auditFailures, "notify.create: auditFailures", NotifyError);
151
141
  if (opts.redact !== undefined && opts.redact !== null && typeof opts.redact !== "function") {
152
142
  throw _err("BAD_OPT", "notify.create: redact must be a function returning a redacted message");
153
143
  }
@@ -169,9 +159,7 @@ function _validateCreateOpts(opts) {
169
159
  throw _err("BAD_OPT", "notify.create: queue must be a b.queue-shaped handle (enqueue fn)");
170
160
  }
171
161
  }
172
- if (opts.clock !== undefined && typeof opts.clock !== "function") {
173
- throw _err("BAD_OPT", "notify.create: clock must be a function");
174
- }
162
+ validateOpts.optionalFunction(opts.clock, "notify.create: clock", NotifyError);
175
163
  }
176
164
 
177
165
  // ---- Built-in transports ----
@@ -183,9 +171,7 @@ function httpJson(opts) {
183
171
  if (!opts || typeof opts !== "object") {
184
172
  throw _err("BAD_OPT", "notify.transports.httpJson: opts must be { url, ... }");
185
173
  }
186
- if (typeof opts.url !== "string" || opts.url.length === 0) {
187
- throw _err("BAD_OPT", "notify.transports.httpJson: url is required");
188
- }
174
+ validateOpts.requireNonEmptyString(opts.url, "notify.transports.httpJson: url", NotifyError, "BAD_OPT");
189
175
  var allowedProtocols = opts.allowHttp ? safeUrl.ALLOW_HTTP_ALL : safeUrl.ALLOW_HTTP_TLS;
190
176
  var allowInternal = opts.allowInternal != null ? opts.allowInternal : null;
191
177
  // Validate URL at create time so bad URLs surface at boot, not at first send.
@@ -270,18 +256,14 @@ function httpJson(opts) {
270
256
  // log — fire-and-forget developer logger. Never throws; audit + obs still emit.
271
257
  function logTransport(opts) {
272
258
  opts = opts || {};
259
+ // bootLog.boot() returns a callable with .info / .warn / .error
260
+ // attached; that shape satisfies the operator-supplied opts.logger
261
+ // contract directly. No fallback wrapper needed.
273
262
  var logger;
274
263
  if (opts.logger && typeof opts.logger.info === "function") {
275
264
  logger = opts.logger;
276
265
  } else {
277
266
  logger = bootLog.boot("notify.log");
278
- // boot returns a fn-with-fields shape; tolerate both.
279
- if (typeof logger === "function") {
280
- logger = { info: logger, error: function (m) { try { console.error(m); } catch (_e) {} } };
281
- }
282
- if (typeof logger.error !== "function") {
283
- logger.error = function (m) { try { console.error(m); } catch (_e) {} };
284
- }
285
267
  }
286
268
  return {
287
269
  name: opts.name || "log",
@@ -321,16 +303,16 @@ function create(opts) {
321
303
  "queue", "clock",
322
304
  ], "notify");
323
305
  _validateCreateOpts(opts);
306
+ var cfg = validateOpts.applyDefaults(opts, DEFAULTS);
324
307
 
325
- var auditSuccess = (opts.auditSuccess === undefined) ? DEFAULTS.auditSuccess : opts.auditSuccess;
326
- var auditFailures = (opts.auditFailures === undefined) ? DEFAULTS.auditFailures : opts.auditFailures;
308
+ var auditSuccess = cfg.auditSuccess;
309
+ var auditFailures = cfg.auditFailures;
327
310
  var audit = opts.audit || null;
328
311
  var redactFn = (typeof opts.redact === "function")
329
312
  ? opts.redact
330
313
  // Default: b.redact.redact — the framework's PII detector chain.
331
314
  : function (m) { return redactModule().redact(m); };
332
- var defaultTimeoutMs = (opts.defaultTimeoutMs === undefined)
333
- ? DEFAULTS.defaultTimeoutMs : opts.defaultTimeoutMs;
315
+ var defaultTimeoutMs = cfg.defaultTimeoutMs;
334
316
  var defaultRetry = opts.defaultRetry || null;
335
317
  var defaultBreaker = opts.defaultBreaker || null;
336
318
  var operatorQueue = opts.queue || null;
@@ -362,11 +344,7 @@ function create(opts) {
362
344
  catch (_e) { /* drop-silent — observability sink must not crash send() */ }
363
345
  }
364
346
 
365
- function _emitAudit(action, info) {
366
- if (!audit) return;
367
- try { audit.safeEmit(Object.assign({ action: action }, info || {})); }
368
- catch (_e) { /* audit best-effort */ }
369
- }
347
+ var _emitAudit = validateOpts.makeAuditEmitter(audit);
370
348
 
371
349
  function _actor(callerOpts) {
372
350
  return requestHelpers.resolveActorWithOverride(callerOpts);
package/lib/ntp-check.js CHANGED
@@ -34,7 +34,8 @@ var NTP_TO_UNIX_OFFSET_SECONDS = 2208988800;
34
34
 
35
35
  var DEFAULT_SERVERS = ["pool.ntp.org", "time.cloudflare.com"];
36
36
  var DEFAULT_PORT = 123;
37
- var DEFAULT_TIMEOUT_MS = 3000;
37
+ var DEFAULT_TIMEOUT_MS = C.TIME.seconds(3);
38
+ var NTP_PACKET_BYTES = C.BYTES.bytes(48); // RFC 5905 §7.3
38
39
 
39
40
  var DEFAULT_DRIFT_WARN_MS = C.TIME.minutes(5);
40
41
  var DEFAULT_DRIFT_FATAL_MS = C.TIME.hours(1);
@@ -105,9 +106,9 @@ function querySingle(server, opts) {
105
106
  }, timeoutMs);
106
107
  timer.unref();
107
108
 
108
- // SNTPv4 client request: 48-byte buffer, byte 0 = 0b00_100_011 = 0x23
109
+ // SNTPv4 client request: NTP_PACKET_BYTES buffer, byte 0 = 0b00_100_011 = 0x23
109
110
  // LI=0 (no warning), VN=4, Mode=3 (client). Other bytes zero.
110
- var req = Buffer.alloc(48);
111
+ var req = Buffer.alloc(NTP_PACKET_BYTES);
111
112
  req[0] = 0x23;
112
113
  var sendTimeMs = Date.now();
113
114
 
@@ -119,15 +120,15 @@ function querySingle(server, opts) {
119
120
  socket.on("message", function (msg) {
120
121
  clearTimeout(timer);
121
122
  var receiveTimeMs = Date.now();
122
- if (!Buffer.isBuffer(msg) || msg.length < 48) {
123
+ if (!Buffer.isBuffer(msg) || msg.length < NTP_PACKET_BYTES) {
123
124
  return done({ code: "ntp/bad-reply", message: "reply too short (" + (msg && msg.length) + " bytes)" });
124
125
  }
125
126
  // Bytes 40-47 = Transmit Timestamp (NTP epoch seconds.fraction)
126
127
  var ntpSeconds = msg.readUInt32BE(40);
127
128
  var ntpFraction = msg.readUInt32BE(44);
128
129
  var serverUnixSeconds = ntpSeconds - NTP_TO_UNIX_OFFSET_SECONDS;
129
- var fracMs = Math.round(ntpFraction / 0x100000000 * 1000);
130
- var serverTimeMs = serverUnixSeconds * 1000 + fracMs;
130
+ var fracMs = Math.round(C.TIME.seconds(ntpFraction / 0x100000000));
131
+ var serverTimeMs = C.TIME.seconds(serverUnixSeconds) + fracMs;
131
132
 
132
133
  // Round-trip-corrected drift: assume the server's reply transmit
133
134
  // timestamp is approximately at the midpoint of our send/receive.