@zudojs/logger 0.1.0 → 1.1.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 (167) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +98 -8
  3. package/dist/loggerCore/core/loggerCore.context.d.ts +9 -2
  4. package/dist/loggerCore/core/loggerCore.context.js +12 -6
  5. package/dist/loggerCore/core/loggerCore.core.d.ts +10 -0
  6. package/dist/loggerCore/core/loggerCore.core.js +52 -1
  7. package/dist/loggerCore/helpers/loggerCore.helper.d.ts +7 -2
  8. package/dist/loggerCore/helpers/loggerCore.helper.js +10 -9
  9. package/dist/loggerCore/helpers/loggerCoreMethods/index.d.ts +1 -1
  10. package/dist/loggerCore/helpers/loggerCoreMethods/index.js +1 -1
  11. package/dist/loggerCore/helpers/loggerCoreMethods/loggerCoreMethods.child.js +3 -1
  12. package/dist/loggerCore/helpers/loggerCoreMethods/loggerCoreMethods.dispatch.d.ts +11 -0
  13. package/dist/loggerCore/helpers/loggerCoreMethods/loggerCoreMethods.dispatch.js +121 -11
  14. package/dist/loggerCore/helpers/loggerCoreMethods/loggerCoreMethods.entry.js +18 -2
  15. package/dist/loggerCore/helpers/loggerCoreMethods/loggerCoreMethods.level.js +9 -2
  16. package/dist/loggerCore/helpers/loggerCoreMethods/loggerCoreMethods.lifecycle.js +33 -0
  17. package/dist/loggerEntry/loggerEntry.core.js +0 -1
  18. package/dist/loggerEntry/loggerEntryHelpers/index.d.ts +2 -0
  19. package/dist/loggerEntry/loggerEntryHelpers/index.js +1 -0
  20. package/dist/loggerEntry/loggerEntryHelpers/loggerEntryHelpers.sanitize.d.ts +69 -0
  21. package/dist/loggerEntry/loggerEntryHelpers/loggerEntryHelpers.sanitize.js +113 -0
  22. package/dist/loggerEntry/loggerEntryHelpers/loggerEntryHelpers.serialize.d.ts +5 -1
  23. package/dist/loggerEntry/loggerEntryHelpers/loggerEntryHelpers.serialize.js +9 -5
  24. package/dist/loggerEntry/loggerEntryHelpers/loggerEntryHelpers.valueSerialize.js +9 -1
  25. package/dist/loggerFormatter/loggerFormatterFormatters/loggerFormatterFormatters.context.js +15 -3
  26. package/dist/loggerFormatter/loggerFormatterFormatters/loggerFormatterFormatters.js +3 -2
  27. package/dist/loggerFormatter/loggerFormatterFormatters/loggerFormatterFormatters.json.js +19 -3
  28. package/dist/loggerFormatter/loggerFormatterFormatters/loggerFormatterFormatters.metadata.js +14 -6
  29. package/dist/loggerFormatter/loggerFormatterFormatters/loggerFormatterFormatters.text.js +31 -4
  30. package/dist/loggerOptions/loggerOptions.type.d.ts +12 -1
  31. package/dist/loggerOptions/loggerOptions.type.js +4 -1
  32. package/dist/loggerTransport/loggerTransportComposite/loggerTransportComposite.buffered.js +4 -0
  33. package/dist/loggerTransport/loggerTransportHelpers/loggerTransportHelpers.js +4 -3
  34. package/package.json +23 -12
  35. package/dist/.tsbuildinfo +0 -1
  36. package/dist/index.d.ts.map +0 -1
  37. package/dist/index.js.map +0 -1
  38. package/dist/loggerContext/index.d.ts.map +0 -1
  39. package/dist/loggerContext/index.js.map +0 -1
  40. package/dist/loggerContext/loggerContext.core.d.ts.map +0 -1
  41. package/dist/loggerContext/loggerContext.core.js.map +0 -1
  42. package/dist/loggerContext/loggerContext.type.d.ts.map +0 -1
  43. package/dist/loggerContext/loggerContext.type.js.map +0 -1
  44. package/dist/loggerContext/loggerContextCreate.d.ts.map +0 -1
  45. package/dist/loggerContext/loggerContextCreate.js.map +0 -1
  46. package/dist/loggerContext/loggerContextSerialize.d.ts.map +0 -1
  47. package/dist/loggerContext/loggerContextSerialize.js.map +0 -1
  48. package/dist/loggerContext/loggerContextStorage.d.ts.map +0 -1
  49. package/dist/loggerContext/loggerContextStorage.js.map +0 -1
  50. package/dist/loggerCore/core/index.d.ts.map +0 -1
  51. package/dist/loggerCore/core/index.js.map +0 -1
  52. package/dist/loggerCore/core/loggerCore.context.d.ts.map +0 -1
  53. package/dist/loggerCore/core/loggerCore.context.js.map +0 -1
  54. package/dist/loggerCore/core/loggerCore.core.d.ts.map +0 -1
  55. package/dist/loggerCore/core/loggerCore.core.js.map +0 -1
  56. package/dist/loggerCore/core/loggerCore.type.d.ts.map +0 -1
  57. package/dist/loggerCore/core/loggerCore.type.js.map +0 -1
  58. package/dist/loggerCore/helpers/index.d.ts.map +0 -1
  59. package/dist/loggerCore/helpers/index.js.map +0 -1
  60. package/dist/loggerCore/helpers/loggerCore.helper.d.ts.map +0 -1
  61. package/dist/loggerCore/helpers/loggerCore.helper.js.map +0 -1
  62. package/dist/loggerCore/helpers/loggerCoreHelpers.d.ts.map +0 -1
  63. package/dist/loggerCore/helpers/loggerCoreHelpers.js.map +0 -1
  64. package/dist/loggerCore/helpers/loggerCoreMethods/index.d.ts.map +0 -1
  65. package/dist/loggerCore/helpers/loggerCoreMethods/index.js.map +0 -1
  66. package/dist/loggerCore/helpers/loggerCoreMethods/loggerCoreMethods.child.d.ts.map +0 -1
  67. package/dist/loggerCore/helpers/loggerCoreMethods/loggerCoreMethods.child.js.map +0 -1
  68. package/dist/loggerCore/helpers/loggerCoreMethods/loggerCoreMethods.dispatch.d.ts.map +0 -1
  69. package/dist/loggerCore/helpers/loggerCoreMethods/loggerCoreMethods.dispatch.js.map +0 -1
  70. package/dist/loggerCore/helpers/loggerCoreMethods/loggerCoreMethods.entry.d.ts.map +0 -1
  71. package/dist/loggerCore/helpers/loggerCoreMethods/loggerCoreMethods.entry.js.map +0 -1
  72. package/dist/loggerCore/helpers/loggerCoreMethods/loggerCoreMethods.level.d.ts.map +0 -1
  73. package/dist/loggerCore/helpers/loggerCoreMethods/loggerCoreMethods.level.js.map +0 -1
  74. package/dist/loggerCore/helpers/loggerCoreMethods/loggerCoreMethods.lifecycle.d.ts.map +0 -1
  75. package/dist/loggerCore/helpers/loggerCoreMethods/loggerCoreMethods.lifecycle.js.map +0 -1
  76. package/dist/loggerCore/helpers/loggerCoreMethods.logLevels.d.ts.map +0 -1
  77. package/dist/loggerCore/helpers/loggerCoreMethods.logLevels.js.map +0 -1
  78. package/dist/loggerCore/helpers/loggerCoreMethods.loggerProps.d.ts.map +0 -1
  79. package/dist/loggerCore/helpers/loggerCoreMethods.loggerProps.js.map +0 -1
  80. package/dist/loggerCore/index.d.ts.map +0 -1
  81. package/dist/loggerCore/index.js.map +0 -1
  82. package/dist/loggerEntry/index.d.ts.map +0 -1
  83. package/dist/loggerEntry/index.js.map +0 -1
  84. package/dist/loggerEntry/loggerEntry.core.d.ts.map +0 -1
  85. package/dist/loggerEntry/loggerEntry.core.js.map +0 -1
  86. package/dist/loggerEntry/loggerEntry.type.d.ts.map +0 -1
  87. package/dist/loggerEntry/loggerEntry.type.js.map +0 -1
  88. package/dist/loggerEntry/loggerEntryCreate.d.ts.map +0 -1
  89. package/dist/loggerEntry/loggerEntryCreate.js.map +0 -1
  90. package/dist/loggerEntry/loggerEntryHelpers/index.d.ts.map +0 -1
  91. package/dist/loggerEntry/loggerEntryHelpers/index.js.map +0 -1
  92. package/dist/loggerEntry/loggerEntryHelpers/loggerEntryHelpers.interfaces.d.ts.map +0 -1
  93. package/dist/loggerEntry/loggerEntryHelpers/loggerEntryHelpers.interfaces.js.map +0 -1
  94. package/dist/loggerEntry/loggerEntryHelpers/loggerEntryHelpers.serialize.d.ts.map +0 -1
  95. package/dist/loggerEntry/loggerEntryHelpers/loggerEntryHelpers.serialize.js.map +0 -1
  96. package/dist/loggerEntry/loggerEntryHelpers/loggerEntryHelpers.types.d.ts.map +0 -1
  97. package/dist/loggerEntry/loggerEntryHelpers/loggerEntryHelpers.types.js.map +0 -1
  98. package/dist/loggerEntry/loggerEntryHelpers/loggerEntryHelpers.valueSerialize.d.ts.map +0 -1
  99. package/dist/loggerEntry/loggerEntryHelpers/loggerEntryHelpers.valueSerialize.js.map +0 -1
  100. package/dist/loggerEntry/loggerEntrySerialize.d.ts.map +0 -1
  101. package/dist/loggerEntry/loggerEntrySerialize.js.map +0 -1
  102. package/dist/loggerErrors/index.d.ts.map +0 -1
  103. package/dist/loggerErrors/index.js.map +0 -1
  104. package/dist/loggerErrors/loggerError.base.d.ts.map +0 -1
  105. package/dist/loggerErrors/loggerError.base.js.map +0 -1
  106. package/dist/loggerErrors/loggerError.helpers.d.ts.map +0 -1
  107. package/dist/loggerErrors/loggerError.helpers.js.map +0 -1
  108. package/dist/loggerFactory/index.d.ts.map +0 -1
  109. package/dist/loggerFactory/index.js.map +0 -1
  110. package/dist/loggerFactory/loggerFactory.core.d.ts.map +0 -1
  111. package/dist/loggerFactory/loggerFactory.core.js.map +0 -1
  112. package/dist/loggerFormatter/index.d.ts.map +0 -1
  113. package/dist/loggerFormatter/index.js.map +0 -1
  114. package/dist/loggerFormatter/loggerFormatter.core.d.ts.map +0 -1
  115. package/dist/loggerFormatter/loggerFormatter.core.js.map +0 -1
  116. package/dist/loggerFormatter/loggerFormatter.type.d.ts.map +0 -1
  117. package/dist/loggerFormatter/loggerFormatter.type.js.map +0 -1
  118. package/dist/loggerFormatter/loggerFormatterFormatters/index.d.ts.map +0 -1
  119. package/dist/loggerFormatter/loggerFormatterFormatters/index.js.map +0 -1
  120. package/dist/loggerFormatter/loggerFormatterFormatters/loggerFormatterFormatters.context.d.ts.map +0 -1
  121. package/dist/loggerFormatter/loggerFormatterFormatters/loggerFormatterFormatters.context.js.map +0 -1
  122. package/dist/loggerFormatter/loggerFormatterFormatters/loggerFormatterFormatters.d.ts.map +0 -1
  123. package/dist/loggerFormatter/loggerFormatterFormatters/loggerFormatterFormatters.js.map +0 -1
  124. package/dist/loggerFormatter/loggerFormatterFormatters/loggerFormatterFormatters.json.d.ts.map +0 -1
  125. package/dist/loggerFormatter/loggerFormatterFormatters/loggerFormatterFormatters.json.js.map +0 -1
  126. package/dist/loggerFormatter/loggerFormatterFormatters/loggerFormatterFormatters.metadata.d.ts.map +0 -1
  127. package/dist/loggerFormatter/loggerFormatterFormatters/loggerFormatterFormatters.metadata.js.map +0 -1
  128. package/dist/loggerFormatter/loggerFormatterFormatters/loggerFormatterFormatters.text.d.ts.map +0 -1
  129. package/dist/loggerFormatter/loggerFormatterFormatters/loggerFormatterFormatters.text.js.map +0 -1
  130. package/dist/loggerFormatter/loggerFormatterGuard.d.ts.map +0 -1
  131. package/dist/loggerFormatter/loggerFormatterGuard.js.map +0 -1
  132. package/dist/loggerLevel/index.d.ts.map +0 -1
  133. package/dist/loggerLevel/index.js.map +0 -1
  134. package/dist/loggerLevel/loggerLevel.type.d.ts.map +0 -1
  135. package/dist/loggerLevel/loggerLevel.type.js.map +0 -1
  136. package/dist/loggerManager/index.d.ts.map +0 -1
  137. package/dist/loggerManager/index.js.map +0 -1
  138. package/dist/loggerManager/loggerManager.core.d.ts.map +0 -1
  139. package/dist/loggerManager/loggerManager.core.js.map +0 -1
  140. package/dist/loggerOptions/index.d.ts.map +0 -1
  141. package/dist/loggerOptions/index.js.map +0 -1
  142. package/dist/loggerOptions/loggerOptions.type.d.ts.map +0 -1
  143. package/dist/loggerOptions/loggerOptions.type.js.map +0 -1
  144. package/dist/loggerTransport/index.d.ts.map +0 -1
  145. package/dist/loggerTransport/index.js.map +0 -1
  146. package/dist/loggerTransport/loggerTransport.core.d.ts.map +0 -1
  147. package/dist/loggerTransport/loggerTransport.core.js.map +0 -1
  148. package/dist/loggerTransport/loggerTransport.registry.d.ts.map +0 -1
  149. package/dist/loggerTransport/loggerTransport.registry.js.map +0 -1
  150. package/dist/loggerTransport/loggerTransport.type.d.ts.map +0 -1
  151. package/dist/loggerTransport/loggerTransport.type.js.map +0 -1
  152. package/dist/loggerTransport/loggerTransportComposite/index.d.ts.map +0 -1
  153. package/dist/loggerTransport/loggerTransportComposite/index.js.map +0 -1
  154. package/dist/loggerTransport/loggerTransportComposite/loggerTransportComposite.buffered.d.ts.map +0 -1
  155. package/dist/loggerTransport/loggerTransportComposite/loggerTransportComposite.buffered.js.map +0 -1
  156. package/dist/loggerTransport/loggerTransportComposite/loggerTransportComposite.d.ts.map +0 -1
  157. package/dist/loggerTransport/loggerTransportComposite/loggerTransportComposite.js.map +0 -1
  158. package/dist/loggerTransport/loggerTransportConsole/index.d.ts.map +0 -1
  159. package/dist/loggerTransport/loggerTransportConsole/index.js.map +0 -1
  160. package/dist/loggerTransport/loggerTransportConsole/loggerTransportConsole.core.d.ts.map +0 -1
  161. package/dist/loggerTransport/loggerTransportConsole/loggerTransportConsole.core.js.map +0 -1
  162. package/dist/loggerTransport/loggerTransportGuard.d.ts.map +0 -1
  163. package/dist/loggerTransport/loggerTransportGuard.js.map +0 -1
  164. package/dist/loggerTransport/loggerTransportHelpers/index.d.ts.map +0 -1
  165. package/dist/loggerTransport/loggerTransportHelpers/index.js.map +0 -1
  166. package/dist/loggerTransport/loggerTransportHelpers/loggerTransportHelpers.d.ts.map +0 -1
  167. package/dist/loggerTransport/loggerTransportHelpers/loggerTransportHelpers.js.map +0 -1
