@bymax-one/nest-logger 1.2.9 → 1.3.1

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,80 @@ heading here.
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [1.3.1] - 2026-08-18
15
+
16
+ ### Documentation
17
+
18
+ - **`excludePaths` says what its defaults do not reach.** The defaults are
19
+ `[/^\/health$/, /^\/metrics$/]`, and `@bymax-one/nest-core` serves liveness and readiness at
20
+ `/health/live` and `/health/ready` with no bare `/health` at all — so the default excludes a
21
+ route that does not exist while logging both probes. A consumer running both measured **8272 of
22
+ 8274** HTTP entries as the liveness probe, at one container healthcheck every ten seconds; at
23
+ orchestrator volume it is the same shape with more zeros. There is no error and no warning, and
24
+ the config reads as though it were handled.
25
+
26
+ The defaults are **not** widened, and the reason is checkable rather than a preference: both
27
+ prefixes are configurable there (`DEFAULT_HEALTH_PATH`, `DEFAULT_METRICS_PATH`). A default
28
+ naming specific subpaths would be less wrong rather than correct, and a prefix pattern would
29
+ silently swallow a consumer's own `/health/*` route they did want logged. Excluding a path
30
+ decides which requests vanish from the record, so it stays explicit — the docblock now carries
31
+ the one-line fix for the stock layout, and it ships in the `.d.ts` where someone wiring the two
32
+ libraries reads it.
33
+
34
+ ### Internal
35
+
36
+ - **The last surviving mutant is gone, by deletion rather than annotation.** A `catch` in
37
+ `report-destination-failure` re-assigned `undefined` to a variable already `undefined` from its
38
+ declaration, so emptying the block changed no output for any input and no test could kill it. A
39
+ Stryker directive was tried first and was not honoured at that position. The project scores
40
+ **100.00** on a cold run: 1515 mutants, 841 killed, zero survivors, 145 documented equivalents.
41
+
42
+ ## [1.3.0] - 2026-08-18
43
+
44
+ ### Fixed
45
+
46
+ - **A frozen `Error` lost every field, including its own message.** A consumer that freezes its
47
+ errors — deliberate and documented practice, so nobody mutates an issue list after the throw —
48
+ hands over properties that are neither writable nor configurable. The redactor's clone inherited
49
+ those descriptors, and rewriting any key whose walked value is a fresh object (a censored secret,
50
+ or simply the structural copy the walk makes of a nested array) could not redefine it. That
51
+ failure reached the fail-closed guard and dropped the **whole** record, so the entry became
52
+ `_redactionFailed: true` and nothing else — total silent loss on the error that crashed the
53
+ process, which is the one entry worth logging.
54
+
55
+ A frozen error carrying only scalars never showed it, because redefining a property with the same
56
+ primitive succeeds; it took a nested object to expose it. Reported against 1.2.7 by a consumer and
57
+ reproduced unchanged on 1.2.9.
58
+
59
+ The clone is transient — built, serialized, discarded — so it is now built with relaxed
60
+ descriptors and the write always lands. **The caller's error is never touched**, and freezing
61
+ remains the caller's guarantee about the caller's own object.
62
+
63
+ A non-configurable secret is now **censored** rather than dropped: same protection, without
64
+ losing the surrounding fields. The frozen error also keeps its **stack** again — V8 exposes it as
65
+ an own property bound to the original's internal state, so the clone pins a resolved copy, and on
66
+ a frozen error that pin was failing silently too. The case that did NOT drop the record was
67
+ therefore already reaching the sink with its trace erased, which is the same defect wearing a
68
+ healthy face; a consumer measured that independently. `LOGGER_REDACTION_FAILED` is unchanged for the case it was written
69
+ for — a record the walk cannot READ, such as a throwing getter or a hostile proxy.
70
+
71
+ ### Documentation
72
+
73
+ - **The limit of field-based redaction now ships.** A value interpolated into the message has no
74
+ field name for any strategy to match — not an uncovered one, none — so no configuration closes
75
+ that gap and the rule is "a value that matters is a field, not text". It was stated only in
76
+ `docs/OBSERVABILITY-CONTRACT.md`, which is not in `files`: it reached whoever had the repository
77
+ and nobody who installs from npm. The decision it governs is made at a call site in a derived
78
+ backend whose only copy of this contract is what the registry delivers, so it is in the README's
79
+ security model now, beside the case it sharpens — that one covers a secret inside a field's
80
+ **value**, this one a value with no field at all.
81
+
82
+ - **The `minLevel` entry below, under 1.2.9, now names the direction of its change.** The behaviour
83
+ changed in that release and its record stays there; what shipped here is the paragraph explaining
84
+ what upgrading does. A sink configured with an unrecognised level was silent and starts delivering
85
+ at the module level, which arrives as a step change in ingestion cost and reads as a noise
86
+ regression unless the note says which way it moves. Two consumers asked for this independently.
87
+
14
88
  ## [1.2.9] - 2026-08-16
