@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
@@ -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.
@@ -52,8 +52,40 @@ function isNonNegativeFiniteInt(value) {
52
52
  Number.isInteger(value) && value >= 0;
53
53
  }
54
54
 
55
+ // requirePositiveFiniteIntIfPresent / requireNonNegativeFiniteIntIfPresent —
56
+ // optional-shape gates that throw via the caller's framework-error class
57
+ // when the value is present but invalid. Replaces the per-file
58
+ // `if (opts.X !== undefined) { if (!nb.isYFiniteInt(opts.X)) throw new
59
+ // XError(...); }` cascade that app-shutdown / bundler / dev /
60
+ // http-client-cookie-jar / external-db-migrate / restore-rollback
61
+ // previously rolled by hand.
62
+ //
63
+ // var nb = require("./numeric-bounds");
64
+ // nb.requirePositiveFiniteIntIfPresent(opts.graceMs,
65
+ // "app-shutdown.create: opts.graceMs", AppShutdownError,
66
+ // "app-shutdown/bad-grace-ms");
67
+ function requirePositiveFiniteIntIfPresent(value, label, errorClass, code) {
68
+ if (value === undefined) return value;
69
+ if (!isPositiveFiniteInt(value)) {
70
+ throw new errorClass(code,
71
+ (label || "value") + " must be a positive finite integer; got " + shape(value));
72
+ }
73
+ return value;
74
+ }
75
+
76
+ function requireNonNegativeFiniteIntIfPresent(value, label, errorClass, code) {
77
+ if (value === undefined) return value;
78
+ if (!isNonNegativeFiniteInt(value)) {
79
+ throw new errorClass(code,
80
+ (label || "value") + " must be a non-negative finite integer; got " + shape(value));
81
+ }
82
+ return value;
83
+ }
84
+
55
85
  module.exports = {
56
- shape: shape,
57
- isPositiveFiniteInt: isPositiveFiniteInt,
58
- isNonNegativeFiniteInt: isNonNegativeFiniteInt,
86
+ shape: shape,
87
+ isPositiveFiniteInt: isPositiveFiniteInt,
88
+ isNonNegativeFiniteInt: isNonNegativeFiniteInt,
89
+ requirePositiveFiniteIntIfPresent: requirePositiveFiniteIntIfPresent,
90
+ requireNonNegativeFiniteIntIfPresent: requireNonNegativeFiniteIntIfPresent,
59
91
  };
@@ -29,12 +29,42 @@
29
29
  * gap rather than half-implementing one auth path.
30
30
  */
31
31
  var azureBlob = require("./azure-blob");
32
+ var C = require("../constants");
32
33
  var httpClient = require("../http-client");
34
+ var requestHelpers = require("../request-helpers");
33
35
  var safeUrl = require("../safe-url");
34
36
  var { ObjectStoreError } = require("../framework-error");
35
37
 
36
38
  var _err = ObjectStoreError.factory;
37
39
 
40
+ // HTTP status constants used in the bucket-ops expectStatus arrays.
41
+ // OK / NOT_FOUND / CONFLICT come from the framework's HTTP_STATUS table;
42
+ // CREATED (201) and ACCEPTED (202) are not in the table, so they're
43
+ // named locally with their RFC 7231 values.
44
+ var HTTP_OK = requestHelpers.HTTP_STATUS.OK;
45
+ var HTTP_NOT_FOUND = requestHelpers.HTTP_STATUS.NOT_FOUND;
46
+ var HTTP_CONFLICT = requestHelpers.HTTP_STATUS.CONFLICT;
47
+ var HTTP_CREATED = 201; // RFC 7231 §6.3.2
48
+ var HTTP_ACCEPTED = 202; // RFC 7231 §6.3.3
49
+
50
+ // Internal URL builder — endpoint + path string from validated config.
51
+ // Routes through safeUrl.parse so the protocol allowlist + length cap
52
+ // apply uniformly.
53
+ function _internalUrl(input, allowedProtocols) {
54
+ return safeUrl.parse(input, {
55
+ allowedProtocols: allowedProtocols || safeUrl.ALLOW_HTTP_TLS,
56
+ errorClass: ObjectStoreError,
57
+ maxUrlLength: C.BYTES.kib(32),
58
+ });
59
+ }
60
+
61
+ // Maximum length for a tag passed into the framework-internal XML
62
+ // extractor. The XML payloads come back from Azure (server-controlled)
63
+ // but the tag name is framework-supplied; cap it explicitly so the
64
+ // open/close substrings used for indexOf walks can't be driven against
65
+ // an oversize pattern.
66
+ var MAX_XML_TAG_LENGTH = 128;
67
+
38
68
  // Azure container names: 3-63 chars, lowercase alphanumeric + hyphens,
