@zudojs/logger 1.0.0 → 1.2.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 (23) hide show
  1. package/README.md +53 -10
  2. package/dist/loggerCore/core/loggerCore.core.d.ts +10 -1
  3. package/dist/loggerCore/core/loggerCore.core.js +55 -9
  4. package/dist/loggerCore/helpers/loggerCoreMethods/loggerCoreMethods.entry.js +9 -0
  5. package/dist/loggerCore/helpers/loggerCoreMethods/loggerCoreMethods.lifecycle.d.ts +9 -0
  6. package/dist/loggerCore/helpers/loggerCoreMethods/loggerCoreMethods.lifecycle.js +53 -26
  7. package/dist/loggerEntry/index.d.ts +1 -0
  8. package/dist/loggerEntry/index.js +1 -0
  9. package/dist/loggerEntry/loggerEntry.secretFields.d.ts +19 -0
  10. package/dist/loggerEntry/loggerEntry.secretFields.js +90 -0
  11. package/dist/loggerEntry/loggerEntryHelpers/loggerEntryHelpers.sanitize.d.ts +11 -7
  12. package/dist/loggerEntry/loggerEntryHelpers/loggerEntryHelpers.sanitize.js +28 -9
  13. package/dist/loggerErrors/loggerError.helpers.d.ts +11 -0
  14. package/dist/loggerErrors/loggerError.helpers.js +21 -0
  15. package/dist/loggerFormatter/loggerFormatterFormatters/loggerFormatterFormatters.context.js +6 -0
  16. package/dist/loggerFormatter/loggerFormatterFormatters/loggerFormatterFormatters.js +1 -1
  17. package/dist/loggerFormatter/loggerFormatterFormatters/loggerFormatterFormatters.metadata.d.ts +6 -0
  18. package/dist/loggerFormatter/loggerFormatterFormatters/loggerFormatterFormatters.metadata.js +43 -8
  19. package/dist/loggerFormatter/loggerFormatterFormatters/loggerFormatterFormatters.text.js +1 -1
  20. package/dist/loggerTransport/loggerTransportComposite/loggerTransportComposite.buffered.js +61 -10
  21. package/dist/loggerTransport/loggerTransportComposite/loggerTransportComposite.d.ts +9 -0
  22. package/dist/loggerTransport/loggerTransportComposite/loggerTransportComposite.js +29 -8
  23. package/package.json +6 -2
package/README.md CHANGED
@@ -3,6 +3,12 @@
3
3
  Structured logging with transports, formatters, log levels, secret
4
4
  redaction, and context propagation for Zudojs applications.
5
5
 
