@bymax-one/nest-logger 1.2.6 → 1.2.7

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.
package/CHANGELOG.md CHANGED
@@ -11,6 +11,107 @@ heading here.
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [1.2.7] - 2026-08-15
15
+
16
+ ### Security
17
+
18
+ - **Serializer output is redacted by name under `redactStrategy: 'paths'` too.** Carrying an
19
+ error's own properties at every depth of the `cause` chain (below) opened a hole in that legacy
20
+ mode: the name walk is a pass-through there, so redaction fell entirely to `fast-redact`, and
21
+ `DEFAULT_REDACT_PATHS` reaches `*.*.*.*.password` — four wildcard levels. A serialized cause chain
22
+ goes deeper. Measured on the built artifact with a password on the deepest link of
23
+ `err.errors[0].cause.cause`:
24
+
25
+ ```json
26
+ { "cause": { "name": "Error", "message": "deep", "password": "LEAK-deep" } }
27
+ ```
28
+
29
+ Confirmed absent from `1.2.6`, where the field was not carried at all — so this was a regression
30
+ introduced inside this release rather than an inherited one, and it never reached a published
31
+ artifact.
32
+
33
+ `redactStrategy` is the consumer's choice about **their own payload**; what a serializer
34
+ synthesizes is this library's output, and the name walk — which has no depth ceiling — now covers
35
+ it either way. The caller's record is untouched by this: `'paths'` still means exactly what it
36
+ documented, four-level ceiling included, and the e2e case that pins that ceiling is what caught
37
+ the first attempt at this fix collapsing both hooks onto one redactor.
38
+
39
+ Reported by Copilot on the pull request that introduced the deeper copy.
40
+
41
+ - **An own `__proto__` key on an error is now stored as data instead of being applied as a
42
+ prototype.** The own-property copy assigned each key, and `__proto__` is an accessor inherited
43
+ from `Object.prototype` — so the assignment walked the chain, invoked that setter, and replaced
44
+ the sanitized node's prototype. An error-like object arriving from JSON (a worker boundary, a
45
+ queue, an HTTP body) carries `__proto__` as an ordinary own enumerable key, and `Object.entries`
46
+ hands it over like any other.
47
+
48
+ Measured rather than assumed. `Object.prototype` itself is never touched, so this is prototype
49
+ injection into a single log node rather than global pollution, and with a JSON-sourced payload the
50
+ emitted line was unchanged — `JSON.stringify` ignores inherited fields. The live hazard is the
51
+ node: given `{"__proto__": null}` it loses `hasOwnProperty` and `toString`, which then **throw**
52
+ for anything downstream that touches them, inside the one path whose contract is that logging
53
+ never crashes the application.
54
+
55
+ The copy now defines an own data property, which makes the key inert and keeps its value — an
56
+ error that genuinely carries a `__proto__` field gets it logged as the data it is. Reported by
57
+ Copilot on the pull request that introduced the deeper copy.
58
+
59
+ ### Documentation
60
+
61
+ - **`ignore` needs an escaped dot for `event.name`, and every example here taught the opposite.**
62
+ `pino-pretty` reads a dot as a nested path, and this library emits `event.name` as a literal
63
+ top-level key — so `ignore: '…,event.name'` looks for an `event` object with a `name` inside,
64
+ finds neither, and hides nothing without a word of complaint. Measured on one entry with only the
65
+ option changing:
66
+
67
+ ```
68
+ ignore: '…,event.name' → INFO: msg {"logKey":"X","event.name":"x"} ← still there
69
+ ignore: '…,event\\.name' → INFO: msg {"logKey":"X"} ← hidden
70
+ ```
71
+
72
+ Found by a consumer who copied the `1.2.6` example verbatim, configured it, and saw the field
73
+ still on screen. The escaped form is now in the `PrettyViewOptions` JSDoc — where an editor's
74
+ autocomplete shows it, which is where someone actually is when writing the option — in the README,
75
+ and in the `1.2.6` example itself, which is corrected rather than left to keep teaching it.
76
+
77
+ ### Fixed
78
+
79
+ - **An error's own properties now survive at every depth of the `cause` chain, not just at the top.**
80
+ `code`, `statusCode` and any domain field an application attaches were copied onto the error handed
81
+ to the log call and **dropped the moment that same error was wrapped as someone else's `cause`** —
82
+ which is what a `catch` in `main.ts` does routinely. Measured on the published `1.2.6`:
83
+
84
+ ```json
85
+ {
86
+ "type": "Error",
87
+ "message": "bootstrap failed",
88
+ "code": "EBOOT",
89
+ "cause": { "name": "Error", "message": "config invalid" }
90
+ }
91
+ ```
92
+
93
+ The inner error carried `code: "BYMAX_CONFIG_VALIDATION"` and an `issues` array holding one entry
94
+ per invalid variable. Both vanished. The reporting consumer framed the severity exactly right:
95
+ because `message` survives and their message is the full aggregated report, **the human stayed
96
+ served while the machine went blind** — the text an operator reads was intact, and the fields an
97
+ alert or a dashboard keys on were gone.
98
+
99
+ Nothing justified the asymmetry. The reason own properties are "part of the contract" at the top —
100
+ `pino.stdSerializers.err` copied them, so dropping them is a silent compatibility loss — is the
101
+ same reason one level down, and a wrapped error is the common case rather than the exotic one.
102
+ They still never shadow a field the serializer derives, and they still pass through the same
103
+ name-based redaction and size bound as any other serializer output.
104
+
105
+ Found by following up an asymmetry noticed while answering a consumer's question about how to pass
106
+ an error through the NestJS bridge — not by a bug report.
107
+
108
+ ### Changed
109
+
110
+ - **The own-property copy lives in one place instead of two.** The `err` serializer read the raw
111
+ thrown value and walked its properties itself, duplicating what the sanitizer now does at every
112
+ node; it copies from the sanitized node instead. Same fields, one walk — and no second copy to
113
+ drift away from the first.
114
+
14
115
  ## [1.2.6] - 2026-08-15
