@warlock.js/logger 5.29.0 → 5.30.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 (62) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +6 -6
  3. package/cjs/index.cjs +233 -29
  4. package/cjs/index.cjs.map +1 -1
  5. package/esm/channels/console-log.d.mts +1 -1
  6. package/esm/channels/console-log.d.mts.map +1 -1
  7. package/esm/channels/console-log.mjs +1 -1
  8. package/esm/channels/console-log.mjs.map +1 -1
  9. package/esm/channels/file-log.d.mts +1 -1
  10. package/esm/channels/file-log.d.mts.map +1 -1
  11. package/esm/channels/file-log.mjs +1 -1
  12. package/esm/channels/file-log.mjs.map +1 -1
  13. package/esm/channels/json-file-log.d.mts +1 -1
  14. package/esm/channels/json-file-log.d.mts.map +1 -1
  15. package/esm/channels/json-file-log.mjs +1 -1
  16. package/esm/channels/json-file-log.mjs.map +1 -1
  17. package/esm/channels/sentry-log.d.mts +1 -1
  18. package/esm/channels/sentry-log.d.mts.map +1 -1
  19. package/esm/channels/sentry-log.mjs +1 -1
  20. package/esm/channels/sentry-log.mjs.map +1 -1
  21. package/esm/index.d.mts +4 -3
  22. package/esm/index.mjs +3 -2
  23. package/esm/log-channel.d.mts +1 -1
  24. package/esm/log-channel.d.mts.map +1 -1
  25. package/esm/log-channel.mjs +1 -1
  26. package/esm/log-channel.mjs.map +1 -1
  27. package/esm/logger.d.mts +73 -18
  28. package/esm/logger.d.mts.map +1 -1
  29. package/esm/logger.mjs +152 -20
  30. package/esm/logger.mjs.map +1 -1
  31. package/esm/redact/default-keys.d.mts +1 -1
  32. package/esm/redact/default-keys.d.mts.map +1 -1
  33. package/esm/redact/default-keys.mjs +1 -1
  34. package/esm/redact/default-keys.mjs.map +1 -1
  35. package/esm/redact/index.mjs +1 -0
  36. package/esm/redact/mask-email.d.mts +40 -0
  37. package/esm/redact/mask-email.d.mts.map +1 -0
  38. package/esm/redact/mask-email.mjs +70 -0
  39. package/esm/redact/mask-email.mjs.map +1 -0
  40. package/esm/redact/redact.d.mts +1 -1
  41. package/esm/redact/redact.d.mts.map +1 -1
  42. package/esm/redact/redact.mjs +1 -1
  43. package/esm/redact/redact.mjs.map +1 -1
  44. package/esm/types.d.mts +9 -2
  45. package/esm/types.d.mts.map +1 -1
  46. package/esm/utils/capture-unhandled-errors.d.mts +1 -1
  47. package/esm/utils/capture-unhandled-errors.d.mts.map +1 -1
  48. package/esm/utils/capture-unhandled-errors.mjs +1 -1
  49. package/esm/utils/capture-unhandled-errors.mjs.map +1 -1
  50. package/esm/utils/clear-message.d.mts +1 -1
  51. package/esm/utils/clear-message.d.mts.map +1 -1
  52. package/esm/utils/clear-message.mjs +1 -1
  53. package/esm/utils/clear-message.mjs.map +1 -1
  54. package/esm/utils/safe-json-stringify.d.mts +1 -1
  55. package/esm/utils/safe-json-stringify.d.mts.map +1 -1
  56. package/esm/utils/safe-json-stringify.mjs +1 -1
  57. package/esm/utils/safe-json-stringify.mjs.map +1 -1
  58. package/llms-full.txt +6 -6
  59. package/package.json +4 -3
  60. package/skills/flush-logs-on-shutdown/SKILL.md +2 -2
  61. package/skills/logger-basics/SKILL.md +3 -3
  62. package/skills/write-custom-log-channel/SKILL.md +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,31 @@ All notable changes to `@warlock.js/logger` are documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). `@warlock.js/*` packages are released in lockstep — every package shares the same version number, so a version below may list only the changes that affected this package.
6
6
 
