@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
@@ -1 +1 @@
1
- {"version":3,"file":"redact.mjs","names":[],"sources":["../../../../../../../logger/src/redact/redact.ts"],"sourcesContent":["import type { LoggingData, RedactCensor, RedactConfig } from \"../types\";\nimport {\n DEFAULT_REDACT_KEY_SET,\n normalizeRedactKey,\n} from \"./default-keys\";\n\nconst DEFAULT_CENSOR = \"[REDACTED]\";\n\n/**\n * Per-key redaction settings threaded through the clone walk. Absent when the\n * walk is a plain structural clone (path-only redaction).\n */\ntype KeyRedaction = {\n /** Normalized key names to censor wherever they appear. */\n keys: ReadonlySet<string>;\n censor: RedactCensor;\n /**\n * Dotted paths the path-glob pass already censored. Skipped here so a\n * function censor is never invoked twice on the same leaf (the second call\n * would receive the already-masked value, not the original).\n */\n alreadyCensored: ReadonlySet<string>;\n};\n\n/**\n * Values we copy by reference rather than walking. Expanding these with\n * `Object.keys` would be destructive, not protective — a `Buffer` becomes a\n * multi-thousand-key index map, a `Map`/`Set`/`RegExp` becomes `{}`. Their\n * contents are consequently *not* reachable by redaction; see the residual\n * gaps documented on `applyRedact`.\n */\nfunction isOpaque(value: object): boolean {\n return (\n ArrayBuffer.isView(value) ||\n value instanceof ArrayBuffer ||\n value instanceof Map ||\n value instanceof Set ||\n value instanceof WeakMap ||\n value instanceof WeakSet ||\n value instanceof Promise ||\n value instanceof RegExp\n );\n}\n\n/**\n * Deep-clone a value with structural fidelity for log entries — handles plain\n * objects, arrays, `Date`, `Error`, and primitives. Anything else (buffers,\n * maps, sets, promises, regexes) is copied by reference: we only redact\n * through walkable structures anyway, and rebuilding e.g. a `Buffer` would\n * change semantics.\n *\n * Purpose-built rather than reaching for `structuredClone`: `Error` instances\n * lose their `message`/`stack` under `structuredClone` in some Node versions,\n * and the logger pipeline carries them often.\n *\n * When `redaction` is supplied, keys matching its denylist are censored during\n * the same pass — one traversal, not two.\n */\nfunction cloneEntry<T>(\n value: T,\n seen = new WeakMap<object, any>(),\n redaction?: KeyRedaction,\n trail: string[] = [],\n): T {\n if (value === null || typeof value !== \"object\") {\n return value;\n }\n\n const asObject = value as unknown as object;\n\n if (seen.has(asObject)) {\n return seen.get(asObject);\n }\n\n if (value instanceof Date) {\n return new Date(value.getTime()) as unknown as T;\n }\n\n if (isOpaque(asObject)) {\n return value;\n }\n\n if (value instanceof Error) {\n return cloneError(value, seen, redaction, trail) as unknown as T;\n }\n\n if (Array.isArray(value)) {\n const arr: any[] = [];\n seen.set(asObject, arr);\n value.forEach((item, index) => {\n // Array indices are positions, not names — never key-matched, but the\n // trail still carries them so function censors see a usable path.\n arr.push(cloneEntry(item, seen, redaction, [...trail, String(index)]));\n });\n return arr as unknown as T;\n }\n\n const out: Record<string, any> = {};\n seen.set(asObject, out);\n copyOwnKeys(value as Record<string, any>, out, seen, redaction, trail);\n return out as unknown as T;\n}\n\n/**\n * Copy every own enumerable key from `source` onto `target`, censoring the\n * ones the key denylist matches and recursing into the rest.\n */\nfunction copyOwnKeys(\n source: Record<string, any>,\n target: Record<string, any>,\n seen: WeakMap<object, any>,\n redaction: KeyRedaction | undefined,\n trail: string[],\n): void {\n for (const key of Object.keys(source)) {\n const childTrail = [...trail, key];\n\n if (\n redaction &&\n redaction.keys.has(normalizeRedactKey(key)) &&\n !redaction.alreadyCensored.has(childTrail.join(\".\"))\n ) {\n target[key] = applyCensor(source[key], redaction.censor, childTrail);\n continue;\n }\n\n target[key] = cloneEntry(source[key], seen, redaction, childTrail);\n }\n}\n\n/**\n * Clone an `Error`, preserving its own enumerable properties.\n *\n * Those extra properties matter for redaction: HTTP clients (axios, got)\n * attach `.config`/`.request`/`.response` to the errors they throw, and those\n * routinely carry the *outgoing* `Authorization` header of the failed request.\n * Copying them through the walk is what lets the key denylist reach in and\n * censor them — dropping them instead (the pre-4.15.0 behavior) hid the\n * secret only when redaction happened to be configured, and took `.code` and\n * friends with it.\n *\n * `name`/`message`/`stack` are non-enumerable on `Error`, so they are carried\n * over explicitly.\n */\nfunction cloneError(\n value: Error,\n seen: WeakMap<object, any>,\n redaction: KeyRedaction | undefined,\n trail: string[],\n): Error {\n let copy: Error;\n\n try {\n copy = new (value.constructor as ErrorConstructor)(value.message);\n } catch {\n // A subclass whose constructor demands a different signature must not\n // take the whole log call down with it.\n copy = new Error(value.message);\n }\n\n // Assigned rather than trusted to the constructor: a subclass that derives\n // its message from a structured argument (`new HttpError({ detail })`)\n // produces an empty message when replayed with a plain string. Defined\n // non-enumerably to match native `Error`, so the clone serializes with the\n // same shape as the original.\n for (const [key, source] of [\n [\"message\", value.message],\n [\"name\", value.name],\n [\"stack\", value.stack],\n ] as const) {\n Object.defineProperty(copy, key, {\n value: source,\n writable: true,\n enumerable: false,\n configurable: true,\n });\n }\n\n seen.set(value, copy);\n copyOwnKeys(value as unknown as Record<string, any>, copy as any, seen, redaction, trail);\n\n return copy;\n}\n\n/**\n * Apply a single censor decision to a value. String censors are returned\n * verbatim; function censors receive the original value plus the dotted\n * path so callers can implement value-aware redaction (mask all but the\n * last 4 chars, hash, etc.).\n */\nfunction applyCensor(value: any, censor: RedactCensor, path: string[]): any {\n if (typeof censor === \"function\") {\n return censor(value, path.join(\".\"));\n }\n return censor;\n}\n\n/**\n * Walk `target` following the remaining `segments` of a path pattern,\n * replacing matched leaves via `censor`. Operates in place — the caller\n * is responsible for cloning before calling.\n *\n * Every censored leaf's dotted path is recorded in `censored` so a following\n * key-denylist pass can leave it alone.\n *\n * Wildcards:\n * - `*` matches exactly one segment (any key on a plain object, any index\n * on an array — stringified for the path that's passed to a function\n * censor).\n * - `**` matches zero or more segments greedily; the rest of the pattern\n * is then attempted at the current level and at every descendant.\n */\nfunction redactAtPath(\n target: any,\n segments: string[],\n censor: RedactCensor,\n pathTrail: string[],\n censored: Set<string>,\n): void {\n if (target === null || typeof target !== \"object\") {\n return;\n }\n\n if (segments.length === 0) {\n return;\n }\n\n const [head, ...rest] = segments;\n\n if (head === undefined) {\n // The `segments.length === 0` guard above already makes this unreachable,\n // but the compiler cannot narrow a DESTRUCTURED element from a length\n // check, so under `noUncheckedIndexedAccess` `head` is `string |\n // undefined` and every use of it below inherits that.\n //\n // Stated rather than asserted away with `!`, because of what this function\n // is: if `head` were ever undefined, `keysToVisit` would carry an\n // `undefined` key, `target[key]` would read and WRITE the literal property\n // \"undefined\", and the field that was supposed to be censored would be\n // left in the log untouched. A redaction path is the wrong place to be\n // approximately right.\n return;\n }\n\n if (head === \"**\") {\n // Try matching `rest` at the current level (the zero-segment match\n // case), then recurse into every child carrying the `**` forward so\n // it keeps matching at deeper levels too.\n if (rest.length > 0) {\n redactAtPath(target, rest, censor, pathTrail, censored);\n }\n const keys = Array.isArray(target)\n ? target.map((_, index) => String(index))\n : Object.keys(target);\n for (const key of keys) {\n redactAtPath(target[key], segments, censor, [...pathTrail, key], censored);\n }\n return;\n }\n\n const keysToVisit =\n head === \"*\"\n ? Array.isArray(target)\n ? target.map((_, index) => String(index))\n : Object.keys(target)\n : Array.isArray(target)\n ? // Numeric segment can index into an array.\n /^\\d+$/.test(head) && Number(head) < target.length\n ? [head]\n : []\n : Object.prototype.hasOwnProperty.call(target, head)\n ? [head]\n : [];\n\n for (const key of keysToVisit) {\n if (rest.length === 0) {\n const leafTrail = [...pathTrail, key];\n target[key] = applyCensor(target[key], censor, leafTrail);\n censored.add(leafTrail.join(\".\"));\n } else {\n redactAtPath(target[key], rest, censor, [...pathTrail, key], censored);\n }\n }\n}\n\n/**\n * Cheap pre-scan: does this graph contain any denylisted key at all?\n *\n * Lets the default-on key pass stay allocation-free for the overwhelming\n * majority of entries, which carry no secrets — we only pay for a clone when\n * there is actually something to censor. Also keeps `applyRedact`'s\n * \"returns the input by reference when nothing changed\" contract intact.\n */\nfunction hasDenylistedKey(\n value: any,\n keys: ReadonlySet<string>,\n seen = new WeakSet<object>(),\n): boolean {\n if (value === null || typeof value !== \"object\") {\n return false;\n }\n\n const asObject = value as object;\n\n if (seen.has(asObject)) return false;\n seen.add(asObject);\n\n if (value instanceof Date || isOpaque(asObject)) {\n return false;\n }\n\n if (Array.isArray(value)) {\n return value.some((item) => hasDenylistedKey(item, keys, seen));\n }\n\n for (const key of Object.keys(value)) {\n if (keys.has(normalizeRedactKey(key))) return true;\n if (hasDenylistedKey(value[key], keys, seen)) return true;\n }\n\n return false;\n}\n\n/**\n * Cache of resolved key sets, keyed by the config object they came from.\n * Logger-wide and channel configs are long-lived references, so this makes\n * the per-entry cost a single map lookup. Merged configs (rebuilt per entry\n * by `mergeRedact`) fall out of the `WeakMap` on their own.\n */\nconst keySetCache = new WeakMap<RedactConfig, ReadonlySet<string>>();\n\n/**\n * Resolve the effective key denylist for a config: the built-in set (unless\n * `defaultKeys: false`) plus any `keys` the application added.\n *\n * Returns `undefined` only when there is nothing to match — i.e. defaults are\n * explicitly off and no custom keys were supplied.\n */\nexport function resolveRedactKeys(\n config: RedactConfig | undefined,\n): ReadonlySet<string> | undefined {\n const useDefaults = config?.defaultKeys !== false;\n const extra = config?.keys;\n\n if (!extra || extra.length === 0) {\n return useDefaults ? DEFAULT_REDACT_KEY_SET : undefined;\n }\n\n const cached = config && keySetCache.get(config);\n if (cached) return cached;\n\n const resolved = new Set<string>(useDefaults ? DEFAULT_REDACT_KEY_SET : []);\n for (const key of extra) {\n resolved.add(normalizeRedactKey(key));\n }\n\n if (config) keySetCache.set(config, resolved);\n\n return resolved;\n}\n\n/**\n * Produce a new `LoggingData` with sensitive data censored:\n *\n * 1. every path in `config.paths` (opt-in globs), then\n * 2. every key matching the denylist — the built-in\n * {@link DEFAULT_REDACT_KEYS} plus `config.keys`, at any depth of\n * `context`, `message`, and an `Error`'s own enumerable properties.\n *\n * Step 2 runs **with no config at all**: passing `undefined` still censors\n * `password`, `authorization`, `token`, `apiKey` and friends. Pass\n * `{ defaultKeys: false }` to opt out.\n *\n * Paths run first so a function censor sees the original value rather than a\n * mask; leaves the path pass already censored are skipped by the key pass.\n *\n * The original entry is never mutated — channels and other call sites can\n * hold references to the input safely. Returns the input **by reference**\n * when nothing matched, so the fast path stays allocation-free.\n *\n * ## Residual gaps (by design, documented rather than silently absent)\n *\n * - **Secrets interpolated into a `message` string** (`` `token=${t}` ``)\n * cannot be reached — neither a path nor a key names a substring.\n * - **`Map`/`Set`/`Buffer` contents** are not traversed (see {@link isOpaque}).\n * - **Non-enumerable / getter-backed properties** are not walked, so an HTTP\n * client that exposes request config behind a getter still slips through.\n * Enumerable ones (axios's `.config`, `.response`) *are* covered.\n */\nexport function applyRedact(\n data: LoggingData,\n config: RedactConfig | undefined,\n): LoggingData {\n const paths = config?.paths ?? [];\n const keys = resolveRedactKeys(config);\n\n if (paths.length === 0 && !keys) {\n return data;\n }\n\n const censor = config?.censor ?? DEFAULT_CENSOR;\n\n let result = data;\n const censored = new Set<string>();\n\n if (paths.length > 0) {\n result = cloneEntry(data);\n\n for (const pattern of paths) {\n const segments = pattern.split(\".\").filter((segment) => segment.length > 0);\n if (segments.length === 0) continue;\n redactAtPath(result, segments, censor, [], censored);\n }\n }\n\n if (keys && hasDenylistedKey(result, keys)) {\n result = cloneEntry(result, new WeakMap(), {\n keys,\n censor,\n alreadyCensored: censored,\n });\n }\n\n return result;\n}\n\n/**\n * Combine two redact configs into one effective config. Used to merge a\n * channel's additive paths on top of the logger-wide floor.\n *\n * - `paths` and `keys` are concatenated; duplicates are kept (the matcher\n * tolerates them, and de-duping cross-config would mask a developer typo).\n * - `censor` from the channel wins; falls back to the logger's; falls back\n * to the default `\"[REDACTED]\"`.\n * - `defaultKeys` follows the additive-only contract: a channel can turn the\n * built-in denylist back *on* (`true`) but can never turn off one the\n * logger-wide floor left enabled. When the channel is silent, the logger's\n * choice is inherited — opting out logger-wide is not quietly undone by\n * any channel that happens to set a `redact` option.\n */\nexport function mergeRedact(\n base: RedactConfig | undefined,\n extra: RedactConfig | undefined,\n): RedactConfig | undefined {\n if (!base && !extra) return undefined;\n\n if (!base) {\n // An absent logger-wide config still means \"built-in denylist on\" — it is\n // the floor, not the absence of one. A channel may not switch it off, so\n // its `defaultKeys: false` is overridden here rather than inherited.\n return extra!.defaultKeys === false\n ? { ...extra!, defaultKeys: true }\n : extra;\n }\n\n if (!extra) return base;\n\n return {\n paths: [...(base.paths ?? []), ...(extra.paths ?? [])],\n keys: [...(base.keys ?? []), ...(extra.keys ?? [])],\n defaultKeys: extra.defaultKeys === true ? true : base.defaultKeys,\n censor: extra.censor ?? base.censor,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"redact.mjs","names":[],"sources":["../../../../../../../../packages/logger/src/redact/redact.ts"],"sourcesContent":["import type { LoggingData, RedactCensor, RedactConfig } from \"../types\";\nimport {\n DEFAULT_REDACT_KEY_SET,\n normalizeRedactKey,\n} from \"./default-keys\";\n\nconst DEFAULT_CENSOR = \"[REDACTED]\";\n\n/**\n * Per-key redaction settings threaded through the clone walk. Absent when the\n * walk is a plain structural clone (path-only redaction).\n */\ntype KeyRedaction = {\n /** Normalized key names to censor wherever they appear. */\n keys: ReadonlySet<string>;\n censor: RedactCensor;\n /**\n * Dotted paths the path-glob pass already censored. Skipped here so a\n * function censor is never invoked twice on the same leaf (the second call\n * would receive the already-masked value, not the original).\n */\n alreadyCensored: ReadonlySet<string>;\n};\n\n/**\n * Values we copy by reference rather than walking. Expanding these with\n * `Object.keys` would be destructive, not protective — a `Buffer` becomes a\n * multi-thousand-key index map, a `Map`/`Set`/`RegExp` becomes `{}`. Their\n * contents are consequently *not* reachable by redaction; see the residual\n * gaps documented on `applyRedact`.\n */\nfunction isOpaque(value: object): boolean {\n return (\n ArrayBuffer.isView(value) ||\n value instanceof ArrayBuffer ||\n value instanceof Map ||\n value instanceof Set ||\n value instanceof WeakMap ||\n value instanceof WeakSet ||\n value instanceof Promise ||\n value instanceof RegExp\n );\n}\n\n/**\n * Deep-clone a value with structural fidelity for log entries — handles plain\n * objects, arrays, `Date`, `Error`, and primitives. Anything else (buffers,\n * maps, sets, promises, regexes) is copied by reference: we only redact\n * through walkable structures anyway, and rebuilding e.g. a `Buffer` would\n * change semantics.\n *\n * Purpose-built rather than reaching for `structuredClone`: `Error` instances\n * lose their `message`/`stack` under `structuredClone` in some Node versions,\n * and the logger pipeline carries them often.\n *\n * When `redaction` is supplied, keys matching its denylist are censored during\n * the same pass — one traversal, not two.\n */\nfunction cloneEntry<T>(\n value: T,\n seen = new WeakMap<object, any>(),\n redaction?: KeyRedaction,\n trail: string[] = [],\n): T {\n if (value === null || typeof value !== \"object\") {\n return value;\n }\n\n const asObject = value as unknown as object;\n\n if (seen.has(asObject)) {\n return seen.get(asObject);\n }\n\n if (value instanceof Date) {\n return new Date(value.getTime()) as unknown as T;\n }\n\n if (isOpaque(asObject)) {\n return value;\n }\n\n if (value instanceof Error) {\n return cloneError(value, seen, redaction, trail) as unknown as T;\n }\n\n if (Array.isArray(value)) {\n const arr: any[] = [];\n seen.set(asObject, arr);\n value.forEach((item, index) => {\n // Array indices are positions, not names — never key-matched, but the\n // trail still carries them so function censors see a usable path.\n arr.push(cloneEntry(item, seen, redaction, [...trail, String(index)]));\n });\n return arr as unknown as T;\n }\n\n const out: Record<string, any> = {};\n seen.set(asObject, out);\n copyOwnKeys(value as Record<string, any>, out, seen, redaction, trail);\n return out as unknown as T;\n}\n\n/**\n * Copy every own enumerable key from `source` onto `target`, censoring the\n * ones the key denylist matches and recursing into the rest.\n */\nfunction copyOwnKeys(\n source: Record<string, any>,\n target: Record<string, any>,\n seen: WeakMap<object, any>,\n redaction: KeyRedaction | undefined,\n trail: string[],\n): void {\n for (const key of Object.keys(source)) {\n const childTrail = [...trail, key];\n\n if (\n redaction &&\n redaction.keys.has(normalizeRedactKey(key)) &&\n !redaction.alreadyCensored.has(childTrail.join(\".\"))\n ) {\n target[key] = applyCensor(source[key], redaction.censor, childTrail);\n continue;\n }\n\n target[key] = cloneEntry(source[key], seen, redaction, childTrail);\n }\n}\n\n/**\n * Clone an `Error`, preserving its own enumerable properties.\n *\n * Those extra properties matter for redaction: HTTP clients (axios, got)\n * attach `.config`/`.request`/`.response` to the errors they throw, and those\n * routinely carry the *outgoing* `Authorization` header of the failed request.\n * Copying them through the walk is what lets the key denylist reach in and\n * censor them — dropping them instead (the pre-4.15.0 behavior) hid the\n * secret only when redaction happened to be configured, and took `.code` and\n * friends with it.\n *\n * `name`/`message`/`stack` are non-enumerable on `Error`, so they are carried\n * over explicitly.\n */\nfunction cloneError(\n value: Error,\n seen: WeakMap<object, any>,\n redaction: KeyRedaction | undefined,\n trail: string[],\n): Error {\n let copy: Error;\n\n try {\n copy = new (value.constructor as ErrorConstructor)(value.message);\n } catch {\n // A subclass whose constructor demands a different signature must not\n // take the whole log call down with it.\n copy = new Error(value.message);\n }\n\n // Assigned rather than trusted to the constructor: a subclass that derives\n // its message from a structured argument (`new HttpError({ detail })`)\n // produces an empty message when replayed with a plain string. Defined\n // non-enumerably to match native `Error`, so the clone serializes with the\n // same shape as the original.\n for (const [key, source] of [\n [\"message\", value.message],\n [\"name\", value.name],\n [\"stack\", value.stack],\n ] as const) {\n Object.defineProperty(copy, key, {\n value: source,\n writable: true,\n enumerable: false,\n configurable: true,\n });\n }\n\n seen.set(value, copy);\n copyOwnKeys(value as unknown as Record<string, any>, copy as any, seen, redaction, trail);\n\n return copy;\n}\n\n/**\n * Apply a single censor decision to a value. String censors are returned\n * verbatim; function censors receive the original value plus the dotted\n * path so callers can implement value-aware redaction (mask all but the\n * last 4 chars, hash, etc.).\n */\nfunction applyCensor(value: any, censor: RedactCensor, path: string[]): any {\n if (typeof censor === \"function\") {\n return censor(value, path.join(\".\"));\n }\n return censor;\n}\n\n/**\n * Walk `target` following the remaining `segments` of a path pattern,\n * replacing matched leaves via `censor`. Operates in place — the caller\n * is responsible for cloning before calling.\n *\n * Every censored leaf's dotted path is recorded in `censored` so a following\n * key-denylist pass can leave it alone.\n *\n * Wildcards:\n * - `*` matches exactly one segment (any key on a plain object, any index\n * on an array — stringified for the path that's passed to a function\n * censor).\n * - `**` matches zero or more segments greedily; the rest of the pattern\n * is then attempted at the current level and at every descendant.\n */\nfunction redactAtPath(\n target: any,\n segments: string[],\n censor: RedactCensor,\n pathTrail: string[],\n censored: Set<string>,\n): void {\n if (target === null || typeof target !== \"object\") {\n return;\n }\n\n if (segments.length === 0) {\n return;\n }\n\n const [head, ...rest] = segments;\n\n if (head === undefined) {\n // The `segments.length === 0` guard above already makes this unreachable,\n // but the compiler cannot narrow a DESTRUCTURED element from a length\n // check, so under `noUncheckedIndexedAccess` `head` is `string |\n // undefined` and every use of it below inherits that.\n //\n // Stated rather than asserted away with `!`, because of what this function\n // is: if `head` were ever undefined, `keysToVisit` would carry an\n // `undefined` key, `target[key]` would read and WRITE the literal property\n // \"undefined\", and the field that was supposed to be censored would be\n // left in the log untouched. A redaction path is the wrong place to be\n // approximately right.\n return;\n }\n\n if (head === \"**\") {\n // Try matching `rest` at the current level (the zero-segment match\n // case), then recurse into every child carrying the `**` forward so\n // it keeps matching at deeper levels too.\n if (rest.length > 0) {\n redactAtPath(target, rest, censor, pathTrail, censored);\n }\n const keys = Array.isArray(target)\n ? target.map((_, index) => String(index))\n : Object.keys(target);\n for (const key of keys) {\n redactAtPath(target[key], segments, censor, [...pathTrail, key], censored);\n }\n return;\n }\n\n const keysToVisit =\n head === \"*\"\n ? Array.isArray(target)\n ? target.map((_, index) => String(index))\n : Object.keys(target)\n : Array.isArray(target)\n ? // Numeric segment can index into an array.\n /^\\d+$/.test(head) && Number(head) < target.length\n ? [head]\n : []\n : Object.prototype.hasOwnProperty.call(target, head)\n ? [head]\n : [];\n\n for (const key of keysToVisit) {\n if (rest.length === 0) {\n const leafTrail = [...pathTrail, key];\n target[key] = applyCensor(target[key], censor, leafTrail);\n censored.add(leafTrail.join(\".\"));\n } else {\n redactAtPath(target[key], rest, censor, [...pathTrail, key], censored);\n }\n }\n}\n\n/**\n * Cheap pre-scan: does this graph contain any denylisted key at all?\n *\n * Lets the default-on key pass stay allocation-free for the overwhelming\n * majority of entries, which carry no secrets — we only pay for a clone when\n * there is actually something to censor. Also keeps `applyRedact`'s\n * \"returns the input by reference when nothing changed\" contract intact.\n */\nfunction hasDenylistedKey(\n value: any,\n keys: ReadonlySet<string>,\n seen = new WeakSet<object>(),\n): boolean {\n if (value === null || typeof value !== \"object\") {\n return false;\n }\n\n const asObject = value as object;\n\n if (seen.has(asObject)) return false;\n seen.add(asObject);\n\n if (value instanceof Date || isOpaque(asObject)) {\n return false;\n }\n\n if (Array.isArray(value)) {\n return value.some((item) => hasDenylistedKey(item, keys, seen));\n }\n\n for (const key of Object.keys(value)) {\n if (keys.has(normalizeRedactKey(key))) return true;\n if (hasDenylistedKey(value[key], keys, seen)) return true;\n }\n\n return false;\n}\n\n/**\n * Cache of resolved key sets, keyed by the config object they came from.\n * Logger-wide and channel configs are long-lived references, so this makes\n * the per-entry cost a single map lookup. Merged configs (rebuilt per entry\n * by `mergeRedact`) fall out of the `WeakMap` on their own.\n */\nconst keySetCache = new WeakMap<RedactConfig, ReadonlySet<string>>();\n\n/**\n * Resolve the effective key denylist for a config: the built-in set (unless\n * `defaultKeys: false`) plus any `keys` the application added.\n *\n * Returns `undefined` only when there is nothing to match — i.e. defaults are\n * explicitly off and no custom keys were supplied.\n */\nexport function resolveRedactKeys(\n config: RedactConfig | undefined,\n): ReadonlySet<string> | undefined {\n const useDefaults = config?.defaultKeys !== false;\n const extra = config?.keys;\n\n if (!extra || extra.length === 0) {\n return useDefaults ? DEFAULT_REDACT_KEY_SET : undefined;\n }\n\n const cached = config && keySetCache.get(config);\n if (cached) return cached;\n\n const resolved = new Set<string>(useDefaults ? DEFAULT_REDACT_KEY_SET : []);\n for (const key of extra) {\n resolved.add(normalizeRedactKey(key));\n }\n\n if (config) keySetCache.set(config, resolved);\n\n return resolved;\n}\n\n/**\n * Produce a new `LoggingData` with sensitive data censored:\n *\n * 1. every path in `config.paths` (opt-in globs), then\n * 2. every key matching the denylist — the built-in\n * {@link DEFAULT_REDACT_KEYS} plus `config.keys`, at any depth of\n * `context`, `message`, and an `Error`'s own enumerable properties.\n *\n * Step 2 runs **with no config at all**: passing `undefined` still censors\n * `password`, `authorization`, `token`, `apiKey` and friends. Pass\n * `{ defaultKeys: false }` to opt out.\n *\n * Paths run first so a function censor sees the original value rather than a\n * mask; leaves the path pass already censored are skipped by the key pass.\n *\n * The original entry is never mutated — channels and other call sites can\n * hold references to the input safely. Returns the input **by reference**\n * when nothing matched, so the fast path stays allocation-free.\n *\n * ## Residual gaps (by design, documented rather than silently absent)\n *\n * - **Secrets interpolated into a `message` string** (`` `token=${t}` ``)\n * cannot be reached — neither a path nor a key names a substring.\n * - **`Map`/`Set`/`Buffer` contents** are not traversed (see {@link isOpaque}).\n * - **Non-enumerable / getter-backed properties** are not walked, so an HTTP\n * client that exposes request config behind a getter still slips through.\n * Enumerable ones (axios's `.config`, `.response`) *are* covered.\n */\nexport function applyRedact(\n data: LoggingData,\n config: RedactConfig | undefined,\n): LoggingData {\n const paths = config?.paths ?? [];\n const keys = resolveRedactKeys(config);\n\n if (paths.length === 0 && !keys) {\n return data;\n }\n\n const censor = config?.censor ?? DEFAULT_CENSOR;\n\n let result = data;\n const censored = new Set<string>();\n\n if (paths.length > 0) {\n result = cloneEntry(data);\n\n for (const pattern of paths) {\n const segments = pattern.split(\".\").filter((segment) => segment.length > 0);\n if (segments.length === 0) continue;\n redactAtPath(result, segments, censor, [], censored);\n }\n }\n\n if (keys && hasDenylistedKey(result, keys)) {\n result = cloneEntry(result, new WeakMap(), {\n keys,\n censor,\n alreadyCensored: censored,\n });\n }\n\n return result;\n}\n\n/**\n * Combine two redact configs into one effective config. Used to merge a\n * channel's additive paths on top of the logger-wide floor.\n *\n * - `paths` and `keys` are concatenated; duplicates are kept (the matcher\n * tolerates them, and de-duping cross-config would mask a developer typo).\n * - `censor` from the channel wins; falls back to the logger's; falls back\n * to the default `\"[REDACTED]\"`.\n * - `defaultKeys` follows the additive-only contract: a channel can turn the\n * built-in denylist back *on* (`true`) but can never turn off one the\n * logger-wide floor left enabled. When the channel is silent, the logger's\n * choice is inherited — opting out logger-wide is not quietly undone by\n * any channel that happens to set a `redact` option.\n */\nexport function mergeRedact(\n base: RedactConfig | undefined,\n extra: RedactConfig | undefined,\n): RedactConfig | undefined {\n if (!base && !extra) return undefined;\n\n if (!base) {\n // An absent logger-wide config still means \"built-in denylist on\" — it is\n // the floor, not the absence of one. A channel may not switch it off, so\n // its `defaultKeys: false` is overridden here rather than inherited.\n return extra!.defaultKeys === false\n ? { ...extra!, defaultKeys: true }\n : extra;\n }\n\n if (!extra) return base;\n\n return {\n paths: [...(base.paths ?? []), ...(extra.paths ?? [])],\n keys: [...(base.keys ?? []), ...(extra.keys ?? [])],\n defaultKeys: extra.defaultKeys === true ? true : base.defaultKeys,\n censor: extra.censor ?? base.censor,\n };\n}\n"],"mappings":";;;AAMA,MAAM,iBAAiB;;;;;;;;AAyBvB,SAAS,SAAS,OAAwB;CACxC,OACE,YAAY,OAAO,KAAK,KACxB,iBAAiB,eACjB,iBAAiB,OACjB,iBAAiB,OACjB,iBAAiB,WACjB,iBAAiB,WACjB,iBAAiB,WACjB,iBAAiB;AAErB;;;;;;;;;;;;;;;AAgBA,SAAS,WACP,OACA,uBAAO,IAAI,QAAqB,GAChC,WACA,QAAkB,CAAC,GAChB;CACH,IAAI,UAAU,QAAQ,OAAO,UAAU,UACrC,OAAO;CAGT,MAAM,WAAW;CAEjB,IAAI,KAAK,IAAI,QAAQ,GACnB,OAAO,KAAK,IAAI,QAAQ;CAG1B,IAAI,iBAAiB,MACnB,OAAO,IAAI,KAAK,MAAM,QAAQ,CAAC;CAGjC,IAAI,SAAS,QAAQ,GACnB,OAAO;CAGT,IAAI,iBAAiB,OACnB,OAAO,WAAW,OAAO,MAAM,WAAW,KAAK;CAGjD,IAAI,MAAM,QAAQ,KAAK,GAAG;EACxB,MAAM,MAAa,CAAC;EACpB,KAAK,IAAI,UAAU,GAAG;EACtB,MAAM,SAAS,MAAM,UAAU;GAG7B,IAAI,KAAK,WAAW,MAAM,MAAM,WAAW,CAAC,GAAG,OAAO,OAAO,KAAK,CAAC,CAAC,CAAC;EACvE,CAAC;EACD,OAAO;CACT;CAEA,MAAM,MAA2B,CAAC;CAClC,KAAK,IAAI,UAAU,GAAG;CACtB,YAAY,OAA8B,KAAK,MAAM,WAAW,KAAK;CACrE,OAAO;AACT;;;;;AAMA,SAAS,YACP,QACA,QACA,MACA,WACA,OACM;CACN,KAAK,MAAM,OAAO,OAAO,KAAK,MAAM,GAAG;EACrC,MAAM,aAAa,CAAC,GAAG,OAAO,GAAG;EAEjC,IACE,aACA,UAAU,KAAK,IAAI,mBAAmB,GAAG,CAAC,KAC1C,CAAC,UAAU,gBAAgB,IAAI,WAAW,KAAK,GAAG,CAAC,GACnD;GACA,OAAO,OAAO,YAAY,OAAO,MAAM,UAAU,QAAQ,UAAU;GACnE;EACF;EAEA,OAAO,OAAO,WAAW,OAAO,MAAM,MAAM,WAAW,UAAU;CACnE;AACF;;;;;;;;;;;;;;;AAgBA,SAAS,WACP,OACA,MACA,WACA,OACO;CACP,IAAI;CAEJ,IAAI;EACF,OAAO,IAAK,MAAM,YAAiC,MAAM,OAAO;CAClE,QAAQ;EAGN,OAAO,IAAI,MAAM,MAAM,OAAO;CAChC;CAOA,KAAK,MAAM,CAAC,KAAK,WAAW;EAC1B,CAAC,WAAW,MAAM,OAAO;EACzB,CAAC,QAAQ,MAAM,IAAI;EACnB,CAAC,SAAS,MAAM,KAAK;CACvB,GACE,OAAO,eAAe,MAAM,KAAK;EAC/B,OAAO;EACP,UAAU;EACV,YAAY;EACZ,cAAc;CAChB,CAAC;CAGH,KAAK,IAAI,OAAO,IAAI;CACpB,YAAY,OAAyC,MAAa,MAAM,WAAW,KAAK;CAExF,OAAO;AACT;;;;;;;AAQA,SAAS,YAAY,OAAY,QAAsB,MAAqB;CAC1E,IAAI,OAAO,WAAW,YACpB,OAAO,OAAO,OAAO,KAAK,KAAK,GAAG,CAAC;CAErC,OAAO;AACT;;;;;;;;;;;;;;;;AAiBA,SAAS,aACP,QACA,UACA,QACA,WACA,UACM;CACN,IAAI,WAAW,QAAQ,OAAO,WAAW,UACvC;CAGF,IAAI,SAAS,WAAW,GACtB;CAGF,MAAM,CAAC,MAAM,GAAG,QAAQ;CAExB,IAAI,SAAS,QAYX;CAGF,IAAI,SAAS,MAAM;EAIjB,IAAI,KAAK,SAAS,GAChB,aAAa,QAAQ,MAAM,QAAQ,WAAW,QAAQ;EAExD,MAAM,OAAO,MAAM,QAAQ,MAAM,IAC7B,OAAO,KAAK,GAAG,UAAU,OAAO,KAAK,CAAC,IACtC,OAAO,KAAK,MAAM;EACtB,KAAK,MAAM,OAAO,MAChB,aAAa,OAAO,MAAM,UAAU,QAAQ,CAAC,GAAG,WAAW,GAAG,GAAG,QAAQ;EAE3E;CACF;CAEA,MAAM,cACJ,SAAS,MACL,MAAM,QAAQ,MAAM,IAClB,OAAO,KAAK,GAAG,UAAU,OAAO,KAAK,CAAC,IACtC,OAAO,KAAK,MAAM,IACpB,MAAM,QAAQ,MAAM,IAElB,QAAQ,KAAK,IAAI,KAAK,OAAO,IAAI,IAAI,OAAO,SAC1C,CAAC,IAAI,IACL,CAAC,IACH,OAAO,UAAU,eAAe,KAAK,QAAQ,IAAI,IAC/C,CAAC,IAAI,IACL,CAAC;CAEX,KAAK,MAAM,OAAO,aAChB,IAAI,KAAK,WAAW,GAAG;EACrB,MAAM,YAAY,CAAC,GAAG,WAAW,GAAG;EACpC,OAAO,OAAO,YAAY,OAAO,MAAM,QAAQ,SAAS;EACxD,SAAS,IAAI,UAAU,KAAK,GAAG,CAAC;CAClC,OACE,aAAa,OAAO,MAAM,MAAM,QAAQ,CAAC,GAAG,WAAW,GAAG,GAAG,QAAQ;AAG3E;;;;;;;;;AAUA,SAAS,iBACP,OACA,MACA,uBAAO,IAAI,QAAgB,GAClB;CACT,IAAI,UAAU,QAAQ,OAAO,UAAU,UACrC,OAAO;CAGT,MAAM,WAAW;CAEjB,IAAI,KAAK,IAAI,QAAQ,GAAG,OAAO;CAC/B,KAAK,IAAI,QAAQ;CAEjB,IAAI,iBAAiB,QAAQ,SAAS,QAAQ,GAC5C,OAAO;CAGT,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,MAAM,MAAM,SAAS,iBAAiB,MAAM,MAAM,IAAI,CAAC;CAGhE,KAAK,MAAM,OAAO,OAAO,KAAK,KAAK,GAAG;EACpC,IAAI,KAAK,IAAI,mBAAmB,GAAG,CAAC,GAAG,OAAO;EAC9C,IAAI,iBAAiB,MAAM,MAAM,MAAM,IAAI,GAAG,OAAO;CACvD;CAEA,OAAO;AACT;;;;;;;AAQA,MAAM,8BAAc,IAAI,QAA2C;;;;;;;;AASnE,SAAgB,kBACd,QACiC;CACjC,MAAM,cAAc,QAAQ,gBAAgB;CAC5C,MAAM,QAAQ,QAAQ;CAEtB,IAAI,CAAC,SAAS,MAAM,WAAW,GAC7B,OAAO,cAAc,yBAAyB;CAGhD,MAAM,SAAS,UAAU,YAAY,IAAI,MAAM;CAC/C,IAAI,QAAQ,OAAO;CAEnB,MAAM,WAAW,IAAI,IAAY,cAAc,yBAAyB,CAAC,CAAC;CAC1E,KAAK,MAAM,OAAO,OAChB,SAAS,IAAI,mBAAmB,GAAG,CAAC;CAGtC,IAAI,QAAQ,YAAY,IAAI,QAAQ,QAAQ;CAE5C,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,SAAgB,YACd,MACA,QACa;CACb,MAAM,QAAQ,QAAQ,SAAS,CAAC;CAChC,MAAM,OAAO,kBAAkB,MAAM;CAErC,IAAI,MAAM,WAAW,KAAK,CAAC,MACzB,OAAO;CAGT,MAAM,SAAS,QAAQ,UAAU;CAEjC,IAAI,SAAS;CACb,MAAM,2BAAW,IAAI,IAAY;CAEjC,IAAI,MAAM,SAAS,GAAG;EACpB,SAAS,WAAW,IAAI;EAExB,KAAK,MAAM,WAAW,OAAO;GAC3B,MAAM,WAAW,QAAQ,MAAM,GAAG,CAAC,CAAC,QAAQ,YAAY,QAAQ,SAAS,CAAC;GAC1E,IAAI,SAAS,WAAW,GAAG;GAC3B,aAAa,QAAQ,UAAU,QAAQ,CAAC,GAAG,QAAQ;EACrD;CACF;CAEA,IAAI,QAAQ,iBAAiB,QAAQ,IAAI,GACvC,SAAS,WAAW,wBAAQ,IAAI,QAAQ,GAAG;EACzC;EACA;EACA,iBAAiB;CACnB,CAAC;CAGH,OAAO;AACT;;;;;;;;;;;;;;;AAgBA,SAAgB,YACd,MACA,OAC0B;CAC1B,IAAI,CAAC,QAAQ,CAAC,OAAO,OAAO;CAE5B,IAAI,CAAC,MAIH,OAAO,MAAO,gBAAgB,QAC1B;EAAE,GAAG;EAAQ,aAAa;CAAK,IAC/B;CAGN,IAAI,CAAC,OAAO,OAAO;CAEnB,OAAO;EACL,OAAO,CAAC,GAAI,KAAK,SAAS,CAAC,GAAI,GAAI,MAAM,SAAS,CAAC,CAAE;EACrD,MAAM,CAAC,GAAI,KAAK,QAAQ,CAAC,GAAI,GAAI,MAAM,QAAQ,CAAC,CAAE;EAClD,aAAa,MAAM,gBAAgB,OAAO,OAAO,KAAK;EACtD,QAAQ,MAAM,UAAU,KAAK;CAC/B;AACF"}
package/esm/types.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- //#region ../logger/src/types.d.ts
1
+ //#region ../../packages/logger/src/types.d.ts
2
2
  /**
3
3
  * Severity levels, ordered debug < info ≈ success < warn < error < fatal.
4
4
  *
@@ -169,6 +169,13 @@ interface LogContract {
169
169
  */