15
116
 
16
117
  ### Added
@@ -30,7 +131,7 @@ heading here.
30
131
 
31
132
  ```ts
32
133
  new PrettyDevDestination({
33
- view: { singleLine: true, ignore: 'pid,hostname,service,deployment,event.name' }
134
+ view: { singleLine: true, ignore: 'pid,hostname,service,deployment,event\\.name' }
34
135
  })
35
136
  ```
36
137
 
@@ -1130,7 +1231,8 @@ published `dist/` is identical — no runtime behaviour changes for consumers.
1130
1231
  - Professional CI suite: `ci.yml`, `bench.yml`, `codeql.yml`, `scorecard.yml`,
1131
1232
  `release.yml`, Dependabot, and issue templates
1132
1233
 
1133
- [Unreleased]: https://github.com/bymaxone/nest-logger/compare/v1.2.6...HEAD
1234
+ [Unreleased]: https://github.com/bymaxone/nest-logger/compare/v1.2.7...HEAD
1235
+ [1.2.7]: https://github.com/bymaxone/nest-logger/compare/v1.2.6...v1.2.7
1134
1236
  [1.2.6]: https://github.com/bymaxone/nest-logger/compare/v1.2.5...v1.2.6
1135
1237
  [1.2.5]: https://github.com/bymaxone/nest-logger/compare/v1.2.4...v1.2.5
1136
1238
  [1.2.4]: https://github.com/bymaxone/nest-logger/compare/v1.2.3...v1.2.4
package/README.md CHANGED
@@ -534,6 +534,32 @@ any other field. `AggregateError` members reach the entry as `err.errors`. There
534
534
  OpenTelemetry attribute for a cause chain, so these stay namespaced under `err` rather than
535
535
  inventing an `exception.cause` the spec does not define.
536
536
 
