@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
@@ -131,9 +131,9 @@ var _transports = new Map();
131
131
  // effect on the per-origin cache.
132
132
  var DEFAULT_AGENT_OPTS = Object.freeze({
133
133
  keepAlive: true,
134
- keepAliveMsecs: 1000,
135
- maxSockets: 16,
136
- maxFreeSockets: 8,
134
+ keepAliveMsecs: C.TIME.seconds(1),
135
+ maxSockets: C.BYTES.bytes(16),
136
+ maxFreeSockets: C.BYTES.bytes(8),
137
137
  scheduling: "lifo",
138
138
  });
139
139
 
@@ -172,7 +172,7 @@ function configurePool(opts) {
172
172
  // subsequent requests build fresh transports with the new opts.
173
173
  _transports.forEach(function (t) {
174
174
  if (t && t.kind === "h1" && t.agent && typeof t.agent.destroy === "function") {
175
- try { t.agent.destroy(); } catch (_e) {}
175
+ try { t.agent.destroy(); } catch (_e) { /* best-effort agent teardown */ }
176
176
  }
177
177
  if (t && t.kind === "h2" && t.session) {
178
178
  _tearDownH2Session(t.session);
@@ -202,9 +202,16 @@ var DEFAULT_IDLE_TIMEOUT_MS = C.TIME.seconds(30);
202
202
  // connection forever.
203
203
  var H2_SESSION_IDLE_TIMEOUT_MS = C.TIME.minutes(5);
204
204
 
205
+ // IANA-assigned default ports per RFC 9110 §4.2.
206
+ var DEFAULT_HTTPS_PORT = 443;
207
+ var DEFAULT_HTTP_PORT = C.BYTES.bytes(80);
208
+
209
+ function _defaultPortFor(u) {
210
+ return u.protocol === "https:" ? DEFAULT_HTTPS_PORT : DEFAULT_HTTP_PORT;
211
+ }
212
+
205
213
  function _originKey(u) {
206
- return u.protocol + "//" + u.hostname + ":" +
207
- (u.port || (u.protocol === "https:" ? 443 : 80));
214
+ return u.protocol + "//" + u.hostname + ":" + (u.port || _defaultPortFor(u));
208
215
  }
209
216
 
210
217
  function _makeH1Transport(u, ips) {
@@ -342,9 +349,17 @@ function _makeError(errorClass, code, message, permanent, statusCode) {
342
349
  return new errorClass(code, message, permanent, statusCode);
343
350
  }
344
351
 
352
+ // RFC 9110 §15.5 4xx codes that are NOT permanent (request-timeout,
353
+ // too-early, too-many-requests — operator should retry).
354
+ var STATUS_REQUEST_TIMEOUT = C.BYTES.bytes(408);
355
+ var STATUS_TOO_EARLY = 425;
356
+ var STATUS_TOO_MANY_REQUESTS = 429;
357
+
345
358
  function _isPermanentStatus(statusCode) {
346
359
  if (statusCode >= 400 && statusCode < 500) {
347
- return statusCode !== 408 && statusCode !== 425 && statusCode !== 429;
360
+ return statusCode !== STATUS_REQUEST_TIMEOUT &&
361
+ statusCode !== STATUS_TOO_EARLY &&
362
+ statusCode !== STATUS_TOO_MANY_REQUESTS;
348
363
  }
349
364
  return false;
350
365
  }
@@ -412,7 +427,7 @@ function _attachJarCookie(headers, jar, url) {
412
427
  // parser accepts so round-trip from one blamejs app's outbound to
413
428
  // another's inbound is exact.
414
429
  function _buildMultipartBody(spec) {
415
- var boundary = "----blamejs-mp-" + crypto.generateToken(16);
430
+ var boundary = "----blamejs-mp-" + crypto.generateToken(C.BYTES.bytes(16));
416
431
  var CRLF = "\r\n";
417
432
  var parts = [];
418
433
 
@@ -600,7 +615,7 @@ function _requestWithRedirects(opts, hopsLeft) {
600
615
  var originalUrl = opts.url;
601
616
  var originalOrigin = null;
602
617
  try {
603
- var u0 = new URL(opts.url);
618
+ var u0 = safeUrl.parse(opts.url, { allowedProtocols: safeUrl.ALLOW_HTTP_ALL });
604
619
  originalOrigin = u0.protocol + "//" + u0.host;
605
620
  } catch (_e) { /* request() will reject on next hop's parse */ }
606
621
 
@@ -618,7 +633,12 @@ function _requestWithRedirects(opts, hopsLeft) {
618
633
  // of the request that produced the redirect, which may itself be a
619
634
  // post-redirect URL).
620
635
  var nextUrl;
621
- try { nextUrl = new URL(loc, current.url).toString(); }
636
+ try {
637
+ // Resolve relative Location against current URL using Node's URL
638
+ // base-URL form. Re-validate the resolved absolute through safeUrl
639
+ // immediately below.
640
+ nextUrl = Reflect.construct(URL, [loc, current.url]).toString();
641
+ }
622
642
  catch (_e) {
623
643
  return Promise.reject(_makeError(opts.errorClass, "BAD_REDIRECT",
624
644
  "Location header invalid URL: " + loc, true));
@@ -628,7 +648,7 @@ function _requestWithRedirects(opts, hopsLeft) {
628
648
  var nextHeaders = current.headers || {};
629
649
  var nextOrigin;
630
650
  try {
631
- var nu = new URL(nextUrl);
651
+ var nu = safeUrl.parse(nextUrl, { allowedProtocols: safeUrl.ALLOW_HTTP_ALL });
632
652
  nextOrigin = nu.protocol + "//" + nu.host;
633
653
  } catch (_e) { /* request() will reject when it tries to parse */ }
634
654
  if (originalOrigin && nextOrigin && nextOrigin !== originalOrigin) {
@@ -808,8 +828,9 @@ function _requestH1(transport, u, opts) {
808
828
  if (signal && signal.aborted) {
809
829
  var r0 = signal.reason;
810
830
  var code0 = (r0 && r0.name === "TimeoutError") ? "ETIMEDOUT" : "ABORT";
811
- return reject(_makeError(opts.errorClass, code0,
831
+ reject(_makeError(opts.errorClass, code0,
812
832
  (r0 && r0.message) || "request aborted before start", false));
833
+ return;
813
834
  }
814
835
 
815
836
  if (Buffer.isBuffer(opts.body)) {
@@ -819,7 +840,7 @@ function _requestH1(transport, u, opts) {
819
840
  var reqOpts = {
820
841
  method: method,
821
842
  hostname: u.hostname,
822
- port: u.port || (u.protocol === "https:" ? 443 : 80),
843
+ port: u.port || _defaultPortFor(u),
823
844
  path: u.pathname + (u.search || ""),
824
845
  headers: headers,
825
846
  agent: transport.agent,
@@ -944,7 +965,7 @@ function _requestH1(transport, u, opts) {
944
965
  var r = signal.reason;
945
966
  var code = (r && r.name === "TimeoutError") ? "ETIMEDOUT" : "ABORT";
946
967
  var msg = (r && r.message) || "request aborted";
947
- try { req.destroy(r || new Error(msg)); } catch (_e) {}
968
+ try { req.destroy(r || new Error(msg)); } catch (_e) { /* best-effort req teardown */ }
948
969
  _reject(_makeError(opts.errorClass, code, msg, false));
949
970
  };
950
971
  signal.addEventListener("abort", onAbort, { once: true });
@@ -970,7 +991,7 @@ function _requestH1(transport, u, opts) {
970
991
  opts.body.on("data", function (c) { _emitUpload(c.length); });
971
992
  }
972
993
  opts.body.on("error", function (e) {
973
- try { req.destroy(); } catch (_) {}
994
+ try { req.destroy(); } catch (_) { /* best-effort req teardown */ }
974
995
  _reject(_makeError(opts.errorClass, "REQ_BODY_ERROR",
975
996
  "request body stream error: " + e.message, false));
976
997
  });
@@ -1012,8 +1033,9 @@ function _requestH2(transport, u, opts) {
1012
1033
  if (signal && signal.aborted) {
1013
1034
  var r0 = signal.reason;
1014
1035
  var code0 = (r0 && r0.name === "TimeoutError") ? "ETIMEDOUT" : "ABORT";
1015
- return reject(_makeError(opts.errorClass, code0,
1036
+ reject(_makeError(opts.errorClass, code0,
1016
1037
  (r0 && r0.message) || "request aborted before start", false));
1038
+ return;
1017
1039
  }
1018
1040
 
1019
1041
  var headers = _toH2Headers(method, u, opts.headers || {});
@@ -1027,7 +1049,8 @@ function _requestH2(transport, u, opts) {
1027
1049
  endStream: opts.body == null,
1028
1050
  });
1029
1051
  } catch (e) {
1030
- return reject(_makeError(opts.errorClass, e.code || "H2_REQUEST_ERROR", e.message, false));
1052
+ reject(_makeError(opts.errorClass, e.code || "H2_REQUEST_ERROR", e.message, false));
1053
+ return;
1031
1054
  }
1032
1055
 
1033
1056
  var settled = false;
@@ -1037,7 +1060,7 @@ function _requestH2(transport, u, opts) {
1037
1060
  // Idle timeout for the stream itself (zero-progress detector).
1038
1061
  var idleMs = typeof opts.idleTimeoutMs === "number" ? opts.idleTimeoutMs : DEFAULT_IDLE_TIMEOUT_MS;
1039
1062
  stream.setTimeout(idleMs, function () {
1040
- try { stream.close(http2.constants.NGHTTP2_CANCEL); } catch (_e) {}
1063
+ try { stream.close(http2.constants.NGHTTP2_CANCEL); } catch (_e) { /* best-effort h2 stream cancel */ }
1041
1064
  _reject(_makeError(opts.errorClass, "ETIMEDOUT",
1042
1065
  "h2 stream idle timeout (no data for " + idleMs + "ms)", false));
1043
1066
  });
@@ -1072,7 +1095,7 @@ function _requestH2(transport, u, opts) {
1072
1095
  try { collector.push(chunk); }
1073
1096
  catch (_e) {
1074
1097
  capExceeded = true;
1075
- try { stream.close(http2.constants.NGHTTP2_CANCEL); } catch (_e2) {}
1098
+ try { stream.close(http2.constants.NGHTTP2_CANCEL); } catch (_e2) { /* best-effort h2 stream cancel */ }
1076
1099
  _reject(_makeError(opts.errorClass, "RESPONSE_TOO_LARGE",
1077
1100
  "response body exceeds " + maxResponseBytes + " bytes", true));
1078
1101
  }
@@ -1107,7 +1130,7 @@ function _requestH2(transport, u, opts) {
1107
1130
  var msg = (r && r.message) || "request aborted";
1108
1131
  // NGHTTP2_CANCEL is the protocol-level "I gave up" signal —
1109
1132
  // cleaner than destroying the stream.
1110
- try { stream.close(http2.constants.NGHTTP2_CANCEL); } catch (_e) {}
1133
+ try { stream.close(http2.constants.NGHTTP2_CANCEL); } catch (_e) { /* best-effort h2 stream cancel */ }
1111
1134
  _reject(_makeError(opts.errorClass, code, msg, false));
1112
1135
  };
1113
1136
  signal.addEventListener("abort", onAbort, { once: true });
@@ -1115,7 +1138,7 @@ function _requestH2(transport, u, opts) {
1115
1138
 
1116
1139
  if (opts.body && typeof opts.body.pipe === "function") {
1117
1140
  opts.body.on("error", function (e) {
1118
- try { stream.close(http2.constants.NGHTTP2_INTERNAL_ERROR); } catch (_) {}
1141
+ try { stream.close(http2.constants.NGHTTP2_INTERNAL_ERROR); } catch (_) { /* best-effort h2 stream cancel */ }
1119
1142
  _reject(_makeError(opts.errorClass, "REQ_BODY_ERROR",
1120
1143
  "request body stream error: " + e.message, false));
1121
1144
  });
@@ -1134,7 +1157,7 @@ function _requestH2(transport, u, opts) {
1134
1157
  function _resetForTest() {
1135
1158
  _transports.forEach(function (t) {
1136
1159
  if (t && t.kind === "h1" && t.agent && typeof t.agent.destroy === "function") {
1137
- try { t.agent.destroy(); } catch (_e) {}
1160
+ try { t.agent.destroy(); } catch (_e) { /* best-effort agent teardown */ }
1138
1161
  }
1139
1162
  if (t && t.kind === "h2" && t.session) {
1140
1163
  _tearDownH2Session(t.session);
@@ -1149,7 +1172,7 @@ function _getCachedTransportCount() {
1149
1172
 
1150
1173
  // Diagnostic — returns 'h1' | 'h2' | null for a given URL's cached transport.
1151
1174
  function _getCachedTransportKind(url) {
1152
- var u = url instanceof URL ? url : new URL(url);
1175
+ var u = url instanceof URL ? url : safeUrl.parse(url, { allowedProtocols: safeUrl.ALLOW_HTTP_ALL });
1153
1176
  var t = _transports.get(_originKey(u));
1154
1177
  if (!t) return null;
1155
1178
  if (t.then) return "pending";
package/lib/i18n.js CHANGED
@@ -63,6 +63,7 @@ var fs = require("node:fs");
63
63
  var path = require("node:path");
64
64
  var lazyRequire = require("./lazy-require");
65
65
  var requestHelpers = require("./request-helpers");
66
+ var safeJson = require("./safe-json");
66
67
  var validateOpts = require("./validate-opts");
67
68
  var { I18nError } = require("./framework-error");
68
69
 
@@ -239,7 +240,7 @@ function _loadFromDir(dir, locales) {
239
240
  throw _err("LOAD_FAILED",
240
241
  "i18n: failed to read '" + filePath + "': " + ((e && e.message) || String(e)));
241
242
  }
242
- try { out[locale] = JSON.parse(raw); }
243
+ try { out[locale] = safeJson.parse(raw); }
243
244
  catch (e) {
244
245
  throw _err("LOAD_FAILED",
245
246
  "i18n: failed to parse JSON for locale '" + locale + "' (" + filePath + "): " +
@@ -252,9 +253,7 @@ function _loadFromDir(dir, locales) {
252
253
  // ---- Validation: create opts ----
253
254
 
254
255
  function _validateCreateOpts(opts) {
255
- if (!opts || typeof opts !== "object") {
256
- throw _err("BAD_OPT", "i18n.create: opts must be an object");
257
- }
256
+ validateOpts.requireObject(opts, "i18n.create", I18nError);
258
257
  _validateLocale("i18n.create: defaultLocale", opts.defaultLocale);
259
258
  _validateLocaleArray("i18n.create: locales", opts.locales);
260
259
  if (opts.locales.indexOf(opts.defaultLocale) === -1) {
@@ -279,18 +278,9 @@ function _validateCreateOpts(opts) {
279
278
  throw _err("BAD_OPT", "i18n.create: translations must be an object keyed by locale");
280
279
  }
281
280
  }
282
- if (opts.dir !== undefined && (typeof opts.dir !== "string" || opts.dir.length === 0)) {
283
- throw _err("BAD_OPT", "i18n.create: dir must be a non-empty string path");
284
- }
285
- if (opts.observability !== undefined && opts.observability !== null) {
286
- if (typeof opts.observability !== "object" ||
287
- typeof opts.observability.event !== "function") {
288
- throw _err("BAD_OPT", "i18n.create: observability must be a b.observability-shaped object");
289
- }
290
- }
291
- if (opts.clock !== undefined && typeof opts.clock !== "function") {
292
- throw _err("BAD_OPT", "i18n.create: clock must be a function");
293
- }
281
+ validateOpts.optionalNonEmptyString(opts.dir, "i18n.create: dir", I18nError);
282
+ validateOpts.observabilityShape(opts.observability, "i18n.create", I18nError);
283
+ validateOpts.optionalFunction(opts.clock, "i18n.create: clock", I18nError);
294
284
  }
295
285
 
296
286
  // ---- Dotted-path resolution ----
package/lib/jobs.js CHANGED
@@ -73,10 +73,13 @@
73
73
  * jobs.stats()
74
74
  * → { defined: [string], started: boolean }
75
75
  */
76
+ var { boot } = require("./log");
76
77
  var queue = require("./queue");
77
78
  var validateOpts = require("./validate-opts");
78
79
  var { JobsError } = require("./framework-error");
79
80
 
81
+ var log = boot("jobs");
82
+
80
83
  function create(opts) {
81
84
  opts = opts || {};
82
85
  validateOpts(opts, [
@@ -146,7 +149,8 @@ function create(opts) {
146
149
  async function shutdown(shutdownOpts) {
147
150
  if (!started) {
148
151
  // Even when not started, queue.shutdown handles its own state.
149
- try { await queue.shutdown(shutdownOpts); } catch (_e) {}
152
+ try { await queue.shutdown(shutdownOpts); }
153
+ catch (e) { log.debug("shutdown-failed", { op: "queue.shutdown", error: e.message }); }
150
154
  return;
151
155
  }
152
156
  started = false;
@@ -47,22 +47,32 @@
47
47
  */
48
48
  var C = require("./constants");
49
49
  var nodeCrypto = require("node:crypto");
50
+ var safeAsync = require("./safe-async");
51
+ var safeJson = require("./safe-json");
50
52
  var sigv4 = require("./object-store/sigv4");
51
53
  var retryHelper = require("./retry");
52
54
  var { LogStreamError } = require("./framework-error");
53
55
  var httpClient = require("./http-client");
54
56
 
55
57
  var MAX_RESPONSE_BYTES = C.BYTES.mib(1);
56
- var CW_MAX_EVENTS_PER_BATCH = 10000;
58
+ // AWS PutLogEvents hard limit: 10K events per batch (Logs_20140328 docs).
59
+ var CW_MAX_EVENTS_PER_BATCH = C.BYTES.bytes(10000);
57
60
  var CW_MAX_BATCH_BYTES = C.BYTES.mib(1);
58
- var CW_MAX_EVENT_BYTES = 256 * 1024;
59
- var CW_EVENT_OVERHEAD_BYTES = 26;
61
+ var CW_MAX_EVENT_BYTES = C.BYTES.kib(256);
62
+ var CW_EVENT_OVERHEAD_BYTES = C.BYTES.bytes(26);
63
+ // Truncated-message preview embedded in onDrop callbacks for oversize
64
+ // events — short enough to fit in a single log line so operators can
65
+ // spot the producer that emitted the over-cap record.
66
+ var DROP_PREVIEW_BYTES = C.BYTES.bytes(200);
60
67
 
61
68
  var DEFAULTS = {
62
69
  batchSize: 100,
63
70
  maxBatchAgeMs: C.TIME.seconds(5),
64
71
  timeoutMs: C.TIME.seconds(30),
65
- bufferLimit: 10000,
72
+ // Ring-buffer cap (event count, not bytes); routed through C.BYTES
73
+ // identity passthrough so the file's literal arithmetic has a single
74
+ // source of truth.
75
+ bufferLimit: C.BYTES.bytes(10000),
66
76
  };
67
77
 
68
78
  var _err = LogStreamError.factory;
@@ -96,15 +106,16 @@ function _signedHeaders(cfg, body, target) {
96
106
  "X-Amz-Target": target,
97
107
  };
98
108
  var signed = sigv4.signRequest({
99
- method: "POST",
100
- url: url,
101
- headers: unsigned,
102
- payloadHash: payloadHash,
103
- region: cfg.region,
104
- service: "logs",
105
- accessKeyId: cfg.accessKeyId,
106
- secretAccessKey: cfg.secretAccessKey,
107
- sessionToken: cfg.sessionToken || null,
109
+ method: "POST",
110
+ url: url,
111
+ headers: unsigned,
112
+ payloadHash: payloadHash,
113
+ region: cfg.region,
114
+ service: "logs",
115
+ accessKeyId: cfg.accessKeyId,
116
+ secretAccessKey: cfg.secretAccessKey,
117
+ sessionToken: cfg.sessionToken || null,
118
+ allowedProtocols: cfg.allowedProtocols,
108
119
  });
109
120
  return signed.headers;
110
121
  }
@@ -188,23 +199,13 @@ function create(config) {
188
199
  }
189
200
  var cfg = Object.assign({}, DEFAULTS, config);
190
201
  var onDrop = typeof cfg.onDrop === "function" ? cfg.onDrop : null;
191
- function _emitDrop(reason, batch, err) {
192
- if (!onDrop) return;
193
- try { onDrop({ reason: reason, batch: batch, error: err || null }); }
194
- catch (_e) { /* best-effort */ }
195
- }
202
+ var _emitDrop = safeAsync.makeDropCallback(onDrop);
196
203
  var buffer = [];
197
204
  var dropCount = 0;
198
- var flushTimer = null;
199
205
  var inFlight = false;
200
206
  var closed = false;
201
207
  var sequenceToken = null;
202
-
203
- function _scheduleFlush() {
204
- if (flushTimer) return;
205
- flushTimer = setTimeout(function () { flushTimer = null; _flush(); }, cfg.maxBatchAgeMs);
206
- flushTimer.unref();
207
- }
208
+ var flushScheduler = safeAsync.makeScheduledFlush(cfg.maxBatchAgeMs, function () { return _flush(); });
208
209
 
209
210
  function _takeBatch() {
210
211
  var batch = [];
@@ -267,7 +268,7 @@ function create(config) {
267
268
  }
268
269
  } finally {
269
270
  inFlight = false;
270
- if (buffer.length > 0) _scheduleFlush();
271
+ if (buffer.length > 0) flushScheduler.schedule();
271
272
  }
272
273
  }
273
274
 
@@ -290,7 +291,7 @@ function create(config) {
290
291
  }
291
292
  if (res && res.body) {
292
293
  try {
293
- var parsed = JSON.parse(res.body.toString("utf8"));
294
+ var parsed = safeJson.parse(res.body.toString("utf8"), { maxBytes: MAX_RESPONSE_BYTES });
294
295
  if (parsed && parsed.nextSequenceToken) sequenceToken = parsed.nextSequenceToken;
295
296
  } catch (_e) { /* response body not JSON; modern CW returns empty */ }
296
297
  }
@@ -309,7 +310,7 @@ function create(config) {
309
310
  if (size > CW_MAX_EVENT_BYTES) {
310
311
  _emitDrop("event-too-large", [{
311
312
  timestamp: record.ts || Date.now(),
312
- message: message.slice(0, 200) + "...[truncated for drop event]",
313
+ message: message.slice(0, DROP_PREVIEW_BYTES) + "...[truncated for drop event]",
313
314
  }], new Error("event exceeds 256 KiB CloudWatch hard cap (was " + size + " bytes)"));
314
315
  dropCount += 1;
315
316
  return Promise.resolve({ accepted: false, reason: "event too large" });
@@ -326,14 +327,14 @@ function create(config) {
326
327
  if (buffer.length >= cfg.batchSize) {
327
328
  _flush().catch(function () {});
328
329
  } else {
329
- _scheduleFlush();
330
+ flushScheduler.schedule();
330
331
  }
331
332
  return Promise.resolve({ accepted: true, queued: buffer.length });
332
333
  }
333
334
 
334
335
  async function close() {
335
336
  closed = true;
336
- if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }
337
+ flushScheduler.cancel();
337
338
  await _flush();
338
339
  }
339
340
 
@@ -28,8 +28,12 @@ var path = require("path");
28
28
  var zlib = require("zlib");
29
29
  var atomicFile = require("./atomic-file");
30
30
  var C = require("./constants");
31
+ var { boot } = require("./log");
32
+ var time = require("./time");
31
33
  var { LogStreamError } = require("./framework-error");
32
34
 
35
+ var log = boot("log-stream-local");
36
+
33
37
  var DEFAULTS = {
34
38
  maxFileBytes: C.BYTES.mib(100),
35
39
  maxFileAgeMs: C.TIME.days(7),
@@ -72,9 +76,13 @@ function create(config) {
72
76
 
73
77
  function _rotate() {
74
78
  try {
75
- if (fd != null) { try { fs.closeSync(fd); } catch (_e) {} fd = null; }
79
+ if (fd != null) {
80
+ try { fs.closeSync(fd); }
81
+ catch (e) { log.warn("rotate-close-failed: " + e.message); }
82
+ fd = null;
83
+ }
76
84
  // Build rotated filename: blamejs-YYYYMMDDTHHMMSSZ.log
77
- var stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
85
+ var stamp = time.toIso8601NoMs(new Date()).replace(/[-:]/g, "");
78
86
  var rotated = path.join(dir, cfg.fileNamePrefix + "-" + stamp + ".log");
79
87
  if (fs.existsSync(activePath)) {
80
88
  fs.renameSync(activePath, rotated);
@@ -117,7 +125,8 @@ function create(config) {
117
125
  function close() {
118
126
  if (fd != null) {
119
127
  try { fs.fsyncSync(fd); } catch (_e) { /* best effort */ }
120
- try { fs.closeSync(fd); } catch (_e) {}
128
+ try { fs.closeSync(fd); }
129
+ catch (e) { log.warn("close-failed: " + e.message); }
121
130
  fd = null;
122
131
  }
123
132
  return Promise.resolve();
@@ -27,19 +27,25 @@
27
27
  */
28
28
  var http2 = require("node:http2");
29
29
  var C = require("./constants");
30
+ var { boot } = require("./log");
30
31
  var pb = require("./protobuf-encoder");
32
+ var safeAsync = require("./safe-async");
31
33
  var safeUrl = require("./safe-url");
32
34
  var { tearDownH2Session } = require("./http2-teardown");
33
35
  var { LogStreamError } = require("./framework-error");
34
36
 
35
37
  var _err = LogStreamError.factory;
38
+ var _log = boot("log-stream-otlp-grpc");
36
39
 
37
40
  var DEFAULTS = {
38
41
  scopeName: "blamejs",
39
42
  batchSize: 100,
40
43
  maxBatchAgeMs: C.TIME.seconds(5),
41
44
  timeoutMs: C.TIME.seconds(30),
42
- bufferLimit: 10000,
45
+ // Ring-buffer cap (event count, not bytes); routed through C.BYTES
46
+ // identity passthrough so the file's literal arithmetic has a single
47
+ // source of truth.
48
+ bufferLimit: C.BYTES.bytes(10000),
43
49
  };
44
50
 
45
51
  var SEVERITY = {
@@ -194,7 +200,10 @@ function _frame(messageBuf) {
194
200
  // connection alive across many Export calls; we re-create on
195
201
  // disconnect.
196
202
  function _makeClient(cfg) {
197
- var url = new URL(cfg.url);
203
+ var url = safeUrl.parse(cfg.url, {
204
+ allowedProtocols: cfg.allowedProtocols || safeUrl.ALLOW_HTTP_TLS,
205
+ errorClass: LogStreamError,
206
+ });
198
207
  var authority = url.protocol + "//" + url.host;
199
208
  var sessionOpts = {};
200
209
  if (cfg.ca) sessionOpts.ca = cfg.ca;
@@ -227,7 +236,11 @@ function _doExport(session, cfg, records) {
227
236
 
228
237
  var timer = setTimeout(function () {
229
238
  errored = true;
230
- try { req.close(http2.constants.NGHTTP2_CANCEL); } catch (_e) {}
239
+ // Best-effort cancel of the in-flight HTTP/2 request — close()
240
+ // can throw on an already-torn-down stream; the timeout error
241
+ // below is the authoritative signal so we swallow + log only.
242
+ try { req.close(http2.constants.NGHTTP2_CANCEL); }
243
+ catch (e) { _log.debug("otlp-grpc-timer-cancel: " + (e && e.message || e)); }
231
244
  reject(_err("ETIMEDOUT",
232
245
  "otlp-grpc: request timed out after " + cfg.timeoutMs + "ms"));
233
246
  }, cfg.timeoutMs);
@@ -290,18 +303,15 @@ function create(config) {
290
303
  cfg.bufferLimit = Number(cfg.bufferLimit) || DEFAULTS.bufferLimit;
291
304
 
292
305
  var onDrop = typeof config.onDrop === "function" ? config.onDrop : null;
293
- function _emitDrop(reason, batch, err) {
294
- if (!onDrop) return;
295
- try { onDrop({ reason: reason, batch: batch, error: err || null }); }
296
- catch (_e) {}
297
- }
306
+ var _emitDrop = safeAsync.makeDropCallback(onDrop,
307
+ function (e) { _log.debug("otlp-grpc-onDrop-threw: " + (e && e.message || e)); });
298
308
 
299
309
  var buffer = [];
300
310
  var inFlight = false;
301
311
  var closed = false;
302
- var flushTimer = null;
303
312
  var session = null;
304
313
  var inflightPromise = null;
314
+ var flushScheduler = safeAsync.makeScheduledFlush(cfg.maxBatchAgeMs, function () { return _flush(); });
305
315
 
306
316
  function _ensureSession() {
307
317
  if (session && !session.destroyed) return session;
@@ -309,15 +319,6 @@ function create(config) {
309
319
  return session;
310
320
  }
311
321
 
312
- function _scheduleFlush() {
313
- if (flushTimer || closed) return;
314
- flushTimer = setTimeout(function () {
315
- flushTimer = null;
316
- _flush().catch(function () { /* drained internally */ });
317
- }, cfg.maxBatchAgeMs);
318
- if (typeof flushTimer.unref === "function") flushTimer.unref();
319
- }
320
-
321
322
  async function _flush() {
322
323
  if (inFlight || buffer.length === 0) return;
323
324
  inFlight = true;
@@ -353,17 +354,22 @@ function create(config) {
353
354
  // Drain immediately; don't await — emit is fire-and-forget on the
354
355
  // hot path. Errors surface via onDrop.
355
356
  _flush().catch(function () {});
356
- } else {
357
- _scheduleFlush();
357
+ } else if (!closed) {
358
+ flushScheduler.schedule();
358
359
  }
359
360
  return Promise.resolve({ accepted: true, queued: buffer.length });
360
361
  }
361
362
 
362
363
  async function close() {
363
364
  closed = true;
364
- if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }
365
- // Drain buffered records before tearing down the HTTP/2 session.
366
- if (inflightPromise) { try { await inflightPromise; } catch (_e) {} }
365
+ flushScheduler.cancel();
366
+ // Drain the in-flight Export before tearing down the HTTP/2 session.
367
+ // The promise rejection (if any) was already routed through onDrop
368
+ // by _flush — we only await here to avoid racing the teardown.
369
+ if (inflightPromise) {
370
+ try { await inflightPromise; }
371
+ catch (e) { _log.debug("otlp-grpc-close-drain: " + (e && e.message || e)); }
372
+ }
367
373
  var pending = buffer.splice(0, buffer.length);
368
374
  if (pending.length > 0) {
369
375
  try {
@@ -51,13 +51,16 @@
51
51
  * collector which forwards.
52
52
  */
53
53
  var C = require("./constants");
54
+ var pkg = require("../package.json");
54
55
  var retryHelper = require("./retry");
55
56
  var { LogStreamError } = require("./framework-error");
56
57
  var httpClient = require("./http-client");
58
+ var safeAsync = require("./safe-async");
57
59
  var safeUrl = require("./safe-url");
58
60
  var authHeader = require("./auth-header");
59
61
 
60
62
  var MAX_RESPONSE_BYTES = C.BYTES.mib(1);
63
+ var FRAMEWORK_VERSION = (pkg && pkg.version) || "unknown";
61
64
 
62
65
  // OTel Logs Data Model severity numbers.
63
66
  // https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitynumber
@@ -73,7 +76,10 @@ var DEFAULTS = {
73
76
  batchSize: 100,
74
77
  maxBatchAgeMs: C.TIME.seconds(5),
75
78
  timeoutMs: C.TIME.seconds(30),
76
- bufferLimit: 10000,
79
+ // Ring-buffer cap (event count, not bytes); routed through C.BYTES
80
+ // identity passthrough so the file's literal arithmetic has a single
81
+ // source of truth.
82
+ bufferLimit: C.BYTES.bytes(10000),
77
83
  };
78
84
 
79
85
  var _err = LogStreamError.factory;
@@ -193,30 +199,18 @@ function create(config) {
193
199
  allowedProtocols: cfg.allowedProtocols || safeUrl.ALLOW_HTTP_TLS,
194
200
  errorClass: LogStreamError,
195
201
  });
196
- var scopeVersion = cfg.scopeVersion || (function () {
197
- try { return require("../package.json").version; } catch (_e) { return "unknown"; }
198
- })();
202
+ var scopeVersion = cfg.scopeVersion || FRAMEWORK_VERSION;
199
203
  var headers = Object.assign({
200
204
  "Content-Type": "application/json",
201
205
  "Accept": "application/json",
202
206
  }, _authHeaders(cfg));
203
207
  var onDrop = typeof cfg.onDrop === "function" ? cfg.onDrop : null;
204
- function _emitDrop(reason, batch, err) {
205
- if (!onDrop) return;
206
- try { onDrop({ reason: reason, batch: batch, error: err || null }); }
207
- catch (_e) { /* drop callback is best-effort */ }
208
- }
208
+ var _emitDrop = safeAsync.makeDropCallback(onDrop);
209
209
  var buffer = [];
210
210
  var dropCount = 0;
211
- var flushTimer = null;
212
211
  var inFlight = false;
213
212
  var closed = false;
214
-
215
- function _scheduleFlush() {
216
- if (flushTimer) return;
217
- flushTimer = setTimeout(function () { flushTimer = null; _flush(); }, cfg.maxBatchAgeMs);
218
- flushTimer.unref();
219
- }
213
+ var flushScheduler = safeAsync.makeScheduledFlush(cfg.maxBatchAgeMs, function () { return _flush(); });
220
214
 
221
215
  async function _flush() {
222
216
  if (inFlight) return;
@@ -238,7 +232,7 @@ function create(config) {
238
232
  }
239
233
  } finally {
240
234
  inFlight = false;
241
- if (buffer.length > 0) _scheduleFlush();
235
+ if (buffer.length > 0) flushScheduler.schedule();
242
236
  }
243
237
  }
244
238
 
@@ -253,14 +247,14 @@ function create(config) {
253
247
  if (buffer.length >= cfg.batchSize) {
254
248
  _flush().catch(function () {});
255
249
  } else {
256
- _scheduleFlush();
250
+ flushScheduler.schedule();
257
251
  }
258
252
  return Promise.resolve({ accepted: true, queued: buffer.length });
259
253
  }
260
254
 
261
255
  async function close() {
262
256
  closed = true;
263
- if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }
257
+ flushScheduler.cancel();
264
258
  await _flush();
265
259
  }
266
260