6
+ <!-- zudo-docs:start -->
7
+
8
+ **Documentation:** [zudojs.oyinlola.site/docs/packages-logger](https://zudojs.oyinlola.site/docs/packages-logger) · **For AI agents:** [Markdown version](https://zudojs.oyinlola.site/docs/packages-logger.md), [llms.txt](https://zudojs.oyinlola.site/llms.txt)
9
+
10
+ <!-- zudo-docs:end -->
11
+
6
12
  ## Installation
7
13
 
8
14
  ```bash
@@ -39,24 +45,53 @@ Built in: `createConsoleLoggerTransport`, and the composites
39
45
  `createBufferedLoggerTransport`. File and HTTP transports are not
40
46
  included — implement the `LoggerTransport` interface for those.
41
47
 
48
+ The multi and conditional composites forward `flush()` and `close()` to
49
+ the transports they wrap, so a buffered or file transport nested inside
50
+ is drained and released by the logger's own `flush()`/`close()`. The
51
+ multi transport writes to every sink even when one throws; the failures
52
+ are reported afterwards (several as one `AggregateError`). The buffered
53
+ transport writes each entry independently, so a failing write loses only
54
+ that entry, and a failure from a timer-triggered flush is rethrown by the
55
+ next `flush()` or `close()`.
56
+
42
57
  `transportTimeout` (default 10s) bounds every transport write, so a
43
58
  transport that stops responding cannot hang `flush()` or `close()`.
44
59
 
60
+ `throwTransportErrors` (default `false`) rethrows transport and formatter
61
+ failures instead of dropping them. A synchronous transport throws from the
62
+ log call itself; a failure from an asynchronous transport (or with
63
+ `asynchronous: true`) cannot, so it is rethrown by the next `flush()` or
64
+ `close()`, which still flush and close the transports first.
65
+
66
+ `flush()` and `close()` isolate each transport: one that throws does not
67
+ stop the rest from being flushed and closed, and `close()` always leaves
68
+ the logger disposed. The failures are rethrown afterwards (several as one
69
+ `AggregateError`).
70
+
45
71
  ## Flushing
46
72
 
47
73
  Dispatch completes synchronously when every transport is synchronous.
48
74
  With an asynchronous transport — or with `asynchronous: true`, which
49
75
  always defers so the caller stays off the transport's critical path —
50
76
  writes are in flight until drained. `flush()` and `close()` drain them,
51
- so nothing is lost at exit.
77
+ including writes started by child loggers (`child()`, `withContext()`),
78
+ so nothing is lost at exit. A child reports itself disposed once its root
79
+ logger is closed.
52
80
 
53
81
  ## Secret redaction
54
82
 
55
83
  Redaction is **on by default**. Metadata and context fields whose NAME
56
- looks like a secret — password, secret, token, api key, private key,
57
- credential, authorization, cookie — are replaced with `"[REDACTED]"`
58
- before the entry reaches any formatter or transport. Nested objects,
59
- arrays and getters are all covered.
84
+ looks like a secret are replaced with `"[REDACTED]"` before the entry
85
+ reaches any formatter or transport. Names are split into words
86
+ (`x-api-key`, `api_key` and `apiKey` all read as `api key`) and matched
87
+ against `DEFAULT_LOGGER_SECRET_FIELDS` — password, passphrase, secret,
88
+ token, jwt, bearer, auth, authorization, cookie, session, sid,
89
+ credential, api key, private key, client secret, card number, cvv, ssn,
90
+ pin, otp and more — so `sessionId` and `cardNumber` are redacted while
91
+ `passenger` and `authorId` are not. Nested objects, arrays and getters
92
+ are all covered. Passing `redact.pattern` replaces the word matcher with
93
+ your own RegExp (the old substring default is still exported as
94
+ `DEFAULT_LOGGER_SECRET_PATTERN`).
60
95
 
61
96
  ```typescript
62
97
  logger.info("login", { user: "alice", password: "hunter2" });
@@ -68,11 +103,14 @@ createLogger({ redact: { enabled: false } }); // opt out
68
103
 
69
104
  ## Log injection
70
105
 
71
- Text-shaped formatters escape control characters in the message, the
72
- logger name, metadata keys and values, context values and source
73
- locations. A newline or ANSI escape inside attacker-supplied text
74
- becomes `\n` / `` rather than forging an extra log record or
75
- driving the operator's terminal. The JSON formatter relies on
106
+ Text-shaped formatters escape control characters (C0, DEL, C1 and
107
+ U+2028/U+2029) in the message, the level, the logger name, metadata keys
108
+ and values, context values, source locations and error stacks. A newline
109
+ or ANSI escape inside attacker-supplied text becomes `\n` / `\u001b`
110
+ rather than forging an extra log record or driving the operator's
111
+ terminal. In a stack trace only the frame lines break the line: the
112
+ error's name and message are escaped as one line, and every frame line is
113
+ indented so none can start at column 0 and pass for a record. The JSON formatter relies on
76
114
  `JSON.stringify`, which escapes the same characters.
77
115
 
78
116
  Metadata is normalized before serialization, so circular references
@@ -94,6 +132,11 @@ withLoggerContext(logger, createLoggerContext({ traceId }), (scoped) => {
94
132
  });
95
133
  ```
96
134
 
135
+ Per-call context is merged into the entry's metadata the same way:
136
+ `logger.log(LoggerLevel.INFO, "handled", { context: { tenant: "acme" } })`.
137
+ The entry's `context` also carries the identifiers (`requestId`, `traceId`,
138
+ ...) of the active context for transports that read them directly.
139
+
97
140
  Child loggers inherit name, level, formatter, transports, metadata and
98
141
  redaction settings: `logger.child({ name: "api.db" })`.
99
142
 
@@ -31,8 +31,17 @@ export declare class ZudojsLogger implements Logger, ZudojsLoggerContext {
31
31
  private _configuration;
32
32
  private readonly _contextStorage;
33
33
  private _disposed;
34
+ private _closing;
34
35
  private readonly _pending;
35
- constructor(options?: LoggerOptions, contextStorage?: LoggerContextStorage);
36
+ private readonly _dispatchFailures;
37
+ private _droppedFailures;
38
+ private readonly _root;
39
+ /**
40
+ * @param root - The logger this one was derived from. A child's
41
+ * dispatches are tracked by its root, so the root's flush()/close()
42
+ * drains them, and a child reports itself disposed once its root is.
43
+ */
44
+ constructor(options?: LoggerOptions, contextStorage?: LoggerContextStorage, root?: ZudojsLogger);
36
45
  get configuration(): LoggerConfiguration;
37
46
  get contextStorage(): LoggerContextStorage;
38
47
  get name(): string;
@@ -7,6 +7,8 @@ import { resolveLoggerOptions } from "../../loggerOptions/loggerOptions.type.js"
7
7
  import { normalizeConfiguration, assertActive as assertActiveHelper, assertMutable as assertMutableHelper, handleInfrastructureError as handleInfrastructureErrorHelper, } from "../helpers/loggerCoreHelpers.js";
8
8
  import { logAtLevel, childLogger, withContextLogger, setLoggerLevel, enableLogger, disableLogger, flushLogger, closeLogger, } from "../helpers/loggerCoreMethods/index.js";
9
9
  import { getLoggerName, getLoggerLevel, getLoggerEnabled, } from "../helpers/loggerCoreMethods.loggerProps.js";
10
+ /** Upper bound on dispatch failures retained between two flushes. */
11
+ const MAX_RETAINED_DISPATCH_FAILURES = 32;
10
12
  /**
11
13
  * Logger implementation.
12
14
  */
@@ -14,8 +16,18 @@ export class ZudojsLogger {
14
16
  _configuration;
15
17
  _contextStorage;
16
18
  _disposed = false;
19
+ _closing;
17
20
  _pending = new Set();
18
- constructor(options = {}, contextStorage) {
21
+ _dispatchFailures = [];
22
+ _droppedFailures = 0;
23
+ _root;
24
+ /**
25
+ * @param root - The logger this one was derived from. A child's
26
+ * dispatches are tracked by its root, so the root's flush()/close()
27
+ * drains them, and a child reports itself disposed once its root is.
28
+ */
29
+ constructor(options = {}, contextStorage, root) {
30
+ this._root = root;
19
31
  this._configuration = resolveLoggerOptions(options);
20
32
  this._contextStorage = contextStorage ?? createLoggerContextStorage();
21
33
  this._configuration = normalizeConfiguration(this._configuration);
@@ -75,10 +87,17 @@ export class ZudojsLogger {
75
87
  return flushLogger(this);
76
88
  }
77
89
  close() {
78
- return closeLogger(this);
90
+ // Concurrent callers share one closure: two overlapping close() calls
91
+ // used to drain and close every transport twice.
92
+ if (this._closing)
93
+ return this._closing;
94
+ this._closing = closeLogger(this).finally(() => {
95
+ this._closing = undefined;
96
+ });
97
+ return this._closing;
79
98
  }
80
99
  assertActive() {
81
- assertActiveHelper(this._disposed, this._configuration.name);
100
+ assertActiveHelper(this.isDisposed(), this._configuration.name);
82
101
  }
83
102
  assertMutable() {
84
103
  assertMutableHelper(this._configuration.mutable);
@@ -87,7 +106,7 @@ export class ZudojsLogger {
87
106
  handleInfrastructureErrorHelper(this._configuration.throwTransportErrors, error);
88
107
  }
89
108
  isDisposed() {
90
- return this._disposed;
109
+ return this._disposed || (this._root?.isDisposed() ?? false);
91
110
  }
92
111
  markDisposed() {
93
112
  this._disposed = true;
@@ -96,25 +115,52 @@ export class ZudojsLogger {
96
115
  this._configuration = Object.freeze(config);
97
116
  }
98
117
  createChildLogger(options) {
99
- return new ZudojsLogger(options, this._contextStorage);
118
+ return new ZudojsLogger(options, this._contextStorage, this._root ?? this);
100
119
  }
101
120
  trackDispatch(dispatch) {
102
- // dispatchEntry routes its own failures through handleError, so a
103
- // rejection here is unexpected; swallow it to keep an unawaited log
104
- // call from crashing the process, and keep the entry drainable.
121
+ if (this._root) {
122
+ this._root.trackDispatch(dispatch);
123
+ return;
124
+ }
125
+ // A dispatch rejects only when handleError threw — i.e. when
126
+ // `throwTransportErrors` is on and an asynchronous transport failed.
127
+ // Nothing can throw from the log call that started it, so the failure
128
+ // is kept (bounded) and surfaced by the next flush()/close(); it was
129
+ // previously swallowed outright, which made the option a no-op for
130
+ // every asynchronous transport.
105
131
  const tracked = dispatch
106
- .catch(() => { })
132
+ .then(() => { }, (error) => {
133
+ if (!this._configuration.throwTransportErrors)
134
+ return;
135
+ if (this._dispatchFailures.length < MAX_RETAINED_DISPATCH_FAILURES) {
136
+ this._dispatchFailures.push(error);
137
+ }
138
+ else {
139
+ this._droppedFailures += 1;
140
+ }
141
+ })
107
142
  .finally(() => {
108
143
  this._pending.delete(tracked);
109
144
  });
110
145
  this._pending.add(tracked);
111
146
  }
112
147
  async drainDispatches() {
148
+ if (this._root) {
149
+ return this._root.drainDispatches();
150
+ }
113
151
  // A dispatch can start further dispatches (a transport that logs),
114
152
  // so drain until the set is genuinely empty.
115
153
  while (this._pending.size > 0) {
116
154
  await Promise.all([...this._pending]);
117
155
  }
156
+ if (this._dispatchFailures.length === 0)
157
+ return;
158
+ const failures = this._dispatchFailures.splice(0);
159
+ const dropped = this._droppedFailures;
160
+ this._droppedFailures = 0;
161
+ if (failures.length === 1 && dropped === 0)
162
+ throw failures[0];
163
+ throw new AggregateError(failures, `${failures.length + dropped} log dispatch(es) failed since the last flush.`);
118
164
  }
119
165
  }
120
166
  /**
@@ -14,9 +14,14 @@ export function createEntry(configuration, contextStorage, level, message, optio
14
14
  const contextMetadata = activeContext
15
15
  ? contextToLogMetadata(activeContext)
16
16
  : {};
17
+ // Per-call `options.context` flows into metadata exactly like the ambient
18
+ // context does. It used to reach only `entry.context.metadata`, which the
19
+ // text formatters never print, so `log(level, msg, { context })` silently
20
+ // dropped the data from every text-shaped line.
17
21
  const rawMetadata = {
18
22
  ...configuration.metadata,
19
23
  ...contextMetadata,
24
+ ...(options.context ?? {}),
20
25
  ...(options.metadata ?? {}),
21
26
  };
22
27
  // Redaction is applied HERE, before the entry is frozen, so every
@@ -35,8 +40,12 @@ export function createEntry(configuration, contextStorage, level, message, optio
35
40
  level,
36
41
  message,
37
42
  metadata,
43
+ // `LoggerEntryContext` declares the correlation identifiers, but they
44
+ // were never copied here, so a transport reading `entry.context.requestId`
45
+ // always saw `undefined`.
38
46
  context: context
39
47
  ? {
48
+ ...context.identifiers,
40
49
  metadata: redactLogValue(context.metadata, isSecret, replacement),
41
50
  }
42
51
  : undefined,
@@ -17,10 +17,19 @@ export declare function enableLogger(ctx: ZudojsLoggerContext): void;
17
17
  export declare function disableLogger(ctx: ZudojsLoggerContext): void;
18
18
  /**
19
19
  * Flushes all transport buffers.
20
+ *
21
+ * In-flight dispatches land in the transports before they are flushed.
22
+ * Every transport is flushed even when a dispatch or another transport
23
+ * failed; the failures are rethrown afterwards (several as one
24
+ * AggregateError).
20
25
  */
21
26
  export declare function flushLogger(ctx: ZudojsLoggerContext): Promise<void>;
22
27
  /**
23
28
  * Closes all transports and marks logger as disposed.
29
+ *
30
+ * Closing is terminal: every transport is flushed and closed and the
31
+ * logger is marked disposed even when a dispatch or a transport failed;
32
+ * the failures are rethrown afterwards.
24
33
  */
25
34
  export declare function closeLogger(ctx: ZudojsLoggerContext): Promise<void>;
26
35
  //# sourceMappingURL=loggerCoreMethods.lifecycle.d.ts.map
@@ -5,6 +5,7 @@ import { LoggerLevel } from "../../../loggerLevel/loggerLevel.type.js";
5
5
  import { createLoggerTransport } from "../../../loggerTransport/loggerTransport.core.js";
6
6
  import { isLoggerTransport } from "../../../loggerTransport/loggerTransportGuard.js";
7
7
  import { LoggerConfigurationError } from "../../../loggerErrors/loggerError.base.js";
8
+ import { throwCollectedFailures } from "../../../loggerErrors/loggerError.helpers.js";
8
9
  /**
9
10
  * Sets the logger level.
10
11
  */
@@ -44,47 +45,73 @@ export function disableLogger(ctx) {
44
45
  });
45
46
  }
46
47
  /**
47
- * Flushes all transport buffers.
48
+ * Flushes (and optionally closes) every configured transport, isolating
49
+ * each one: a failing sink is collected and the walk continues, so one
50
+ * bad transport cannot leave every later transport unflushed/unclosed.
48
51
  */
49
- export async function flushLogger(ctx) {
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.
54
- await ctx.drainDispatches();
52
+ async function settleTransports(ctx, close, failures) {
55
53
  for (const transport of ctx.configuration.transports) {
56
- if (!isLoggerTransport(transport)) {
54
+ if (!isLoggerTransport(transport))
57
55
  continue;
58
- }
59
56
  const registered = createLoggerTransport(transport);
60
- if (!registered.enabled) {
57
+ if (!close && !registered.enabled)
61
58
  continue;
62
- }
63
- if (registered.flush) {
64
- await registered.flush();
59
+ const steps = close
60
+ ? [() => registered.flush?.(), () => registered.close?.()]
61
+ : [() => registered.flush?.()];
62
+ for (const step of steps) {
63
+ try {
64
+ await step();
65
+ }
66
+ catch (error) {
67
+ failures.push(error);
68
+ }
65
69
  }
66
70
  }
67
71
  }
72
+ /** Drains in-flight dispatches, collecting (not throwing) a failure. */
73
+ async function drainInto(ctx, failures) {
74
+ try {
75
+ await ctx.drainDispatches();
76
+ }
77
+ catch (error) {
78
+ failures.push(error);
79
+ }
80
+ }
81
+ /**
82
+ * Flushes all transport buffers.
83
+ *
84
+ * In-flight dispatches land in the transports before they are flushed.
85
+ * Every transport is flushed even when a dispatch or another transport
86
+ * failed; the failures are rethrown afterwards (several as one
87
+ * AggregateError).
88
+ */
89
+ export async function flushLogger(ctx) {
90
+ ctx.assertActive();
91
+ const failures = [];
92
+ await drainInto(ctx, failures);
93
+ await settleTransports(ctx, false, failures);
94
+ throwCollectedFailures(failures, "Logger flush failed.");
95
+ }
68
96
  /**
69
97
  * Closes all transports and marks logger as disposed.
98
+ *
99
+ * Closing is terminal: every transport is flushed and closed and the
100
+ * logger is marked disposed even when a dispatch or a transport failed;
101
+ * the failures are rethrown afterwards.
70
102
  */
71
103
  export async function closeLogger(ctx) {
72
104
  if (ctx.isDisposed()) {
73
105
  return;
74
106
  }
75
- await ctx.drainDispatches();
76
- for (const transport of ctx.configuration.transports) {
77
- if (!isLoggerTransport(transport)) {
78
- continue;
79
- }
80
- const registered = createLoggerTransport(transport);
81
- if (registered.flush) {
82
- await registered.flush();
83
- }
84
- if (registered.close) {
85
- await registered.close();
86
- }
107
+ const failures = [];
108
+ try {
109
+ await drainInto(ctx, failures);
110
+ await settleTransports(ctx, true, failures);
111
+ }
112
+ finally {
113
+ ctx.markDisposed();
87
114
  }
88
- ctx.markDisposed();
115
+ throwCollectedFailures(failures, "Logger close failed.");
89
116
  }
90
117
  //# sourceMappingURL=loggerCoreMethods.lifecycle.js.map
@@ -5,4 +5,5 @@
5
5
  */
6
6
  export * from "./loggerEntryHelpers/index.js";
7
7
  export * from "./loggerEntry.core.js";
8
+ export { DEFAULT_LOGGER_SECRET_FIELDS, createDefaultSecretFieldMatcher, } from "./loggerEntry.secretFields.js";
8
9
  //# sourceMappingURL=index.d.ts.map
@@ -5,4 +5,5 @@
5
5
  */
6
6
  export * from "./loggerEntryHelpers/index.js";
7
7
  export * from "./loggerEntry.core.js";
8
+ export { DEFAULT_LOGGER_SECRET_FIELDS, createDefaultSecretFieldMatcher, } from "./loggerEntry.secretFields.js";
8
9
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Default secret-field matching for logger redaction.
3
+ *
4
+ * Field names are split into words (`x-api-key`, `api_key` and `apiKey`
5
+ * all become `api key`) and matched on whole-word runs, the same scheme
6
+ * @zudojs/observability's `isSensitiveField` uses. A raw substring test
7
+ * redacted `passenger`, `compass` and `bypassCache` (they contain `pass`)
8
+ * while missing `auth`, `sessionId`, `sid`, `jwt`, `ssn`, `cardNumber`,
9
+ * `cvv` and `otp` entirely.
10
+ */
11
+ /**
12
+ * Secret field names matched on whole words by default.
13
+ */
14
+ export declare const DEFAULT_LOGGER_SECRET_FIELDS: readonly string[];
15
+ /**
16
+ * Builds the default secret-field predicate from a word list.
17
+ */
18
+ export declare function createDefaultSecretFieldMatcher(fields?: readonly string[]): (key: string) => boolean;
19
+ //# sourceMappingURL=loggerEntry.secretFields.d.ts.map
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Default secret-field matching for logger redaction.
3
+ *
4
+ * Field names are split into words (`x-api-key`, `api_key` and `apiKey`
5
+ * all become `api key`) and matched on whole-word runs, the same scheme
6
+ * @zudojs/observability's `isSensitiveField` uses. A raw substring test
7
+ * redacted `passenger`, `compass` and `bypassCache` (they contain `pass`)
8
+ * while missing `auth`, `sessionId`, `sid`, `jwt`, `ssn`, `cardNumber`,
9
+ * `cvv` and `otp` entirely.
10
+ */
11
+ /**
12
+ * Secret field names matched on whole words by default.
13
+ */
14
+ export const DEFAULT_LOGGER_SECRET_FIELDS = Object.freeze([
15
+ "password",
16
+ "passwd",
17
+ "passphrase",
18
+ "pwd",
19
+ "secret",
20
+ "token",
21
+ "jwt",
22
+ "bearer",
23
+ "authorization",
24
+ "auth",
25
+ "cookie",
26
+ "session",
27
+ "sid",
28
+ "credential",
29
+ "credentials",
30
+ "api_key",
31
+ "private_key",
32
+ "client_secret",
33
+ "credit_card",
34
+ "card_number",
35
+ "cvv",
36
+ "cvc",
37
+ "ssn",
38
+ "social_security",
39
+ "pin",
40
+ "otp",
41
+ ]);
42
+ /**
43
+ * Unambiguous secret words that are also matched as a substring of a
44
+ * lowercase run-together name (`userpassword`, `accesstoken`), which the
45
+ * word split alone cannot see.
46
+ */
47
+ const SUBSTRING_SECRET_WORDS = Object.freeze([
48
+ "password",
49
+ "passwd",
50
+ "secret",
51
+ "token",
52
+ "apikey",
53
+ "privatekey",
54
+ "credential",
55
+ "authorization",
56
+ "cookie",
57
+ ]);
58
+ /** Splits a field name into lowercase words. */
59
+ function splitWords(key) {
60
+ return key
61
+ .replace(/([a-z0-9])([A-Z])/gu, "$1 $2")
62
+ .replace(/([A-Z]+)([A-Z][a-z])/gu, "$1 $2")
63
+ .toLowerCase()
64
+ .split(/[^a-z0-9]+/u)
65
+ .filter(Boolean);
66
+ }
67
+ /** Normalizes a field-list entry the way word runs are joined. */
68
+ function normalizeField(field) {
69
+ return field.toLowerCase().replace(/[^a-z0-9]/gu, "");
70
+ }
71
+ /**
72
+ * Builds the default secret-field predicate from a word list.
73
+ */
74
+ export function createDefaultSecretFieldMatcher(fields = DEFAULT_LOGGER_SECRET_FIELDS) {
75
+ const targets = new Set(fields.map(normalizeField).filter(Boolean));
76
+ return (key) => {
77
+ const words = splitWords(key);
78
+ for (let start = 0; start < words.length; start += 1) {
79
+ let joined = "";
80
+ for (let end = start; end < words.length; end += 1) {
81
+ joined += words[end] ?? "";
82
+ if (targets.has(joined))
83
+ return true;
84
+ }
85
+ }
86
+ const compact = key.toLowerCase().replace(/[^a-z0-9]/gu, "");
87
+ return SUBSTRING_SECRET_WORDS.some((word) => compact.includes(word));
88
+ };
89
+ }
90
+ //# sourceMappingURL=loggerEntry.secretFields.js.map
@@ -13,10 +13,13 @@
13
13
  */
14
14
  export declare const LOGGER_REDACTION_TOKEN = "[REDACTED]";
15
15
  /**
16
- * Field names treated as secrets by default.
16
+ * Legacy substring pattern for secret field names.
17
17
  *
18
- * Matching is case-insensitive and substring-based so `dbPassword`,
19
- * `X-Api-Key` and `refresh_token` are all covered.
18
+ * @deprecated No longer the default: it redacted `passenger`/`compass`
19
+ * and missed `auth`, `sessionId`, `ssn`, `cardNumber`, `cvv` and `otp`.
20
+ * The default is now the word-based matcher over
21
+ * `DEFAULT_LOGGER_SECRET_FIELDS`. Pass this as `redact.pattern` to opt
22
+ * back into the old behaviour.
20
23
  */
21
24
  export declare const DEFAULT_LOGGER_SECRET_PATTERN: RegExp;
22
25
  /**
@@ -32,8 +35,9 @@ export interface LoggerRedactionOptions {
32
35
  */
33
36
  readonly keys?: readonly string[];
34
37
  /**
35
- * Field-name pattern. Defaults to DEFAULT_LOGGER_SECRET_PATTERN.
36
- * Pass a pattern that never matches to rely on `keys` alone.
38
+ * Field-name pattern. When omitted, the word-based default matcher over
39
+ * `DEFAULT_LOGGER_SECRET_FIELDS` is used. Pass a pattern that never
40
+ * matches to rely on `keys` alone.
37
41
  */
38
42
  readonly pattern?: RegExp;
39
43
  /**
@@ -45,8 +49,8 @@ export interface LoggerRedactionOptions {
45
49
  * Escapes control characters that could forge log records.
46
50
  *
47
51
  * 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.
52
+ * other C0/C1 control character and DEL becomes `\xNN`, and U+2028 /
53
+ * U+2029 become `\u2028` / `\u2029`. All other text is unchanged.
50
54
  */
51
55
  export declare function escapeLogText(value: string): string;
52
56
  /**
@@ -8,28 +8,33 @@
8
8
  * terminal. Every string that reaches a text-shaped formatter is
9
9
  * therefore escaped here first.
10
10
  */
11
+ import { createDefaultSecretFieldMatcher } from "../loggerEntry.secretFields.js";
11
12
  /**
12
- * Matches C0 control characters plus DEL — everything that can forge a
13
- * record boundary or drive a terminal.
13
+ * Matches C0 controls, DEL, C1 controls (NEL, CSI) and the Unicode
14
+ * line/paragraph separators — everything that can forge a record
15
+ * boundary or drive a terminal.
14
16
  */
15
- const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/g;
17
+ const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g;
16
18
  /**
17
19
  * Replacement token written in place of a redacted value.
18
20
  */
19
21
  export const LOGGER_REDACTION_TOKEN = "[REDACTED]";
20
22
  /**
21
- * Field names treated as secrets by default.
23
+ * Legacy substring pattern for secret field names.
22
24
  *
23
- * Matching is case-insensitive and substring-based so `dbPassword`,
24
- * `X-Api-Key` and `refresh_token` are all covered.
25
+ * @deprecated No longer the default: it redacted `passenger`/`compass`
26
+ * and missed `auth`, `sessionId`, `ssn`, `cardNumber`, `cvv` and `otp`.
27
+ * The default is now the word-based matcher over
28
+ * `DEFAULT_LOGGER_SECRET_FIELDS`. Pass this as `redact.pattern` to opt
29
+ * back into the old behaviour.
25
30
  */
26
31
  export const DEFAULT_LOGGER_SECRET_PATTERN = /(pass(word|wd)?|secret|token|api[-_.]?key|private[-_.]?key|credential|authorization|cookie)/i;
27
32
  /**
28
33
  * Escapes control characters that could forge log records.
29
34
  *
30
35
  * 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.
36
+ * other C0/C1 control character and DEL becomes `\xNN`, and U+2028 /
37
+ * U+2029 become `\u2028` / `\u2029`. All other text is unchanged.
33
38
  */
34
39
  export function escapeLogText(value) {
35
40
  return value.replace(CONTROL_CHARACTERS, (character) => {
@@ -42,6 +47,10 @@ export function escapeLogText(value) {
42
47
  return "\\t";
43
48
  case "\u001b":
44
49
  return "\\u001b";
50
+ case "\u2028":
51
+ return "\\u2028";
52
+ case "\u2029":
53
+ return "\\u2029";
45
54
  default: {
46
55
  const code = character.charCodeAt(0);
47
56
  return `\\x${code.toString(16).padStart(2, "0")}`;
@@ -62,8 +71,18 @@ export function createSecretMatcher(options = {}) {
62
71
  if (options.enabled === false) {
63
72
  return () => false;
64
73
  }
65
- const pattern = options.pattern ?? DEFAULT_LOGGER_SECRET_PATTERN;
66
74
  const exact = new Set((options.keys ?? []).map((key) => key.toLowerCase()));
75
+ const configured = options.pattern;
76
+ if (configured === undefined) {
77
+ const isDefaultSecret = createDefaultSecretFieldMatcher();
78
+ return (key) => exact.has(key.toLowerCase()) || isDefaultSecret(key);
79
+ }
80
+ // Copy the pattern without `g`/`y`: those flags make `test()` advance
81
+ // `lastIndex`, so a shared pattern would match a secret-named field on
82
+ // one entry and let it through unredacted on the next.
83
+ const pattern = configured.global || configured.sticky
84
+ ? new RegExp(configured.source, configured.flags.replace(/[gy]/gu, ""))
85
+ : configured;
67
86
  return (key) => exact.has(key.toLowerCase()) || pattern.test(key);
68
87
  }
69
88
  /**
@@ -12,4 +12,15 @@ export declare function getLoggerErrorCause(error: unknown): unknown;
12
12
  export declare function createLoggerTransportError(transportName: string, error: unknown): LoggerTransportError;
13
13
  /** Creates a formatter error while preserving the original failure. */
14
14
  export declare function createLoggerFormatterError(formatterName: string, error: unknown): LoggerFormatterError;
15
+ /**
16
+ * Rethrows collected failures: the single failure itself, or an
17
+ * AggregateError when more than one step failed. Does nothing when the
18
+ * list is empty.
19
+ */
20
+ export declare function throwCollectedFailures(failures: readonly unknown[], message: string): void;
21
+ /**
22
+ * Runs every step even when earlier ones fail, then rethrows the
23
+ * collected failures via {@link throwCollectedFailures}.
24
+ */
25
+ export declare function settleAllOrThrow(steps: readonly (() => unknown)[], message: string): Promise<void>;
15
26
  //# sourceMappingURL=loggerError.helpers.d.ts.map
@@ -38,4 +38,25 @@ export function createLoggerFormatterError(formatterName, error) {
38
38
  const message = error instanceof Error ? error.message : String(error);
39
39
  return new LoggerFormatterError(`Logger formatter "${formatterName}" failed: ${message}`, { formatterName, cause });
40
40
  }
41
+ /**
42
+ * Rethrows collected failures: the single failure itself, or an
43
+ * AggregateError when more than one step failed. Does nothing when the
44
+ * list is empty.
45
+ */
46
+ export function throwCollectedFailures(failures, message) {
47
+ if (failures.length === 0)
48
+ return;
49
+ if (failures.length === 1)
50
+ throw failures[0];
51
+ throw new AggregateError(failures, message);
52
+ }
53
+ /**
54
+ * Runs every step even when earlier ones fail, then rethrows the
55
+ * collected failures via {@link throwCollectedFailures}.
56
+ */
57
+ export async function settleAllOrThrow(steps, message) {
58
+ const results = await Promise.allSettled(steps.map(async (step) => step()));
59
+ const failures = results.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
60
+ throwCollectedFailures(failures, message);
61
+ }
41
62
  //# sourceMappingURL=loggerError.helpers.js.map
@@ -17,6 +17,12 @@ export function formatContext(entry) {
17
17
  if (typeof value === "object") {
18
18
  continue;
19
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
+ }
20
26
  values.push(`${escapeLogText(key)}=${escapeLogText(String(value))}`);
21
27
  }
22
28
  return values.length > 0 ? `[${values.join(" ")}]` : "";
@@ -11,7 +11,7 @@ import { createTextLoggerFormatter } from "./loggerFormatterFormatters.text.js";
11
11
  */
12
12
  export function createCompactLoggerFormatter(options = {}) {
13
13
  return createLoggerFormatter((entry) => {
14
- const level = entry.levelName.toUpperCase();
14
+ const level = escapeLogText(entry.levelName.toUpperCase());
15
15
  const logger = entry.logger ? ` ${escapeLogText(entry.logger)}:` : "";
16
16
  return `${level}${logger} ${escapeLogText(entry.message)}`;
17
17
  }, {
@@ -11,6 +11,12 @@ export declare function formatMetadata(metadata: Record<string, unknown>, separa
11
11
  export declare function formatValue(value: unknown): string;
12
12
  /**
13
13
  * Formats an Error.
14
+ *
15
+ * With a stack trace the output is intentionally multi-line, but only
16
+ * the frame lines break the line: the header (which carries the
17
+ * attacker-influenceable message) is escaped as a single line, and every
18
+ * frame line is escaped and indented so none of them can start at
19
+ * column 0 and parse as a record of its own.
14
20
  */
15
21
  export declare function formatError(error: Error, includeStackTrace: boolean): string;
16
22
  //# sourceMappingURL=loggerFormatterFormatters.metadata.d.ts.map
@@ -23,28 +23,63 @@ export function formatValue(value) {
23
23
  }
24
24
  if (typeof value === "string") {
25
25
  if (/\s/.test(value)) {
26
- return JSON.stringify(value);
26
+ // JSON escapes C0 controls but not DEL, C1 or U+2028/U+2029.
27
+ return escapeLogText(JSON.stringify(value));
27
28
  }
28
29
  // A value with no whitespace can still carry ANSI escapes or other
29
30
  // C0 controls, which used to reach the sink verbatim.
30
31
  return escapeLogText(value);
31
32
  }
32
33
  if (typeof value === "object") {
33
- return JSON.stringify(serializeLoggerValue(value));
34
+ return escapeLogText(JSON.stringify(serializeLoggerValue(value)));
34
35
  }
35
36
  return escapeLogText(String(value));
36
37
  }
38
+ /**
39
+ * Splits an error's stack into its header (name and message) and its
40
+ * frame lines.
41
+ *
42
+ * The header is derived from the error itself rather than from the first
43
+ * stack line, because the message can contain newlines of its own and
44
+ * would otherwise spill across several "lines" of the stack.
45
+ */
46
+ function splitStack(error, stack) {
47
+ const message = String(error.message ?? "");
48
+ const header = message ? `${error.name}: ${message}` : String(error.name);
49
+ if (stack.startsWith(header)) {
50
+ const rest = stack.slice(header.length).replace(/^\r?\n/u, "");
51
+ return { header, frames: rest ? rest.split("\n") : [] };
52
+ }
53
+ const lines = stack.split("\n");
54
+ const firstFrame = lines.findIndex((line) => /^\s+at\s/u.test(line));
55
+ if (firstFrame === -1) {
56
+ return { header: stack, frames: [] };
57
+ }
58
+ return {
59
+ header: lines.slice(0, firstFrame).join("\n"),
60
+ frames: lines.slice(firstFrame),
61
+ };
62
+ }
37
63
  /**
38
64
  * Formats an Error.
65
+ *
66
+ * With a stack trace the output is intentionally multi-line, but only
67
+ * the frame lines break the line: the header (which carries the
68
+ * attacker-influenceable message) is escaped as a single line, and every
69
+ * frame line is escaped and indented so none of them can start at
70
+ * column 0 and parse as a record of its own.
39
71
  */
40
72
  export function formatError(error, includeStackTrace) {
41
- if (includeStackTrace && 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")}`;
73
+ if (includeStackTrace && typeof error.stack === "string" && error.stack) {
74
+ const { header, frames } = splitStack(error, error.stack);
75
+ const lines = [escapeLogText(header)];
76
+ for (const frame of frames) {
77
+ const escaped = escapeLogText(frame);
78
+ lines.push(/^\s/u.test(escaped) ? escaped : ` ${escaped}`);
79
+ }
80
+ return `\n${lines.join("\n")}`;
46
81
  }
47
82
  const serialized = serializeLoggerError(error);
48
- return `error=${JSON.stringify(serialized)}`;
83
+ return `error=${escapeLogText(JSON.stringify(serialized))}`;
49
84
  }