15
89
 
16
90
  > **`1.2.8` was merged but never published.** The defect below was found in it after the merge and
@@ -33,6 +107,15 @@ heading here.
33
107
  `validateOptions` already held `options.level` to that same list; this is the check reaching
34
108
  the one place it had not.
35
109
 
110
+ **On upgrade this changes your log volume, upward, and it will not look like a fix.** A sink
111
+ that was configured with an unrecognised level and was therefore silent starts delivering at
112
+ the module level the moment you install this version. Read as a graph, that is a step change in
113
+ ingestion cost arriving with a patch; read correctly, it is entries that were always supposed
114
+ to be delivered and never were. If you see one, check that destination's `minLevel` before
115
+ treating it as a regression — the value has to be one of `'trace'`, `'debug'`, `'info'`,
116
+ `'warn'`, `'error'` or `'fatal'`. `'verbose'` and `'log'` are NestJS level names, not Pino
117
+ ones, and are the likely culprits for anyone arriving from `LoggerService`.
118
+
36
119
  - **A write returning a thenable that is not `instanceof Promise` no longer takes the synchronous
37
120
  path, where its rejection escaped and its entry could be discarded.** `instanceof` is realm-local:
38
121
  it answers `false` for a promise built in another realm — a worker, a `vm` context — and for any
@@ -1739,7 +1822,9 @@ published `dist/` is identical — no runtime behaviour changes for consumers.
1739
1822
  - Professional CI suite: `ci.yml`, `bench.yml`, `codeql.yml`, `scorecard.yml`,
1740
1823
  `release.yml`, Dependabot, and issue templates
1741
1824
 
1742
- [Unreleased]: https://github.com/bymaxone/nest-logger/compare/v1.2.9...HEAD
1825
+ [Unreleased]: https://github.com/bymaxone/nest-logger/compare/v1.3.1...HEAD
1826
+ [1.3.1]: https://github.com/bymaxone/nest-logger/compare/v1.3.0...v1.3.1
1827
+ [1.3.0]: https://github.com/bymaxone/nest-logger/compare/v1.2.9...v1.3.0
1743
1828
  [1.2.9]: https://github.com/bymaxone/nest-logger/compare/v1.2.7...v1.2.9
1744
1829
 
1745
1830
  <!-- 1.2.8 has no link: it was merged but never tagged, so there is no v1.2.8 to compare against. See its section above. -->
package/README.md CHANGED
@@ -1070,6 +1070,28 @@ There is no name rule that reaches the second one, and scanning inside values fo
1070
1070
 
1071
1071
  `redactStrategy` governs **your** payload — the record you log, your child bindings, and the output of any serializer you supply. The built-in `err` serializer is the library's own output and is always name-walked, including under `'paths'`, because `fast-redact`'s four-wildcard ceiling cannot reach the depth a `cause` chain serializes to.
1072
1072
 
1073
+ #### A value interpolated into the message has no name to redact
1074
+
1075
+ The case above is a secret inside a field's **value**. The sharper version is a value interpolated into the message itself, where there is no field at all:
1076
+
1077
+ ```ts
1078
+ // Both identifiers are now message text: there is no `userId` field and no
1079
+ // `familyId` field for any strategy to match, only a string that happens to
1080
+ // contain them.
1081
+ logger.warn(`reissueTokens: family revoked userId=${userId} familyId=${familyId}`)
1082
+
1083
+ // Same facts, as fields. The logKey is yours — `RESERVED_LOG_KEYS` holds the
1084
+ // library's own keys, not your domain's — and follows MODULE_ACTION_RESULT:
1085
+ // module AUTH, action REUSE, result DETECTED.
1086
+ logger.warnStructured('AUTH_REUSE_DETECTED', 'token family revoked after reuse detection', userId, {
1087
+ familyId
1088
+ })
1089
+ ```
1090
+
1091
+ Redaction matches names. An interpolated value does not have one — not an uncovered name, none — so there is nothing for any strategy to match, and no configuration you can add will change that. This is not a gap to be closed later: scanning message text for secret-shaped substrings would mean rewriting what you asked to be logged, which is the wrong trade in both directions.
1092
+
1093
+ The rule that follows is short: **a value that matters is a field, not text.** A field can be redacted, queried, aggregated and correlated; the same value inside `msg` can only be read by a human who happens to be looking. That applies most to the values you would least like to interpolate — tokens, identifiers, addresses — and it is the one part of this contract the library cannot enforce for you, because the decision is made at the call site.
1094
+
1073
1095
  ### A closed pipe does not kill the process