39
69
  // no consecutive hyphens, must start and end with letter or digit.
40
70
  var CONTAINER_NAME_RE = /^[a-z0-9](?:[a-z0-9]|-(?!-))*[a-z0-9]$/;
@@ -49,7 +79,9 @@ function _validateContainerName(name) {
49
79
  "azure-blob bucketOps: container name must be 3-63 chars (got " +
50
80
  name.length + ")", true);
51
81
  }
52
- if (!CONTAINER_NAME_RE.test(name)) {
82
+ // Length is bounded above (3..63) before the regex runs so the
83
+ // pattern can't be driven against an unbounded input.
84
+ if (name.length > 63 || !CONTAINER_NAME_RE.test(name)) {
53
85
  throw _err("BUCKET_INVALID_NAME",
54
86
  "azure-blob bucketOps: container name '" + name + "' is invalid; " +
55
87
  "lowercase letters / digits / hyphens only, no consecutive hyphens, " +
@@ -119,16 +151,42 @@ function _buildCorsXml(rules) {
119
151
 
120
152
  // Tiny XML extractor — pulls every occurrence of <Tag>value</Tag>
121
153
  // and returns an array of value strings. Sufficient for the limited
122
- // shapes we read (Containers list, container metadata).
154
+ // shapes we read (Containers list, container metadata). The tag is
155
+ // framework-supplied (call sites pass hardcoded tag names such as
156
+ // "Container", "Name", "Last-Modified") but cap the length explicitly
157
+ // so the open/close substrings used for indexOf walks can't be driven
158
+ // against an oversize pattern under any future call shape.
159
+ function _validateTag(tag) {
160
+ if (typeof tag !== "string" || tag.length === 0 || tag.length > MAX_XML_TAG_LENGTH) {
161
+ throw _err("BUCKET_INVALID_NAME",
162
+ "azure-blob bucketOps: XML tag must be a non-empty string of length " +
163
+ "<= " + MAX_XML_TAG_LENGTH, true);
164
+ }
165
+ if (!/^[A-Za-z][A-Za-z0-9_.-]*$/.test(tag)) {
166
+ throw _err("BUCKET_INVALID_NAME",
167
+ "azure-blob bucketOps: XML tag '" + tag + "' has invalid characters", true);
168
+ }
169
+ }
170
+
123
171
  function _extractAll(xml, tag) {
172
+ _validateTag(tag);
173
+ var open = "<" + tag + ">";
174
+ var close = "</" + tag + ">";
124
175
  var out = [];
125
- var re = new RegExp("<" + tag + ">([\\s\\S]*?)</" + tag + ">", "g");
126
- var m;
127
- while ((m = re.exec(xml)) !== null) out.push(m[1]);
176
+ var i = 0;
177
+ while (true) {
178
+ var s = xml.indexOf(open, i);
179
+ if (s === -1) break;
180
+ var e = xml.indexOf(close, s + open.length);
181
+ if (e === -1) break;
182
+ out.push(xml.slice(s + open.length, e));
183
+ i = e + close.length;
184
+ }
128
185
  return out;
129
186
  }
130
187
 
131
188
  function _extractBlocks(xml, tag) {
189
+ _validateTag(tag);
132
190
  var open = "<" + tag + ">";
133
191
  var close = "</" + tag + ">";
134
192
  var blocks = [];
@@ -202,7 +260,7 @@ function create(config) {
202
260
  async function createContainer(name, opts) {
203
261
  _validateContainerName(name);
204
262
  opts = opts || {};
205
- var url = new URL(endpoint + "/" + name + "?restype=container");
263
+ var url = _internalUrl(endpoint + "/" + name + "?restype=container", allowedProtocols);
206
264
  var headers = { "Content-Length": "0" };
207
265
  if (opts.publicAccess) {
208
266
  if (opts.publicAccess !== "blob" && opts.publicAccess !== "container") {
@@ -213,8 +271,8 @@ function create(config) {
213
271
  headers["x-ms-blob-public-access"] = opts.publicAccess;
214
272
  }
215
273
  var signed = _sign("PUT", url, headers);
216
- var res = await _request("PUT", url, signed, null, [201, 409]);
217
- if (res.statusCode === 409) {
274
+ var res = await _request("PUT", url, signed, null, [HTTP_CREATED, HTTP_CONFLICT]);
275
+ if (res.statusCode === HTTP_CONFLICT) {
218
276
  throw _err("BUCKET_ALREADY_OWNED",
219
277
  "azure-blob bucketOps: container '" + name +
220
278
  "' already exists or was recently deleted", true);
@@ -224,19 +282,19 @@ function create(config) {
224
282
 
225
283
  async function deleteContainer(name) {
226
284
  _validateContainerName(name);
227
- var url = new URL(endpoint + "/" + name + "?restype=container");
285
+ var url = _internalUrl(endpoint + "/" + name + "?restype=container", allowedProtocols);
228
286
  var signed = _sign("DELETE", url, {});
229
- var res = await _request("DELETE", url, signed, null, [202, 404]);
230
- return res.statusCode === 202;
287
+ var res = await _request("DELETE", url, signed, null, [HTTP_ACCEPTED, HTTP_NOT_FOUND]);
288
+ return res.statusCode === HTTP_ACCEPTED;
231
289
  }
232
290
 
233
291
  async function listContainers(opts) {
234
292
  opts = opts || {};
235
- var url = new URL(endpoint + "/?comp=list");
293
+ var url = _internalUrl(endpoint + "/?comp=list", allowedProtocols);
236
294
  if (opts.prefix) url.searchParams.set("prefix", opts.prefix);
237
295
  if (opts.maxResults != null) url.searchParams.set("maxresults", String(opts.maxResults));
238
296
  var signed = _sign("GET", url, {});
239
- var res = await _request("GET", url, signed, null, [200]);
297
+ var res = await _request("GET", url, signed, null, [HTTP_OK]);
240
298
  var xml = Buffer.isBuffer(res.body) ? res.body.toString("utf8") :
241
299
  typeof res.body === "string" ? res.body :
242
300
  "";
@@ -261,13 +319,13 @@ function create(config) {
261
319
  rules.forEach(_validateCorsRule);
262
320
  var xml = _buildCorsXml(rules);
263
321
  var bodyBuf = Buffer.from(xml, "utf8");
264
- var url = new URL(endpoint + "/?restype=service&comp=properties");
322
+ var url = _internalUrl(endpoint + "/?restype=service&comp=properties", allowedProtocols);
265
323
  var headers = {
266
324
  "Content-Type": "application/xml",
267
325
  "Content-Length": String(bodyBuf.length),
268
326
  };
269
327
  var signed = _sign("PUT", url, headers);
270
- await _request("PUT", url, signed, bodyBuf, [202]);
328
+ await _request("PUT", url, signed, bodyBuf, [HTTP_ACCEPTED]);
271
329
  return { rulesApplied: rules.length };
272
330
  }
273
331