@bymax-one/nest-logger 1.0.7 → 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 CHANGED
@@ -11,6 +11,30 @@ 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
+
14
38
  ## [1.0.7] - 2026-08-07
15
39
 
16
40
  **Documentation and tooling.** `dist/` differs from `1.0.6` only in the text of the comments
@@ -316,7 +340,8 @@ published `dist/` is identical — no runtime behaviour changes for consumers.
316
340
  - Professional CI suite: `ci.yml`, `bench.yml`, `codeql.yml`, `scorecard.yml`,
317
341
  `release.yml`, Dependabot, and issue templates
318
342
 
319
- [Unreleased]: https://github.com/bymaxone/nest-logger/compare/v1.0.7...HEAD
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
320
345
  [1.0.7]: https://github.com/bymaxone/nest-logger/compare/v1.0.6...v1.0.7
321
346
  [1.0.6]: https://github.com/bymaxone/nest-logger/compare/v1.0.5...v1.0.6
322
347
  [1.0.5]: https://github.com/bymaxone/nest-logger/compare/v1.0.4...v1.0.5
package/README.md CHANGED
@@ -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** — 113 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`.
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** — 113 paths covering passwords, tokens, and generic secrets — powered by `fast-redact`
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 113 default PII paths. ⚠️ Emits a bootstrap warning — document why |
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 (113 paths)
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** | 113 `fast-redact` paths compile once at module bootstrap into a specialized function; no per-log regex matching, under 3% throughput impact |
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 **113 redact paths** compiled at initialization into a single `fast-redact` function (< 3% throughput impact). These cover:
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 | 113 `fast-redact` paths compiled once at bootstrap — no per-log regex matching |
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` |
@@ -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
  ];
@@ -1344,8 +1345,9 @@ var PrettyDevDestination = class {
1344
1345
  };
1345
1346
  var REQUEST_ID_HEADER = "x-request-id";
1346
1347
  var MAX_CORRELATION_ID_LENGTH = 256;
1348
+ var CORRELATION_ID_CHARSET = /^[A-Za-z0-9._:/+=-]+$/;
1347
1349
  function isAcceptableHeaderValue(value) {
1348
- 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);
1349
1351
  }
1350
1352
  exports.RequestIdMiddleware = class RequestIdMiddleware {
1351
1353
  /**
@@ -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); only `id` is ever read, for the acting-user log field.
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
- /** Authenticated principal, when upstream auth attached one. */
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 absolute = **113**.
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
 
@@ -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); only `id` is ever read, for the acting-user log field.
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
- /** Authenticated principal, when upstream auth attached one. */
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 absolute = **113**.
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
 
@@ -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
  ];
@@ -1338,8 +1339,9 @@ var PrettyDevDestination = class {
1338
1339
  };
1339
1340
  var REQUEST_ID_HEADER = "x-request-id";
1340
1341
  var MAX_CORRELATION_ID_LENGTH = 256;
1342
+ var CORRELATION_ID_CHARSET = /^[A-Za-z0-9._:/+=-]+$/;
1341
1343
  function isAcceptableHeaderValue(value) {
1342
- 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);
1343
1345
  }
1344
1346
  var RequestIdMiddleware = class {
1345
1347
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bymax-one/nest-logger",
3
- "version": "1.0.7",
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",
@@ -63,8 +63,8 @@
63
63
  "lint": "eslint src",
64
64
  "lint:fix": "eslint src --fix",
65
65
  "mutation": "stryker run",
66
+ "mutation:full": "node -e \"require('node:fs').rmSync('reports/stryker-incremental.json',{force:true,recursive:true})\" && stryker run",
66
67
  "mutation:dry-run": "stryker run --dryRunOnly",
67
- "mutation:incremental": "stryker run --incremental",
68
68
  "prepare": "husky",
69
69
  "prepublishOnly": "pnpm clean && pnpm typecheck && pnpm lint && pnpm check:mutants && pnpm test:cov:all && pnpm build && pnpm check:published",
70
70
  "release": "npm publish --provenance --access public",