@@ -4,7 +4,7 @@
4
4
  import { LoggerLevel, shouldLog, } from "../../../loggerLevel/loggerLevel.type.js";
5
5
  import { LoggerConfigurationError } from "../../../loggerErrors/loggerError.base.js";
6
6
  import { createEntry } from "./loggerCoreMethods.entry.js";
7
- import { dispatchEntry } from "./loggerCoreMethods.dispatch.js";
7
+ import { dispatchEntrySync } from "./loggerCoreMethods.dispatch.js";
8
8
  /**
9
9
  * Level logging methods extracted from ZudojsLogger.
10
10
  */
@@ -18,6 +18,13 @@ export function logAtLevel(ctx, level, message, options = {}) {
18
18
  throw new LoggerConfigurationError("Logger message must be a string.");
19
19
  }
20
20
  const entry = createEntry(ctx.configuration, ctx.contextStorage, level, message, options);
21
- void dispatchEntry(ctx.configuration, entry, (error) => ctx.handleInfrastructureError(error));
21
+ // Dispatch is asynchronous. It used to be fired and forgotten, so
22
+ // flush() and close() could return while entries were still in
23
+ // flight — messages were lost on process exit. Registering the
24
+ // promise lets the lifecycle methods drain it.
25
+ const dispatch = dispatchEntrySync(ctx.configuration, entry, (error) => ctx.handleInfrastructureError(error));
26
+ if (dispatch) {
27
+ ctx.trackDispatch(dispatch);
28
+ }
22
29
  }