170
170
  flushSync?(): void;
171
171
  }
172
+ /**
173
+ * Receives a channel's delivery failure: a synchronous throw from its `log()`
174
+ * or a rejection of the promise it returned, a redaction that threw, or a
175
+ * `flush()` that timed out. Set with `Logger.onChannelError()` or
176
+ * `configure({ onChannelError })`. It may be async; a rejection is swallowed.
177
+ */
178
+ type ChannelErrorHandler = (error: unknown, channel: LogContract, entry: LoggingData) => void | Promise<void>;
172
179
  type LoggingData = {
173
180
  type: LogLevel;
174
181
  module: string;
@@ -178,5 +185,5 @@ type LoggingData = {
178
185
  };
179
186
  type OmittedLoggingData = Omit<LoggingData, "type">;
180
187
  //#endregion
181
- export { AutoFlushEvent, BasicLogConfigurations, DebugMode, LogContract, LogLevel, LogMessage, LoggingData, OmittedLoggingData, RedactCensor, RedactConfig };
188
+ export { AutoFlushEvent, BasicLogConfigurations, ChannelErrorHandler, DebugMode, LogContract, LogLevel, LogMessage, LoggingData, OmittedLoggingData, RedactCensor, RedactConfig };
182
189
  //# sourceMappingURL=types.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../logger/src/types.ts"],"mappings":";;AASA;;;;AAAoB;AAUpB;;;KAVY,QAAA;AAUc;AAQ1B;;;;AAAqB;AAOrB;;AAf0B,KAAd,cAAA;AAAA,KAQA,SAAA;AASkB;AAuC9B;;;;AAvC8B,KAFlB,YAAA,cAEN,KAAA,OAAY,IAAY;;;;;;AA2EP;AAGvB;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BuB;AAGvB;;;KAtEY,YAAA;EAuEV;;;;;;;EA/DA,KAAA;EAqEU;;;AACD;AAGX;;EAlEE,IAAA;EAqFU;;;;;;;;;;;;;;EAtEV,WAAA;EAmFS;AAAA;AAGX;;;EAhFE,MAAA,GAAS,YAAY;AAAA;AAAA,KAGX,sBAAA;EA+EV;;;;;EAzEA,MAAA,GAAS,QAAA;EA4EO;AAGlB;;EA3EE,UAAA;IACE,IAAA;IACA,IAAA;EAAA;;;;EAKF,MAAA,IAAU,IAAA,EAAM,WAAA;;;;EAIhB,OAAA,IAAW,IAAA,EAAM,WAAA,KAAgB,OAAA,CAAQ,MAAA;;;;;;;EAOzC,MAAA,GAAS,YAAA;AAAA;AAAA,KAGC,UAAA;EACV,OAAA;EACA,KAAA,EAAO,QAAA;EACP,IAAA;EACA,MAAA;EACA,MAAA;EACA,KAAA;EACA,OAAA,GAAU,MAAM;EAChB,SAAA;AAAA;AAAA,UAGe,WAAA;;;;EAIf,IAAA;;;;EAKA,WAAA;;;;EAKA,QAAA;;;;EAKA,GAAA,CAAI,IAAA,EAAM,WAAA,UAAqB,OAAA;;;;;;;EAQ/B,KAAA,YAAiB,OAAA;;;;EAKjB,SAAA;AAAA;AAAA,KAGU,WAAA;EACV,IAAA,EAAM,QAAA;EACN,MAAA;EACA,MAAA;EACA,OAAA;EACA,OAAA,GAAU,MAAM;AAAA;AAAA,KAGN,kBAAA,GAAqB,IAAI,CAAC,WAAA"}
1
+ {"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../packages/logger/src/types.ts"],"mappings":";;AASA;;;;AAAoB;AAUpB;;;KAVY,QAAA;AAUc;AAQ1B;;;;AAAqB;AAOrB;;AAf0B,KAAd,cAAA;AAAA,KAQA,SAAA;AASkB;AAuC9B;;;;AAvC8B,KAFlB,YAAA,cAEN,KAAA,OAAY,IAAY;;;;;;AA2EP;AAGvB;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BuB;AAGvB;;;KAtEY,YAAA;EAuEV;;;;;;;EA/DA,KAAA;EAqEU;;;AACD;AAGX;;EAlEE,IAAA;EAqFU;;;;;;;;;;;;;;EAtEV,WAAA;EAmFS;AAAA;AASX;;;EAtFE,MAAA,GAAS,YAAY;AAAA;AAAA,KAGX,sBAAA;EAuFO;;;;;EAjFjB,MAAA,GAAS,QAAA;EAgFT;;;EA5EA,UAAA;IACE,IAAA;IACA,IAAA;EAAA;EAkFc;;;EA7EhB,MAAA,IAAU,IAAA,EAAM,WAAA;EA2EhB;;;EAvEA,OAAA,IAAW,IAAA,EAAM,WAAA,KAAgB,OAAA,CAAQ,MAAA;EAyEzB;AAAA;AAGlB;;;;EArEE,MAAA,GAAS,YAAA;AAAA;AAAA,KAGC,UAAA;EACV,OAAA;EACA,KAAA,EAAO,QAAA;EACP,IAAA;EACA,MAAA;EACA,MAAA;EACA,KAAA;EACA,OAAA,GAAU,MAAM;EAChB,SAAA;AAAA;AAAA,UAGe,WAAA;;;;EAIf,IAAA;;;;EAKA,WAAA;;;;EAKA,QAAA;;;;EAKA,GAAA,CAAI,IAAA,EAAM,WAAA,UAAqB,OAAA;;;;;;;EAQ/B,KAAA,YAAiB,OAAA;;;;EAKjB,SAAA;AAAA;;;;;;;KASU,mBAAA,IACV,KAAA,WACA,OAAA,EAAS,WAAA,EACT,KAAA,EAAO,WAAA,YACG,OAAA;AAAA,KAEA,WAAA;EACV,IAAA,EAAM,QAAA;EACN,MAAA;EACA,MAAA;EACA,OAAA;EACA,OAAA,GAAU,MAAM;AAAA;AAAA,KAGN,kBAAA,GAAqB,IAAI,CAAC,WAAA"}
@@ -1,4 +1,4 @@
1
- //#region ../logger/src/utils/capture-unhandled-errors.d.ts
1
+ //#region ../../packages/logger/src/utils/capture-unhandled-errors.d.ts
2
2
  /**
3
3
  * Options for {@link captureAnyUnhandledRejection}.
4
4
  */