7
+ ## 5.30.0 - 2026-10-16
8
+
9
+ ### Upgrading
10
+
11
+ - **BREAKING (types):** `log.info/warn/error/debug/success/fatal/log/assert` and the function `timer()` returns now return the logger synchronously instead of `Promise<this>`. Remove `await` before log calls (it is harmless but pointless) and any `.then()` chained on one; code that needs the write to finish calls `await log.flush()`.
12
+
13
+ ### Changed
14
+
15
+ - Logging is fire-and-forget: level methods no longer return a promise, so `@typescript-eslint/no-floating-promises` no longer flags every call site (WR-019).
16
+ - `log.flush()` drains every channel concurrently: each waits for its own in-flight async deliveries (including entries logged while the flush runs), then runs its `flush()`, so a hung network channel no longer holds up the file channel.
17
+ - `log.flush({ timeout })` is bounded: it resolves after `timeout` ms (default 5000, `DEFAULT_FLUSH_TIMEOUT_MS`; `Infinity` waits without a limit) and reports each channel still draining to the channel error handler. Before, one delivery that never settled made `flush()` (and a graceful shutdown awaiting it) wait forever.
18
+ - `maskEmailsInText` cuts text longer than `MASK_EMAILS_MAX_LENGTH` (16,384 characters) and marks the cut, so masking a huge cache key or message has a bounded cost.
19
+
20
+ ### Added
21
+
22
+ - `log.onChannelError(handler)` / `configure({ onChannelError })` and the `ChannelErrorHandler` type: receives a channel's sync throw or async rejection from `log()`, a redaction that threw, or a timed-out `flush()`. Default: one stderr line. The handler may be async; a rejection is swallowed.
23
+ - `maskEmail(address)` and `maskEmailsInText(text)`: mask email addresses (`m***@example.com`) inside message strings and cache keys, which key-based redaction cannot reach. Core's mail logs and the cache drivers' key logs use them (FW-04).
24
+
25
+ ### Fixed
26
+
27
+ - A channel whose `log()` rejected became an unhandled rejection (looping through `captureAnyUnhandledRejection`), and one that threw synchronously stopped delivery to the channels after it; both are now isolated and reported to the channel error handler.
28
+ - A redaction that threw (a `censor` function that fails on an unexpected value) threw at the call site and skipped the remaining channels. A channel's own redaction failure is now isolated like any channel failure; a logger-wide one drops the entry (it is never delivered unredacted) and is reported to the channel error handler.
29
+ - An async `onChannelError` handler that rejected became an unhandled rejection.
30
+ - **Security:** `maskEmailsInText` backtracked quadratically on long runs of address-like characters with no domain (about 11 s on 75 KB), and it runs on cache keys built from request input. Its pattern is now bounded and linear. It also masks addresses with letters from any script (`jöhn@exämple.de`), and `maskEmail` no longer keeps half of a surrogate pair.
31
+
7
32
  ## 5.29.0 - 2026-10-01
8
33
 
9
34
  ### Changed
package/README.md CHANGED
@@ -28,8 +28,8 @@ log.setChannels([
28
28
  new FileLog({ chunk: "daily" }),
29
29
  ]);
30
30
 
31
- await log.info("users", "register", "New user created");
32
- await log.error("payments", "charge", new Error("Card declined"));
31
+ log.info("users", "register", "New user created");
32
+ log.error("payments", "charge", new Error("Card declined"));
33
33
  ```
34
34
 
35
35
  The logger starts with no channels — nothing is printed or written until you register at least one.
@@ -39,15 +39,15 @@ The logger starts with no channels — nothing is printed or written until you r
39
39
  Every call carries three pieces of context. Modules and actions become searchable keys in file and JSON channels:
40
40
 
41
41
  ```ts
42
- await log.info("auth", "login", "User signed in");
43
- await log.warn("api", "rateLimitApproaching", "80% of quota used");
44
- await log.success("payments", "captured", "Payment of $49.99 captured");
42
+ log.info("auth", "login", "User signed in");
43
+ log.warn("api", "rateLimitApproaching", "80% of quota used");
44
+ log.success("payments", "captured", "Payment of $49.99 captured");
45
45
  ```
46
46
 
47
47
  Pass an object instead of positional args when you need `context` metadata:
48
48
 
49
49
  ```ts