537
+ **An error's own enumerable properties travel with it at every depth** — `code` on a Node system
538
+ error, `statusCode` from an HTTP layer, whatever domain fields you attached — including on a nested
539
+ `cause` and on each `AggregateError` member:
540
+
541
+ ```json
542
+ {
543
+ "err": {
544
+ "type": "Error",
545
+ "message": "bootstrap failed",
546
+ "code": "EBOOT",
547
+ "cause": {
548
+ "name": "Error",
549
+ "message": "config invalid",
550
+ "code": "BYMAX_CONFIG_VALIDATION",
551
+ "issues": [{ "variable": "DATABASE_URL", "code": "invalid_url" }]
552
+ }
553
+ }
554
+ }
555
+ ```
556
+
557
+ This matters because `code` is usually the field you alert on. Until 1.2.7 those properties survived
558
+ only on the error handed to the log call and were dropped the moment the same error was wrapped as
559
+ someone else's `cause` — which is what a `catch` in `main.ts` does routinely. They never shadow a
560
+ field the serializer derives, and they pass through the same name-based redaction and size bound as
561
+ any other serializer output.
562
+
537
563
  An error's own enumerable properties (`code`, `statusCode`, domain fields) are carried through, as
538
564
  Pino's standard serializer did.
539
565
 