@@ -1 +1 @@
1
- {"version":3,"file":"capture-unhandled-errors.d.mts","names":[],"sources":["../../../../../../../logger/src/utils/capture-unhandled-errors.ts"],"mappings":";;AAKA;;KAAY,uBAAA;EAca;AAAA;AAkCzB;;;;AAAkF;;;;;;;EAlChF,uBAAuB;AAAA;;;;;;;;;;;;;;;;;;;;;;;;iBAkCT,4BAAA,CAA6B,OAAqC,GAA5B,uBAA4B"}
1
+ {"version":3,"file":"capture-unhandled-errors.d.mts","names":[],"sources":["../../../../../../../../packages/logger/src/utils/capture-unhandled-errors.ts"],"mappings":";;AAKA;;KAAY,uBAAA;EAca;AAAA;AAkCzB;;;;AAAkF;;;;;;;EAlChF,uBAAuB;AAAA;;;;;;;;;;;;;;;;;;;;;;;;iBAkCT,4BAAA,CAA6B,OAAqC,GAA5B,uBAA4B"}
@@ -1,6 +1,6 @@
1
1
  import { log } from "../logger.mjs";
2
2
 
3
- //#region ../logger/src/utils/capture-unhandled-errors.ts
3
+ //#region ../../packages/logger/src/utils/capture-unhandled-errors.ts
4
4
  /**
5
5
  * Best-effort budget (ms) for draining async channels (file, Sentry, …) before
6
6
  * the forced exit. Bounded so a single stuck channel can't hang the process —
@@ -1 +1 @@
1
- {"version":3,"file":"capture-unhandled-errors.mjs","names":[],"sources":["../../../../../../../logger/src/utils/capture-unhandled-errors.ts"],"sourcesContent":["import { log } from \"../logger\";\n\n/**\n * Options for {@link captureAnyUnhandledRejection}.\n */\nexport type CaptureUnhandledOptions = {\n /**\n * Take the process down with `process.exit(1)` after an `uncaughtException`\n * is logged. Defaults to `true` — an uncaught exception leaves the process\n * in an undefined state, so Node's own default is to exit non-zero.\n *\n * Registering an `uncaughtException` listener *suppresses* that default, so a\n * listener that only logs turns an unrecoverable crash into a silent\n * `exit 0` (which is how a config file throwing at boot looks like \"the\n * server just stopped\"). Keep this `true` in production so supervisors\n * restart and the failure is never silent. Set it to `false` where the\n * process is expected to recover on its own — e.g. a dev server that reloads\n * via HMR — to log the exception without exiting.\n */\n exitOnUncaughtException?: boolean;\n};\n\n/**\n * Best-effort budget (ms) for draining async channels (file, Sentry, …) before\n * the forced exit. Bounded so a single stuck channel can't hang the process —\n * `process.exit()` skips `beforeExit`, so an `autoFlushOn: [\"beforeExit\"]`\n * handler would not cover this path.\n */\nconst FLUSH_BUDGET_BEFORE_EXIT = 1000;\n\n/**\n * Route Node's process-level failure events through the logger so they land in\n * every configured channel with full stack context, and make an\n * `uncaughtException` loud + visible instead of silently swallowed.\n *\n * Registers one listener for `unhandledRejection` and one for\n * `uncaughtException`; call once at startup after channels are configured.\n *\n * - `uncaughtException` → `log.fatal`, then (by default) `process.exit(1)`.\n * When no terminal channel has been configured yet — the early-boot window a\n * config file throwing at import time falls into — the stack is also written\n * to `console.error`, standing in for the Node default this listener\n * suppresses, so a fatal boot error is never invisible. A configured\n * `ConsoleLog` already prints it, so the fallback is skipped when a terminal\n * channel exists (no double output).\n * - `unhandledRejection` → `log.error` — a rejected promise is a failure but\n * not necessarily process-ending, so it stays at error and never exits.\n *\n * @example\n * log.configure({ channels: [new ConsoleLog(), new FileLog()] });\n * captureAnyUnhandledRejection(); // exits on uncaught\n * captureAnyUnhandledRejection({ exitOnUncaughtException: false }); // dev: log only\n */\nexport function captureAnyUnhandledRejection(options: CaptureUnhandledOptions = {}) {\n const { exitOnUncaughtException = true } = options;\n\n process.on(\"unhandledRejection\", (reason: any) => {\n log.error(\"app\", \"unhandledRejection\", reason);\n });\n\n process.on(\"uncaughtException\", (error) => {\n // Route through the logger first so file/Sentry channels capture it. A\n // crash handler must never itself throw (that would re-enter Node's\n // uncaught path), so the logging call is guarded.\n try {\n log.fatal(\"app\", \"uncaughtException\", error);\n } catch {\n // fall through to the stderr fallback + exit below\n }\n\n // Guarantee terminal visibility. `log.fatal` reaches nothing the user can\n // see when this fires before any terminal channel is configured — exactly\n // the early-boot window a config file throwing at import time falls into,\n // which is how a fatal boot error used to masquerade as a silent\n // `exit 0`. Stand in for the Node default this listener suppressed, but\n // only when no terminal channel already printed it, so a configured\n // `ConsoleLog` is never doubled.\n const hasTerminalChannel = log.channels.some((channel) => channel.terminal !== false);\n\n if (!hasTerminalChannel) {\n console.error(error);\n }\n\n if (!exitOnUncaughtException) {\n return;\n }\n\n // Best-effort, time-bounded flush of async channels, then exit non-zero so\n // process supervisors restart and `warlock start` surfaces the failure.\n void Promise.race([\n log.flush(),\n new Promise<void>((resolve) => {\n const timer = setTimeout(resolve, FLUSH_BUDGET_BEFORE_EXIT);\n timer.unref?.();\n }),\n ]).then(\n () => process.exit(1),\n () => process.exit(1),\n );\n });\n}\n"],"mappings":";;;;;;;;;AA4BA,MAAM,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;AAyBjC,SAAgB,6BAA6B,UAAmC,CAAC,GAAG;CAClF,MAAM,EAAE,0BAA0B,SAAS;CAE3C,QAAQ,GAAG,uBAAuB,WAAgB;EAChD,IAAI,MAAM,OAAO,sBAAsB,MAAM;CAC/C,CAAC;CAED,QAAQ,GAAG,sBAAsB,UAAU;EAIzC,IAAI;GACF,IAAI,MAAM,OAAO,qBAAqB,KAAK;EAC7C,QAAQ,CAER;EAWA,IAAI,CAFuB,IAAI,SAAS,MAAM,YAAY,QAAQ,aAAa,KAEzD,GACpB,QAAQ,MAAM,KAAK;EAGrB,IAAI,CAAC,yBACH;EAKF,AAAK,QAAQ,KAAK,CAChB,IAAI,MAAM,GACV,IAAI,SAAe,YAAY;GAE7B,AADc,WAAW,SAAS,wBAC9B,CAAC,CAAC,QAAQ;EAChB,CAAC,CACH,CAAC,CAAC,CAAC,WACK,QAAQ,KAAK,CAAC,SACd,QAAQ,KAAK,CAAC,CACtB;CACF,CAAC;AACH"}
1
+ {"version":3,"file":"capture-unhandled-errors.mjs","names":[],"sources":["../../../../../../../../packages/logger/src/utils/capture-unhandled-errors.ts"],"sourcesContent":["import { log } from \"../logger\";\n\n/**\n * Options for {@link captureAnyUnhandledRejection}.\n */\nexport type CaptureUnhandledOptions = {\n /**\n * Take the process down with `process.exit(1)` after an `uncaughtException`\n * is logged. Defaults to `true` — an uncaught exception leaves the process\n * in an undefined state, so Node's own default is to exit non-zero.\n *\n * Registering an `uncaughtException` listener *suppresses* that default, so a\n * listener that only logs turns an unrecoverable crash into a silent\n * `exit 0` (which is how a config file throwing at boot looks like \"the\n * server just stopped\"). Keep this `true` in production so supervisors\n * restart and the failure is never silent. Set it to `false` where the\n * process is expected to recover on its own — e.g. a dev server that reloads\n * via HMR — to log the exception without exiting.\n */\n exitOnUncaughtException?: boolean;\n};\n\n/**\n * Best-effort budget (ms) for draining async channels (file, Sentry, …) before\n * the forced exit. Bounded so a single stuck channel can't hang the process —\n * `process.exit()` skips `beforeExit`, so an `autoFlushOn: [\"beforeExit\"]`\n * handler would not cover this path.\n */\nconst FLUSH_BUDGET_BEFORE_EXIT = 1000;\n\n/**\n * Route Node's process-level failure events through the logger so they land in\n * every configured channel with full stack context, and make an\n * `uncaughtException` loud + visible instead of silently swallowed.\n *\n * Registers one listener for `unhandledRejection` and one for\n * `uncaughtException`; call once at startup after channels are configured.\n *\n * - `uncaughtException` → `log.fatal`, then (by default) `process.exit(1)`.\n * When no terminal channel has been configured yet — the early-boot window a\n * config file throwing at import time falls into — the stack is also written\n * to `console.error`, standing in for the Node default this listener\n * suppresses, so a fatal boot error is never invisible. A configured\n * `ConsoleLog` already prints it, so the fallback is skipped when a terminal\n * channel exists (no double output).\n * - `unhandledRejection` → `log.error` — a rejected promise is a failure but\n * not necessarily process-ending, so it stays at error and never exits.\n *\n * @example\n * log.configure({ channels: [new ConsoleLog(), new FileLog()] });\n * captureAnyUnhandledRejection(); // exits on uncaught\n * captureAnyUnhandledRejection({ exitOnUncaughtException: false }); // dev: log only\n */\nexport function captureAnyUnhandledRejection(options: CaptureUnhandledOptions = {}) {\n const { exitOnUncaughtException = true } = options;\n\n process.on(\"unhandledRejection\", (reason: any) => {\n log.error(\"app\", \"unhandledRejection\", reason);\n });\n\n process.on(\"uncaughtException\", (error) => {\n // Route through the logger first so file/Sentry channels capture it. A\n // crash handler must never itself throw (that would re-enter Node's\n // uncaught path), so the logging call is guarded.\n try {\n log.fatal(\"app\", \"uncaughtException\", error);\n } catch {\n // fall through to the stderr fallback + exit below\n }\n\n // Guarantee terminal visibility. `log.fatal` reaches nothing the user can\n // see when this fires before any terminal channel is configured — exactly\n // the early-boot window a config file throwing at import time falls into,\n // which is how a fatal boot error used to masquerade as a silent\n // `exit 0`. Stand in for the Node default this listener suppressed, but\n // only when no terminal channel already printed it, so a configured\n // `ConsoleLog` is never doubled.\n const hasTerminalChannel = log.channels.some((channel) => channel.terminal !== false);\n\n if (!hasTerminalChannel) {\n console.error(error);\n }\n\n if (!exitOnUncaughtException) {\n return;\n }\n\n // Best-effort, time-bounded flush of async channels, then exit non-zero so\n // process supervisors restart and `warlock start` surfaces the failure.\n void Promise.race([\n log.flush(),\n new Promise<void>((resolve) => {\n const timer = setTimeout(resolve, FLUSH_BUDGET_BEFORE_EXIT);\n timer.unref?.();\n }),\n ]).then(\n () => process.exit(1),\n () => process.exit(1),\n );\n });\n}\n"],"mappings":";;;;;;;;;AA4BA,MAAM,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;AAyBjC,SAAgB,6BAA6B,UAAmC,CAAC,GAAG;CAClF,MAAM,EAAE,0BAA0B,SAAS;CAE3C,QAAQ,GAAG,uBAAuB,WAAgB;EAChD,IAAI,MAAM,OAAO,sBAAsB,MAAM;CAC/C,CAAC;CAED,QAAQ,GAAG,sBAAsB,UAAU;EAIzC,IAAI;GACF,IAAI,MAAM,OAAO,qBAAqB,KAAK;EAC7C,QAAQ,CAER;EAWA,IAAI,CAFuB,IAAI,SAAS,MAAM,YAAY,QAAQ,aAAa,KAEzD,GACpB,QAAQ,MAAM,KAAK;EAGrB,IAAI,CAAC,yBACH;EAKF,AAAK,QAAQ,KAAK,CAChB,IAAI,MAAM,GACV,IAAI,SAAe,YAAY;GAE7B,AADc,WAAW,SAAS,wBAC9B,CAAC,CAAC,QAAQ;EAChB,CAAC,CACH,CAAC,CAAC,CAAC,WACK,QAAQ,KAAK,CAAC,SACd,QAAQ,KAAK,CAAC,CACtB;CACF,CAAC;AACH"}
@@ -1,4 +1,4 @@
1
- //#region ../logger/src/utils/clear-message.d.ts
1
+ //#region ../../packages/logger/src/utils/clear-message.d.ts
2
2
  /**
3
3
  * Clear message from any terminal codes
4
4
  */