1074
1096
 
1075
1097
  `node app | head` closes the read end of stdout while the application is still writing. The stream reports that as `EPIPE` **asynchronously**, through its `'error'` event, after `write()` has already returned — so a `try/catch` around the write catches nothing, and because Node attaches no default handler to `process.stdout`, the emit becomes an uncaught exception and the process dies. Measured, not assumed: `0` listeners, no synchronous throw, `UNCAUGHT:EPIPE`, exit 42.
@@ -1137,7 +1137,6 @@ function safeMinLevel(destination) {
1137
1137
  const read = destination.minLevel;
1138
1138
  resolved = isLogLevel(read) ? read : void 0;
1139
1139
  } catch {
1140
- resolved = void 0;
1141
1140
  }
1142
1141
  resolvedMinLevels.set(destination, resolved);
1143
1142
  return resolved;
@@ -1224,10 +1223,11 @@ function resolveToJson(value) {
1224
1223
  };
1225
1224
  }
1226
1225
  function cloneError(error) {
1227
- const clone = Object.create(
1228
- Object.getPrototypeOf(error),
1229
- Object.getOwnPropertyDescriptors(error)
1230
- );
1226
+ const descriptors = Object.getOwnPropertyDescriptors(error);
1227
+ for (const descriptor of Object.values(descriptors)) {
1228
+ descriptor.configurable = true;
1229
+ }
1230
+ const clone = Object.create(Object.getPrototypeOf(error), descriptors);
1231
1231
  const { stack } = error;
1232
1232
  if (typeof stack === "string") {
1233
1233
  Reflect.defineProperty(clone, "stack", { value: stack, enumerable: false });
@@ -1235,11 +1235,8 @@ function cloneError(error) {
1235
1235
  return clone;
1236
1236
  }
1237
1237
  var BUFFER_TO_JSON = Buffer.prototype.toJSON;
1238
- var WRITE_FAILED = new Error();
1239
1238
  function defineOwn(target, key, value) {
1240
- if (!Reflect.defineProperty(target, key, { value, enumerable: true })) {
1241
- throw WRITE_FAILED;
1242
- }
1239
+ Reflect.defineProperty(target, key, { value, enumerable: true });
1243
1240
  }
1244
1241
  function walkArray(value, sensitive, censor, depth2, ancestors) {
1245
1242
  const length = value.length;
@@ -260,6 +260,21 @@ interface HttpOptions {
260
260
  * Paths that bypass HTTP logging (health checks, metrics). Defaults:
261
261
  * `/health`, `/metrics`.
262
262
  *
263
+ * The defaults do NOT cover `@bymax-one/nest-core`, and that is the common case
264
+ * worth knowing before you rely on them. It serves liveness and readiness at
265
+ * `/health/live` and `/health/ready`, and no bare `/health` at all, so
266
+ * `/^\/health$/` excludes a route that does not exist while both probes are
267
+ * logged. Measured on one deployment: 8272 of 8274 HTTP entries were the
268
+ * liveness probe. Set `excludePaths` yourself when wiring the two together —
269
+ * `[/^\/health\/(live|ready)$/, /^\/metrics$/]` matches the stock layout.
270
+ *
271
+ * The defaults are not widened to include those subpaths because both prefixes
272
+ * are CONFIGURABLE there (`DEFAULT_HEALTH_PATH`, `DEFAULT_METRICS_PATH`): any
273
+ * default naming specific subpaths would be less wrong rather than correct, and
274
+ * a prefix pattern would silently swallow a consumer's own `/health/*` route
275
+ * that they did want logged. Excluding a path is a decision about which requests
276
+ * disappear from the record, so it stays explicit.
277
+ *
263
278
  * SECURITY: each pattern is `.test()`-ed against the (attacker-controllable)
264
279
  * request path on every request. Supply only anchored, linear-time regexes
265
280
  * (e.g. `/^\/health$/`); a pattern with catastrophic backtracking would turn a
@@ -639,9 +654,11 @@ type ReservedLogKey = (typeof RESERVED_LOG_KEYS)[keyof typeof RESERVED_LOG_KEYS]
639
654
  * costs ~26 % of the entry throughput, measured, and the walk is still ~36x
640
655
  * faster than the path expansion it replaced.
641
656
  *
642
- * Any internal failure — including a censor that cannot be written — degrades the
643
- * whole record to a marked, secret-free envelope rather than crashing the request
644
- * that produced it.
657
+ * Any internal failure — a throwing getter, a hostile proxy, a value the walk
658
+ * cannot READ — degrades the whole record to a marked, secret-free envelope rather
659
+ * than crashing the request that produced it. Writing the censor is not among those
660
+ * failures: every copy is built so the write cannot fail, which is why a frozen
661
+ * error keeps its fields instead of costing the record.
645
662
  *
646
663
  * @see {@link createNameRedactor}
647
664
  */
@@ -260,6 +260,21 @@ interface HttpOptions {
260
260
  * Paths that bypass HTTP logging (health checks, metrics). Defaults:
261
261
  * `/health`, `/metrics`.
262
262
  *
263
+ * The defaults do NOT cover `@bymax-one/nest-core`, and that is the common case
264
+ * worth knowing before you rely on them. It serves liveness and readiness at
265
+ * `/health/live` and `/health/ready`, and no bare `/health` at all, so
266
+ * `/^\/health$/` excludes a route that does not exist while both probes are
267
+ * logged. Measured on one deployment: 8272 of 8274 HTTP entries were the
268
+ * liveness probe. Set `excludePaths` yourself when wiring the two together —
269
+ * `[/^\/health\/(live|ready)$/, /^\/metrics$/]` matches the stock layout.
270
+ *
271
+ * The defaults are not widened to include those subpaths because both prefixes
272
+ * are CONFIGURABLE there (`DEFAULT_HEALTH_PATH`, `DEFAULT_METRICS_PATH`): any
273
+ * default naming specific subpaths would be less wrong rather than correct, and
274
+ * a prefix pattern would silently swallow a consumer's own `/health/*` route
275
+ * that they did want logged. Excluding a path is a decision about which requests
276
+ * disappear from the record, so it stays explicit.
277
+ *
263
278
  * SECURITY: each pattern is `.test()`-ed against the (attacker-controllable)
264
279
  * request path on every request. Supply only anchored, linear-time regexes
265
280
  * (e.g. `/^\/health$/`); a pattern with catastrophic backtracking would turn a
@@ -639,9 +654,11 @@ type ReservedLogKey = (typeof RESERVED_LOG_KEYS)[keyof typeof RESERVED_LOG_KEYS]
639
654
  * costs ~26 % of the entry throughput, measured, and the walk is still ~36x
640
655
  * faster than the path expansion it replaced.
641
656
  *
642
- * Any internal failure — including a censor that cannot be written — degrades the
643
- * whole record to a marked, secret-free envelope rather than crashing the request
644
- * that produced it.
657
+ * Any internal failure — a throwing getter, a hostile proxy, a value the walk
658
+ * cannot READ — degrades the whole record to a marked, secret-free envelope rather
659
+ * than crashing the request that produced it. Writing the censor is not among those
660
+ * failures: every copy is built so the write cannot fail, which is why a frozen
661
+ * error keeps its fields instead of costing the record.
645
662
  *
646
663
  * @see {@link createNameRedactor}
647
664
  */
@@ -1134,7 +1134,6 @@ function safeMinLevel(destination) {
1134
1134
  const read = destination.minLevel;
1135
1135
  resolved = isLogLevel(read) ? read : void 0;
1136
1136
  } catch {
1137
- resolved = void 0;
1138
1137
  }
1139
1138
  resolvedMinLevels.set(destination, resolved);
1140
1139
  return resolved;
@@ -1221,10 +1220,11 @@ function resolveToJson(value) {
1221
1220
  };
1222
1221
  }
1223
1222
  function cloneError(error) {
1224
- const clone = Object.create(
1225
- Object.getPrototypeOf(error),
1226
- Object.getOwnPropertyDescriptors(error)
1227
- );
1223
+ const descriptors = Object.getOwnPropertyDescriptors(error);
1224
+ for (const descriptor of Object.values(descriptors)) {
1225
+ descriptor.configurable = true;
1226
+ }
1227
+ const clone = Object.create(Object.getPrototypeOf(error), descriptors);
1228
1228
  const { stack } = error;
1229
1229
  if (typeof stack === "string") {
1230
1230
  Reflect.defineProperty(clone, "stack", { value: stack, enumerable: false });
@@ -1232,11 +1232,8 @@ function cloneError(error) {
1232
1232
  return clone;
1233
1233
  }
1234
1234
  var BUFFER_TO_JSON = Buffer.prototype.toJSON;
1235
- var WRITE_FAILED = new Error();
1236
1235
  function defineOwn(target, key, value) {
1237
- if (!Reflect.defineProperty(target, key, { value, enumerable: true })) {
1238
- throw WRITE_FAILED;
1239
- }
1236
+ Reflect.defineProperty(target, key, { value, enumerable: true });
1240
1237
  }
1241
1238
  function walkArray(value, sensitive, censor, depth2, ancestors) {
1242
1239
  const length = value.length;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bymax-one/nest-logger",
3
- "version": "1.2.9",
3
+ "version": "1.3.1",
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",