@blamejs/core 0.15.11 → 0.15.13

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 (305) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/index.js +2 -0
  3. package/lib/a2a-tasks.js +7 -23
  4. package/lib/acme.js +13 -16
  5. package/lib/agent-event-bus.js +5 -4
  6. package/lib/agent-idempotency.js +2 -5
  7. package/lib/agent-orchestrator.js +2 -5
  8. package/lib/agent-saga.js +3 -5
  9. package/lib/agent-tenant.js +2 -5
  10. package/lib/ai-adverse-decision.js +2 -15
  11. package/lib/ai-capability.js +1 -6
  12. package/lib/ai-dp.js +1 -5
  13. package/lib/ai-input.js +2 -2
  14. package/lib/ai-pref.js +3 -8
  15. package/lib/ai-quota.js +3 -14
  16. package/lib/api-key.js +37 -28
  17. package/lib/archive-adapters.js +2 -4
  18. package/lib/archive-entry-policy.js +32 -0
  19. package/lib/archive-read.js +5 -17
  20. package/lib/archive-tar-read.js +5 -16
  21. package/lib/archive.js +2 -10
  22. package/lib/arg-parser.js +7 -6
  23. package/lib/asyncapi-traits.js +2 -6
  24. package/lib/atomic-file.js +108 -31
  25. package/lib/audit-chain.js +133 -53
  26. package/lib/audit-daily-review.js +24 -14
  27. package/lib/audit-emit.js +82 -0
  28. package/lib/audit-sign.js +257 -0
  29. package/lib/audit.js +84 -0
  30. package/lib/auth/access-lock.js +5 -27
  31. package/lib/auth/dpop.js +23 -35
  32. package/lib/auth/fido-mds3.js +9 -15
  33. package/lib/auth/jwt-external.js +26 -8
  34. package/lib/auth/jwt.js +13 -15
  35. package/lib/auth/lockout.js +6 -25
  36. package/lib/auth/oauth.js +67 -45
  37. package/lib/auth/oid4vci.js +55 -32
  38. package/lib/auth/oid4vp.js +3 -2
  39. package/lib/auth/openid-federation.js +6 -6
  40. package/lib/auth/passkey.js +3 -3
  41. package/lib/auth/password.js +7 -1
  42. package/lib/auth/saml.js +37 -27
  43. package/lib/auth/sd-jwt-vc-holder.js +2 -14
  44. package/lib/auth/sd-jwt-vc-issuer.js +2 -14
  45. package/lib/auth/sd-jwt-vc.js +1 -1
  46. package/lib/auth/status-list.js +7 -7
  47. package/lib/auth/step-up.js +6 -12
  48. package/lib/auth-bot-challenge.js +6 -37
  49. package/lib/backup/bundle.js +11 -18
  50. package/lib/backup/index.js +14 -47
  51. package/lib/bounded-map.js +112 -1
  52. package/lib/breach-deadline.js +1 -11
  53. package/lib/cache.js +7 -18
  54. package/lib/cbor.js +2 -1
  55. package/lib/cdn-cache-control.js +8 -9
  56. package/lib/cert.js +3 -10
  57. package/lib/chain-writer.js +162 -47
  58. package/lib/cli.js +5 -1
  59. package/lib/client-hints.js +10 -10
  60. package/lib/cloud-events.js +40 -31
  61. package/lib/cluster-storage.js +2 -38
  62. package/lib/cluster.js +4 -2
  63. package/lib/cms-codec.js +2 -1
  64. package/lib/codepoint-class.js +67 -1
  65. package/lib/compliance-ai-act-logging.js +1 -6
  66. package/lib/compliance-ai-act-transparency.js +2 -4
  67. package/lib/compliance-eaa.js +1 -11
  68. package/lib/compliance-sanctions-fetcher.js +21 -28
  69. package/lib/compliance-sanctions.js +2 -14
  70. package/lib/compliance.js +2 -9
  71. package/lib/config-drift.js +2 -12
  72. package/lib/content-digest.js +10 -8
  73. package/lib/cookies.js +5 -11
  74. package/lib/cose.js +19 -11
  75. package/lib/cra-report.js +1 -11
  76. package/lib/crypto-field.js +5 -10
  77. package/lib/crypto.js +120 -3
  78. package/lib/csp.js +235 -3
  79. package/lib/daemon.js +42 -41
  80. package/lib/data-act.js +19 -9
  81. package/lib/db-query.js +6 -40
  82. package/lib/db.js +34 -12
  83. package/lib/dbsc.js +5 -6
  84. package/lib/ddl-change-control.js +18 -14
  85. package/lib/deprecate.js +4 -5
  86. package/lib/did.js +6 -2
  87. package/lib/dsr.js +8 -12
  88. package/lib/external-db-migrate.js +21 -22
  89. package/lib/external-db.js +14 -26
  90. package/lib/fda-21cfr11.js +12 -8
  91. package/lib/fdx.js +22 -18
  92. package/lib/file-upload.js +80 -66
  93. package/lib/flag-evaluation-context.js +5 -8
  94. package/lib/framework-error.js +12 -0
  95. package/lib/framework-schema.js +19 -0
  96. package/lib/fsm.js +7 -12
  97. package/lib/gate-contract.js +869 -38
  98. package/lib/gdpr-ropa.js +4 -13
  99. package/lib/graphql-federation.js +1 -1
  100. package/lib/guard-agent-registry.js +2 -1
  101. package/lib/guard-all.js +1 -0
  102. package/lib/guard-archive.js +9 -30
  103. package/lib/guard-auth.js +23 -80
  104. package/lib/guard-cidr.js +20 -96
  105. package/lib/guard-csv.js +135 -196
  106. package/lib/guard-domain.js +23 -106
  107. package/lib/guard-dsn.js +16 -13
  108. package/lib/guard-email.js +46 -53
  109. package/lib/guard-envelope.js +1 -1
  110. package/lib/guard-event-bus-topic.js +2 -1
  111. package/lib/guard-filename.js +18 -62
  112. package/lib/guard-graphql.js +28 -75
  113. package/lib/guard-html-wcag.js +15 -2
  114. package/lib/guard-html.js +65 -117
  115. package/lib/guard-idempotency-key.js +2 -1
  116. package/lib/guard-image.js +280 -77
  117. package/lib/guard-imap-command.js +8 -9
  118. package/lib/guard-json.js +87 -103
  119. package/lib/guard-jsonpath.js +20 -88
  120. package/lib/guard-jwt.js +32 -114
  121. package/lib/guard-list-id.js +2 -7
  122. package/lib/guard-list-unsubscribe.js +2 -7
  123. package/lib/guard-mail-compose.js +5 -6
  124. package/lib/guard-mail-move.js +2 -1
  125. package/lib/guard-mail-query.js +5 -3
  126. package/lib/guard-mail-sieve.js +6 -7
  127. package/lib/guard-managesieve-command.js +5 -4
  128. package/lib/guard-markdown.js +76 -110
  129. package/lib/guard-message-id.js +5 -6
  130. package/lib/guard-mime.js +20 -99
  131. package/lib/guard-oauth.js +19 -73
  132. package/lib/guard-pdf.js +65 -72
  133. package/lib/guard-pop3-command.js +12 -13
  134. package/lib/guard-posture-chain.js +2 -1
  135. package/lib/guard-regex.js +24 -99
  136. package/lib/guard-saga-config.js +2 -1
  137. package/lib/guard-shell.js +22 -87
  138. package/lib/guard-smtp-command.js +8 -11
  139. package/lib/guard-sql.js +15 -13
  140. package/lib/guard-stream-args.js +2 -1
  141. package/lib/guard-svg.js +95 -140
  142. package/lib/guard-template.js +23 -80
  143. package/lib/guard-tenant-id.js +2 -1
  144. package/lib/guard-text.js +592 -0
  145. package/lib/guard-time.js +27 -95
  146. package/lib/guard-uuid.js +21 -93
  147. package/lib/guard-xml.js +76 -106
  148. package/lib/guard-yaml.js +24 -60
  149. package/lib/http-client-cache.js +8 -13
  150. package/lib/http-client-cookie-jar.js +2 -4
  151. package/lib/http-client.js +8 -21
  152. package/lib/http-message-signature.js +26 -8
  153. package/lib/i18n-messageformat.js +5 -7
  154. package/lib/i18n.js +83 -26
  155. package/lib/inbox.js +8 -8
  156. package/lib/incident-report.js +3 -21
  157. package/lib/ip-utils.js +49 -6
  158. package/lib/jobs.js +3 -2
  159. package/lib/keychain.js +6 -18
  160. package/lib/legal-hold.js +6 -15
  161. package/lib/log-stream-cloudwatch.js +44 -112
  162. package/lib/log-stream-otlp-grpc.js +28 -14
  163. package/lib/log-stream-otlp.js +16 -80
  164. package/lib/log-stream-syslog.js +6 -0
  165. package/lib/log-stream-webhook.js +20 -92
  166. package/lib/log.js +24 -2
  167. package/lib/mail-arc-sign.js +8 -3
  168. package/lib/mail-arf.js +3 -2
  169. package/lib/mail-auth.js +40 -66
  170. package/lib/mail-bimi.js +19 -39
  171. package/lib/mail-crypto-pgp.js +3 -2
  172. package/lib/mail-dav.js +8 -35
  173. package/lib/mail-deploy.js +6 -9
  174. package/lib/mail-dkim.js +19 -38
  175. package/lib/mail-greylist.js +15 -26
  176. package/lib/mail-helo.js +2 -3
  177. package/lib/mail-journal.js +2 -1
  178. package/lib/mail-mdn.js +4 -3
  179. package/lib/mail-rbl.js +5 -8
  180. package/lib/mail-scan.js +2 -2
  181. package/lib/mail-send-deliver.js +33 -20
  182. package/lib/mail-server-imap.js +32 -87
  183. package/lib/mail-server-jmap.js +35 -51
  184. package/lib/mail-server-managesieve.js +29 -83
  185. package/lib/mail-server-mx.js +33 -74
  186. package/lib/mail-server-net.js +177 -0
  187. package/lib/mail-server-pop3.js +30 -83
  188. package/lib/mail-server-rate-limit.js +30 -6
  189. package/lib/mail-server-submission.js +34 -73
  190. package/lib/mail-server-tls.js +89 -0
  191. package/lib/mail-spam-score.js +7 -8
  192. package/lib/mail-store.js +3 -2
  193. package/lib/mail.js +11 -11
  194. package/lib/markup-escape.js +31 -0
  195. package/lib/markup-tokenizer.js +54 -0
  196. package/lib/mcp-tool-registry.js +26 -23
  197. package/lib/mcp.js +1 -1
  198. package/lib/mdoc.js +11 -12
  199. package/lib/metrics.js +32 -30
  200. package/lib/middleware/age-gate.js +3 -23
  201. package/lib/middleware/api-encrypt.js +11 -22
  202. package/lib/middleware/assetlinks.js +2 -7
  203. package/lib/middleware/bearer-auth.js +2 -1
  204. package/lib/middleware/body-parser.js +79 -54
  205. package/lib/middleware/bot-disclose.js +7 -10
  206. package/lib/middleware/bot-guard.js +2 -10
  207. package/lib/middleware/csrf-protect.js +13 -2
  208. package/lib/middleware/daily-byte-quota.js +6 -26
  209. package/lib/middleware/deny-response.js +19 -1
  210. package/lib/middleware/fetch-metadata.js +7 -0
  211. package/lib/middleware/idempotency-key.js +4 -20
  212. package/lib/middleware/rate-limit.js +20 -28
  213. package/lib/middleware/scim-server.js +3 -2
  214. package/lib/middleware/security-headers.js +9 -1
  215. package/lib/middleware/security-txt.js +2 -6
  216. package/lib/middleware/tus-upload.js +12 -27
  217. package/lib/middleware/web-app-manifest.js +2 -7
  218. package/lib/migrations.js +4 -13
  219. package/lib/mime-parse.js +34 -17
  220. package/lib/module-loader.js +44 -0
  221. package/lib/money.js +105 -0
  222. package/lib/mtls-ca.js +116 -36
  223. package/lib/network-byte-quota.js +4 -18
  224. package/lib/network-dane.js +8 -6
  225. package/lib/network-dns-resolver.js +97 -6
  226. package/lib/network-dns.js +22 -26
  227. package/lib/network-dnssec.js +16 -16
  228. package/lib/network-heartbeat.js +6 -5
  229. package/lib/network-proxy.js +3 -7
  230. package/lib/network-smtp-policy.js +29 -41
  231. package/lib/network-tls.js +74 -55
  232. package/lib/network.js +2 -6
  233. package/lib/nis2-report.js +1 -11
  234. package/lib/nonce-store.js +81 -3
  235. package/lib/notify.js +7 -12
  236. package/lib/numeric-bounds.js +22 -8
  237. package/lib/object-store/azure-blob-bucket-ops.js +2 -6
  238. package/lib/object-store/azure-blob.js +5 -45
  239. package/lib/object-store/gcs.js +8 -71
  240. package/lib/object-store/http-put.js +1 -5
  241. package/lib/object-store/http-request.js +128 -0
  242. package/lib/object-store/sigv4-bucket-ops.js +2 -1
  243. package/lib/object-store/sigv4.js +9 -77
  244. package/lib/observability-otlp-exporter.js +9 -25
  245. package/lib/observability.js +95 -0
  246. package/lib/openapi-paths-builder.js +7 -3
  247. package/lib/otel-export.js +7 -10
  248. package/lib/outbox.js +43 -37
  249. package/lib/pagination.js +11 -15
  250. package/lib/parsers/safe-env.js +5 -4
  251. package/lib/parsers/safe-ini.js +10 -4
  252. package/lib/parsers/safe-toml.js +11 -9
  253. package/lib/parsers/safe-xml.js +2 -2
  254. package/lib/parsers/safe-yaml.js +7 -6
  255. package/lib/pick.js +63 -5
  256. package/lib/pipl-cn.js +69 -59
  257. package/lib/privacy-pass.js +8 -6
  258. package/lib/problem-details.js +2 -5
  259. package/lib/pubsub.js +4 -8
  260. package/lib/redact.js +2 -1
  261. package/lib/render.js +4 -2
  262. package/lib/request-helpers.js +133 -6
  263. package/lib/restore.js +5 -22
  264. package/lib/retention.js +3 -5
  265. package/lib/safe-async.js +457 -0
  266. package/lib/safe-buffer.js +137 -6
  267. package/lib/safe-decompress.js +2 -1
  268. package/lib/safe-dns.js +2 -1
  269. package/lib/safe-ical.js +12 -26
  270. package/lib/safe-json.js +7 -8
  271. package/lib/safe-jsonpath.js +6 -5
  272. package/lib/safe-mime.js +25 -29
  273. package/lib/safe-redirect.js +2 -5
  274. package/lib/safe-schema.js +7 -6
  275. package/lib/safe-sql.js +126 -0
  276. package/lib/safe-vcard.js +12 -26
  277. package/lib/sandbox-worker.js +9 -2
  278. package/lib/sandbox.js +3 -2
  279. package/lib/scheduler.js +5 -12
  280. package/lib/sec-cyber.js +82 -37
  281. package/lib/seeders.js +9 -21
  282. package/lib/self-update.js +92 -69
  283. package/lib/session-device-binding.js +112 -66
  284. package/lib/session.js +194 -6
  285. package/lib/sql.js +225 -78
  286. package/lib/sse.js +6 -10
  287. package/lib/static.js +136 -98
  288. package/lib/storage.js +4 -2
  289. package/lib/structured-fields.js +313 -17
  290. package/lib/tenant-quota.js +2 -20
  291. package/lib/tsa.js +11 -15
  292. package/lib/validate-opts.js +154 -8
  293. package/lib/vault/seal-pem-file.js +30 -48
  294. package/lib/vault-aad.js +3 -2
  295. package/lib/vc.js +2 -7
  296. package/lib/vex.js +35 -13
  297. package/lib/web-push-vapid.js +23 -20
  298. package/lib/webhook-dispatcher.js +706 -0
  299. package/lib/webhook.js +43 -18
  300. package/lib/websocket-channels.js +9 -7
  301. package/lib/websocket.js +7 -3
  302. package/lib/worm.js +2 -3
  303. package/lib/ws-client.js +8 -10
  304. package/package.json +1 -1
  305. package/sbom.cdx.json +6 -6