50
- await log.error({
50
+ log.error({
51
51
  module: "orders",
52
52
  action: "checkout",
53
53
  message: "Card declined",
package/cjs/index.cjs CHANGED
@@ -39,7 +39,7 @@ path = __toESM(path, 1);
39
39
  let safe_stable_stringify = require("safe-stable-stringify");
40
40
  let _mongez_reinforcements = require("@mongez/reinforcements");
41
41
 
42
- //#region ../logger/src/log-channel.ts
42
+ //#region ../../packages/logger/src/log-channel.ts
43
43
  var LogChannel = class {
44
44
  /**
45
45
  * Constructor
@@ -112,7 +112,7 @@ var LogChannel = class {
112
112
  };
113
113
 
114
114
  //#endregion
115
- //#region ../logger/src/channels/console-log.ts
115
+ //#region ../../packages/logger/src/channels/console-log.ts
116
116
  /**
117
117
  * The longest level name (`success`) sets the tag column width. Padding every
118
118
  * level name to this width keeps the timestamp / module / action columns
@@ -195,7 +195,7 @@ var ConsoleLog = class extends LogChannel {
195
195
  };
196
196
 
197
197
  //#endregion
198
- //#region ../logger/src/channels/file-log.ts
198
+ //#region ../../packages/logger/src/channels/file-log.ts
199
199
  var FileLog = class extends LogChannel {
200
200
  constructor(..._args) {
201
201
  super(..._args);
@@ -467,7 +467,7 @@ var FileLog = class extends LogChannel {
467
467
  };
468
468
 
469
469
  //#endregion
470
- //#region ../logger/src/utils/safe-json-stringify.ts
470
+ //#region ../../packages/logger/src/utils/safe-json-stringify.ts
471
471
  /**
472
472
  * Replacer that surfaces Error data — `name`, `message`, and `stack` are
473
473
  * non-enumerable on `Error`, so neither default JSON serialization nor an
@@ -498,7 +498,7 @@ function safeJsonStringify(value, space) {
498
498
  }
499
499
 
500
500
  //#endregion
501
- //#region ../logger/src/channels/json-file-log.ts
501
+ //#region ../../packages/logger/src/channels/json-file-log.ts
502
502
  var JSONFileLog = class extends FileLog {
503
503
  constructor(..._args) {
504
504
  super(..._args);
@@ -626,7 +626,7 @@ var JSONFileLog = class extends FileLog {
626
626
  };
627
627
 
628
628
  //#endregion
629
- //#region ../logger/src/channels/sentry-log.ts
629
+ //#region ../../packages/logger/src/channels/sentry-log.ts
630
630
  let Sentry;
631
631
  let isModuleExists = null;
632
632
  let loadingPromise;
@@ -823,7 +823,7 @@ var SentryLog = class extends LogChannel {
823
823
  };
824
824
 
825
825
  //#endregion
826
- //#region ../logger/src/redact/default-keys.ts
826
+ //#region ../../packages/logger/src/redact/default-keys.ts
827
827
  /**
828
828
  * Built-in secret-key denylist — the redaction floor that applies with no
829
829
  * configuration at all.
@@ -937,7 +937,7 @@ const DEFAULT_REDACT_KEYS = [
937
937
  const DEFAULT_REDACT_KEY_SET = new Set(DEFAULT_REDACT_KEYS.map(normalizeRedactKey));
938
938
 
939
939
  //#endregion
940
- //#region ../logger/src/redact/redact.ts
940
+ //#region ../../packages/logger/src/redact/redact.ts
941
941
  const DEFAULT_CENSOR = "[REDACTED]";
942
942
  /**
943
943
  * Values we copy by reference rather than walking. Expanding these with
@@ -1201,7 +1201,75 @@ function mergeRedact(base, extra) {
1201
1201
  }
1202
1202
 
1203
1203
  //#endregion
1204
- //#region ../logger/src/utils/clear-message.ts
1204
+ //#region ../../packages/logger/src/redact/mask-email.ts
1205
+ /**
1206
+ * Email addresses inside free text: a local part and a dotted domain, letters
1207
+ * and digits from any script (`jöhn@exämple.de`). Kept deliberately simple (no
1208
+ * quoted local parts, no IP-literal domains, no percent-encoded `@`): it masks
1209
+ * the addresses that show up in log messages and cache keys, not every string
1210
+ * RFC 5322 allows.
1211
+ *
1212
+ * Every repetition is bounded (local part 64, label 63, at most 12 labels), so
1213
+ * matching stays linear in the input length. Cache keys and messages are often
1214
+ * built from request input; an unbounded pattern backtracks quadratically on a
1215
+ * long run of address-like characters with no domain.
1216
+ */
1217
+ const EMAIL_IN_TEXT = /[\p{L}\p{N}._%+-]{1,64}@[\p{L}\p{N}-]{1,63}(?:\.[\p{L}\p{N}-]{1,63}){0,10}\.\p{L}{2,63}/gu;
1218
+ /**
1219
+ * Longest text {@link maskEmailsInText} scans. Anything past it is cut off and
1220
+ * replaced by a marker, so a huge cache key or message cannot cost more than a
1221
+ * bounded amount of time per log line, and nothing past the cut can leak.
1222
+ */
1223
+ const MASK_EMAILS_MAX_LENGTH = 16384;
1224
+ /**
1225
+ * Mask one email address, keeping its first character and its domain:
1226
+ * `member@example.com` → `m***@example.com`. A value without an `@` is
1227
+ * returned as `***`.
1228
+ *
1229
+ * Key-based redaction cannot reach an address that sits inside a message
1230
+ * string or a cache key; mask it before it is logged.
1231
+ *
1232
+ * @example
1233
+ * log.info("mail", "send", `Sending mail to: ${maskEmail(to)}`);
1234
+ */
1235
+ function maskEmail(address) {
1236
+ const at = address.lastIndexOf("@");
1237
+ if (at <= 0) return "***";
1238
+ return `${String.fromCodePoint(address.codePointAt(0))}***${address.slice(at)}`;
1239
+ }
1240
+ /**
1241
+ * Mask every email address the pattern above recognises in a piece of text
1242
+ * with {@link maskEmail}. Text longer than {@link MASK_EMAILS_MAX_LENGTH}
1243
+ * characters is truncated first (the cut is marked).
1244
+ *
1245
+ * Dots are valid in the local part of an address, so a dotted prefix that
1246
+ * runs into the address is masked with it: in a cache key such as
1247
+ * `email.member@example.com` the whole `email.member` reads as the local part.
1248
+ *
1249
+ * Not recognised (left as they are): quoted local parts (`"john doe"@x.com`),
1250
+ * IP-literal domains (`user@[127.0.0.1]`) and percent-encoded addresses
1251
+ * (`user%40example.com`).
1252
+ *
1253
+ * @example
1254
+ * maskEmailsInText("sent to member@example.com") // "sent to m***@example.com"
1255
+ * maskEmailsInText("email.member@example.com") // "e***@example.com"
1256
+ */
1257
+ function maskEmailsInText(text) {
1258
+ let scanned = text;
1259
+ let marker = "";
1260
+ if (scanned.length > 16384) {
1261
+ let cut = MASK_EMAILS_MAX_LENGTH;
1262
+ const last = scanned.charCodeAt(cut - 1);
1263
+ if (last >= 55296 && last <= 56319) cut -= 1;
1264
+ marker = `…[${scanned.length - cut} chars truncated]`;
1265
+ scanned = scanned.slice(0, cut);
1266
+ }
1267
+ if (!scanned.includes("@")) return scanned + marker;
1268
+ return scanned.replace(EMAIL_IN_TEXT, (match) => maskEmail(match)) + marker;
1269
+ }
1270
+
1271
+ //#endregion
1272
+ //#region ../../packages/logger/src/utils/clear-message.ts
1205
1273
  /**
1206
1274
  * Clear message from any terminal codes
1207
1275
  */
@@ -1211,7 +1279,7 @@ function clearMessage(message) {
1211
1279
  }
1212
1280
 
1213
1281
  //#endregion
1214
- //#region ../logger/src/logger.ts
1282
+ //#region ../../packages/logger/src/logger.ts
1215
1283
  const SIGNAL_EVENTS = new Set([
1216
1284
  "SIGINT",
1217
1285
  "SIGTERM",
@@ -1239,6 +1307,8 @@ var Logger = class {
1239
1307
  this.channels = [];
1240
1308
  this.id = "logger-" + _mongez_reinforcements.Random.string(32);
1241
1309
  this.autoFlushHandlers = /* @__PURE__ */ new Map();
1310
+ this.pendingDeliveries = /* @__PURE__ */ new Map();
1311
+ this.channelErrorHandler = reportChannelError;
1242
1312
  }
1243
1313
  /**
1244
1314
  * Add a new channel
@@ -1255,6 +1325,21 @@ var Logger = class {
1255
1325
  if (config.autoFlushOn) this.enableAutoFlush(config.autoFlushOn);
1256
1326
  if (config.minLevel !== void 0) this.setMinLevel(config.minLevel);
1257
1327
  if (config.redact !== void 0) this.setRedact(config.redact);
1328
+ if (config.onChannelError) this.onChannelError(config.onChannelError);
1329
+ return this;
1330
+ }
1331
+ /**
1332
+ * Handle a channel's delivery failure. Logging is fire-and-forget, so a
1333
+ * channel that throws or rejects never reaches the call site and never
1334
+ * becomes an unhandled rejection; it goes to this handler instead. The
1335
+ * default writes one line to stderr. The handler must not log through this
1336
+ * logger (a failing channel would loop).
1337
+ *
1338
+ * @example
1339
+ * log.onChannelError((error, channel) => metrics.increment(`log.${channel.name}.failed`));
1340
+ */
1341
+ onChannelError(handler) {
1342
+ this.channelErrorHandler = handler;
1258
1343
  return this;
1259
1344
  }
1260
1345
  /**
@@ -1332,21 +1417,62 @@ var Logger = class {
1332
1417
  * — each channel sees its own shallow clone so one channel cannot observe
1333
1418
  * another's mutations (e.g. a later terminal channel still sees the original
1334
1419
  * colored message).
1420
+ *
1421
+ * Synchronous and fire-and-forget: it returns the logger, not a promise.
1422
+ * Async channel deliveries are tracked and awaited by {@link flush}; a
1423
+ * channel that throws or rejects is isolated from the others and reported
1424
+ * to {@link onChannelError}.
1335
1425
  */
1336
- async log(data) {
1426
+ log(data) {
1337
1427
  if (this.minLevel && LEVEL_RANK[data.type] < LEVEL_RANK[this.minLevel]) return this;
1338
- const baseEntry = applyRedact(data, this.redactConfig);
1339
- for (const channel of this.channels) {
1428
+ let baseEntry;
1429
+ try {
1430
+ baseEntry = applyRedact(data, this.redactConfig);
1431
+ } catch (error) {
1432
+ for (const channel of this.channels) this.reportChannelError(error, channel, redactionFailedEntry(data));
1433
+ return this;
1434
+ }
1435
+ for (const channel of this.channels) this.deliver(channel, data, baseEntry);
1436
+ return this;
1437
+ }
1438
+ /**
1439
+ * Prepare and hand one entry to one channel, isolating its failure (a
1440
+ * channel redaction that throws included) and tracking an async delivery
1441
+ * until it settles.
1442
+ */
1443
+ deliver(channel, data, baseEntry) {
1444
+ let payload = baseEntry;
1445
+ let result;
1446
+ try {
1340
1447
  const channelRedact = channel.getRedactConfig?.();
1341
- const effectiveRedact = channelRedact ? mergeRedact(this.redactConfig, channelRedact) : void 0;
1342
- let payload = effectiveRedact ? applyRedact(data, effectiveRedact) : baseEntry;
1448
+ if (channelRedact) payload = applyRedact(data, mergeRedact(this.redactConfig, channelRedact));
1343
1449
  if (channel.terminal === false) payload = {
1344
1450
  ...payload,
1345
1451
  message: clearMessage(payload.message)
1346
1452
  };
1347
- channel.log(payload);
1453
+ result = channel.log(payload);
1454
+ } catch (error) {
1455
+ this.reportChannelError(error, channel, payload);
1456
+ return;
1348
1457
  }
1349
- return this;
1458
+ if (!result || typeof result.then !== "function") return;
1459
+ let pending = this.pendingDeliveries.get(channel);
1460
+ if (!pending) {
1461
+ pending = /* @__PURE__ */ new Set();
1462
+ this.pendingDeliveries.set(channel, pending);
1463
+ }
1464
+ const tracked = pending;
1465
+ const delivery = Promise.resolve(result).catch((error) => this.reportChannelError(error, channel, payload)).finally(() => {
1466
+ tracked.delete(delivery);
1467
+ if (tracked.size === 0 && this.pendingDeliveries.get(channel) === tracked) this.pendingDeliveries.delete(channel);
1468
+ });
1469
+ tracked.add(delivery);
1470
+ }
1471
+ reportChannelError(error, channel, entry) {
1472
+ try {
1473
+ const outcome = this.channelErrorHandler(error, channel, entry);
1474
+ if (outcome && typeof outcome.then === "function") outcome.then(void 0, () => {});
1475
+ } catch {}
1350
1476
  }
1351
1477
  /**
1352
1478
  * Make debug log
@@ -1442,7 +1568,9 @@ var Logger = class {
1442
1568
  for (const channel of this.channels) if (channel.flushSync) channel.flushSync();
1443
1569
  }
1444
1570
  /**
1445
- * Asynchronously drain every channel that implements `flush()`.
1571
+ * Wait for every in-flight async delivery, then drain every channel that
1572
+ * implements `flush()` — bounded by `timeout` (default
1573
+ * {@link DEFAULT_FLUSH_TIMEOUT_MS}).
1446
1574
  *
1447
1575
  * Unlike {@link flushSync}, this awaits each channel's async I/O — the
1448
1576
  * correct call for a graceful shutdown that can afford to wait
@@ -1450,24 +1578,68 @@ var Logger = class {
1450
1578
  * `process.exit`). A channel whose delivery is async (a network transport,
1451
1579
  * an async disk write) implements `flush()`, not `flushSync()`.
1452
1580
  *
1453
- * Channels are isolated: a channel whose flush rejects can neither prevent
1454
- * the others from draining nor escape as an unhandled rejection. Channels
1455
- * without `flush()` are skipped.
1581
+ * Channels drain concurrently and independently: each waits for its own
1582
+ * pending deliveries (including ones logged while the flush runs), then
1583
+ * runs its `flush()`. A channel whose delivery hangs therefore cannot hold
1584
+ * up another channel's drain, and a channel whose flush rejects can neither
1585
+ * stop the others nor escape as an unhandled rejection. When `timeout`
1586
+ * elapses, `flush()` resolves anyway and each channel still draining is
1587
+ * reported to {@link onChannelError}. Channels without `flush()` only wait
1588
+ * for their deliveries.
1456
1589
  *
1457
1590
  * @example
1458
1591
  * async function shutdown() {
1459
1592
  * await httpServer.close();
1460
- * await log.flush();
1593
+ * await log.flush({ timeout: 3000 });
1461
1594
  * process.exit(0);
1462
1595
  * }
1463
1596
  */
1464
- async flush() {
1465
- await Promise.allSettled(this.channels.map(async (channel) => {
1466
- if (!channel.flush) return;
1597
+ async flush(options = {}) {
1598
+ const timeout = options.timeout ?? 5e3;
1599
+ const draining = new Set(this.channels);
1600
+ for (const channel of this.pendingDeliveries.keys()) draining.add(channel);
1601
+ const unfinished = new Set(draining);
1602
+ const drains = [...draining].map(async (channel) => {
1467
1603
  try {
1468
- await channel.flush();
1469
- } catch {}
1470
- }));
1604
+ await this.drainChannel(channel);
1605
+ } catch {} finally {
1606
+ unfinished.delete(channel);
1607
+ }
1608
+ });
1609
+ const settled = Promise.allSettled(drains).then(() => false);
1610
+ if (!Number.isFinite(timeout)) {
1611
+ await settled;
1612
+ return;
1613
+ }
1614
+ let resolveTimeout;
1615
+ const timeoutReached = new Promise((resolve) => {
1616
+ resolveTimeout = resolve;
1617
+ });
1618
+ const timer = setTimeout(() => resolveTimeout(true), Math.max(0, timeout));
1619
+ timer.unref?.();
1620
+ const timedOut = await Promise.race([settled, timeoutReached]);
1621
+ clearTimeout(timer);
1622
+ if (!timedOut) return;
1623
+ for (const channel of unfinished) this.reportChannelError(/* @__PURE__ */ new Error(`flush timed out after ${timeout}ms; entries still in flight may be lost`), channel, {
1624
+ type: "warn",
1625
+ module: "logger",
1626
+ action: "flush",
1627
+ message: "flush timed out"
1628
+ });
1629
+ }
1630
+ /**
1631
+ * Wait until the channel has no delivery in flight, run its `flush()`, and
1632
+ * repeat while that flush let new deliveries in.
1633
+ */
1634
+ async drainChannel(channel) {
1635
+ do {
1636
+ let pending = this.pendingDeliveries.get(channel);
1637
+ while (pending && pending.size > 0) {
1638
+ await Promise.allSettled([...pending]);
1639
+ pending = this.pendingDeliveries.get(channel);
1640
+ }
1641
+ if (channel.flush) await channel.flush();
1642
+ } while ((this.pendingDeliveries.get(channel)?.size ?? 0) > 0);
1471
1643
  }
1472
1644
  /**
1473
1645
  * Register one process-level handler per event that calls `flushSync()`
@@ -1513,6 +1685,34 @@ var Logger = class {
1513
1685
  }
1514
1686
  };
1515
1687
  /**
1688
+ * Default `timeout` of {@link Logger.flush}: long enough for a healthy network
1689
+ * channel, short enough that a hung one cannot eat a shutdown budget.
1690
+ */
1691
+ const DEFAULT_FLUSH_TIMEOUT_MS = 5e3;
1692
+ /**
1693
+ * What the error handler receives when the logger-wide redaction itself
1694
+ * failed: the entry's identity only, never the unredacted message or context.
1695
+ */
1696
+ function redactionFailedEntry(data) {
1697
+ return {
1698
+ type: data.type,
1699
+ module: data.module,
1700
+ action: data.action,
1701
+ message: "[entry dropped: redaction failed]"
1702
+ };
1703
+ }
1704
+ /**
1705
+ * Default {@link ChannelErrorHandler}: one stderr line per failure. Written
1706
+ * directly to the stream, never through a logger, so a broken channel cannot
1707
+ * recurse.
1708
+ */
1709
+ function reportChannelError(error, channel) {
1710
+ const reason = error instanceof Error ? error.message : String(error);
1711
+ try {
1712
+ process.stderr.write(`[@warlock.js/logger] channel "${channel.name}" failed to log: ${reason}\n`);
1713
+ } catch {}
1714
+ }
1715
+ /**
1516
1716
  * The package singleton. Use this for everyday logging — `log.info(...)`,
1517
1717
  * `log.error(...)`, `log.configure(...)`. Custom logger instances can be
1518
1718
  * created by instantiating `Logger` directly.
@@ -1529,7 +1729,7 @@ var Logger = class {
1529
1729
  const log = new Logger();
1530
1730
 
1531
1731
  //#endregion
1532
- //#region ../logger/src/utils/capture-unhandled-errors.ts
1732
+ //#region ../../packages/logger/src/utils/capture-unhandled-errors.ts
1533
1733
  /**
1534
1734
  * Best-effort budget (ms) for draining async channels (file, Sentry, …) before
1535
1735
  * the forced exit. Bounded so a single stuck channel can't hang the process —
@@ -1579,17 +1779,21 @@ function captureAnyUnhandledRejection(options = {}) {
1579
1779
 
1580
1780
  //#endregion
1581
1781
  exports.ConsoleLog = ConsoleLog;
1782
+ exports.DEFAULT_FLUSH_TIMEOUT_MS = DEFAULT_FLUSH_TIMEOUT_MS;
1582
1783
  exports.DEFAULT_REDACT_KEYS = DEFAULT_REDACT_KEYS;
1583
1784
  exports.DEFAULT_REDACT_KEY_SET = DEFAULT_REDACT_KEY_SET;
1584
1785
  exports.FileLog = FileLog;
1585
1786
  exports.JSONFileLog = JSONFileLog;
1586
1787
  exports.LogChannel = LogChannel;
1587
1788
  exports.Logger = Logger;
1789
+ exports.MASK_EMAILS_MAX_LENGTH = MASK_EMAILS_MAX_LENGTH;
1588
1790
  exports.SentryLog = SentryLog;
1589
1791
  exports.applyRedact = applyRedact;
1590
1792
  exports.captureAnyUnhandledRejection = captureAnyUnhandledRejection;
1591
1793
  exports.clearMessage = clearMessage;
1592
1794
  exports.log = log;
1795
+ exports.maskEmail = maskEmail;
1796
+ exports.maskEmailsInText = maskEmailsInText;
1593
1797
  exports.mergeRedact = mergeRedact;
1594
1798
  exports.normalizeRedactKey = normalizeRedactKey;
1595
1799
  exports.resolveRedactKeys = resolveRedactKeys;