@@ -1 +1 @@
1
- {"version":3,"file":"clear-message.d.mts","names":[],"sources":["../../../../../../../logger/src/utils/clear-message.ts"],"mappings":";;AAGA;;iBAAgB,YAAA,CAAa,OAAY"}
1
+ {"version":3,"file":"clear-message.d.mts","names":[],"sources":["../../../../../../../../packages/logger/src/utils/clear-message.ts"],"mappings":";;AAGA;;iBAAgB,YAAA,CAAa,OAAY"}
@@ -1,4 +1,4 @@
1
- //#region ../logger/src/utils/clear-message.ts
1
+ //#region ../../packages/logger/src/utils/clear-message.ts
2
2
  /**
3
3
  * Clear message from any terminal codes
4
4
  */
@@ -1 +1 @@
1
- {"version":3,"file":"clear-message.mjs","names":[],"sources":["../../../../../../../logger/src/utils/clear-message.ts"],"sourcesContent":["/**\n * Clear message from any terminal codes\n */\nexport function clearMessage(message: any) {\n if (typeof message !== \"string\") return message;\n\n // eslint-disable-next-line no-control-regex\n return message.replace(/\\u001b[^m]*?m/g, \"\");\n}\n"],"mappings":";;;;AAGA,SAAgB,aAAa,SAAc;CACzC,IAAI,OAAO,YAAY,UAAU,OAAO;CAGxC,OAAO,QAAQ,QAAQ,kBAAkB,EAAE;AAC7C"}
1
+ {"version":3,"file":"clear-message.mjs","names":[],"sources":["../../../../../../../../packages/logger/src/utils/clear-message.ts"],"sourcesContent":["/**\n * Clear message from any terminal codes\n */\nexport function clearMessage(message: any) {\n if (typeof message !== \"string\") return message;\n\n // eslint-disable-next-line no-control-regex\n return message.replace(/\\u001b[^m]*?m/g, \"\");\n}\n"],"mappings":";;;;AAGA,SAAgB,aAAa,SAAc;CACzC,IAAI,OAAO,YAAY,UAAU,OAAO;CAGxC,OAAO,QAAQ,QAAQ,kBAAkB,EAAE;AAC7C"}
@@ -1,4 +1,4 @@
1
- //#region ../logger/src/utils/safe-json-stringify.d.ts
1
+ //#region ../../packages/logger/src/utils/safe-json-stringify.d.ts
2
2
  /**
3
3
  * JSON-serialize log payloads safely. Circular refs, BigInt, and repeated
4
4
  * non-tree references are handled by `safe-stable-stringify`; functions and
@@ -1 +1 @@
1
- {"version":3,"file":"safe-json-stringify.d.mts","names":[],"sources":["../../../../../../../logger/src/utils/safe-json-stringify.ts"],"mappings":";;AAyCA;;;;AAAgE;;;;iBAAhD,iBAAA,CAAkB,KAAA,WAAgB,KAAc"}
1
+ {"version":3,"file":"safe-json-stringify.d.mts","names":[],"sources":["../../../../../../../../packages/logger/src/utils/safe-json-stringify.ts"],"mappings":";;AAyCA;;;;AAAgE;;;;iBAAhD,iBAAA,CAAkB,KAAA,WAAgB,KAAc"}
@@ -1,6 +1,6 @@
1
1
  import { stringify } from "safe-stable-stringify";
2
2
 
3
- //#region ../logger/src/utils/safe-json-stringify.ts
3
+ //#region ../../packages/logger/src/utils/safe-json-stringify.ts
4
4
  /**
5
5
  * Replacer that surfaces Error data — `name`, `message`, and `stack` are
6
6
  * non-enumerable on `Error`, so neither default JSON serialization nor an
@@ -1 +1 @@
1
- {"version":3,"file":"safe-json-stringify.mjs","names":["safeStableStringify"],"sources":["../../../../../../../logger/src/utils/safe-json-stringify.ts"],"sourcesContent":["import { stringify as safeStableStringify } from \"safe-stable-stringify\";\n\n/**\n * Replacer that surfaces Error data — `name`, `message`, and `stack` are\n * non-enumerable on `Error`, so neither default JSON serialization nor an\n * object spread captures them (both produce `{}`). They are copied explicitly;\n * the trailing spread then captures any additional enumerable props the caller\n * (or a subclass) attached, such as a `code` field.\n */\nfunction errorReplacer<Value = unknown>(\n _key: string,\n value: Value,\n): Record<string, unknown> | Value {\n if (value instanceof Error) {\n // Spread first, then the explicit Error fields. `name`/`message`/`stack`\n // are non-enumerable on `Error`, so the spread never carries them — the\n // explicit assignments are what surface them. Placing the spread first\n // means those explicit keys legitimately take precedence over any\n // identically-named *enumerable* prop a subclass attached (resolving the\n // duplicate-key warning). Because `safe-stable-stringify` sorts keys, the\n // insertion order here does not affect the serialized bytes.\n return {\n ...value,\n name: value.name,\n message: value.message,\n stack: value.stack,\n };\n }\n\n return value;\n}\n\n/**\n * JSON-serialize log payloads safely. Circular refs, BigInt, and repeated\n * non-tree references are handled by `safe-stable-stringify`; functions and\n * symbols are dropped (standard JSON behavior); Errors are expanded via\n * `errorReplacer`. Class instances serialize as their enumerable props.\n *\n * @example\n * await fs.promises.writeFile(filePath, safeJsonStringify(payload, 2));\n */\nexport function safeJsonStringify(value: unknown, space?: number): string {\n return safeStableStringify(value, errorReplacer, space) ?? \"\";\n}\n"],"mappings":";;;;;;;;;;AASA,SAAS,cACP,MACA,OACiC;CACjC,IAAI,iBAAiB,OAQnB,OAAO;EACL,GAAG;EACH,MAAM,MAAM;EACZ,SAAS,MAAM;EACf,OAAO,MAAM;CACf;CAGF,OAAO;AACT;;;;;;;;;;AAWA,SAAgB,kBAAkB,OAAgB,OAAwB;CACxE,OAAOA,UAAoB,OAAO,eAAe,KAAK,KAAK;AAC7D"}
1
+ {"version":3,"file":"safe-json-stringify.mjs","names":["safeStableStringify"],"sources":["../../../../../../../../packages/logger/src/utils/safe-json-stringify.ts"],"sourcesContent":["import { stringify as safeStableStringify } from \"safe-stable-stringify\";\n\n/**\n * Replacer that surfaces Error data — `name`, `message`, and `stack` are\n * non-enumerable on `Error`, so neither default JSON serialization nor an\n * object spread captures them (both produce `{}`). They are copied explicitly;\n * the trailing spread then captures any additional enumerable props the caller\n * (or a subclass) attached, such as a `code` field.\n */\nfunction errorReplacer<Value = unknown>(\n _key: string,\n value: Value,\n): Record<string, unknown> | Value {\n if (value instanceof Error) {\n // Spread first, then the explicit Error fields. `name`/`message`/`stack`\n // are non-enumerable on `Error`, so the spread never carries them — the\n // explicit assignments are what surface them. Placing the spread first\n // means those explicit keys legitimately take precedence over any\n // identically-named *enumerable* prop a subclass attached (resolving the\n // duplicate-key warning). Because `safe-stable-stringify` sorts keys, the\n // insertion order here does not affect the serialized bytes.\n return {\n ...value,\n name: value.name,\n message: value.message,\n stack: value.stack,\n };\n }\n\n return value;\n}\n\n/**\n * JSON-serialize log payloads safely. Circular refs, BigInt, and repeated\n * non-tree references are handled by `safe-stable-stringify`; functions and\n * symbols are dropped (standard JSON behavior); Errors are expanded via\n * `errorReplacer`. Class instances serialize as their enumerable props.\n *\n * @example\n * await fs.promises.writeFile(filePath, safeJsonStringify(payload, 2));\n */\nexport function safeJsonStringify(value: unknown, space?: number): string {\n return safeStableStringify(value, errorReplacer, space) ?? \"\";\n}\n"],"mappings":";;;;;;;;;;AASA,SAAS,cACP,MACA,OACiC;CACjC,IAAI,iBAAiB,OAQnB,OAAO;EACL,GAAG;EACH,MAAM,MAAM;EACZ,SAAS,MAAM;EACf,OAAO,MAAM;CACf;CAGF,OAAO;AACT;;;;;;;;;;AAWA,SAAgB,kBAAkB,OAAgB,OAAwB;CACxE,OAAOA,UAAoB,OAAO,eAAe,KAAK,KAAK;AAC7D"}
package/llms-full.txt CHANGED
@@ -419,14 +419,14 @@ process.once("SIGTERM", gracefulShutdown);
419
419
  ```ts
420
420
  async function gracefulShutdown() {
421
421
  await httpServer.close();
422
- await log.flush(); // awaits every channel's async flush() to completion
422
+ await log.flush({ timeout: 3000 }); // bounded drain of every channel
423
423
  process.exit(0);
424
424
  }
425
425
 
426
426
  process.once("SIGTERM", gracefulShutdown);
427
427
  ```
428
428
 
429
- `log.flush()` fans out to every channel that implements `flush()` and awaits them together (`Promise.allSettled`). Each channel is isolated — one channel's flush rejecting neither aborts the others nor escapes as an unhandled rejection. Channels without `flush()` are skipped.
429
+ `log.flush()` drains every channel concurrently: each waits for its own async deliveries still in flight (level methods are fire-and-forget and never return a promise; entries logged during the flush are awaited too), then runs its `flush()`. A hung network delivery can't hold up the file channel, and one channel's flush rejecting neither aborts the others nor escapes as an unhandled rejection. It is bounded: `log.flush({ timeout })` resolves after `timeout` ms (default 5000) and reports each channel still draining to `onChannelError`.
430
430
 
431
431
  | | `flushSync()` | `flush()` |
432
432
  |---|---|---|
@@ -521,8 +521,8 @@ log.configure({
521
521
  autoFlushOn: ["SIGINT", "SIGTERM", "beforeExit"],
522
522
  });
523
523
 
524
- await log.info("users", "register", "New user created");
525
- await log.error("payments", "charge", new Error("Card declined"));
524
+ log.info("users", "register", "New user created");
525
+ log.error("payments", "charge", new Error("Card declined"));
526
526
  ```
527
527
 
528
528
  ## The six levels
@@ -560,7 +560,7 @@ Every call signature is the same — `module`, `action`, `message`, optional `co
560
560
  - Don't set `extension` on `JSONFileLog` — it's hardcoded to `"json"` and your value is silently ignored.
561
561
  - Don't register multiple `FileLog` instances with the same `name` in the same `storagePath` — the lookup via `log.channel("file")` returns only one, and they'll fight over the same file.
562
562
  - Don't mix `autoFlushOn: ["SIGINT"]` with your own `process.on("SIGINT", ...)` handler — both fire, and ours re-raises mid-way through your async work.
563
- - Don't `await log.info(...)` expecting the write to be on disk — `FileLog` buffers. Call `log.flushSync()` (or rely on `autoFlushOn`) before the process exits.
563
+ - Don't `await log.info(...)` — level methods return the logger synchronously (fire-and-forget, since 5.30.0), so there is nothing to wait for. The write is not on disk yet either: `FileLog` buffers. Call `await log.flush()` / `log.flushSync()` (or rely on `autoFlushOn`) before the process exits.
564
564
  - Don't call `captureAnyUnhandledRejection()` more than once — it re-registers listeners every call and your rejections get logged N times.
565
565
  - Don't shadow the import in local code: `for (const log of logEntries) { ... }` will hide the singleton inside that block. Rename loop variables (`entry`, `record`) when working with logger imports.
566
566
 
@@ -1544,7 +1544,7 @@ Prefer extending `LogChannel` unless you have a concrete reason not to — the l
1544
1544
  ## Don't do
1545
1545
 
1546
1546
  - Don't mutate `data` inside `log()`. Later channels see the mutation if the logger passes the same reference.
1547
- - Don't throw synchronously from `log()`. The logger fires it without awaiting; an unhandled rejection takes down the process (unless `captureAnyUnhandledRejection` is wired up — and then it's embarrassing to be the cause).
1547
+ - Don't rely on throwing from `log()` to surface a failure. The logger isolates each channel: a sync throw or a rejected promise from `log()` goes to the logger's `onChannelError` handler (default: one stderr line), never to the caller, the other channels or `unhandledRejection`. Return the promise from an async `log()` so `await log.flush()` can wait for the delivery.
1548
1548
  - Don't block the event loop. `log()` may be sync or async; if your work takes >100ms, make it async and return the promise.
1549
1549
  - Don't forget `shouldBeLogged(data)` at the top of `log()` — or your channel silently ignores `levels` / `filter` config.
1550
1550
 
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "dependencies": {
5
5
  "@mongez/copper": "^2.1.3",
6
6
  "@mongez/reinforcements": "^4.0.2",
7
- "@warlock.js/fs": "5.29.0",
7
+ "@warlock.js/fs": "5.30.0",
8
8
  "dayjs": "^1.11.9",
9
9
  "safe-stable-stringify": "^2.5.0"
10
10
  },
@@ -25,14 +25,15 @@
25
25
  ],