package/lib/jobs.js CHANGED
@@ -77,6 +77,7 @@ var { boot } = require("./log");
77
77
  var queue = require("./queue");
78
78
  var validateOpts = require("./validate-opts");
79
79
  var { JobsError } = require("./framework-error");
80
+ var boundedMap = require("./bounded-map");
80
81
 
81
82
  var log = boot("jobs");
82
83
 
@@ -102,10 +103,10 @@ function create(opts) {
102
103
  if (typeof handler !== "function") {
103
104
  throw _err("INVALID_HANDLER", "jobs.define: handler must be a function", true);
104
105
  }
105
- if (registry.has(name)) {
106
+ boundedMap.requireAbsent(registry, name, function () {
106
107
  throw _err("DUPLICATE_NAME",
107
108
  "jobs.define: '" + name + "' is already defined", true);
108
- }
109
+ });
109
110
  if (started) {
110
111
  // Defining after start would mean the new handler doesn't run
111
112
  // until the next start cycle. Reject loudly so operators don't
package/lib/keychain.js CHANGED
@@ -242,24 +242,12 @@ async function _writeFile(fallbackFile, doc, passphrase) {
242
242
  // Drain a stream into a Buffer, honoring an upper byte cap so a
243
243
  // runaway tool can't OOM the framework.
244
244
  function _drain(stream, capBytes) {
245
- return new Promise(function (resolve, reject) {
246
- if (!stream) { resolve(Buffer.alloc(0)); return; }
247
- var collector = safeBuffer.boundedChunkCollector({
248
- maxBytes: capBytes,
249
- errorClass: KeychainError,
250
- sizeCode: "keychain/native-output-too-large",
251
- sizeMessage: "native tool output exceeded " + capBytes + " bytes",
252
- });
253
- stream.on("data", function (chunk) {
254
- try {
255
- collector.push(chunk);
256
- } catch (e) {
257
- try { stream.destroy(); } catch (_e) { /* destroy best-effort */ }
258
- reject(e);
259
- }
260
- });
261
- stream.on("end", function () { resolve(collector.result()); });
262
- stream.on("error", function (e) { reject(e); });
245
+ if (!stream) return Promise.resolve(Buffer.alloc(0));
246
+ return safeBuffer.collectStream(stream, {
247
+ maxBytes: capBytes,
248
+ errorClass: KeychainError,
249
+ sizeCode: "keychain/native-output-too-large",
250
+ sizeMessage: "native tool output exceeded " + capBytes + " bytes",
263
251
  });
264
252
  }
265
253
 
package/lib/legal-hold.js CHANGED
@@ -60,7 +60,6 @@
60
60
  * on policy denials (already-held / not-held / invalid-citation).
61
61
  */
62
62
  var bCrypto = require("./crypto");
63
- var lazyRequire = require("./lazy-require");
64
63
  var safeJson = require("./safe-json");
65
64
  var validateOpts = require("./validate-opts");
66
65
  var sql = require("./sql");
@@ -77,7 +76,7 @@ var { defineClass } = require("./framework-error");
77
76
  var HOLD_TABLE = "_blamejs_legal_hold"; // allow:hand-rolled-sql — canonical local table-name; passed to b.sql with quoteName
78
77
  var AUDIT_TABLE = "audit_log";
79
78
 
80
- var audit = lazyRequire(function () { return require("./audit"); });
79
+ var auditEmit = require("./audit-emit");
81
80
 
82
81
  var LegalHoldError = defineClass("LegalHoldError", { alwaysPermanent: true });
83
82
  var _err = LegalHoldError.factory;
@@ -151,19 +150,11 @@ function create(opts) {
151
150
  validateOpts.optionalObjectWithMethod(opts.signWith, "sign",
152
151
  "create: opts.signWith", LegalHoldError, "BAD_OPT", "b.auditSign-shaped object");
153
152
 
154
- function _emit(action, info, outcome) {
155
- if (!auditOn) return;
156
- var sink = auditInstance || audit();
157
- try {
158
- sink.safeEmit({
159
- action: action,
160
- outcome: outcome,
161
- resource: { kind: "legal-hold", id: info && info.subjectId },
162
- reason: (info && info.reason) || null,
163
- metadata: info || {},
164
- });
165
- } catch (_e) { /* best-effort */ }
166
- }
153
+ var _emit = auditEmit.gatedReasonEmitter({
154
+ audit: auditOn,
155
+ sink: auditInstance,
156
+ extra: function (info) { return { resource: { kind: "legal-hold", id: info && info.subjectId } }; },
157
+ });
167
158
 
168
159
  function _ensureSchema() {
169
160
  // Idempotent migration. The framework SQLite path installs the
@@ -198,20 +198,9 @@ function create(config) {
198
198
  "ResourceAlreadyExistsException treated as success).");
199
199
  }
200
200
  var cfg = Object.assign({}, DEFAULTS, config);
201
- var onDrop = typeof cfg.onDrop === "function" ? cfg.onDrop : null;
202
- var _emitDrop = safeAsync.makeDropCallback(onDrop);
203
- var buffer = [];
204
- var dropCount = 0;
205
- var inFlight = false;
206
- var closed = false;
207
201
  var sequenceToken = null;
208
- // Captures the in-flight drain (the IIFE inside _flush) so close() can await
209
- // the actual run before flipping `closed` — otherwise the _flush while-loop's
210
- // `&& !closed` bails and buffered records are stranded at shutdown.
211
- var inFlightPromise = null;
212
- var flushScheduler = safeAsync.makeScheduledFlush(cfg.maxBatchAgeMs, function () { return _flush(); });
213
202
 
214
- function _takeBatch() {
203
+ function _takeBatch(buffer) {
215
204
  var batch = [];
216
205
  var totalBytes = 0;
217
206
  while (buffer.length > 0) {
@@ -239,45 +228,25 @@ function create(config) {
239
228
  return autoCreatePromise;
240
229
  }
241
230
 
242
- async function _flush() {
243
- if (inFlight) return inFlightPromise;
244
- if (buffer.length === 0) return;
245
- inFlight = true;
246
- inFlightPromise = (async function () {
247
- try {
248
- try { await _ensureAutoCreated(); }
249
- catch (acErr) {
250
- // autoCreate failure is permanent — every subsequent batch
251
- // would hit the same error. Drop the queue with the
252
- // operator-supplied onDrop callback so they see exactly which
253
- // events were lost AND why, then bail.
254
- var allBuffered = buffer.splice(0, buffer.length);
255
- dropCount += allBuffered.length;
256
- _emitDrop("autocreate-failed", allBuffered, acErr);
257
- return;
258
- }
259
- while (buffer.length > 0 && !closed) {
260
- var batch = _takeBatch();
261
- if (batch.length === 0) break;
262
- try {
263
- await retryHelper.withRetry(function () {
264
- return _send(batch);
265
- }, Object.assign({
266
- isPermanent: _isPermanentAwsError,
267
- }, cfg.retry || {}));
268
- } catch (e) {
269
- dropCount += batch.length;
270
- _emitDrop("retry-exhausted", batch, e);
271
- break;
272
- }
273
- }
274
- } finally {
275
- inFlight = false;
276
- inFlightPromise = null;
277
- if (buffer.length > 0) flushScheduler.schedule();
278
- }
279
- })();
280
- return inFlightPromise;
231
+ // CloudWatch rejects any single event above its 256 KiB hard cap — drop it
232
+ // (with a truncated preview) before it can poison a batch; otherwise build
233
+ // the { timestamp, message } event the API expects.
234
+ function _prepareRecord(record) {
235
+ var message = typeof record.message === "string" ? record.message : JSON.stringify(record);
236
+ var size = _eventByteSize(message);
237
+ if (size > CW_MAX_EVENT_BYTES) {
238
+ return {
239
+ rejected: true,
240
+ reason: "event too large",
241
+ dropKind: "event-too-large",
242
+ drop: [{
243
+ timestamp: record.ts || Date.now(),
244
+ message: message.slice(0, DROP_PREVIEW_BYTES) + "...[truncated for drop event]",
245
+ }],
246
+ error: new Error("event exceeds 256 KiB CloudWatch hard cap (was " + size + " bytes)"),
247
+ };
248
+ }
249
+ return { entry: { timestamp: record.ts || Date.now(), message: message } };
281
250
  }
282
251
 
283
252
  async function _send(batch) {
@@ -306,70 +275,33 @@ function create(config) {
306
275
  return res;
307
276
  }
308
277
 
309
- function emit(record) {
310
- if (closed) return Promise.resolve({ accepted: false, reason: "sink closed" });
311
- var message;
312
- if (typeof record.message === "string") {
313
- message = record.message;
314
- } else {
315
- message = JSON.stringify(record);
316
- }
317
- var size = _eventByteSize(message);
318
- if (size > CW_MAX_EVENT_BYTES) {
319
- _emitDrop("event-too-large", [{
320
- timestamp: record.ts || Date.now(),
321
- message: message.slice(0, DROP_PREVIEW_BYTES) + "...[truncated for drop event]",
322
- }], new Error("event exceeds 256 KiB CloudWatch hard cap (was " + size + " bytes)"));
323
- dropCount += 1;
324
- return Promise.resolve({ accepted: false, reason: "event too large" });
325
- }
326
- if (buffer.length >= cfg.bufferLimit) {
327
- var dropped = buffer.shift();
328
- dropCount += 1;
329
- _emitDrop("overflow", [dropped], null);
330
- }
331
- buffer.push({
332
- timestamp: record.ts || Date.now(),
333
- message: message,
334
- });
335
- if (buffer.length >= cfg.batchSize) {
336
- _flush().catch(function () {});
337
- } else {
338
- flushScheduler.schedule();
339
- }
340
- return Promise.resolve({ accepted: true, queued: buffer.length });
341
- }
342
-
343
- async function close() {
344
- // Drain BEFORE flipping closed=true — _flush()'s `while (... && !closed)`
345
- // loop bails on !closed, so flipping the flag first strands the records the
346
- // operator queued just before shutdown (the b.logStream drain contract).
347
- // Stop the timer, await any in-flight drain, drain the remainder, THEN
348
- // refuse new enqueues. Mirrors the webhook sink.
349
- flushScheduler.cancel();
350
- if (inFlightPromise) {
351
- try { await inFlightPromise; } catch (_e) { /* surfaced via onDrop */ }
352
- }
353
- await _flush();
354
- closed = true;
355
- }
356
-
357
- function stats() {
358
- return {
359
- queued: buffer.length,
360
- dropped: dropCount,
361
- inFlight: inFlight,
362
- sequenceToken: sequenceToken,
363
- endpoint: _resolveEndpoint(cfg),
364
- };
365
- }
278
+ // beforeDrain runs the autoCreate handshake once; its failure is permanent
279
+ // (every batch would hit the same error) so the whole buffer is dropped under
280
+ // "autocreate-failed". takeBatch enforces CloudWatch's per-batch byte + count
281
+ // caps; sendBatch wraps the signed PutLogEvents in retry, treating throttling
282
+ // / sequence-token errors per _isPermanentAwsError.
283
+ var sink = safeAsync.makeBatchingSink({
284
+ batchSize: cfg.batchSize,
285
+ bufferLimit: cfg.bufferLimit,
286
+ maxBatchAgeMs: cfg.maxBatchAgeMs,
287
+ onDrop: cfg.onDrop,
288
+ prepareRecord: _prepareRecord,
289
+ takeBatch: _takeBatch,
290
+ beforeDrain: _ensureAutoCreated,
291
+ beforeDrainDropKind: "autocreate-failed",
292
+ sendBatch: function (batch) {
293
+ return retryHelper.withRetry(function () {
294
+ return _send(batch);
295
+ }, Object.assign({ isPermanent: _isPermanentAwsError }, cfg.retry || {}));
296
+ },
297
+ });
366
298
 
367
299
  return {
368
300
  protocol: "cloudwatch",
369
- emit: emit,
370
- close: close,
371
- stats: stats,
372
- flush: _flush,
301
+ emit: sink.emit,
302
+ close: sink.close,
303
+ stats: function () { return sink.stats({ sequenceToken: sequenceToken, endpoint: _resolveEndpoint(cfg) }); },
304
+ flush: sink.flush,
373
305
  };
374
306
  }
375
307
 
@@ -38,6 +38,9 @@ var lazyRequire = require("./lazy-require");
38
38
  // scrub attribute values through the telemetry redactor before they cross the
39
39
  // OTLP egress boundary (CWE-532).
40
40
  var observability = lazyRequire(function () { return require("./observability"); });
41
+ // Lazy — network-tls is widely required; audit an insecure (cert-validation-
42
+ // disabled) outbound TLS session at honor time, same surface as connectWithEch.
43
+ var networkTls = lazyRequire(function () { return require("./network-tls"); });
41
44
 
42
45
  var _err = LogStreamError.factory;
43
46
  var _log = boot("log-stream-otlp-grpc");
@@ -215,7 +218,17 @@ function _makeClient(cfg) {
215
218
  var sessionOpts = {};
216
219
  if (cfg.ca) sessionOpts.ca = cfg.ca;
217
220
  if (cfg.servername) sessionOpts.servername = cfg.servername;
218
- if (cfg.allowInsecure) sessionOpts.rejectUnauthorized = false;
221
+ if (cfg.allowInsecure && url.protocol === "https:") {
222
+ // allowInsecure only has meaning on a TLS session. For an h2c endpoint
223
+ // (http://, cleartext HTTP/2) there is no certificate to validate and
224
+ // nothing to skip, so neither rejectUnauthorized nor the insecure-TLS
225
+ // audit applies — emitting it there would be a false security event.
226
+ // Operator-governed (not a hardcoded literal): cfg.allowInsecure is true in
227
+ // this branch, so this resolves to false — but derived from the operator's
228
+ // own flag, audited, and never a framework default.
229
+ sessionOpts.rejectUnauthorized = !cfg.allowInsecure;
230
+ networkTls().auditInsecureTls({ host: authority, source: "log-stream.otlp-grpc" });
231
+ }
219
232
  var session = http2.connect(authority, sessionOpts);
220
233
  session.on("error", function () { /* surfaced through request err */ });
221
234
  if (typeof session.unref === "function") session.unref();
@@ -347,24 +360,24 @@ function create(config) {
347
360
  }
348
361
  }
349
362
 
363
+ // Fire-and-forget enqueue: full batch drains immediately (not awaited —
364
+ // emit is hot-path), partial batch coalesces via the scheduler. Errors
365
+ // surface through onDrop. No dropCount here — gRPC drop accounting is the
366
+ // export stream's concern, not the buffer's.
367
+ var _enqueue = safeAsync.makeBufferedEnqueue(buffer, {
368
+ batchSize: cfg.batchSize,
369
+ bufferLimit: cfg.bufferLimit,
370
+ flush: _flush,
371
+ schedule: flushScheduler.schedule,
372
+ onOverflow: function (dropped) { _emitDrop("overflow", [dropped], null); },
373
+ });
374
+
350
375
  function emit(record) {
351
376
  if (closed) {
352
377
  _emitDrop("sink-closed", [record], null);
353
378
  return Promise.resolve({ accepted: false, reason: "closed" });
354
379
  }
355
- if (buffer.length >= cfg.bufferLimit) {
356
- var dropped = buffer.shift();
357
- _emitDrop("overflow", [dropped], null);
358
- }
359
- buffer.push(record);
360
- if (buffer.length >= cfg.batchSize) {
361
- // Drain immediately; don't await — emit is fire-and-forget on the
362
- // hot path. Errors surface via onDrop.
363
- _flush().catch(function () {});
364
- } else if (!closed) {
365
- flushScheduler.schedule();
366
- }
367
- return Promise.resolve({ accepted: true, queued: buffer.length });
380
+ return _enqueue(record);
368
381
  }
369
382
 
370
383
  async function close() {
@@ -409,6 +422,7 @@ module.exports = {
409
422
  create: create,
410
423
  // Exposed for layer-0 tests that verify the wire encoding without
411
424
  // standing up an HTTP/2 server.
425
+ _makeClient: _makeClient,
412
426
  _encodeAnyValue: _encodeAnyValue,
413
427
  _encodeKeyValue: _encodeKeyValue,
414
428
  _encodeLogRecord: _encodeLogRecord,
@@ -212,89 +212,25 @@ function create(config) {
212
212
  "Content-Type": "application/json",
213
213
  "Accept": "application/json",
214
214
  }, _authHeaders(cfg));
215
- var onDrop = typeof cfg.onDrop === "function" ? cfg.onDrop : null;
216
- var _emitDrop = safeAsync.makeDropCallback(onDrop);
217
- var buffer = [];
218
- var dropCount = 0;
219
- var inFlight = false;
220
- var closed = false;
221
- // Captures the in-flight drain so close() awaits the real run before flipping
222
- // `closed` (the _flush while-loop bails on !closed → flipping first strands
223
- // buffered records at shutdown).
224
- var inFlightPromise = null;
225
- var flushScheduler = safeAsync.makeScheduledFlush(cfg.maxBatchAgeMs, function () { return _flush(); });
226
-
227
- async function _flush() {
228
- if (inFlight) return inFlightPromise;
229
- if (buffer.length === 0) return;
230
- inFlight = true;
231
- inFlightPromise = (async function () {
232
- try {
233
- while (buffer.length > 0 && !closed) {
234
- var batch = buffer.splice(0, cfg.batchSize);
235
- var body = _serializeBatch(batch, cfg, scopeVersion);
236
- try {
237
- await retryHelper.withRetry(function () {
238
- return _post(resolvedUrl, body, headers, cfg.timeoutMs, cfg.allowedProtocols, cfg.allowInternal);
239
- }, cfg.retry);
240
- } catch (e) {
241
- dropCount += batch.length;
242
- _emitDrop("retry-exhausted", batch, e);
243
- break;
244
- }
245
- }
246
- } finally {
247
- inFlight = false;
248
- inFlightPromise = null;
249
- if (buffer.length > 0) flushScheduler.schedule();
250
- }
251
- })();
252
- return inFlightPromise;
253
- }
254
-
255
- function emit(record) {
256
- if (closed) return Promise.resolve({ accepted: false, reason: "sink closed" });
257
- if (buffer.length >= cfg.bufferLimit) {
258
- var dropped = buffer.shift();
259
- dropCount += 1;
260
- _emitDrop("overflow", [dropped], null);
261
- }
262
- buffer.push(record);
263
- if (buffer.length >= cfg.batchSize) {
264
- _flush().catch(function () {});
265
- } else {
266
- flushScheduler.schedule();
267
- }
268
- return Promise.resolve({ accepted: true, queued: buffer.length });
269
- }
270
-
271
- async function close() {
272
- // Drain BEFORE flipping closed=true (see _flush's `&& !closed` guard) so
273
- // records queued just before shutdown reach the wire. Mirrors the webhook
274
- // + cloudwatch sinks.
275
- flushScheduler.cancel();
276
- if (inFlightPromise) {
277
- try { await inFlightPromise; } catch (_e) { /* surfaced via onDrop */ }
278
- }
279
- await _flush();
280
- closed = true;
281
- }
282
-
283
- function stats() {
284
- return {
285
- queued: buffer.length,
286
- dropped: dropCount,
287
- inFlight: inFlight,
288
- url: resolvedUrl,
289
- };
290
- }
215
+ var sink = safeAsync.makeBatchingSink({
216
+ batchSize: cfg.batchSize,
217
+ bufferLimit: cfg.bufferLimit,
218
+ maxBatchAgeMs: cfg.maxBatchAgeMs,
219
+ onDrop: cfg.onDrop,
220
+ sendBatch: function (batch) {
221
+ var body = _serializeBatch(batch, cfg, scopeVersion);
222
+ return retryHelper.withRetry(function () {
223
+ return _post(resolvedUrl, body, headers, cfg.timeoutMs, cfg.allowedProtocols, cfg.allowInternal);
224
+ }, cfg.retry);
225
+ },
226
+ });
291
227
 
292
228
  return {
293
229
  protocol: "otlp",
294
- emit: emit,
295
- close: close,
296
- stats: stats,
297
- flush: _flush,
230
+ emit: sink.emit,
231
+ close: sink.close,
232
+ stats: function () { return sink.stats({ url: resolvedUrl }); },
233
+ flush: sink.flush,
298
234
  };
299
235
  }
300
236
 
@@ -37,6 +37,9 @@ var safeAsync = require("./safe-async");
37
37
  var safeBuffer = require("./safe-buffer");
38
38
  var safeUrl = require("./safe-url");
39
39
  var { LogStreamError } = require("./framework-error");
40
+ var lazyRequire = require("./lazy-require");
41
+ // Lazy — audit a cert-validation-disabled syslog/TLS session at honor time.
42
+ var networkTls = lazyRequire(function () { return require("./network-tls"); });
40
43
 
41
44
  var _err = LogStreamError.factory;
42
45
  var log = boot("log-stream-syslog");
@@ -223,6 +226,9 @@ function create(config) {
223
226
  });
224
227
  if (cfg.ca) tlsOpts.ca = cfg.ca;
225
228
  if (cfg.servername) tlsOpts.servername = cfg.servername;
229
+ if (cfg.rejectUnauthorized === false) {
230
+ networkTls().auditInsecureTls({ host: cfg.host, port: cfg.port, source: "log-stream.syslog" });
231
+ }
226
232
  sock = nodeTls.connect(tlsOpts, onConnect);
227
233
  } else {
228
234
  sock = net.connect(connectOpts, onConnect);
@@ -98,101 +98,29 @@ function create(config) {
98
98
  errorClass: LogStreamError,
99
99
  });
100
100
  var headers = Object.assign({ "Content-Type": cfg.contentType }, _authHeaders(cfg));
101
- // onDrop callback: invoked when a batch is dropped, either by buffer
102
- // overflow ("overflow") or by retry exhaustion ("retry-exhausted").
103
- // Operator wiring this directly (without the framework's dispatcher
104
- // wrapping) needs visibility into permanent-drop events; the
105
- // dispatcher path emits its own audit, but a sink used in isolation
106
- // would otherwise lose drops silently. The callback is invoked
107
- // best-effort — a throw inside it is swallowed.
108
- var onDrop = typeof cfg.onDrop === "function" ? cfg.onDrop : null;
109
- var _emitDrop = safeAsync.makeDropCallback(onDrop);
110
- var buffer = [];
111
- var dropCount = 0;
112
- var inFlight = false;
113
- var closed = false;
114
- var flushScheduler = safeAsync.makeScheduledFlush(cfg.maxBatchAgeMs, function () { return _flush(); });
115
-
116
- // Track the in-flight flush as a promise so close() can await it.
117
- // Without this, a record arriving mid-flush gets buffered, the
118
- // emit-time _flush() early-returns on `if (inFlight) return`, and
119
- // the buffered record is stranded if shutdown fires before
120
- // flushScheduler.schedule() drains it.
121
- var inFlightPromise = null;
122
- async function _flush() {
123
- if (inFlight) return inFlightPromise;
124
- if (buffer.length === 0) return;
125
- inFlight = true;
126
- inFlightPromise = (async function () {
127
- try {
128
- while (buffer.length > 0 && !closed) {
129
- var batch = buffer.splice(0, cfg.batchSize);
130
- var body = _serializeBatch(batch, cfg.bodyShape);
131
- try {
132
- await retryHelper.withRetry(function () {
133
- return _post(cfg.url, body, headers, cfg.timeoutMs, cfg.allowedProtocols, cfg.allowInternal);
134
- }, cfg.retry);
135
- } catch (e) {
136
- // Batch permanently rejected — surface via dropCount AND the
137
- // operator-supplied onDrop callback. The dispatcher path
138
- // wraps its own audit hook around emit(); operators using
139
- // this sink directly rely on dropCount + onDrop.
140
- dropCount += batch.length;
141
- _emitDrop("retry-exhausted", batch, e);
142
- break;
143
- }
144
- }
145
- } finally {
146
- inFlight = false;
147
- inFlightPromise = null;
148
- if (buffer.length > 0) flushScheduler.schedule();
149
- }
150
- })();
151
- return inFlightPromise;
152
- }
153
-
154
- function emit(record) {
155
- if (closed) return Promise.resolve({ accepted: false, reason: "sink closed" });
156
- if (buffer.length >= cfg.bufferLimit) {
157
- var dropped = buffer.shift(); // drop oldest
158
- dropCount += 1;
159
- _emitDrop("overflow", [dropped], null);
160
- }
161
- buffer.push(record);
162
- if (buffer.length >= cfg.batchSize) {
163
- // Don't await — non-blocking flush. Caller's emit returns immediately.
164
- _flush().catch(function () {});
165
- } else {
166
- flushScheduler.schedule();
167
- }
168
- return Promise.resolve({ accepted: true, queued: buffer.length });
169
- }
170
-
171
- async function close() {
172
- // Drain BEFORE flipping closed=true. _flush()'s while loop bails on
173
- // !closed, so flipping the flag first leaves any buffered records
174
- // stranded — the very records the operator queued just before
175
- // calling shutdown(). Order: stop the timer, await any in-flight
176
- // flush so its records POST before we touch the buffer, drain
177
- // anything still queued, THEN refuse new enqueues.
178
- flushScheduler.cancel();
179
- if (inFlightPromise) {
180
- try { await inFlightPromise; } catch (_e) { /* surfaced via onDrop */ }
181
- }
182
- await _flush();
183
- closed = true;
184
- }
185
-
186
- function stats() {
187
- return { queued: buffer.length, dropped: dropCount, inFlight: inFlight };
188
- }
101
+ // A batch permanently rejected by retry surfaces via dropCount AND the
102
+ // operator-supplied onDrop the dispatcher wraps its own audit around
103
+ // emit(), but a sink wired directly (or by buffer overflow) would otherwise
104
+ // lose drops silently. onDrop is best-effort; a throw inside it is swallowed.
105
+ var sink = safeAsync.makeBatchingSink({
106
+ batchSize: cfg.batchSize,
107
+ bufferLimit: cfg.bufferLimit,
108
+ maxBatchAgeMs: cfg.maxBatchAgeMs,
109
+ onDrop: cfg.onDrop,
110
+ sendBatch: function (batch) {
111
+ var body = _serializeBatch(batch, cfg.bodyShape);
112
+ return retryHelper.withRetry(function () {
113
+ return _post(cfg.url, body, headers, cfg.timeoutMs, cfg.allowedProtocols, cfg.allowInternal);
114
+ }, cfg.retry);
115
+ },
116
+ });
189
117
 
190
118
  return {
191
119
  protocol: "webhook",
192
- emit: emit,
193
- close: close,
194
- stats: stats,
195
- flush: _flush,
120
+ emit: sink.emit,
121
+ close: sink.close,
122
+ stats: sink.stats,
123
+ flush: sink.flush,
196
124
  };
197
125
  }
198
126
 
package/lib/log.js CHANGED
@@ -464,7 +464,11 @@ function boot(name) {
464
464
  var stream = (LEVELS[levelName] >= LEVELS.warn) ? process.stderr : process.stdout;
465
465
  var isTty = !!(stream && stream.isTTY);
466
466
  if (isTty) {
467
- sink(prefix + String(msg));
467
+ // Raw human-readable line — escape BOTH the C0/newline (line-forging)
468
+ // and bidi (re-ordering) control classes the create() path neutralizes,
469
+ // so a hostile boot message can't inject lines or re-order the visible
470
+ // line on a TTY / syslog reader (CWE-117 / Trojan-Source CVE-2021-42574).
471
+ sink(_escapeBidiControls(_escapeC0Controls(prefix + String(msg))));
468
472
  return;
469
473
  }
470
474
  var entry = {
@@ -474,7 +478,9 @@ function boot(name) {
474
478
  component: name,
475
479
  boot: true,
476
480
  };
477
- sink(JSON.stringify(entry));
481
+ // JSON.stringify already escapes C0/newlines; bidi/format controls survive
482
+ // raw into a piped aggregator, so apply the same bidi escape create() uses.
483
+ sink(_escapeBidiControls(JSON.stringify(entry)));
478
484
  }
479
485
 
480
486
  function debug(msg, fields) {
@@ -560,6 +566,22 @@ function _escapeBidiControls(s) {
560
566
  });
561
567
  }
562
568
 
569
+ // C0 control chars (incl. CR / LF / TAB) + DEL — escaped to `\uXXXX` so a
570
+ // hostile message can't forge extra log lines on a raw (non-JSON) TTY sink
571
+ // (log-injection, CWE-117). The create() path gets this for free from
572
+ // JSON.stringify; the boot() TTY branch writes raw text and needs it
573
+ // explicitly. Pairs with _escapeBidiControls (which only covers the bidi set).
574
+ var _C0_CONTROL_RE = /[\u0000-\u001f\u007f]/g; // eslint-disable-line no-control-regex -- the C0/DEL set is what we escape
575
+
576
+ function _escapeC0Controls(s) {
577
+ if (typeof s !== "string" || s.length === 0) return s;
578
+ return s.replace(_C0_CONTROL_RE, function (ch) {
579
+ var code = ch.charCodeAt(0).toString(16);
580
+ while (code.length < 4) code = "0" + code;
581
+ return "\\u" + code;
582
+ });
583
+ }
584
+
563
585
  // runs before safeEnv on the boot path; safeEnv requires log, so log
564
586
  // can't go through safeEnv to read its own level.
565
587
  function _bootMinLevel() {