23
30
  //# sourceMappingURL=loggerCoreMethods.level.js.map
@@ -48,6 +48,20 @@ export function disableLogger(ctx) {
48
48
  */
49
49
  export async function flushLogger(ctx) {
50
50
  ctx.assertActive();
51
+ // In-flight dispatches must land in the transports before those
52
+ // transports are asked to flush, otherwise flush() is a no-op for
53
+ // everything logged in the same tick. A dispatch failure (surfaced
54
+ // when `throwTransportErrors` is on) is rethrown only after the
55
+ // transports have still been flushed.
56
+ let failure;
57
+ let failed = false;
58
+ try {
59
+ await ctx.drainDispatches();
60
+ }
61
+ catch (error) {
62
+ failure = error;
63
+ failed = true;
64
+ }
51
65
  for (const transport of ctx.configuration.transports) {
52
66
  if (!isLoggerTransport(transport)) {
53
67
  continue;
@@ -60,6 +74,8 @@ export async function flushLogger(ctx) {
60
74
  await registered.flush();
61
75
  }
62
76
  }
77
+ if (failed)
78
+ throw failure;
63
79
  }
64
80
  /**
65
81
  * Closes all transports and marks logger as disposed.
@@ -68,15 +84,32 @@ export async function closeLogger(ctx) {
68
84
  if (ctx.isDisposed()) {
69
85
  return;
70
86
  }
87
+ // Closing is terminal: transports are flushed and closed and the logger
88
+ // is marked disposed even when a dispatch failed; the failure is rethrown
89
+ // afterwards.
90
+ let failure;
91
+ let failed = false;
92
+ try {
93
+ await ctx.drainDispatches();
94
+ }
95
+ catch (error) {
96
+ failure = error;
97
+ failed = true;
98
+ }
71
99
  for (const transport of ctx.configuration.transports) {
72
100
  if (!isLoggerTransport(transport)) {
73
101
  continue;
74
102
  }
75
103
  const registered = createLoggerTransport(transport);
104
+ if (registered.flush) {
105
+ await registered.flush();
106
+ }
76
107
  if (registered.close) {
77
108
  await registered.close();
78
109
  }
79
110
  }
80
111
  ctx.markDisposed();
112
+ if (failed)
113
+ throw failure;
81
114
  }
82
115
  //# sourceMappingURL=loggerCoreMethods.lifecycle.js.map
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * Core logger entry creation functions.
3
3
  */
4
- import { loggerLevelNameFallback } from "./loggerEntryHelpers/loggerEntryHelpers.serialize.js";
5
4
  import { createLoggerEntry } from "./loggerEntryCreate.js";
6
5
  /**
7
6
  * Creates a unique log entry identifier.
@@ -6,4 +6,6 @@
6
6
  export type { LogValue, LogMetadata, LoggerSource, LoggerEntryContext, } from "./loggerEntryHelpers.types.js";
7
7
  export type { LoggerEntry, LoggerEntryInput, } from "./loggerEntryHelpers.interfaces.js";
8
8
  export { serializeLoggerEntry, serializeLoggerError, serializeLoggerValue, loggerLevelNameFallback, } from "./loggerEntryHelpers.serialize.js";
9
+ export { LOGGER_REDACTION_TOKEN, DEFAULT_LOGGER_SECRET_PATTERN, escapeLogText, hasLogControlCharacters, createSecretMatcher, redactLogValue, } from "./loggerEntryHelpers.sanitize.js";
10
+ export type { LoggerRedactionOptions } from "./loggerEntryHelpers.sanitize.js";
9
11
  //# sourceMappingURL=index.d.ts.map
@@ -4,4 +4,5 @@
4
4
  * Logger entry helper types and utilities.
5
5
  */
6
6
  export { serializeLoggerEntry, serializeLoggerError, serializeLoggerValue, loggerLevelNameFallback, } from "./loggerEntryHelpers.serialize.js";
7
+ export { LOGGER_REDACTION_TOKEN, DEFAULT_LOGGER_SECRET_PATTERN, escapeLogText, hasLogControlCharacters, createSecretMatcher, redactLogValue, } from "./loggerEntryHelpers.sanitize.js";
7
8
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Log text sanitization and secret redaction.
3
+ *
4
+ * Log records routinely carry attacker-controlled strings (a username,
5
+ * a URL, an error message echoing request input). Written verbatim to a
6
+ * line-oriented sink, a single newline lets that input FORGE a complete
7
+ * additional log line, and an ESC byte lets it drive the operator's
8
+ * terminal. Every string that reaches a text-shaped formatter is
9
+ * therefore escaped here first.
10
+ */
11
+ /**
12
+ * Replacement token written in place of a redacted value.
13
+ */
14
+ export declare const LOGGER_REDACTION_TOKEN = "[REDACTED]";
15
+ /**
16
+ * Field names treated as secrets by default.
17
+ *
18
+ * Matching is case-insensitive and substring-based so `dbPassword`,
19
+ * `X-Api-Key` and `refresh_token` are all covered.
20
+ */
21
+ export declare const DEFAULT_LOGGER_SECRET_PATTERN: RegExp;
22
+ /**
23
+ * Controls secret redaction for a logger.
24
+ */
25
+ export interface LoggerRedactionOptions {
26
+ /**
27
+ * Whether redaction runs at all. Defaults to true.
28
+ */
29
+ readonly enabled?: boolean;
30
+ /**
31
+ * Exact field names to redact, in addition to `pattern`.
32
+ */
33
+ readonly keys?: readonly string[];
34
+ /**
35
+ * Field-name pattern. Defaults to DEFAULT_LOGGER_SECRET_PATTERN.
36
+ * Pass a pattern that never matches to rely on `keys` alone.
37
+ */
38
+ readonly pattern?: RegExp;
39
+ /**
40
+ * Replacement written in place of a secret.
41
+ */
42
+ readonly replacement?: string;
43
+ }
44
+ /**
45
+ * Escapes control characters that could forge log records.
46
+ *
47
+ * CR, LF, TAB and the ANSI escape byte become printable escapes; every
48
+ * other C0 control character and DEL becomes `\xNN`. Ordinary text,
49
+ * including every non-ASCII character, is returned unchanged.
50
+ */
51
+ export declare function escapeLogText(value: string): string;
52
+ /**
53
+ * Returns whether a string contains characters that can forge a record.
54
+ */
55
+ export declare function hasLogControlCharacters(value: string): boolean;
56
+ /**
57
+ * Builds a predicate deciding whether a field name holds a secret.
58
+ */
59
+ export declare function createSecretMatcher(options?: LoggerRedactionOptions): (key: string) => boolean;
60
+ /**
61
+ * Recursively replaces secret-named fields with a redaction token.
62
+ *
63
+ * Nesting, arrays and getters are all covered: the walk descends into
64
+ * every enumerable own property, and a getter is read here — once,
65
+ * before the value can reach a transport. Cycles resolve to
66
+ * "[Circular]" rather than recursing forever.
67
+ */
68
+ export declare function redactLogValue(value: unknown, isSecret: (key: string) => boolean, replacement?: string, seen?: WeakSet<object>): unknown;
69
+ //# sourceMappingURL=loggerEntryHelpers.sanitize.d.ts.map
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Log text sanitization and secret redaction.
3
+ *
4
+ * Log records routinely carry attacker-controlled strings (a username,
5
+ * a URL, an error message echoing request input). Written verbatim to a
6
+ * line-oriented sink, a single newline lets that input FORGE a complete
7
+ * additional log line, and an ESC byte lets it drive the operator's
8
+ * terminal. Every string that reaches a text-shaped formatter is
9
+ * therefore escaped here first.
10
+ */
11
+ /**
12
+ * Matches C0 control characters plus DEL — everything that can forge a
13
+ * record boundary or drive a terminal.
14
+ */
15
+ const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/g;
16
+ /**
17
+ * Replacement token written in place of a redacted value.
18
+ */
19
+ export const LOGGER_REDACTION_TOKEN = "[REDACTED]";
20
+ /**
21
+ * Field names treated as secrets by default.
22
+ *
23
+ * Matching is case-insensitive and substring-based so `dbPassword`,
24
+ * `X-Api-Key` and `refresh_token` are all covered.
25
+ */
26
+ export const DEFAULT_LOGGER_SECRET_PATTERN = /(pass(word|wd)?|secret|token|api[-_.]?key|private[-_.]?key|credential|authorization|cookie)/i;
27
+ /**
28
+ * Escapes control characters that could forge log records.
29
+ *
30
+ * CR, LF, TAB and the ANSI escape byte become printable escapes; every
31
+ * other C0 control character and DEL becomes `\xNN`. Ordinary text,
32
+ * including every non-ASCII character, is returned unchanged.
33
+ */
34
+ export function escapeLogText(value) {
35
+ return value.replace(CONTROL_CHARACTERS, (character) => {
36
+ switch (character) {
37
+ case "\n":
38
+ return "\\n";
39
+ case "\r":
40
+ return "\\r";
41
+ case "\t":
42
+ return "\\t";
43
+ case "\u001b":
44
+ return "\\u001b";
45
+ default: {
46
+ const code = character.charCodeAt(0);
47
+ return `\\x${code.toString(16).padStart(2, "0")}`;
48
+ }
49
+ }
50
+ });
51
+ }
52
+ /**
53
+ * Returns whether a string contains characters that can forge a record.
54
+ */
55
+ export function hasLogControlCharacters(value) {
56
+ return new RegExp(CONTROL_CHARACTERS.source).test(value);
57
+ }
58
+ /**
59
+ * Builds a predicate deciding whether a field name holds a secret.
60
+ */
61
+ export function createSecretMatcher(options = {}) {
62
+ if (options.enabled === false) {
63
+ return () => false;
64
+ }
65
+ // Copy the pattern without `g`/`y`: those flags make `test()` advance
66
+ // `lastIndex`, so a shared pattern would match a secret-named field on
67
+ // one entry and let it through unredacted on the next.
68
+ const configured = options.pattern ?? DEFAULT_LOGGER_SECRET_PATTERN;
69
+ const pattern = configured.global || configured.sticky
70
+ ? new RegExp(configured.source, configured.flags.replace(/[gy]/gu, ""))
71
+ : configured;
72
+ const exact = new Set((options.keys ?? []).map((key) => key.toLowerCase()));
73
+ return (key) => exact.has(key.toLowerCase()) || pattern.test(key);
74
+ }
75
+ /**
76
+ * Recursively replaces secret-named fields with a redaction token.
77
+ *
78
+ * Nesting, arrays and getters are all covered: the walk descends into
79
+ * every enumerable own property, and a getter is read here — once,
80
+ * before the value can reach a transport. Cycles resolve to
81
+ * "[Circular]" rather than recursing forever.
82
+ */
83
+ export function redactLogValue(value, isSecret, replacement = LOGGER_REDACTION_TOKEN, seen = new WeakSet()) {
84
+ if (value === null || typeof value !== "object") {
85
+ return value;
86
+ }
87
+ if (value instanceof Date || value instanceof Error) {
88
+ return value;
89
+ }
90
+ if (seen.has(value)) {
91
+ return "[Circular]";
92
+ }
93
+ seen.add(value);
94
+ if (Array.isArray(value)) {
95
+ return value.map((item) => redactLogValue(item, isSecret, replacement, seen));
96
+ }
97
+ const result = {};
98
+ for (const [key, item] of Object.entries(value)) {
99
+ // defineProperty, never assignment: a "__proto__" key coming from
100
+ // JSON.parse of untrusted input would otherwise reach the inherited
101
+ // setter and replace this object's prototype.
102
+ Object.defineProperty(result, key, {
103
+ value: isSecret(key)
104
+ ? replacement
105
+ : redactLogValue(item, isSecret, replacement, seen),
106
+ enumerable: true,
107
+ writable: true,
108
+ configurable: true,
109
+ });
110
+ }
111
+ return result;
112
+ }
113
+ //# sourceMappingURL=loggerEntryHelpers.sanitize.js.map
@@ -5,7 +5,11 @@ import type { LoggerEntry } from "./loggerEntryHelpers.interfaces.js";
5
5
  /**
6
6
  * Returns a plain serializable representation of an entry.
7
7
  *
8
- * This intentionally does not call JSON.stringify itself.
8
+ * This intentionally does not call JSON.stringify itself, but every
9
+ * caller-supplied value IS normalized first. Metadata and context used
10
+ * to be embedded by reference, so a circular object or a BigInt made
11
+ * the JSON formatter throw — and because dispatch swallows formatter
12
+ * errors by default, the whole log line vanished silently.
9
13
  */
10
14
  export declare function serializeLoggerEntry(entry: LoggerEntry): Record<string, unknown>;
11
15
  export { serializeLoggerError, serializeLoggerValue, } from "./loggerEntryHelpers.valueSerialize.js";
@@ -1,11 +1,15 @@
1
1
  /**
2
2
  * Logger entry serialization helpers.
3
3
  */
4
- import { serializeLoggerError } from "./loggerEntryHelpers.valueSerialize.js";
4
+ import { serializeLoggerError, serializeLoggerValue, } from "./loggerEntryHelpers.valueSerialize.js";
5
5
  /**
6
6
  * Returns a plain serializable representation of an entry.
7
7
  *
8
- * This intentionally does not call JSON.stringify itself.
8
+ * This intentionally does not call JSON.stringify itself, but every
9
+ * caller-supplied value IS normalized first. Metadata and context used
10
+ * to be embedded by reference, so a circular object or a BigInt made
11
+ * the JSON formatter throw — and because dispatch swallows formatter
12
+ * errors by default, the whole log line vanished silently.
9
13
  */
10
14
  export function serializeLoggerEntry(entry) {
11
15
  return {
@@ -13,9 +17,9 @@ export function serializeLoggerEntry(entry) {
13
17
  level: entry.level,
14
18
  levelName: entry.levelName,
15
19
  message: entry.message,
16
- metadata: entry.metadata,
17
- context: entry.context,
18
- source: entry.source,
20
+ metadata: serializeLoggerValue(entry.metadata),
21
+ context: serializeLoggerValue(entry.context),
22
+ source: serializeLoggerValue(entry.source),
19
23
  error: entry.error ? serializeLoggerError(entry.error) : undefined,
20
24
  logger: entry.logger,
21
25
  timestamp: entry.timestamp.toISOString(),
@@ -49,7 +49,15 @@ export function serializeLoggerValue(value, seen = new WeakSet()) {
49
49
  }
50
50
  const result = {};
51
51
  for (const [key, item] of Object.entries(value)) {
52
- result[key] = serializeLoggerValue(item, seen);
52
+ // defineProperty, never assignment: a "__proto__" key from an
53
+ // untrusted payload would otherwise reach the inherited setter and
54
+ // replace the serialized object's prototype.
55
+ Object.defineProperty(result, key, {
56
+ value: serializeLoggerValue(item, seen),
57
+ enumerable: true,
58
+ writable: true,
59
+ configurable: true,
60
+ });
53
61
  }
54
62
  return result;
55
63
  }
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Logger context and source formatting helpers.
3
3
  */
4
+ import { escapeLogText } from "../../loggerEntry/loggerEntryHelpers/loggerEntryHelpers.sanitize.js";
4
5
  /**
5
6
  * Formats logger context.
6
7
  */
@@ -13,7 +14,16 @@ export function formatContext(entry) {
13
14
  if (value === undefined || value === null) {
14
15
  continue;
15
16
  }
16
- values.push(`${key}=${String(value)}`);
17
+ if (typeof value === "object") {
18
+ continue;
19
+ }
20
+ // Context identifiers are also flattened into metadata; skip the ones
21
+ // the metadata block already prints so a line never repeats itself.
22
+ if (Object.hasOwn(entry.metadata, key) &&
23
+ entry.metadata[key] === value) {
24
+ continue;
25
+ }
26
+ values.push(`${escapeLogText(key)}=${escapeLogText(String(value))}`);
17
27
  }
18
28
  return values.length > 0 ? `[${values.join(" ")}]` : "";
19
29
  }
@@ -27,12 +37,14 @@ export function formatSource(entry) {
27
37
  }
28
38
  const location = [];
29
39
  if (source.file) {
30
- location.push(source.file);
40
+ location.push(escapeLogText(source.file));
31
41
  }
32
42
  if (source.line !== undefined) {
33
43
  location.push(String(source.line));
34
44
  }
35
- const functionName = source.function ? ` ${source.function}` : "";
45
+ const functionName = source.function
46
+ ? ` ${escapeLogText(source.function)}`
47
+ : "";
36
48
  return location.length > 0 ? `[${location.join(":")}${functionName}]` : "";
37
49
  }
38
50
  //# sourceMappingURL=loggerFormatterFormatters.context.js.map
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { serializeLoggerEntry } from "../../loggerEntry/loggerEntrySerialize.js";
5
5
  import { createLoggerFormatter } from "../loggerFormatter.core.js";
6
+ import { escapeLogText } from "../../loggerEntry/loggerEntryHelpers/loggerEntryHelpers.sanitize.js";
6
7
  import { createJsonLoggerFormatter } from "./loggerFormatterFormatters.json.js";
7
8
  import { createTextLoggerFormatter } from "./loggerFormatterFormatters.text.js";
8
9
  /**
@@ -11,8 +12,8 @@ import { createTextLoggerFormatter } from "./loggerFormatterFormatters.text.js";
11
12
  export function createCompactLoggerFormatter(options = {}) {
12
13
  return createLoggerFormatter((entry) => {
13
14
  const level = entry.levelName.toUpperCase();
14
- const logger = entry.logger ? ` ${entry.logger}:` : "";
15
- return `${level}${logger} ${entry.message}`;
15
+ const logger = entry.logger ? ` ${escapeLogText(entry.logger)}:` : "";
16
+ return `${level}${logger} ${escapeLogText(entry.message)}`;
16
17
  }, {
17
18
  name: options.name ?? "compact",
18
19
  });
@@ -6,17 +6,33 @@ import { createLoggerFormatter } from "../loggerFormatter.core.js";
6
6
  /**
7
7
  * Removes undefined values recursively.
8
8
  */
9
- function removeUndefinedValues(value) {
9
+ function removeUndefinedValues(value, seen = new WeakSet()) {
10
10
  if (Array.isArray(value)) {
11
- return value.map((item) => removeUndefinedValues(item));
11
+ if (seen.has(value)) {
12
+ return "[Circular]";
13
+ }
14
+ seen.add(value);
15
+ return value.map((item) => removeUndefinedValues(item, seen));
12
16
  }
13
17
  if (value && typeof value === "object" && !(value instanceof Date)) {
18
+ // Without a cycle guard this walk recursed until the stack blew,
19
+ // and the resulting RangeError was swallowed by dispatch — losing
20
+ // the log line rather than reporting the offending value.
21
+ if (seen.has(value)) {
22
+ return "[Circular]";
23
+ }
24
+ seen.add(value);
14
25
  const result = {};
15
26
  for (const [key, item] of Object.entries(value)) {
16
27
  if (item === undefined) {
17
28
  continue;
18
29
  }
19
- result[key] = removeUndefinedValues(item);
30
+ Object.defineProperty(result, key, {
31
+ value: removeUndefinedValues(item, seen),
32
+ enumerable: true,
33
+ writable: true,
34
+ configurable: true,
35
+ });
20
36
  }
21
37
  return result;
22
38
  }
@@ -2,14 +2,17 @@
2
2
  * Logger metadata and error formatting helpers.
3
3
  */
4
4
  import { serializeLoggerError, serializeLoggerValue, } from "../../loggerEntry/loggerEntryHelpers/loggerEntryHelpers.valueSerialize.js";
5
+ import { escapeLogText } from "../../loggerEntry/loggerEntryHelpers/loggerEntryHelpers.sanitize.js";
5
6
  /**
6
7
  * Formats metadata as key=value pairs.
7
8
  */
8
9
  export function formatMetadata(metadata, separator) {
9
- return Object.entries(metadata)
10
+ return (Object.entries(metadata)
10
11
  .filter(([, value]) => value !== undefined)
11
- .map(([key, value]) => `${key}=${formatValue(value)}`)
12
- .join(separator);
12
+ // Metadata KEYS are as attacker-influenceable as values (a header
13
+ // name, a form field) and were previously interpolated raw.
14
+ .map(([key, value]) => `${escapeLogText(key)}=${formatValue(value)}`)
15
+ .join(separator));
13
16
  }
14
17
  /**
15
18
  * Formats an arbitrary metadata value.
@@ -22,19 +25,24 @@ export function formatValue(value) {
22
25
  if (/\s/.test(value)) {
23
26
  return JSON.stringify(value);
24
27
  }
25
- return value;
28
+ // A value with no whitespace can still carry ANSI escapes or other
29
+ // C0 controls, which used to reach the sink verbatim.
30
+ return escapeLogText(value);
26
31
  }
27
32
  if (typeof value === "object") {
28
33
  return JSON.stringify(serializeLoggerValue(value));
29
34
  }
30
- return String(value);
35
+ return escapeLogText(String(value));
31
36
  }
32
37
  /**
33
38
  * Formats an Error.
34
39
  */
35
40
  export function formatError(error, includeStackTrace) {
36
41
  if (includeStackTrace && error.stack) {
37
- return `\n${error.stack}`;
42
+ // A stack trace is intentionally multi-line, but everything in it
43
+ // that came from user input (the message) must not be able to
44
+ // introduce a further record boundary of its own.
45
+ return `\n${error.stack.split("\n").map(escapeLogText).join("\n")}`;
38
46
  }
39
47
  const serialized = serializeLoggerError(error);
40
48
  return `error=${JSON.stringify(serialized)}`;
@@ -2,8 +2,26 @@
2
2
  * Text logger formatter.
3
3
  */
4
4
  import { createLoggerFormatter } from "../loggerFormatter.core.js";
5
+ import { escapeLogText } from "../../loggerEntry/loggerEntryHelpers/loggerEntryHelpers.sanitize.js";
5
6
  import { formatContext, formatSource, } from "./loggerFormatterFormatters.context.js";
6
7
  import { formatMetadata, formatError, } from "./loggerFormatterFormatters.metadata.js";
8
+ /** ANSI colour per severity, used only when the caller opts in. */
9
+ const LEVEL_COLORS = Object.freeze({
10
+ fatal: "35",
11
+ error: "31",
12
+ warn: "33",
13
+ info: "36",
14
+ debug: "32",
15
+ trace: "90",
16
+ });
17
+ /** Wraps a level tag in an ANSI colour sequence. */
18
+ function colorizeLevel(levelName, tag) {
19
+ const color = LEVEL_COLORS[levelName];
20
+ if (color === undefined) {
21
+ return tag;
22
+ }
23
+ return `\u001b[${color}m${tag}\u001b[0m`;
24
+ }
7
25
  /**
8
26
  * Creates a human-readable text formatter.
9
27
  */
@@ -15,16 +33,25 @@ export function createTextLoggerFormatter(options = {}) {
15
33
  const includeSource = options.includeSource ?? false;
16
34
  const includeStackTrace = options.includeStackTrace ?? true;
17
35
  const metadataSeparator = options.metadataSeparator ?? " ";
18
- return createLoggerFormatter((entry) => {
36
+ return createLoggerFormatter((entry, context) => {
19
37
  const parts = [];
20
38
  if (includeTimestamp) {
21
39
  parts.push(entry.timestamp.toISOString());
22
40
  }
23
- parts.push(`[${entry.levelName.toUpperCase()}]`);
41
+ // LoggerFormatterContext.colors was declared from the first
42
+ // release and read by nothing, so `colors: true` produced plain
43
+ // output. Colour codes are emitted only on explicit opt-in, and
44
+ // only around the fixed level name — never around user text,
45
+ // which stays escaped.
46
+ const levelTag = `[${entry.levelName.toUpperCase()}]`;
47
+ parts.push(context.colors ? colorizeLevel(entry.levelName, levelTag) : levelTag);
24
48
  if (includeLogger && entry.logger) {
25
- parts.push(`[${entry.logger}]`);
49
+ parts.push(`[${escapeLogText(entry.logger)}]`);
26
50
  }
27
- parts.push(entry.message);
51
+ // The message is the single most attacker-influenceable field on
52
+ // an entry. Emitted verbatim, a newline inside it forged a whole
53
+ // extra log record and an ESC byte drove the operator's terminal.
54
+ parts.push(escapeLogText(entry.message));
28
55
  if (includeContext && entry.context) {
29
56
  const context = formatContext(entry);
30
57
  if (context) {
@@ -1,7 +1,8 @@
1
- import type { LoggerLevel } from "../loggerLevel/loggerLevel.type.js";
1
+ import { LoggerLevel } from "../loggerLevel/loggerLevel.type.js";
2
2
  import type { LoggerContextData } from "../loggerContext/loggerContext.core.js";
3
3
  import type { LoggerFormatterLike } from "../loggerFormatter/loggerFormatter.type.js";
4
4
  import type { LoggerTransportLike } from "../loggerTransport/loggerTransport.type.js";
5
+ import type { LoggerRedactionOptions } from "../loggerEntry/loggerEntryHelpers/loggerEntryHelpers.sanitize.js";
5
6
  /** Options used to configure a Zudojs logger. */
6
7
  export interface LoggerOptions {
7
8
  readonly name?: string;
@@ -16,6 +17,15 @@ export interface LoggerOptions {
16
17
  readonly transportTimeout?: number;
17
18
  readonly inheritContext?: boolean;
18
19
  readonly mutable?: boolean;
20
+ /**
21
+ * Secret redaction for metadata and context.
22
+ *
23
+ * Redaction is ON by default: fields whose NAME looks like a secret
24
+ * (password, token, api key, credential, authorization, cookie) are
25
+ * replaced with "[REDACTED]" before an entry reaches a formatter.
26
+ * Pass `{ enabled: false }` to opt out.
27
+ */
28
+ readonly redact?: LoggerRedactionOptions;
19
29
  }
20
30
  /** Options used when creating a child logger. */
21
31
  export interface ChildLoggerOptions {
@@ -53,6 +63,7 @@ export interface LoggerConfiguration {
53
63
  readonly transportTimeout: number;
54
64
  readonly inheritContext: boolean;
55
65
  readonly mutable: boolean;
66
+ readonly redact: LoggerRedactionOptions;
56
67
  }
57
68
  /** Default logger configuration values. */
58
69
  export declare const DEFAULT_LOGGER_OPTIONS: Required<Pick<LoggerOptions, "enabled" | "throwTransportErrors" | "asynchronous" | "transportTimeout" | "inheritContext" | "mutable">>;
@@ -1,3 +1,4 @@
1
+ import { LoggerLevel } from "../loggerLevel/loggerLevel.type.js";
1
2
  /** Default logger configuration values. */
2
3
  export const DEFAULT_LOGGER_OPTIONS = {
3
4
  enabled: true,
@@ -28,7 +29,7 @@ export function resolveLoggerOptions(options = {}) {
28
29
  validateLoggerOptions(options);
29
30
  return Object.freeze({
30
31
  name: options.name ?? "zudojs",
31
- level: options.level ?? 3,
32
+ level: options.level ?? LoggerLevel.INFO,
32
33
  environment: options.environment,
33
34
  metadata: Object.freeze({ ...(options.metadata ?? {}) }),
34
35
  formatter: options.formatter ?? "text",
@@ -40,6 +41,7 @@ export function resolveLoggerOptions(options = {}) {
40
41
  transportTimeout: options.transportTimeout ?? DEFAULT_LOGGER_OPTIONS.transportTimeout,
41
42
  inheritContext: options.inheritContext ?? DEFAULT_LOGGER_OPTIONS.inheritContext,
42
43
  mutable: options.mutable ?? DEFAULT_LOGGER_OPTIONS.mutable,
44
+ redact: Object.freeze({ ...(options.redact ?? {}) }),
43
45
  });
44
46
  }
45
47
  /** Merges two logger option objects. Values from `override` take precedence. */
@@ -66,6 +68,7 @@ export function createChildLoggerOptions(parent, options = {}) {
66
68
  transportTimeout: parent.transportTimeout,
67
69
  inheritContext: parent.inheritContext,
68
70
  mutable: parent.mutable,
71
+ redact: parent.redact,
69
72
  };
70
73
  }
71
74
  //# sourceMappingURL=loggerOptions.type.js.map
@@ -34,6 +34,10 @@ export function createBufferedLoggerTransport(transport, options = {}) {
34
34
  /* deliberate no-op */
35
35
  }
36
36
  }, flushInterval);
37
+ // A pending flush is housekeeping, not work: left referenced it kept a
38
+ // finished process alive for a full `flushInterval`. close() flushes
39
+ // whatever is buffered, so nothing is lost by letting the loop exit.
40
+ timer.unref?.();
37
41
  };
38
42
  const buffered = {
39
43
  name: options.name ?? "buffered",