@@ -760,7 +786,7 @@ So the common accident — adding `new PrettyDevDestination()` without installin
760
786
  ```typescript
761
787
  // One line per entry, with the fields your project repeats on every line hidden.
762
788
  new PrettyDevDestination({
763
- view: { singleLine: true, ignore: 'pid,hostname,service,deployment,event.name' }
789
+ view: { singleLine: true, ignore: 'pid,hostname,service,deployment,event\\.name' }
764
790
  })
765
791
 
766
792
  // Message only, with the context pulled back into the line.
@@ -780,6 +806,15 @@ new PrettyDevDestination({
780
806
 
781
807
  > **`hideObject` hides it everywhere.** In pretty mode there is no JSON copy behind the rendering, because `destinations` **replaces** stdout — so a field hidden here, `logKey` included, is not visible anywhere. That is what the option is for; `messageFormat` is how you pull a specific field back.
782
808
 
809
+ > **A dot in `ignore` means "nested path", so a field name that CONTAINS a dot must escape it.** This library emits `event.name` as a literal top-level key, so the obvious spelling hides nothing at all — `pino-pretty` looks for an `event` object with a `name` inside, finds neither, and says nothing. Measured on one entry with only the option changing:
810
+ >
811
+ > ```
812
+ > ignore: '…,event.name' → INFO: msg {"logKey":"X","event.name":"x"} ← still there
813
+ > ignore: '…,event\\.name' → INFO: msg {"logKey":"X"} ← hidden
814
+ > ```
815
+ >
816
+ > In a TypeScript string literal that is `'event\\.name'` — one real backslash reaching `pino-pretty`. `service` and `deployment` need no escape; they really are nested objects. Reported by a consumer who copied the example above when it still had the unescaped form.
817
+
783
818
  The shape is exported as `PrettyViewOptions` when you want to build the view separately — it is this library's own interface, not a re-export of `pino-pretty`'s `PrettyOptions`, so type-checking without the optional peer installed still resolves.
784
819
 
785
820
  > **`destination` is not exposed, by design.** The library owns where entries go — a redirected stream would route around the fan-out and the last-resort rescue above. It is absent from the type and applied after your options are merged, so it cannot be overridden from untyped JavaScript either.
@@ -232,6 +232,13 @@ function escapeControlCharacters(text) {
232
232
  var DEFAULT_MAX_CAUSE_DEPTH = 3;
233
233
  var MAX_AGGREGATE_ERRORS = 10;
234
234
  var CIRCULAR_SENTINEL = "[Circular]";
235
+ var DERIVED_ERROR_FIELDS = /* @__PURE__ */ new Set([
236
+ "name",
237
+ "message",
238
+ "stack",
239
+ "cause",
240
+ "errors"
241
+ ]);
235
242
  function sanitizeError(err, options) {
236
243
  const maxCauseDepth = DEFAULT_MAX_CAUSE_DEPTH;
237
244
  return toSanitizedError(err, /* @__PURE__ */ new WeakSet(), 0, maxCauseDepth);
@@ -267,11 +274,23 @@ function toSanitizedError(value, seen, depth2, maxDepth) {
267
274
  result.errors.push(createWidthMarker(aggregated.length - MAX_AGGREGATE_ERRORS));
268
275
  }
269
276
  }
277
+ assignOwnErrorFields(result, value);
270
278
  return result;
271
279
  } catch {
272
280
  return { name: "SanitizeFailed", message: "Failed to sanitize the thrown value" };
273
281
  }
274
282
  }
283
+ function assignOwnErrorFields(target, source) {
284
+ try {
285
+ const own = Array.isArray(source) ? {} : source;
286
+ for (const [key, value] of Object.entries(own)) {
287
+ if (!Object.hasOwn(target, key) && !DERIVED_ERROR_FIELDS.has(key)) {
288
+ Object.defineProperty(target, key, { value, enumerable: true });
289
+ }
290
+ }
291
+ } catch {
292
+ }
293
+ }
275
294
  function toSanitizedChild(value, seen, depth2, maxDepth) {
276
295
  if (isObject(value) && seen.has(value)) {
277
296
  return CIRCULAR_SENTINEL;
@@ -1401,13 +1420,6 @@ function leafNameOf(path) {
1401
1420
  }
1402
1421
  return leaf;
1403
1422
  }
1404
- var DERIVED_ERROR_FIELDS = /* @__PURE__ */ new Set([
1405
- "name",
1406
- "message",
1407
- "stack",
1408
- "cause",
1409
- "errors"
1410
- ]);
1411
1423
  function serializeErrorValue(value_) {
1412
1424
  const sanitized = sanitizeError(value_);
1413
1425
  const serialized = {
@@ -1424,18 +1436,7 @@ function serializeErrorValue(value_) {
1424
1436
  if (sanitized.errors !== void 0) {
1425
1437
  serialized["errors"] = sanitized.errors;
1426
1438
  }
1427
- try {
1428
- const source = (
1429
- // Stryker disable next-line ConditionalExpression: equivalent BY CONSTRUCTION, not untested — the never-throw catch below makes the mutant observationally identical: with the null check flipped to true, `Object.entries(null)` throws, the catch swallows it, and the emitted envelope is byte-for-byte the same. The guard stays because routing the ordinary null case through exception control flow would trade an explicit branch for a hidden one.
1430
- typeof value_ === "object" && value_ !== null && !Array.isArray(value_) ? value_ : {}
1431
- );
1432
- for (const [key, value] of Object.entries(source)) {
1433
- if (!Object.hasOwn(serialized, key) && !DERIVED_ERROR_FIELDS.has(key)) {
1434
- Reflect.set(serialized, key, value);
1435
- }
1436
- }
1437
- } catch {
1438
- }
1439
+ assignOwnErrorFields(serialized, sanitized);
1439
1440
  return serialized;
1440
1441
  }
1441
1442
  function withSemconvException(record, format) {
@@ -1485,6 +1486,9 @@ function resolveNameRedactor(options) {
1485
1486
  if (options.redactStrategy !== "names") {
1486
1487
  return PASS_THROUGH;
1487
1488
  }
1489
+ return buildNameRedactor(options);
1490
+ }
1491
+ function buildNameRedactor(options) {
1488
1492
  const names = [
1489
1493
  ...options.shouldDisableDefaultRedact ? [] : REDACT_COMMON_FIELDS,
1490
1494
  ...options.redactPaths.flatMap((path) => leafNameOf(path) ?? [])
@@ -1503,8 +1507,9 @@ function resolveRedactOption(options) {
1503
1507
  }
1504
1508
  function buildPinoInstance(options, logContext, destinations, health) {
1505
1509
  const maxBytes = options.maxEntrySizeBytes;
1506
- const redact = resolveNameRedactor(options);
1507
- const bound = (serializer) => createSizeBoundedSerializer((input) => redact(serializer(input)), maxBytes);
1510
+ const redactRecord = resolveNameRedactor(options);
1511
+ const redactSerialized = buildNameRedactor(options);
1512
+ const bound = (serializer) => createSizeBoundedSerializer((input) => redactSerialized(serializer(input)), maxBytes);
1508
1513
  const serializers = {
1509
1514
  err: bound(serializeErrorValue),
1510
1515
  ...Object.fromEntries(
@@ -1541,14 +1546,17 @@ function buildPinoInstance(options, logContext, destinations, health) {
1541
1546
  // consumer's extra metadata instead of silently dropping it. Runs ONCE, at
1542
1547
  // logger construction — no per-entry cost. `true`: bindings are a record
1543
1548
  // root, iterated rather than serialized.
1544
- bindings: (bindings) => redact(bindings, true),
1549
+ bindings: (bindings) => redactRecord(bindings, true),
1545
1550
  // The name-walk redactor runs here, on the merged record (mixin output +
1546
1551
  // the caller's object), which is exactly the surface a caller controls.
1547
1552
  // `base` and child bindings are NOT visible at this hook — they are
1548
1553
  // library-owned (`service`, the `@InjectLogger` context) and carry nothing
1549
1554
  // sensitive, which `pino-factory.spec.ts` pins.
1550
1555
  log: (record) => withSemconvException(
1551
- withEventName(redact(record, true), options.eventNameField),
1556
+ withEventName(
1557
+ redactRecord(record, true),
1558
+ options.eventNameField
1559
+ ),
1552
1560
  options.errorFormat
1553
1561
  )
1554
1562
  },
@@ -899,7 +899,7 @@ declare class DefaultStdoutDestination implements ILogDestination {
899
899
  * // One line per entry, with the fields a project repeats on every line hidden.
900
900
  * const view: PrettyViewOptions = {
901
901
  * singleLine: true,
902
- * ignore: 'pid,hostname,service,deployment,event.name'
902
+ * ignore: 'pid,hostname,service,deployment,event\\.name'
903
903
  * }
904
904
  */
905
905
  interface PrettyViewOptions {
@@ -914,6 +914,21 @@ interface PrettyViewOptions {
914
914
  * what a real sink receives is untouched. Extend rather than replace when the
915
915
  * goal is quieter output — the default already hides the noisiest fields.
916
916
  * Default: `'pid,hostname,service'`.
917
+ *
918
+ * **A dot means "nested path", so a field name CONTAINING a dot has to escape
919
+ * it.** This library emits `event.name` as a literal top-level key, so the
920
+ * obvious spelling silently hides nothing — `pino-pretty` looks for an `event`
921
+ * object with a `name` inside and finds neither. Measured on one entry, only
922
+ * the option changing:
923
+ *
924
+ * ```
925
+ * ignore: '…,event.name' → INFO: msg {"logKey":"X","event.name":"x"} ← still there
926
+ * ignore: '…,event\\.name' → INFO: msg {"logKey":"X"} ← hidden
927
+ * ```
928
+ *
929
+ * In a TypeScript string literal that is `'event\\.name'` — one real backslash
930
+ * reaching `pino-pretty`. Nested objects (`service`, `deployment`) need no
931
+ * escape; they really are paths.
917
932
  */
918
933
  ignore?: string;
919
934
  /**
@@ -949,7 +964,7 @@ interface PrettyViewOptions {
949
964
  *
950
965
  * // One line per entry, with this project's repeated fields hidden.
951
966
  * new PrettyDevDestination({
952
- * view: { singleLine: true, ignore: 'pid,hostname,service,deployment,event.name' }
967
+ * view: { singleLine: true, ignore: 'pid,hostname,service,deployment,event\\.name' }
953
968
  * })
954
969
  *
955
970
  * // Message-only, with the context pulled back into the line.
@@ -899,7 +899,7 @@ declare class DefaultStdoutDestination implements ILogDestination {
899
899
  * // One line per entry, with the fields a project repeats on every line hidden.
900
900
  * const view: PrettyViewOptions = {
901
901
  * singleLine: true,
902
- * ignore: 'pid,hostname,service,deployment,event.name'
902
+ * ignore: 'pid,hostname,service,deployment,event\\.name'
903
903
  * }
904
904
  */
905
905
  interface PrettyViewOptions {
@@ -914,6 +914,21 @@ interface PrettyViewOptions {
914
914
  * what a real sink receives is untouched. Extend rather than replace when the
915
915
  * goal is quieter output — the default already hides the noisiest fields.
916
916
  * Default: `'pid,hostname,service'`.
917
+ *
918
+ * **A dot means "nested path", so a field name CONTAINING a dot has to escape
919
+ * it.** This library emits `event.name` as a literal top-level key, so the
920
+ * obvious spelling silently hides nothing — `pino-pretty` looks for an `event`
921
+ * object with a `name` inside and finds neither. Measured on one entry, only
922
+ * the option changing:
923
+ *
924
+ * ```
925
+ * ignore: '…,event.name' → INFO: msg {"logKey":"X","event.name":"x"} ← still there
926
+ * ignore: '…,event\\.name' → INFO: msg {"logKey":"X"} ← hidden
927
+ * ```
928
+ *
929
+ * In a TypeScript string literal that is `'event\\.name'` — one real backslash
930
+ * reaching `pino-pretty`. Nested objects (`service`, `deployment`) need no
931
+ * escape; they really are paths.
917
932
  */
918
933
  ignore?: string;
919
934
  /**
@@ -949,7 +964,7 @@ interface PrettyViewOptions {
949
964
  *
950
965
  * // One line per entry, with this project's repeated fields hidden.
951
966
  * new PrettyDevDestination({
952
- * view: { singleLine: true, ignore: 'pid,hostname,service,deployment,event.name' }
967
+ * view: { singleLine: true, ignore: 'pid,hostname,service,deployment,event\\.name' }
953
968
  * })
954
969
  *
955
970
  * // Message-only, with the context pulled back into the line.
@@ -229,6 +229,13 @@ function escapeControlCharacters(text) {
229
229
  var DEFAULT_MAX_CAUSE_DEPTH = 3;
230
230
  var MAX_AGGREGATE_ERRORS = 10;
231
231
  var CIRCULAR_SENTINEL = "[Circular]";
232
+ var DERIVED_ERROR_FIELDS = /* @__PURE__ */ new Set([
233
+ "name",
234
+ "message",
235
+ "stack",
236
+ "cause",
237
+ "errors"
238
+ ]);
232
239
  function sanitizeError(err, options) {
233
240
  const maxCauseDepth = DEFAULT_MAX_CAUSE_DEPTH;
234
241
  return toSanitizedError(err, /* @__PURE__ */ new WeakSet(), 0, maxCauseDepth);
@@ -264,11 +271,23 @@ function toSanitizedError(value, seen, depth2, maxDepth) {
264
271
  result.errors.push(createWidthMarker(aggregated.length - MAX_AGGREGATE_ERRORS));
265
272
  }
266
273
  }
274
+ assignOwnErrorFields(result, value);
267
275
  return result;
268
276
  } catch {
269
277
  return { name: "SanitizeFailed", message: "Failed to sanitize the thrown value" };
270
278
  }
271
279
  }
280
+ function assignOwnErrorFields(target, source) {
281
+ try {
282
+ const own = Array.isArray(source) ? {} : source;
283
+ for (const [key, value] of Object.entries(own)) {
284
+ if (!Object.hasOwn(target, key) && !DERIVED_ERROR_FIELDS.has(key)) {
285
+ Object.defineProperty(target, key, { value, enumerable: true });
286
+ }
287
+ }
288
+ } catch {
289
+ }
290
+ }
272
291
  function toSanitizedChild(value, seen, depth2, maxDepth) {
273
292
  if (isObject(value) && seen.has(value)) {
274
293
  return CIRCULAR_SENTINEL;
@@ -1398,13 +1417,6 @@ function leafNameOf(path2) {
1398
1417
  }
1399
1418
  return leaf;
1400
1419
  }
1401
- var DERIVED_ERROR_FIELDS = /* @__PURE__ */ new Set([
1402
- "name",
1403
- "message",
1404
- "stack",
1405
- "cause",
1406
- "errors"
1407
- ]);
1408
1420
  function serializeErrorValue(value_) {
1409
1421
  const sanitized = sanitizeError(value_);
1410
1422
  const serialized = {
@@ -1421,18 +1433,7 @@ function serializeErrorValue(value_) {
1421
1433
  if (sanitized.errors !== void 0) {
1422
1434
  serialized["errors"] = sanitized.errors;
1423
1435
  }
1424
- try {
1425
- const source = (
1426
- // Stryker disable next-line ConditionalExpression: equivalent BY CONSTRUCTION, not untested — the never-throw catch below makes the mutant observationally identical: with the null check flipped to true, `Object.entries(null)` throws, the catch swallows it, and the emitted envelope is byte-for-byte the same. The guard stays because routing the ordinary null case through exception control flow would trade an explicit branch for a hidden one.
1427
- typeof value_ === "object" && value_ !== null && !Array.isArray(value_) ? value_ : {}
1428
- );
1429
- for (const [key, value] of Object.entries(source)) {
1430
- if (!Object.hasOwn(serialized, key) && !DERIVED_ERROR_FIELDS.has(key)) {
1431
- Reflect.set(serialized, key, value);
1432
- }
1433
- }
1434
- } catch {
1435
- }
1436
+ assignOwnErrorFields(serialized, sanitized);
1436
1437
  return serialized;
1437
1438
  }
1438
1439
  function withSemconvException(record, format) {
@@ -1482,6 +1483,9 @@ function resolveNameRedactor(options) {
1482
1483
  if (options.redactStrategy !== "names") {
1483
1484
  return PASS_THROUGH;
1484
1485
  }
1486
+ return buildNameRedactor(options);
1487
+ }
1488
+ function buildNameRedactor(options) {
1485
1489
  const names = [
1486
1490
  ...options.shouldDisableDefaultRedact ? [] : REDACT_COMMON_FIELDS,
1487
1491
  ...options.redactPaths.flatMap((path2) => leafNameOf(path2) ?? [])
@@ -1500,8 +1504,9 @@ function resolveRedactOption(options) {
1500
1504
  }
1501
1505
  function buildPinoInstance(options, logContext, destinations, health) {
1502
1506
  const maxBytes = options.maxEntrySizeBytes;
1503
- const redact = resolveNameRedactor(options);
1504
- const bound = (serializer) => createSizeBoundedSerializer((input) => redact(serializer(input)), maxBytes);
1507
+ const redactRecord = resolveNameRedactor(options);
1508
+ const redactSerialized = buildNameRedactor(options);
1509
+ const bound = (serializer) => createSizeBoundedSerializer((input) => redactSerialized(serializer(input)), maxBytes);
1505
1510
  const serializers = {
1506
1511
  err: bound(serializeErrorValue),
1507
1512
  ...Object.fromEntries(
@@ -1538,14 +1543,17 @@ function buildPinoInstance(options, logContext, destinations, health) {
1538
1543
  // consumer's extra metadata instead of silently dropping it. Runs ONCE, at
1539
1544
  // logger construction — no per-entry cost. `true`: bindings are a record
1540
1545
  // root, iterated rather than serialized.
1541
- bindings: (bindings) => redact(bindings, true),
1546
+ bindings: (bindings) => redactRecord(bindings, true),
1542
1547
  // The name-walk redactor runs here, on the merged record (mixin output +
1543
1548
  // the caller's object), which is exactly the surface a caller controls.
1544
1549
  // `base` and child bindings are NOT visible at this hook — they are
1545
1550
  // library-owned (`service`, the `@InjectLogger` context) and carry nothing
1546
1551
  // sensitive, which `pino-factory.spec.ts` pins.
1547
1552
  log: (record) => withSemconvException(
1548
- withEventName(redact(record, true), options.eventNameField),
1553
+ withEventName(
1554
+ redactRecord(record, true),
1555
+ options.eventNameField
1556
+ ),
1549
1557
  options.errorFormat
1550
1558
  )
1551
1559
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bymax-one/nest-logger",
3
- "version": "1.2.6",
3
+ "version": "1.2.7",
4
4
  "description": "Structured JSON logging for NestJS based on Pino 10, with optional OpenTelemetry correlation.",
5
5
  "author": "Bymax One <support@bymax.one>",
6
6
  "license": "MIT",