50
85
  //# sourceMappingURL=loggerFormatterFormatters.metadata.js.map
@@ -43,7 +43,7 @@ export function createTextLoggerFormatter(options = {}) {
43
43
  // output. Colour codes are emitted only on explicit opt-in, and
44
44
  // only around the fixed level name — never around user text,
45
45
  // which stays escaped.
46
- const levelTag = `[${entry.levelName.toUpperCase()}]`;
46
+ const levelTag = `[${escapeLogText(entry.levelName.toUpperCase())}]`;
47
47
  parts.push(context.colors ? colorizeLevel(entry.levelName, levelTag) : levelTag);
48
48
  if (includeLogger && entry.logger) {
49
49
  parts.push(`[${escapeLogText(entry.logger)}]`);
@@ -2,7 +2,8 @@
2
2
  * Buffered logger transport.
3
3
  */
4
4
  import { createLoggerTransport, writeLoggerTransport, } from "../loggerTransport.core.js";
5
- import { isLoggerTransportObject } from "../loggerTransportGuard.js";
5
+ import { closeLoggerTransport, flushLoggerTransport, } from "../loggerTransportHelpers/loggerTransportHelpers.js";
6
+ import { throwCollectedFailures } from "../../loggerErrors/loggerError.helpers.js";
6
7
  /**
7
8
  * Creates a transport that buffers entries before forwarding
8
9
  * them to another transport.
@@ -12,14 +13,50 @@ export function createBufferedLoggerTransport(transport, options = {}) {
12
13
  const maxSize = options.maxSize ?? 100;
13
14
  const flushInterval = options.flushInterval ?? 0;
14
15
  let timer;
15
- const flush = async () => {
16
+ let deferredFailure;
17
+ // Each entry is written on its own: one failing write used to abort the
18
+ // loop after the whole batch had already been spliced out, losing every
19
+ // entry behind it. Only the entries that actually failed are dropped.
20
+ const drain = async () => {
16
21
  if (buffer.length === 0) {
17
22
  return;
18
23
  }
19
24
  const entries = buffer.splice(0, buffer.length);
25
+ const failures = [];
20
26
  for (const entry of entries) {
21
- await writeLoggerTransport(transport, entry);
27
+ try {
28
+ await writeLoggerTransport(transport, entry);
29
+ }
30
+ catch (error) {
31
+ failures.push(error);
32
+ }
22
33
  }
34
+ throwCollectedFailures(failures, `${failures.length} buffered log entries failed to write.`);
35
+ };
36
+ // A failure from a timer-triggered drain has no caller to reach, so it
37
+ // is kept and rethrown by the next explicit flush()/close().
38
+ const takeDeferredFailure = () => {
39
+ if (!deferredFailure)
40
+ return [];
41
+ const { error } = deferredFailure;
42
+ deferredFailure = undefined;
43
+ return [error];
44
+ };
45
+ const flush = async () => {
46
+ const failures = takeDeferredFailure();
47
+ try {
48
+ await drain();
49
+ }
50
+ catch (error) {
51
+ failures.push(error);
52
+ }
53
+ try {
54
+ await flushLoggerTransport(transport);
55
+ }
56
+ catch (error) {
57
+ failures.push(error);
58
+ }
59
+ throwCollectedFailures(failures, "Buffered logger transport flush failed.");
23
60
  };
24
61
  const scheduleFlush = () => {
25
62
  if (flushInterval <= 0 || timer) {
@@ -28,12 +65,16 @@ export function createBufferedLoggerTransport(transport, options = {}) {
28
65
  timer = setTimeout(async () => {
29
66
  timer = undefined;
30
67
  try {
31
- await flush();
68
+ await drain();
32
69
  }
33
- catch {
34
- /* deliberate no-op */
70
+ catch (error) {
71
+ deferredFailure ??= { error };
35
72
  }
36
73
  }, flushInterval);
