@bymax-one/nest-logger 1.2.9 → 1.3.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.
- package/CHANGELOG.md +57 -1
- package/README.md +22 -0
- package/dist/server/index.cjs +6 -8
- package/dist/server/index.d.cts +5 -3
- package/dist/server/index.d.ts +5 -3
- package/dist/server/index.mjs +6 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,52 @@ heading here.
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [1.3.0] - 2026-08-18
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **A frozen `Error` lost every field, including its own message.** A consumer that freezes its
|
|
19
|
+
errors — deliberate and documented practice, so nobody mutates an issue list after the throw —
|
|
20
|
+
hands over properties that are neither writable nor configurable. The redactor's clone inherited
|
|
21
|
+
those descriptors, and rewriting any key whose walked value is a fresh object (a censored secret,
|
|
22
|
+
or simply the structural copy the walk makes of a nested array) could not redefine it. That
|
|
23
|
+
failure reached the fail-closed guard and dropped the **whole** record, so the entry became
|
|
24
|
+
`_redactionFailed: true` and nothing else — total silent loss on the error that crashed the
|
|
25
|
+
process, which is the one entry worth logging.
|
|
26
|
+
|
|
27
|
+
A frozen error carrying only scalars never showed it, because redefining a property with the same
|
|
28
|
+
primitive succeeds; it took a nested object to expose it. Reported against 1.2.7 by a consumer and
|
|
29
|
+
reproduced unchanged on 1.2.9.
|
|
30
|
+
|
|
31
|
+
The clone is transient — built, serialized, discarded — so it is now built with relaxed
|
|
32
|
+
descriptors and the write always lands. **The caller's error is never touched**, and freezing
|
|
33
|
+
remains the caller's guarantee about the caller's own object.
|
|
34
|
+
|
|
35
|
+
A non-configurable secret is now **censored** rather than dropped: same protection, without
|
|
36
|
+
losing the surrounding fields. The frozen error also keeps its **stack** again — V8 exposes it as
|
|
37
|
+
an own property bound to the original's internal state, so the clone pins a resolved copy, and on
|
|
38
|
+
a frozen error that pin was failing silently too. The case that did NOT drop the record was
|
|
39
|
+
therefore already reaching the sink with its trace erased, which is the same defect wearing a
|
|
40
|
+
healthy face; a consumer measured that independently. `LOGGER_REDACTION_FAILED` is unchanged for the case it was written
|
|
41
|
+
for — a record the walk cannot READ, such as a throwing getter or a hostile proxy.
|
|
42
|
+
|
|
43
|
+
### Documentation
|
|
44
|
+
|
|
45
|
+
- **The limit of field-based redaction now ships.** A value interpolated into the message has no
|
|
46
|
+
field name for any strategy to match — not an uncovered one, none — so no configuration closes
|
|
47
|
+
that gap and the rule is "a value that matters is a field, not text". It was stated only in
|
|
48
|
+
`docs/OBSERVABILITY-CONTRACT.md`, which is not in `files`: it reached whoever had the repository
|
|
49
|
+
and nobody who installs from npm. The decision it governs is made at a call site in a derived
|
|
50
|
+
backend whose only copy of this contract is what the registry delivers, so it is in the README's
|
|
51
|
+
security model now, beside the case it sharpens — that one covers a secret inside a field's
|
|
52
|
+
**value**, this one a value with no field at all.
|
|
53
|
+
|
|
54
|
+
- **The `minLevel` entry below, under 1.2.9, now names the direction of its change.** The behaviour
|
|
55
|
+
changed in that release and its record stays there; what shipped here is the paragraph explaining
|
|
56
|
+
what upgrading does. A sink configured with an unrecognised level was silent and starts delivering
|
|
57
|
+
at the module level, which arrives as a step change in ingestion cost and reads as a noise
|
|
58
|
+
regression unless the note says which way it moves. Two consumers asked for this independently.
|
|
59
|
+
|
|
14
60
|
## [1.2.9] - 2026-08-16
|
|
15
61
|
|
|
16
62
|
> **`1.2.8` was merged but never published.** The defect below was found in it after the merge and
|
|
@@ -33,6 +79,15 @@ heading here.
|
|
|
33
79
|
`validateOptions` already held `options.level` to that same list; this is the check reaching
|
|
34
80
|
the one place it had not.
|
|
35
81
|
|
|
82
|
+
**On upgrade this changes your log volume, upward, and it will not look like a fix.** A sink
|
|
83
|
+
that was configured with an unrecognised level and was therefore silent starts delivering at
|
|
84
|
+
the module level the moment you install this version. Read as a graph, that is a step change in
|
|
85
|
+
ingestion cost arriving with a patch; read correctly, it is entries that were always supposed
|
|
86
|
+
to be delivered and never were. If you see one, check that destination's `minLevel` before
|
|
87
|
+
treating it as a regression — the value has to be one of `'trace'`, `'debug'`, `'info'`,
|
|
88
|
+
`'warn'`, `'error'` or `'fatal'`. `'verbose'` and `'log'` are NestJS level names, not Pino
|
|
89
|
+
ones, and are the likely culprits for anyone arriving from `LoggerService`.
|
|
90
|
+
|
|
36
91
|
- **A write returning a thenable that is not `instanceof Promise` no longer takes the synchronous
|
|
37
92
|
path, where its rejection escaped and its entry could be discarded.** `instanceof` is realm-local:
|
|
38
93
|
it answers `false` for a promise built in another realm — a worker, a `vm` context — and for any
|
|
@@ -1739,7 +1794,8 @@ published `dist/` is identical — no runtime behaviour changes for consumers.
|
|
|
1739
1794
|
- Professional CI suite: `ci.yml`, `bench.yml`, `codeql.yml`, `scorecard.yml`,
|
|
1740
1795
|
`release.yml`, Dependabot, and issue templates
|
|
1741
1796
|
|
|
1742
|
-
[Unreleased]: https://github.com/bymaxone/nest-logger/compare/v1.
|
|
1797
|
+
[Unreleased]: https://github.com/bymaxone/nest-logger/compare/v1.3.0...HEAD
|
|
1798
|
+
[1.3.0]: https://github.com/bymaxone/nest-logger/compare/v1.2.9...v1.3.0
|
|
1743
1799
|
[1.2.9]: https://github.com/bymaxone/nest-logger/compare/v1.2.7...v1.2.9
|
|
1744
1800
|
|
|
1745
1801
|
<!-- 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.
|
package/dist/server/index.cjs
CHANGED
|
@@ -1224,10 +1224,11 @@ function resolveToJson(value) {
|
|
|
1224
1224
|
};
|
|
1225
1225
|
}
|
|
1226
1226
|
function cloneError(error) {
|
|
1227
|
-
const
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1227
|
+
const descriptors = Object.getOwnPropertyDescriptors(error);
|
|
1228
|
+
for (const descriptor of Object.values(descriptors)) {
|
|
1229
|
+
descriptor.configurable = true;
|
|
1230
|
+
}
|
|
1231
|
+
const clone = Object.create(Object.getPrototypeOf(error), descriptors);
|
|
1231
1232
|
const { stack } = error;
|
|
1232
1233
|
if (typeof stack === "string") {
|
|
1233
1234
|
Reflect.defineProperty(clone, "stack", { value: stack, enumerable: false });
|
|
@@ -1235,11 +1236,8 @@ function cloneError(error) {
|
|
|
1235
1236
|
return clone;
|
|
1236
1237
|
}
|
|
1237
1238
|
var BUFFER_TO_JSON = Buffer.prototype.toJSON;
|
|
1238
|
-
var WRITE_FAILED = new Error();
|
|
1239
1239
|
function defineOwn(target, key, value) {
|
|
1240
|
-
|
|
1241
|
-
throw WRITE_FAILED;
|
|
1242
|
-
}
|
|
1240
|
+
Reflect.defineProperty(target, key, { value, enumerable: true });
|
|
1243
1241
|
}
|
|
1244
1242
|
function walkArray(value, sensitive, censor, depth2, ancestors) {
|
|
1245
1243
|
const length = value.length;
|
package/dist/server/index.d.cts
CHANGED
|
@@ -639,9 +639,11 @@ type ReservedLogKey = (typeof RESERVED_LOG_KEYS)[keyof typeof RESERVED_LOG_KEYS]
|
|
|
639
639
|
* costs ~26 % of the entry throughput, measured, and the walk is still ~36x
|
|
640
640
|
* faster than the path expansion it replaced.
|
|
641
641
|
*
|
|
642
|
-
* Any internal failure —
|
|
643
|
-
* whole record to a marked, secret-free envelope rather
|
|
644
|
-
* that produced it.
|
|
642
|
+
* Any internal failure — a throwing getter, a hostile proxy, a value the walk
|
|
643
|
+
* cannot READ — degrades the whole record to a marked, secret-free envelope rather
|
|
644
|
+
* than crashing the request that produced it. Writing the censor is not among those
|
|
645
|
+
* failures: every copy is built so the write cannot fail, which is why a frozen
|
|
646
|
+
* error keeps its fields instead of costing the record.
|
|
645
647
|
*
|
|
646
648
|
* @see {@link createNameRedactor}
|
|
647
649
|
*/
|
package/dist/server/index.d.ts
CHANGED
|
@@ -639,9 +639,11 @@ type ReservedLogKey = (typeof RESERVED_LOG_KEYS)[keyof typeof RESERVED_LOG_KEYS]
|
|
|
639
639
|
* costs ~26 % of the entry throughput, measured, and the walk is still ~36x
|
|
640
640
|
* faster than the path expansion it replaced.
|
|
641
641
|
*
|
|
642
|
-
* Any internal failure —
|
|
643
|
-
* whole record to a marked, secret-free envelope rather
|
|
644
|
-
* that produced it.
|
|
642
|
+
* Any internal failure — a throwing getter, a hostile proxy, a value the walk
|
|
643
|
+
* cannot READ — degrades the whole record to a marked, secret-free envelope rather
|
|
644
|
+
* than crashing the request that produced it. Writing the censor is not among those
|
|
645
|
+
* failures: every copy is built so the write cannot fail, which is why a frozen
|
|
646
|
+
* error keeps its fields instead of costing the record.
|
|
645
647
|
*
|
|
646
648
|
* @see {@link createNameRedactor}
|
|
647
649
|
*/
|
package/dist/server/index.mjs
CHANGED
|
@@ -1221,10 +1221,11 @@ function resolveToJson(value) {
|
|
|
1221
1221
|
};
|
|
1222
1222
|
}
|
|
1223
1223
|
function cloneError(error) {
|
|
1224
|
-
const
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1224
|
+
const descriptors = Object.getOwnPropertyDescriptors(error);
|
|
1225
|
+
for (const descriptor of Object.values(descriptors)) {
|
|
1226
|
+
descriptor.configurable = true;
|
|
1227
|
+
}
|
|
1228
|
+
const clone = Object.create(Object.getPrototypeOf(error), descriptors);
|
|
1228
1229
|
const { stack } = error;
|
|
1229
1230
|
if (typeof stack === "string") {
|
|
1230
1231
|
Reflect.defineProperty(clone, "stack", { value: stack, enumerable: false });
|
|
@@ -1232,11 +1233,8 @@ function cloneError(error) {
|
|
|
1232
1233
|
return clone;
|
|
1233
1234
|
}
|
|
1234
1235
|
var BUFFER_TO_JSON = Buffer.prototype.toJSON;
|
|
1235
|
-
var WRITE_FAILED = new Error();
|
|
1236
1236
|
function defineOwn(target, key, value) {
|
|
1237
|
-
|
|
1238
|
-
throw WRITE_FAILED;
|
|
1239
|
-
}
|
|
1237
|
+
Reflect.defineProperty(target, key, { value, enumerable: true });
|
|
1240
1238
|
}
|
|
1241
1239
|
function walkArray(value, sensitive, censor, depth2, ancestors) {
|
|
1242
1240
|
const length = value.length;
|
package/package.json
CHANGED