26
26
  "repository": {
27
27
  "type": "git",
28
- "url": "https://github.com/warlockjs/logger"
28
+ "url": "git+https://github.com/warlockjs/framework.git",
29
+ "directory": "packages/logger"
29
30
  },
30
31
  "author": "hassanzohdy",
31
32
  "license": "MIT",
32
33
  "warlock": {
33
34
  "environment": "server"
34
35
  },
35
- "version": "5.29.0",
36
+ "version": "5.30.0",
36
37
  "main": "./cjs/index.cjs",
37
38
  "module": "./esm/index.mjs",
38
39
  "types": "./esm/index.d.mts",
@@ -63,14 +63,14 @@ process.once("SIGTERM", gracefulShutdown);
63
63
  ```ts
64
64
  async function gracefulShutdown() {
65
65
  await httpServer.close();
66
- await log.flush(); // awaits every channel's async flush() to completion
66
+ await log.flush({ timeout: 3000 }); // bounded drain of every channel
67
67
  process.exit(0);
68
68
  }
69
69
 
70
70
  process.once("SIGTERM", gracefulShutdown);
71
71
  ```
72
72
 
73
- `log.flush()` fans out to every channel that implements `flush()` and awaits them together (`Promise.allSettled`). Each channel is isolated — one channel's flush rejecting neither aborts the others nor escapes as an unhandled rejection. Channels without `flush()` are skipped.
73
+ `log.flush()` drains every channel concurrently: each waits for its own async deliveries still in flight (level methods are fire-and-forget and never return a promise; entries logged during the flush are awaited too), then runs its `flush()`. A hung network delivery can't hold up the file channel, and one channel's flush rejecting neither aborts the others nor escapes as an unhandled rejection. It is bounded: `log.flush({ timeout })` resolves after `timeout` ms (default 5000) and reports each channel still draining to `onChannelError`.
74
74
 