74
+ // A pending flush is housekeeping, not work: left referenced it kept a
75
+ // finished process alive for a full `flushInterval`. close() flushes
76
+ // whatever is buffered, so nothing is lost by letting the loop exit.
77
+ timer.unref?.();
37
78
  };
38
79
  const buffered = {
39
80
  name: options.name ?? "buffered",
@@ -41,7 +82,7 @@ export function createBufferedLoggerTransport(transport, options = {}) {
41
82
  async write(entry) {
42
83
  buffer.push(entry);
43
84
  if (buffer.length >= maxSize) {
44
- await flush();
85
+ await drain();
45
86
  }
46
87
  else {
47
88
  scheduleFlush();
@@ -53,10 +94,20 @@ export function createBufferedLoggerTransport(transport, options = {}) {
53
94
  clearTimeout(timer);
54
95
  timer = undefined;
55
96
  }
56
- await flush();
57
- if (isLoggerTransportObject(transport) && transport.close) {
58
- await transport.close();
97
+ const failures = [];
98
+ try {
99
+ await flush();
100
+ }
101
+ catch (error) {
102
+ failures.push(error);
103
+ }
104
+ try {
105
+ await closeLoggerTransport(transport);
106
+ }
107
+ catch (error) {
108
+ failures.push(error);
59
109
  }
110
+ throwCollectedFailures(failures, "Buffered logger transport close failed.");
60
111
  },
61
112
  };
62
113
  return createLoggerTransport(buffered, options);
@@ -7,11 +7,20 @@ import { createBufferedLoggerTransport } from "./loggerTransportComposite.buffer
7
7
  /**
8
8
  * Creates a transport that forwards entries to another
9
9
  * transport only when a predicate passes.
10
+ *
11
+ * `flush()` and `close()` are forwarded to the inner transport, so a
12
+ * buffered or file transport nested inside is drained and released by
13
+ * the logger's own `flush()`/`close()`.
10
14
  */
11
15
  export declare function createConditionalLoggerTransport(transport: LoggerTransportLike, predicate: (entry: LoggerEntry) => boolean | Promise<boolean>, options?: LoggerTransportOptions): RegisteredLoggerTransport;
12
16
  /**
13
17
  * Creates a transport that forwards entries to multiple
14
18
  * transports.
19
+ *
20
+ * Every sink receives every entry even when another sink throws: writes
21
+ * are settled independently and the failures are rethrown afterwards
22
+ * (one failure as itself, several as an AggregateError). `flush()` and
23
+ * `close()` fan out to every inner transport the same way.
15
24
  */
16
25
  export declare function createMultiLoggerTransport(transports: readonly LoggerTransportLike[], options?: LoggerTransportOptions): RegisteredLoggerTransport;
17
26
  export { createBufferedLoggerTransport };
@@ -2,27 +2,48 @@
2
2
  * Composite logger transports.
3
3
  */
4
4
  import { createLoggerTransport, writeLoggerTransport, } from "../loggerTransport.core.js";
5
+ import { createLoggerTransportId } from "../loggerTransportGuard.js";
6
+ import { closeLoggerTransport, flushLoggerTransport, } from "../loggerTransportHelpers/loggerTransportHelpers.js";
7
+ import { settleAllOrThrow } from "../../loggerErrors/loggerError.helpers.js";
5
8
  import { createBufferedLoggerTransport } from "./loggerTransportComposite.buffered.js";
6
9
  /**
7
10
  * Creates a transport that forwards entries to another
8
11
  * transport only when a predicate passes.
12
+ *
13
+ * `flush()` and `close()` are forwarded to the inner transport, so a
14
+ * buffered or file transport nested inside is drained and released by
15
+ * the logger's own `flush()`/`close()`.
9
16
  */
10
17
  export function createConditionalLoggerTransport(transport, predicate, options = {}) {
11
- return createLoggerTransport(async (entry, context) => {
12
- if (await predicate(entry)) {
13
- await writeLoggerTransport(transport, entry, context);
14
- }
18
+ return createLoggerTransport({
19
+ name: options.name ?? createLoggerTransportId(),
20
+ enabled: options.enabled ?? true,
21
+ async write(entry, context) {
22
+ if (await predicate(entry)) {
23
+ await writeLoggerTransport(transport, entry, context);
24
+ }
25
+ },
26
+ flush: () => flushLoggerTransport(transport),
27
+ close: () => closeLoggerTransport(transport),
15
28
  }, options);
16
29
  }
17
30
  /**
18
31
  * Creates a transport that forwards entries to multiple
19
32
  * transports.
33
+ *
34
+ * Every sink receives every entry even when another sink throws: writes
35
+ * are settled independently and the failures are rethrown afterwards
36
+ * (one failure as itself, several as an AggregateError). `flush()` and
37
+ * `close()` fan out to every inner transport the same way.
20
38
  */
21
39
  export function createMultiLoggerTransport(transports, options = {}) {
22
- return createLoggerTransport(async (entry, context) => {
23
- for (const transport of transports) {
24
- await writeLoggerTransport(transport, entry, context);
25
- }
40
+ const sinks = [...transports];
41
+ return createLoggerTransport({
42
+ name: options.name ?? createLoggerTransportId(),
43
+ enabled: options.enabled ?? true,
44
+ write: (entry, context) => settleAllOrThrow(sinks.map((sink) => () => writeLoggerTransport(sink, entry, context)), "Multiple logger transports failed to write an entry."),
45
+ flush: () => settleAllOrThrow(sinks.map((sink) => () => flushLoggerTransport(sink)), "Multiple logger transports failed to flush."),
46
+ close: () => settleAllOrThrow(sinks.map((sink) => () => closeLoggerTransport(sink)), "Multiple logger transports failed to close."),
26
47
  }, options);
27
48
  }
28
49
  export { createBufferedLoggerTransport };
package/package.json CHANGED
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "name": "@zudojs/logger",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Structured logging with transports, log levels, and context propagation for Zudojs applications.",
5
5
  "license": "MIT",
6
+ "author": {
7
+ "name": "Oluwayemi Oyinlola",
8
+ "url": "https://github.com/oyinlola-tech"
9
+ },
6
10
  "type": "module",
7
11
  "main": "./dist/index.js",
8
12
  "module": "./dist/index.js",
@@ -24,7 +28,7 @@
24
28
  "node": ">=24.0.0"
25
29
  },
26
30
  "dependencies": {
27
- "@zudojs/errors": "1.0.0"
31
+ "@zudojs/errors": "1.1.0"
28
32
  },
29
33
  "devDependencies": {
30
34
  "typescript": "7.0.2",