@bymax-one/nest-logger 1.0.6 → 1.0.8
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 +73 -1
- package/README.md +14 -14
- package/dist/server/index.cjs +6 -3
- package/dist/server/index.d.cts +21 -3
- package/dist/server/index.d.ts +21 -3
- package/dist/server/index.mjs +6 -3
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,76 @@ heading here.
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [1.0.8] - 2026-08-10
|
|
15
|
+
|
|
16
|
+
Remediation of a local audit's redaction, attribution and correlation-id findings. No API
|
|
17
|
+
changed; the fixes are in the default redact set, the acting-user resolution and the docs.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- **Root-level metadata is redacted.** The default redact set now lists each sensitive field at
|
|
22
|
+
depth 0 as well as depths 1–4, so a value spread into a log record's own root — as
|
|
23
|
+
`emitStructured` does with caller metadata — is scrubbed. The depth-1-and-deeper wildcards never
|
|
24
|
+
reached the root, so a field named `password` passed as metadata was written in clear.
|
|
25
|
+
- **Acting-user attribution reads the JWT subject first.** The HTTP logging interceptor and the
|
|
26
|
+
exception filter now resolve the acting user as `sub ?? id`, so a JWT principal (every
|
|
27
|
+
`@bymax-one/nest-auth` token) is attributed to its subject rather than an ORM `id` when a
|
|
28
|
+
principal carries both.
|
|
29
|
+
|
|
30
|
+
### Documentation
|
|
31
|
+
|
|
32
|
+
- The advertised default redact-path count is corrected from 113 to **140** (27 fields across
|
|
33
|
+
depths 0–4, plus 5 absolute header paths) across the README and the technical specification.
|
|
34
|
+
- The correlation-id charset comment now describes what the middleware actually does — echo
|
|
35
|
+
`x-request-id` and store `requestId` in the log context — instead of a non-existent
|
|
36
|
+
error-envelope `correlationId` field.
|
|
37
|
+
|
|
38
|
+
## [1.0.7] - 2026-08-07
|
|
39
|
+
|
|
40
|
+
**Documentation and tooling.** `dist/` differs from `1.0.6` only in the text of the comments
|
|
41
|
+
described below; no runtime code changed.
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
|
|
45
|
+
- **Equivalent mutants are documented in the source instead of only in the report, and the
|
|
46
|
+
rule against that was retired on a measurement.** The plan forbade inline
|
|
47
|
+
`// Stryker disable` comments on the ground that they would push the server subpath past
|
|
48
|
+
its 13.5 kB brotli budget. Measured: seven directives took the bundle from **12.84 to
|
|
49
|
+
12.94 kB** — **+0.10 kB** against 0.66 kB of headroom, because brotli compresses their
|
|
50
|
+
repeated prefixes almost for free. The assumed cost was roughly ten times the real one, and
|
|
51
|
+
no budget changed.
|
|
52
|
+
|
|
53
|
+
With the ten survivors now carrying their reason inline, the measured score moves from
|
|
54
|
+
**97.42%** to **100%** — no test and no production logic changed; Stryker excludes an
|
|
55
|
+
ignored mutant from the denominator instead of counting it as one the suite failed to kill.
|
|
56
|
+
|
|
57
|
+
- **One suppression is recorded as what it is, not as an equivalent.** `otel-detector.ts:40`
|
|
58
|
+
is killed by the suite — applying the mutation by hand turns the suite red — but Stryker
|
|
59
|
+
fails to attribute the killing test to it under `perTest` coverage analysis. Its directive
|
|
60
|
+
says exactly that. Calling it equivalent to reach a rounder number would have been false.
|
|
61
|
+
|
|
62
|
+
- Two reasons were corrected after review. `decodeStrings: false` claimed equivalence "for
|
|
63
|
+
every input"; with `true`, Node encodes the string using the write's encoding and this
|
|
64
|
+
stream decodes with `toString('utf-8')`, so a `write(chunk, 'latin1')` would change
|
|
65
|
+
non-ASCII bytes. Nothing writes here but Pino, which emits UTF-8 NDJSON at the default
|
|
66
|
+
encoding, and the reason now says so and names where it stops being true. The
|
|
67
|
+
`{ strict: false }` reason argued from a missing provider; it now gives the actual
|
|
68
|
+
mechanism — `NestApplicationContext.get` branches on `!(options && options.strict)`, so the
|
|
69
|
+
mutant's `{}` leaves `strict` undefined, which is falsy, and takes the same non-strict
|
|
70
|
+
lookup.
|
|
71
|
+
|
|
72
|
+
### Added
|
|
73
|
+
|
|
74
|
+
- `check:mutants` gate (`scripts/check-mutation-directives.mjs`) — validates every
|
|
75
|
+
`// Stryker` comment against the parser's own regular expression, rejecting a reason written
|
|
76
|
+
after `--` instead of a colon, a reason wrapped onto a second comment line, a stray comma in
|
|
77
|
+
the mutator list, and a mutator name Stryker does not know, which matches nothing and so
|
|
78
|
+
silences nothing. Wired into CI and `prepublishOnly`.
|
|
79
|
+
|
|
80
|
+
### Fixed
|
|
81
|
+
|
|
82
|
+
- The `[Unreleased]` compare link still pointed at `v1.0.5` after `1.0.6` shipped.
|
|
83
|
+
|
|
14
84
|
## [1.0.6] - 2026-08-06
|
|
15
85
|
|
|
16
86
|
**Tests and documentation only.** `dist/` is byte-identical to `1.0.5`.
|
|
@@ -270,7 +340,9 @@ published `dist/` is identical — no runtime behaviour changes for consumers.
|
|
|
270
340
|
- Professional CI suite: `ci.yml`, `bench.yml`, `codeql.yml`, `scorecard.yml`,
|
|
271
341
|
`release.yml`, Dependabot, and issue templates
|
|
272
342
|
|
|
273
|
-
[Unreleased]: https://github.com/bymaxone/nest-logger/compare/v1.0.
|
|
343
|
+
[Unreleased]: https://github.com/bymaxone/nest-logger/compare/v1.0.8...HEAD
|
|
344
|
+
[1.0.8]: https://github.com/bymaxone/nest-logger/compare/v1.0.7...v1.0.8
|
|
345
|
+
[1.0.7]: https://github.com/bymaxone/nest-logger/compare/v1.0.6...v1.0.7
|
|
274
346
|
[1.0.6]: https://github.com/bymaxone/nest-logger/compare/v1.0.5...v1.0.6
|
|
275
347
|
[1.0.5]: https://github.com/bymaxone/nest-logger/compare/v1.0.4...v1.0.5
|
|
276
348
|
[1.0.4]: https://github.com/bymaxone/nest-logger/compare/v1.0.3...v1.0.4
|
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
<a href="https://www.npmjs.com/package/@bymax-one/nest-logger"><img src="https://img.shields.io/npm/dm/@bymax-one/nest-logger?style=flat-square&colorA=000000&colorB=000000" alt="npm downloads" /></a>
|
|
15
15
|
<a href="https://github.com/bymaxone/nest-logger/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/bymaxone/nest-logger/ci.yml?branch=main&style=flat-square&colorA=000000&label=CI" alt="CI status" /></a>
|
|
16
16
|
<a href="https://github.com/bymaxone/nest-logger/actions/workflows/ci.yml"><img src="https://img.shields.io/badge/coverage-100%25-brightgreen?style=flat-square&colorA=000000" alt="coverage" /></a>
|
|
17
|
-
<a href="https://github.com/bymaxone/nest-logger/blob/main/docs/mutation_testing_results.md"><img src="https://img.shields.io/badge/mutation-
|
|
17
|
+
<a href="https://github.com/bymaxone/nest-logger/blob/main/docs/mutation_testing_results.md"><img src="https://img.shields.io/badge/mutation-100%25-brightgreen?style=flat-square&colorA=000000" alt="mutation score" /></a>
|
|
18
18
|
<a href="https://scorecard.dev/viewer/?uri=github.com/bymaxone/nest-logger"><img src="https://api.scorecard.dev/projects/github.com/bymaxone/nest-logger/badge?style=flat-square" alt="OpenSSF Scorecard" /></a>
|
|
19
19
|
<a href="https://github.com/bymaxone/nest-logger/blob/main/LICENSE"><img src="https://img.shields.io/github/license/bymaxone/nest-logger?style=flat-square&colorA=000000&colorB=000000" alt="license" /></a>
|
|
20
20
|
<a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-strict-3178C6?style=flat-square&logo=typescript&logoColor=white" alt="TypeScript" /></a>
|
|
@@ -43,7 +43,7 @@ The library has **zero direct dependencies** — all packages arrive as peer dep
|
|
|
43
43
|
|
|
44
44
|
- **🎯 One module, the whole pipeline** — Logger service, HTTP interceptor, exception filter, context propagation, redaction, and destinations arrive in a single `forRoot()`. No gluing together `pino-http`, a redaction layer, and a transport by hand.
|
|
45
45
|
- **🔌 Your sinks, your rules** — The library defines `ILogDestination`. You implement it for Loki, Postgres, a rolling file, or anything else. No vendor lock-in, no hidden transport dependencies.
|
|
46
|
-
- **🔒 Redacted by default** —
|
|
46
|
+
- **🔒 Redacted by default** — 140 redact paths compile at bootstrap covering the standard set: passwords, tokens, PCI DSS card data, MFA secrets, and LGPD documents. Domain-specific field names are yours to add via `redactPaths`.
|
|
47
47
|
- **⚡ On the hot path, so it stays cheap** — Singleton providers, one composed Pino mixin, and a pre-compiled redactor. No `Scope.REQUEST`, no per-log regex matching.
|
|
48
48
|
- **🔭 Correlated when you need it** — When an OpenTelemetry span is active, `traceId`/`spanId`/`traceFlags` land in every entry. When the peer is absent, the mixin steps aside at zero cost.
|
|
49
49
|
|
|
@@ -65,7 +65,7 @@ pnpm add @bymax-one/nest-logger
|
|
|
65
65
|
|
|
66
66
|
### 🛡️ Security & Privacy
|
|
67
67
|
|
|
68
|
-
- ✅ **PII Redaction by Default** —
|
|
68
|
+
- ✅ **PII Redaction by Default** — 140 paths covering passwords, tokens, and generic secrets — powered by `fast-redact`
|
|
69
69
|
- ✅ **PCI DSS & MFA Coverage** — card data and MFA secrets redacted out of the box, with common HTTP auth headers
|
|
70
70
|
- ✅ **LGPD-Aware Paths** — CPF, CNPJ, and RG redacted by default for Brazilian workloads
|
|
71
71
|
- ✅ **Append-Only Redact List** — `DEFAULT_REDACT_PATHS` never shrinks without a major version; extend it via `redactPaths`
|
|
@@ -374,7 +374,7 @@ Full options reference for `BymaxLoggerModule.forRoot(options)`:
|
|
|
374
374
|
| `level` | `LogLevel` | `'info'` | Minimum log level. One of `fatal \| error \| warn \| info \| debug \| trace` |
|
|
375
375
|
| `isPretty` | `boolean` | `!isProduction` | ⚠️ Reserved — not auto-wired. For pretty output add `new PrettyDevDestination()` to `destinations` (needs `pino-pretty`) |
|
|
376
376
|
| `redactPaths` | `string[]` | `[]` | Additional `fast-redact` paths merged with the defaults |
|
|
377
|
-
| `shouldDisableDefaultRedact` | `boolean` | `false` | Skip the
|
|
377
|
+
| `shouldDisableDefaultRedact` | `boolean` | `false` | Skip the 140 default PII paths. ⚠️ Emits a bootstrap warning — document why |
|
|
378
378
|
| `redactCensor` | `string` | `'[REDACTED]'` | Replacement value written in place of every redacted field |
|
|
379
379
|
| `maxEntrySizeBytes` | `number` | `65536` | UTF-8 byte ceiling per serialized field (`err` + any custom serializer); over it the value becomes a truncation envelope |
|
|
380
380
|
| `destinations` | `ILogDestination[]` | `[]` | Additional sinks (Loki, Postgres, rolling file, …) alongside default stdout |
|
|
@@ -605,7 +605,7 @@ Application Service
|
|
|
605
605
|
└── OTel span → { traceId, spanId, traceFlags }
|
|
606
606
|
│
|
|
607
607
|
▼
|
|
608
|
-
fast-redact ← compiled redact function (
|
|
608
|
+
fast-redact ← compiled redact function (140 paths)
|
|
609
609
|
│
|
|
610
610
|
▼
|
|
611
611
|
┌─────────────────────────────┐
|
|
@@ -622,7 +622,7 @@ Application Service
|
|
|
622
622
|
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
623
623
|
| **🪶 Singleton Scope** | `AsyncLocalStorage` delivers per-request context at zero latency overhead — NestJS `Scope.REQUEST` adds ~5% on the injection graph, unacceptable on a logger that runs for every request |
|
|
624
624
|
| **🧬 One Composed Mixin** | ALS context and OTel trace context merge into a single Pino mixin with a deterministic order: ALS first, then OTel — an active span is the authoritative trace identity, so it wins on conflicts |
|
|
625
|
-
| **⚡ Compiled Redaction** |
|
|
625
|
+
| **⚡ Compiled Redaction** | 140 `fast-redact` paths compile once at module bootstrap into a specialized function; no per-log regex matching, under 3% throughput impact |
|
|
626
626
|
| **🔌 Interface-Driven Sinks** | `ILogDestination` is a contract — Loki, Postgres, rolling files, or anything else is a consumer implementation, never a dependency of this package |
|
|
627
627
|
| **🌳 Zero Runtime Deps** | `"dependencies": {}` — every package arrives as a peer dependency, so consumers pin exact versions and the supply-chain surface stays theirs |
|
|
628
628
|
|
|
@@ -634,7 +634,7 @@ A logger sees every payload the application handles, so the security posture is
|
|
|
634
634
|
|
|
635
635
|
### Redaction by default
|
|
636
636
|
|
|
637
|
-
The library ships **
|
|
637
|
+
The library ships **140 redact paths** compiled at initialization into a single `fast-redact` function (< 3% throughput impact). These cover:
|
|
638
638
|
|
|
639
639
|
| Category | Fields |
|
|
640
640
|
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -709,7 +709,7 @@ When integrating `@bymax-one/nest-logger` in production, verify each of the foll
|
|
|
709
709
|
|
|
710
710
|
| Layer | Implementation |
|
|
711
711
|
| ------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
712
|
-
| PII Redaction |
|
|
712
|
+
| PII Redaction | 140 `fast-redact` paths compiled once at bootstrap — no per-log regex matching |
|
|
713
713
|
| Credentials | `password`, `passwordHash`, `token`, `accessToken`, `refreshToken`, `apiKey`, `apiSecret`, `privateKey` |
|
|
714
714
|
| MFA Secrets | `mfaSecret`, `mfaRecoveryCodes`, `totpSecret` — redacted at wildcard depths 1–4 |
|
|
715
715
|
| PCI DSS | `cardNumber`, `cardCvv`, `cvv`, `cvc`, `cardExpiry` |
|
|
@@ -745,12 +745,12 @@ When integrating `@bymax-one/nest-logger` in production, verify each of the foll
|
|
|
745
745
|
A logger runs on every request of every service that installs it, so the suite is held to a bar beyond "it runs" — every behavior is pinned so that a regression **fails a test**.
|
|
746
746
|
|
|
747
747
|
- ✅ **100% line coverage** — statements, branches, functions, and lines, enforced as a release gate across unit + e2e
|
|
748
|
-
- ✅ **
|
|
749
|
-
`break` threshold of 95. The ten
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
[report](./docs/mutation_testing_results.md))
|
|
753
|
-
- ✅ **Every
|
|
748
|
+
- ✅ **100% mutation score** — verified with [Stryker](https://stryker-mutator.io/) against a
|
|
749
|
+
`break` threshold of 95. The ten mutants no test can kill each carry their reason on the line
|
|
750
|
+
they apply to; a rule against those comments was retired once their cost was measured at
|
|
751
|
+
**+0.10 kB brotli** against 0.66 kB of headroom, roughly a tenth of what it had been assumed
|
|
752
|
+
to be (see the [report](./docs/mutation_testing_results.md))
|
|
753
|
+
- ✅ **Every suppression carries its reason** — each `// Stryker disable` names, after the `:` Stryker reads it from, why the mutant it silences cannot be observed; one of them is recorded as what it is, a mutant the suite kills but Stryker fails to attribute, rather than mislabelled an equivalent. `check:mutants` proves those reasons parse, so they reach the report rather than the `Ignored using a comment` fallback, and the score stays an accounting rather than a number
|
|
754
754
|
- ✅ **No real I/O in unit tests** — the Pino instance and every destination are mocked; e2e tests exercise the wired module through `@nestjs/testing` and supertest
|
|
755
755
|
|
|
756
756
|
```bash
|
package/dist/server/index.cjs
CHANGED
|
@@ -520,7 +520,7 @@ exports.HttpExceptionFilter = class HttpExceptionFilter {
|
|
|
520
520
|
const ctx = host.switchToHttp();
|
|
521
521
|
const res = ctx.getResponse();
|
|
522
522
|
const req = ctx.getRequest();
|
|
523
|
-
const userId = req.user?.id;
|
|
523
|
+
const userId = req.user?.sub ?? req.user?.id;
|
|
524
524
|
const url = stripQueryString(req.url);
|
|
525
525
|
if (exception instanceof common.HttpException) {
|
|
526
526
|
const status2 = exception.getStatus();
|
|
@@ -605,7 +605,7 @@ exports.HttpLoggingInterceptor = class HttpLoggingInterceptor {
|
|
|
605
605
|
}
|
|
606
606
|
const rawUserAgent = req.headers["user-agent"];
|
|
607
607
|
const userAgent = typeof rawUserAgent === "string" ? rawUserAgent : UNKNOWN_USER_AGENT;
|
|
608
|
-
const userId = req.user?.id;
|
|
608
|
+
const userId = req.user?.sub ?? req.user?.id;
|
|
609
609
|
const normalizedUrl = normalizeUrl(url);
|
|
610
610
|
const start = Date.now();
|
|
611
611
|
this.logger.info(RESERVED_LOG_KEYS.HTTP_REQUEST_START, `${method} ${normalizedUrl}`, userId, {
|
|
@@ -820,6 +820,7 @@ var REDACT_ABSOLUTE_PATHS = [
|
|
|
820
820
|
'res.headers["set-cookie"]'
|
|
821
821
|
];
|
|
822
822
|
var DEFAULT_REDACT_PATHS = [
|
|
823
|
+
...REDACT_COMMON_FIELDS,
|
|
823
824
|
...REDACT_COMMON_FIELDS.flatMap((field) => depth(field)),
|
|
824
825
|
...REDACT_ABSOLUTE_PATHS
|
|
825
826
|
];
|
|
@@ -849,6 +850,7 @@ function destinationToStream(destination) {
|
|
|
849
850
|
// Keep string chunks as strings (Node's default re-encodes them to Buffers
|
|
850
851
|
// before `_write`): the common Pino path emits NDJSON strings, so this skips
|
|
851
852
|
// a string→Buffer→string round-trip on the hot logging path.
|
|
853
|
+
// Stryker disable next-line BooleanLiteral: equivalent for every chunk this stream receives — with `true`, Node encodes the string to a Buffer and `write()` below decodes it back with `toString('utf-8')`. Pino writes UTF-8 NDJSON strings at the default encoding, so the round-trip returns the identical string. It is NOT equivalent in general: a `write(chunk, 'latin1')` would round-trip through UTF-8 and change non-ASCII bytes, which is unreachable here because nothing writes to this stream but Pino. The flag stays because it skips the round-trip on the hot logging path.
|
|
852
854
|
decodeStrings: false,
|
|
853
855
|
write(chunk, _encoding, callback) {
|
|
854
856
|
const onFailure = (cause) => {
|
|
@@ -1343,8 +1345,9 @@ var PrettyDevDestination = class {
|
|
|
1343
1345
|
};
|
|
1344
1346
|
var REQUEST_ID_HEADER = "x-request-id";
|
|
1345
1347
|
var MAX_CORRELATION_ID_LENGTH = 256;
|
|
1348
|
+
var CORRELATION_ID_CHARSET = /^[A-Za-z0-9._:/+=-]+$/;
|
|
1346
1349
|
function isAcceptableHeaderValue(value) {
|
|
1347
|
-
return typeof value === "string" && value.length <= MAX_CORRELATION_ID_LENGTH;
|
|
1350
|
+
return typeof value === "string" && value.length <= MAX_CORRELATION_ID_LENGTH && CORRELATION_ID_CHARSET.test(value);
|
|
1348
1351
|
}
|
|
1349
1352
|
exports.RequestIdMiddleware = class RequestIdMiddleware {
|
|
1350
1353
|
/**
|
package/dist/server/index.d.cts
CHANGED
|
@@ -778,7 +778,11 @@ type IncomingHeaders = Record<string, string | string[] | undefined>;
|
|
|
778
778
|
* The inbound request members the logging layer reads.
|
|
779
779
|
*
|
|
780
780
|
* `user` is populated by upstream auth in the consumer app (a guard or
|
|
781
|
-
* middleware)
|
|
781
|
+
* middleware). The acting-user id is read as `sub` first, then `id`: a JWT principal
|
|
782
|
+
* (every `@bymax-one/nest-auth` token) names it `sub`, while an ORM-style principal
|
|
783
|
+
* names it `id`. The JWT subject wins when both are present, since it is the
|
|
784
|
+
* authenticated identity. Reading only `id` — as this once did — silently dropped
|
|
785
|
+
* the user field for every JWT-authenticated request.
|
|
782
786
|
*/
|
|
783
787
|
interface LoggableRequest {
|
|
784
788
|
/** Inbound headers — read for correlation ids and the user agent. */
|
|
@@ -789,9 +793,14 @@ interface LoggableRequest {
|
|
|
789
793
|
readonly url: string;
|
|
790
794
|
/** Client address when the adapter resolves one. */
|
|
791
795
|
readonly ip?: string | undefined;
|
|
792
|
-
/**
|
|
796
|
+
/**
|
|
797
|
+
* Authenticated principal, when upstream auth attached one. `id` is the
|
|
798
|
+
* ORM-style identifier; `sub` is the JWT subject claim. The logging layer reads
|
|
799
|
+
* whichever is present.
|
|
800
|
+
*/
|
|
793
801
|
readonly user?: {
|
|
794
802
|
readonly id?: string | undefined;
|
|
803
|
+
readonly sub?: string | undefined;
|
|
795
804
|
} | undefined;
|
|
796
805
|
}
|
|
797
806
|
/**
|
|
@@ -920,7 +929,16 @@ declare const LOG_CONTEXT_TOKEN: unique symbol;
|
|
|
920
929
|
|
|
921
930
|
/**
|
|
922
931
|
* Canonical list of redact paths merged into Pino's `redact.paths` option by
|
|
923
|
-
* default. Expected length: 27 common fields × 4 depths + 5
|
|
932
|
+
* default. Expected length: 27 root fields + 27 common fields × 4 depths + 5
|
|
933
|
+
* absolute = **140**.
|
|
934
|
+
*
|
|
935
|
+
* The bare field names come first, at depth 0 — the log record's own root.
|
|
936
|
+
* `emitStructured` spreads caller metadata there (`{ ...metadata, logKey, ... }`),
|
|
937
|
+
* so a field named `password` passed as metadata lands at the root, which the
|
|
938
|
+
* depth-1-and-deeper wildcards below never reach (`fast-redact`'s `*` matches a
|
|
939
|
+
* level, not the root). Without the bare name the library's own documented call
|
|
940
|
+
* `logger.info(key, msg, userId, { password })` would write the password in clear.
|
|
941
|
+
* Listing it makes the default set cover a sensitive field wherever it appears.
|
|
924
942
|
*/
|
|
925
943
|
declare const DEFAULT_REDACT_PATHS: readonly string[];
|
|
926
944
|
|
package/dist/server/index.d.ts
CHANGED
|
@@ -778,7 +778,11 @@ type IncomingHeaders = Record<string, string | string[] | undefined>;
|
|
|
778
778
|
* The inbound request members the logging layer reads.
|
|
779
779
|
*
|
|
780
780
|
* `user` is populated by upstream auth in the consumer app (a guard or
|
|
781
|
-
* middleware)
|
|
781
|
+
* middleware). The acting-user id is read as `sub` first, then `id`: a JWT principal
|
|
782
|
+
* (every `@bymax-one/nest-auth` token) names it `sub`, while an ORM-style principal
|
|
783
|
+
* names it `id`. The JWT subject wins when both are present, since it is the
|
|
784
|
+
* authenticated identity. Reading only `id` — as this once did — silently dropped
|
|
785
|
+
* the user field for every JWT-authenticated request.
|
|
782
786
|
*/
|
|
783
787
|
interface LoggableRequest {
|
|
784
788
|
/** Inbound headers — read for correlation ids and the user agent. */
|
|
@@ -789,9 +793,14 @@ interface LoggableRequest {
|
|
|
789
793
|
readonly url: string;
|
|
790
794
|
/** Client address when the adapter resolves one. */
|
|
791
795
|
readonly ip?: string | undefined;
|
|
792
|
-
/**
|
|
796
|
+
/**
|
|
797
|
+
* Authenticated principal, when upstream auth attached one. `id` is the
|
|
798
|
+
* ORM-style identifier; `sub` is the JWT subject claim. The logging layer reads
|
|
799
|
+
* whichever is present.
|
|
800
|
+
*/
|
|
793
801
|
readonly user?: {
|
|
794
802
|
readonly id?: string | undefined;
|
|
803
|
+
readonly sub?: string | undefined;
|
|
795
804
|
} | undefined;
|
|
796
805
|
}
|
|
797
806
|
/**
|
|
@@ -920,7 +929,16 @@ declare const LOG_CONTEXT_TOKEN: unique symbol;
|
|
|
920
929
|
|
|
921
930
|
/**
|
|
922
931
|
* Canonical list of redact paths merged into Pino's `redact.paths` option by
|
|
923
|
-
* default. Expected length: 27 common fields × 4 depths + 5
|
|
932
|
+
* default. Expected length: 27 root fields + 27 common fields × 4 depths + 5
|
|
933
|
+
* absolute = **140**.
|
|
934
|
+
*
|
|
935
|
+
* The bare field names come first, at depth 0 — the log record's own root.
|
|
936
|
+
* `emitStructured` spreads caller metadata there (`{ ...metadata, logKey, ... }`),
|
|
937
|
+
* so a field named `password` passed as metadata lands at the root, which the
|
|
938
|
+
* depth-1-and-deeper wildcards below never reach (`fast-redact`'s `*` matches a
|
|
939
|
+
* level, not the root). Without the bare name the library's own documented call
|
|
940
|
+
* `logger.info(key, msg, userId, { password })` would write the password in clear.
|
|
941
|
+
* Listing it makes the default set cover a sensitive field wherever it appears.
|
|
924
942
|
*/
|
|
925
943
|
declare const DEFAULT_REDACT_PATHS: readonly string[];
|
|
926
944
|
|
package/dist/server/index.mjs
CHANGED
|
@@ -514,7 +514,7 @@ var HttpExceptionFilter = class {
|
|
|
514
514
|
const ctx = host.switchToHttp();
|
|
515
515
|
const res = ctx.getResponse();
|
|
516
516
|
const req = ctx.getRequest();
|
|
517
|
-
const userId = req.user?.id;
|
|
517
|
+
const userId = req.user?.sub ?? req.user?.id;
|
|
518
518
|
const url = stripQueryString(req.url);
|
|
519
519
|
if (exception instanceof HttpException) {
|
|
520
520
|
const status2 = exception.getStatus();
|
|
@@ -599,7 +599,7 @@ var HttpLoggingInterceptor = class {
|
|
|
599
599
|
}
|
|
600
600
|
const rawUserAgent = req.headers["user-agent"];
|
|
601
601
|
const userAgent = typeof rawUserAgent === "string" ? rawUserAgent : UNKNOWN_USER_AGENT;
|
|
602
|
-
const userId = req.user?.id;
|
|
602
|
+
const userId = req.user?.sub ?? req.user?.id;
|
|
603
603
|
const normalizedUrl = normalizeUrl(url);
|
|
604
604
|
const start = Date.now();
|
|
605
605
|
this.logger.info(RESERVED_LOG_KEYS.HTTP_REQUEST_START, `${method} ${normalizedUrl}`, userId, {
|
|
@@ -814,6 +814,7 @@ var REDACT_ABSOLUTE_PATHS = [
|
|
|
814
814
|
'res.headers["set-cookie"]'
|
|
815
815
|
];
|
|
816
816
|
var DEFAULT_REDACT_PATHS = [
|
|
817
|
+
...REDACT_COMMON_FIELDS,
|
|
817
818
|
...REDACT_COMMON_FIELDS.flatMap((field) => depth(field)),
|
|
818
819
|
...REDACT_ABSOLUTE_PATHS
|
|
819
820
|
];
|
|
@@ -843,6 +844,7 @@ function destinationToStream(destination) {
|
|
|
843
844
|
// Keep string chunks as strings (Node's default re-encodes them to Buffers
|
|
844
845
|
// before `_write`): the common Pino path emits NDJSON strings, so this skips
|
|
845
846
|
// a string→Buffer→string round-trip on the hot logging path.
|
|
847
|
+
// Stryker disable next-line BooleanLiteral: equivalent for every chunk this stream receives — with `true`, Node encodes the string to a Buffer and `write()` below decodes it back with `toString('utf-8')`. Pino writes UTF-8 NDJSON strings at the default encoding, so the round-trip returns the identical string. It is NOT equivalent in general: a `write(chunk, 'latin1')` would round-trip through UTF-8 and change non-ASCII bytes, which is unreachable here because nothing writes to this stream but Pino. The flag stays because it skips the round-trip on the hot logging path.
|
|
846
848
|
decodeStrings: false,
|
|
847
849
|
write(chunk, _encoding, callback) {
|
|
848
850
|
const onFailure = (cause) => {
|
|
@@ -1337,8 +1339,9 @@ var PrettyDevDestination = class {
|
|
|
1337
1339
|
};
|
|
1338
1340
|
var REQUEST_ID_HEADER = "x-request-id";
|
|
1339
1341
|
var MAX_CORRELATION_ID_LENGTH = 256;
|
|
1342
|
+
var CORRELATION_ID_CHARSET = /^[A-Za-z0-9._:/+=-]+$/;
|
|
1340
1343
|
function isAcceptableHeaderValue(value) {
|
|
1341
|
-
return typeof value === "string" && value.length <= MAX_CORRELATION_ID_LENGTH;
|
|
1344
|
+
return typeof value === "string" && value.length <= MAX_CORRELATION_ID_LENGTH && CORRELATION_ID_CHARSET.test(value);
|
|
1342
1345
|
}
|
|
1343
1346
|
var RequestIdMiddleware = class {
|
|
1344
1347
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bymax-one/nest-logger",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
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",
|
|
@@ -57,15 +57,16 @@
|
|
|
57
57
|
"bench": "node --expose-gc --import tsx bench/throughput.bench.ts",
|
|
58
58
|
"build": "pnpm clean && tsup",
|
|
59
59
|
"check:exports": "attw --pack .",
|
|
60
|
+
"check:mutants": "node scripts/check-mutation-directives.mjs",
|
|
60
61
|
"check:published": "node scripts/check-published-surface.mjs",
|
|
61
62
|
"clean": "rm -rf dist coverage",
|
|
62
63
|
"lint": "eslint src",
|
|
63
64
|
"lint:fix": "eslint src --fix",
|
|
64
65
|
"mutation": "stryker run",
|
|
66
|
+
"mutation:full": "node -e \"require('node:fs').rmSync('reports/stryker-incremental.json',{force:true,recursive:true})\" && stryker run",
|
|
65
67
|
"mutation:dry-run": "stryker run --dryRunOnly",
|
|
66
|
-
"mutation:incremental": "stryker run --incremental",
|
|
67
68
|
"prepare": "husky",
|
|
68
|
-
"prepublishOnly": "pnpm clean && pnpm typecheck && pnpm lint && pnpm test:cov:all && pnpm build && pnpm check:published",
|
|
69
|
+
"prepublishOnly": "pnpm clean && pnpm typecheck && pnpm lint && pnpm check:mutants && pnpm test:cov:all && pnpm build && pnpm check:published",
|
|
69
70
|
"release": "npm publish --provenance --access public",
|
|
70
71
|
"size": "node scripts/check-size.mjs",
|
|
71
72
|
"test": "jest",
|