75
75
  | | `flushSync()` | `flush()` |
76
76
  |---|---|---|
@@ -44,8 +44,8 @@ log.configure({
44
44
  autoFlushOn: ["SIGINT", "SIGTERM", "beforeExit"],
45
45
  });
46
46
 
47
- await log.info("users", "register", "New user created");
48
- await log.error("payments", "charge", new Error("Card declined"));
47
+ log.info("users", "register", "New user created");
48
+ log.error("payments", "charge", new Error("Card declined"));
49
49
  ```
50
50
 
51
51
  ## The six levels
@@ -83,6 +83,6 @@ Every call signature is the same — `module`, `action`, `message`, optional `co
83
83
  - Don't set `extension` on `JSONFileLog` — it's hardcoded to `"json"` and your value is silently ignored.
84
84
  - Don't register multiple `FileLog` instances with the same `name` in the same `storagePath` — the lookup via `log.channel("file")` returns only one, and they'll fight over the same file.
85
85
  - Don't mix `autoFlushOn: ["SIGINT"]` with your own `process.on("SIGINT", ...)` handler — both fire, and ours re-raises mid-way through your async work.
86
- - Don't `await log.info(...)` expecting the write to be on disk — `FileLog` buffers. Call `log.flushSync()` (or rely on `autoFlushOn`) before the process exits.
86
+ - Don't `await log.info(...)` — level methods return the logger synchronously (fire-and-forget, since 5.30.0), so there is nothing to wait for. The write is not on disk yet either: `FileLog` buffers. Call `await log.flush()` / `log.flushSync()` (or rely on `autoFlushOn`) before the process exits.
87
87
  - Don't call `captureAnyUnhandledRejection()` more than once — it re-registers listeners every call and your rejections get logged N times.
88
88
  - Don't shadow the import in local code: `for (const log of logEntries) { ... }` will hide the singleton inside that block. Rename loop variables (`entry`, `record`) when working with logger imports.
@@ -185,6 +185,6 @@ Prefer extending `LogChannel` unless you have a concrete reason not to — the l
185
185
  ## Don't do
186
186
 
187
187
  - Don't mutate `data` inside `log()`. Later channels see the mutation if the logger passes the same reference.
188
- - Don't throw synchronously from `log()`. The logger fires it without awaiting; an unhandled rejection takes down the process (unless `captureAnyUnhandledRejection` is wired up — and then it's embarrassing to be the cause).
188
+ - Don't rely on throwing from `log()` to surface a failure. The logger isolates each channel: a sync throw or a rejected promise from `log()` goes to the logger's `onChannelError` handler (default: one stderr line), never to the caller, the other channels or `unhandledRejection`. Return the promise from an async `log()` so `await log.flush()` can wait for the delivery.
189
189
  - Don't block the event loop. `log()` may be sync or async; if your work takes >100ms, make it async and return the promise.
190
190
  - Don't forget `shouldBeLogged(data)` at the top of `log()` — or your channel silently ignores `levels` / `filter` config.