@bymax-one/nest-logger 1.0.0 → 1.0.2

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +63 -1
  2. package/README.md +258 -136
  3. package/package.json +34 -11
package/CHANGELOG.md CHANGED
@@ -11,6 +11,67 @@ heading here.
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [1.0.2] - 2026-07-29
15
+
16
+ ### Fixed
17
+
18
+ - **CommonJS consumers no longer receive ESM type declarations.** Both subpaths
19
+ now declare `types` per condition, so `require()` resolves to the `.d.cts`
20
+ declarations that match the `.cjs` runtime. Previously a single `types` entry
21
+ pointed at the `.d.ts` files, which — with `"type": "module"` — TypeScript
22
+ reads as ESM, so a project on `moduleResolution: node16` / `nodenext`
23
+ importing from CommonJS got declarations for the wrong module format.
24
+ - Added top-level `main`, `module` and `types` so the root entrypoint also
25
+ resolves under the legacy `moduleResolution: node` algorithm. The `./shared`
26
+ subpath remains unresolvable in that mode, which is inherent to subpath
27
+ exports and not fixable without a directory shim.
28
+ - Exposed `./package.json` through the `exports` map. Reading it previously
29
+ failed with `ERR_PACKAGE_PATH_NOT_EXPORTED`, which breaks tooling that
30
+ inspects an installed package's manifest.
31
+ - **The `./shared` subpath now resolves under `moduleResolution: node`.** A
32
+ `typesVersions` map points the subpath at its `.d.cts` declarations, which is
33
+ the only mechanism that resolution algorithm understands — it predates
34
+ `exports` and ignores it entirely. Without this, a consumer whose tsconfig
35
+ sets `module: commonjs` without an explicit `moduleResolution` (the default
36
+ the Nest CLI scaffolds, which falls back to `node`) got
37
+ `error TS2307: Cannot find module '@bymax-one/nest-logger/shared'` while the
38
+ root entrypoint resolved fine. Runtime was never affected: Node reads
39
+ `exports`, so `require('@bymax-one/nest-logger/shared')` always worked.
40
+
41
+ ### Added
42
+
43
+ - `pnpm check:exports` (`@arethetypeswrong/cli`) — packs the tarball and
44
+ resolves every entrypoint the way each module resolution mode would, in the
45
+ strict profile — every entrypoint in every resolution mode. Wired into CI and
46
+ into the release workflow ahead of the publish step.
47
+
48
+ ## [1.0.1] - 2026-07-29
49
+
50
+ Supply-chain and tooling hardening only. `src/` is unchanged since 1.0.0, so the
51
+ published `dist/` is identical — no runtime behaviour changes for consumers.
52
+
53
+ ### Security
54
+
55
+ - Force patched `brace-expansion` on every major line still present in the dev
56
+ tree (`1.1.17`, `2.1.3`, `5.0.8`, via `pnpm.overrides`) to clear GHSA-mh99-v99m-4gvg
57
+ (unbounded expansion length → out-of-memory crash). The 1.x and 2.x lines are
58
+ kept on their maintenance releases because `minimatch` 3 and 9 consume
59
+ `brace-expansion` as a default export, which the 5.x line no longer provides.
60
+ Dev-only: the published package has zero runtime dependencies.
61
+ - Force `qs` to `^6.15.3` (via `pnpm.overrides`) to clear GHSA-q8mj-m7cp-5q26
62
+ (`qs.stringify` DoS). `typed-rest-client`, pulled in by Stryker, pins `qs` at
63
+ the affected `6.15.1` exactly, so no dependency refresh could resolve it
64
+ - Pin every third-party and GitHub-owned action to a full commit SHA, with the
65
+ release tag kept as a trailing comment so Dependabot can still bump them. The
66
+ org's own `bymaxone/.github` reusable workflows and composite action stay on
67
+ the `@v1` moving tag by design, so shared-pipeline fixes keep propagating
68
+
69
+ ### Changed
70
+
71
+ - Refresh dev dependencies (NestJS 11.1.28, typescript-eslint 8.65, prettier 3.9.6,
72
+ lint-staged 17.2, tsx 4.23.1, ts-jest 29.4.12, tinybench 6.1.2, globals 17.8, and
73
+ their transitive closure)
74
+
14
75
  ## [1.0.0] - 2026-06-18
15
76
 
16
77
  ### Security
@@ -69,6 +130,7 @@ heading here.
69
130
  - Professional CI suite: `ci.yml`, `bench.yml`, `codeql.yml`, `scorecard.yml`,
70
131
  `release.yml`, Dependabot, and issue templates
71
132
 
72
- [Unreleased]: https://github.com/bymaxone/nest-logger/compare/v1.0.0...HEAD
133
+ [Unreleased]: https://github.com/bymaxone/nest-logger/compare/v1.0.1...HEAD
134
+ [1.0.1]: https://github.com/bymaxone/nest-logger/compare/v1.0.0...v1.0.1
73
135
  [1.0.0]: https://github.com/bymaxone/nest-logger/compare/v0.1.0...v1.0.0
74
136
  [0.1.0]: https://github.com/bymaxone/nest-logger/releases/tag/v0.1.0
package/README.md CHANGED
@@ -39,59 +39,116 @@
39
39
 
40
40
  The library has **zero direct dependencies** — all packages arrive as peer dependencies, so you control exact versions and the supply chain surface stays minimal.
41
41
 
42
+ ### Why nest-logger?
43
+
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
+ - **🔌 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`.
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
+ - **🔭 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
+
50
+ ```
51
+ pnpm add @bymax-one/nest-logger
52
+ ```
53
+
42
54
  ---
43
55
 
44
56
  ## 🔥 Features
45
57
 
46
- - **Structured JSON** — every entry has `level`, `time`, `service`, `logKey`, `msg`, and arbitrary metadata fields
47
- - **PII redaction by default** — 113 paths covering passwords, tokens, generic secrets, PCI DSS card data, MFA secrets, CPF/CNPJ/RG (LGPD), and common HTTP headers — powered by `fast-redact`
48
- - **OpenTelemetry correlation** — optional `@opentelemetry/api` peer; injects `traceId`/`spanId`/`traceFlags` into every log via a Pino mixin when an active span is detected
49
- - **AsyncLocalStorage context** — `requestId`, `tenantId`, `userId` flow automatically through the request lifecycle without prop drilling
50
- - **HTTP logging interceptor** — auto-logs all HTTP requests/responses with URL normalization (UUIDs and numeric IDs replaced by `:id` placeholder)
51
- - **Exception filter** — captures NestJS `HttpException` and unexpected errors with structured output
52
- - **Pluggable destinations** — implement `ILogDestination` to ship logs to Loki, Postgres, rolling files, or any sink
53
- - **NestJS `LoggerService` bridge** — drop-in replacement; all NestJS internal logs flow through Pino
54
- - **`MODULE_ACTION_RESULT` log key convention** — enforced by an exported regex for CI validation
55
- - **Pretty-print in dev** — auto-enabled when `NODE_ENV !== 'production'` (requires optional `pino-pretty`)
56
- - **Entry size guard** — truncates oversized entries (default 64 KB) with a structured warning instead of silently dropping them
57
- - **Zero-downtime destination lifecycle** — `onInit()` / `onShutdown()` hooks; a failing destination is removed without affecting others
58
+ ### 📝 Core Logging
59
+
60
+ - ✅ **Structured JSON** — every entry has `level`, `time`, `service`, `logKey`, `msg`, and arbitrary metadata fields
61
+ - ✅ **`MODULE_ACTION_RESULT` Log Keys** — a naming convention enforced by an exported regex for CI validation
62
+ - ✅ **NestJS `LoggerService` Bridge** — drop-in replacement; all NestJS internal logs flow through Pino
63
+ - ✅ **Pretty-Print in Dev** — opt-in `PrettyDevDestination` for readable local output (requires optional `pino-pretty`)
64
+ - ✅ **Field Size Guard** — a serialized field over the ceiling (default 64 KB) is replaced by a compact truncation envelope instead of flooding the sink
65
+
66
+ ### 🛡️ Security & Privacy
67
+
68
+ - ✅ **PII Redaction by Default** — 113 paths covering passwords, tokens, and generic secrets — powered by `fast-redact`
69
+ - ✅ **PCI DSS & MFA Coverage** — card data and MFA secrets redacted out of the box, with common HTTP auth headers
70
+ - ✅ **LGPD-Aware Paths** — CPF, CNPJ, and RG redacted by default for Brazilian workloads
71
+ - ✅ **Append-Only Redact List** — `DEFAULT_REDACT_PATHS` never shrinks without a major version; extend it via `redactPaths`
72
+ - ✅ **Validated Trace IDs** — OTel identifiers pass `isValidTraceId` before injection, never raw user input
73
+
74
+ ### 🔍 Observability & Context
75
+
76
+ - ✅ **OpenTelemetry Correlation** — optional `@opentelemetry/api` peer; injects `traceId`/`spanId`/`traceFlags` into every log via a Pino mixin when an active span is detected
77
+ - ✅ **AsyncLocalStorage Context** — `requestId`, `tenantId`, `userId` flow automatically through the request lifecycle without prop drilling
78
+ - ✅ **HTTP Logging Interceptor** — auto-logs all HTTP requests/responses with URL normalization (UUIDs and numeric IDs replaced by `:id`)
79
+ - ✅ **Exception Filter** — captures NestJS `HttpException` and unexpected errors with structured output
80
+
81
+ ### 🔌 Destinations
82
+
83
+ - ✅ **Pluggable Destinations** — implement `ILogDestination` to ship logs to Loki, Postgres, rolling files, or any sink
84
+ - ✅ **Managed Lifecycle** — `onInit()` / `onShutdown()` hooks; a destination that fails `onInit()` is reported and excluded from shutdown, and boot is never aborted
85
+ - ✅ **Crash-Proof Writes** — every `write()` is wrapped in a try/catch; a failure is reported on `stderr` and swallowed, never propagated to the app
86
+
87
+ ### 🧩 Developer Experience
88
+
89
+ - ✅ **Zero Runtime Dependencies** — everything arrives as a peer dependency, so you control versions and supply-chain surface
90
+ - ✅ **2 Subpath Exports** — `.` for the NestJS server API, `./shared` for zero-dependency types and constants
91
+ - ✅ **Dynamic Module** — configure via `forRoot()` or `forRootAsync()`, sensible defaults included
92
+ - ✅ **Strict TypeScript** — `strict: true`, no `any` in production code, JSDoc on every public export
93
+ - ✅ **100% Coverage + Mutation Tested** — statements, branches, functions, and lines gated at 100%, with Stryker as the deeper gate
58
94
 
59
95
  ---
60
96
 
61
97
  ## 📦 Subpath Exports
62
98
 
63
- The package ships two entry points:
99
+ One package, two entry points — import only what your app needs:
100
+
101
+ | Subpath | Import | Purpose | Dependencies |
102
+ | ---------- | ------------------------------- | ------------------------------------------------------------------------------- | :-------------------------------------: |
103
+ | **Server** | `@bymax-one/nest-logger` | NestJS module, logger service, interceptor, filter, decorators, destinations | NestJS 11, pino, rxjs, reflect-metadata |
104
+ | **Shared** | `@bymax-one/nest-logger/shared` | Types, constants, the log-key regex — `LogLevel`, `LogEntry`, `ServiceMetadata` | None |
64
105
 
65
- | Import path | Contents | Deps |
66
- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ----------------------- |
67
- | `@bymax-one/nest-logger` | Full server API: `BymaxLoggerModule`, `PinoLoggerService`, `LogContextService`, destinations, decorators, interceptors | NestJS + Pino peer deps |
68
- | `@bymax-one/nest-logger/shared` | Zero-dependency utilities: `LOG_KEYS_CONVENTION_REGEX`, `RESERVED_LOG_KEYS`, types (`LogLevel`, `LogEntry`, `ServiceMetadata`) | None |
106
+ ```
107
+ shared (zero deps)
108
+ ↑
109
+ server
110
+ ```
69
111
 
70
112
  The `/shared` subpath is safe to import in isomorphic code, test helpers, CLI scripts, or shared packages that must not pull in NestJS.
71
113
 
72
114
  ---
73
115
 
74
- ## 📥 Installation
116
+ > [!TIP]
117
+ > Prefer to learn from a working app? See the [nest-logger-example](https://github.com/bymaxone/nest-logger-example) — a full NestJS project wired with this library.
118
+
119
+ ## 🚀 Quick Start
120
+
121
+ ### 1. Install
75
122
 
76
123
  ```bash
77
- # Install the library
124
+ # Using pnpm (recommended)
78
125
  pnpm add @bymax-one/nest-logger
79
126
 
80
- # Required peer dependencies
81
- pnpm add pino reflect-metadata
127
+ # Using npm
128
+ npm install @bymax-one/nest-logger
129
+
130
+ # Using yarn
131
+ yarn add @bymax-one/nest-logger
132
+ ```
133
+
134
+ > [!IMPORTANT]
135
+ > You must also install the required **peer dependencies**. The library ships `"dependencies": {}`, so nothing arrives implicitly:
82
136
 
83
- # Optional — pretty dev output
137
+ ```bash
138
+ # Server subpath (required)
139
+ pnpm add @nestjs/common @nestjs/core pino reflect-metadata rxjs
140
+
141
+ # Optional — pretty local output via PrettyDevDestination
84
142
  pnpm add -D pino-pretty
85
143
 
86
- # Optional — OpenTelemetry correlation
144
+ # Optional — OpenTelemetry trace correlation
87
145
  pnpm add @opentelemetry/api @opentelemetry/sdk-node
88
146
  ```
89
147
 
90
- ---
91
-
92
- ## 🚀 Quick Start
148
+ > [!NOTE]
149
+ > `@opentelemetry/api` is resolved lazily when the Pino instance is built. When it is absent the trace mixin silently steps aside — the logger never fails to start, and never warns, over an optional peer.
93
150
 
94
- ### Step 1 — Register the module
151
+ ### 2. Register the module
95
152
 
96
153
  ```typescript
97
154
  // app.module.ts
@@ -110,9 +167,9 @@ import { BymaxLoggerModule } from '@bymax-one/nest-logger'
110
167
  export class AppModule {}
111
168
  ```
112
169
 
113
- `isPretty` defaults to `true` when `NODE_ENV !== 'production'` — no extra config needed in development.
170
+ Stdout is wired automatically — `DefaultStdoutDestination` is always active, so this is all you need to be logging structured JSON.
114
171
 
115
- ### Step 2 — Async configuration with `ConfigService`
172
+ ### 3. Async configuration with `ConfigService`
116
173
 
117
174
  ```typescript
118
175
  // app.module.ts
@@ -140,7 +197,7 @@ import { BymaxLoggerModule } from '@bymax-one/nest-logger'
140
197
  export class AppModule {}
141
198
  ```
142
199
 
143
- ### Step 3 — Inject the logger in a service
200
+ ### 4. Inject the logger in a service
144
201
 
145
202
  ```typescript
146
203
  // payments.service.ts
@@ -209,7 +266,7 @@ Output (pretty-print, development):
209
266
  stripeRefundId: "re_abc"
210
267
  ```
211
268
 
212
- ### Step 4 — HTTP logging (automatic)
269
+ ### 5. HTTP logging (automatic)
213
270
 
214
271
  Enable `http.isEnabled: true` in the module options. The `HttpLoggingInterceptor` is registered globally and emits:
215
272
 
@@ -225,7 +282,7 @@ Enable `http.isEnabled: true` in the module options. The `HttpLoggingInterceptor
225
282
 
226
283
  URLs are automatically normalized — `/users/550e8400-e29b-41d4-a716-446655440000` becomes `/users/:id` so Loki/Grafana cardinality stays bounded.
227
284
 
228
- ### Step 5 — OpenTelemetry correlation
285
+ ### 6. OpenTelemetry correlation
229
286
 
230
287
  Initialize the OTel SDK **before** importing NestJS — this is critical:
231
288
 
@@ -271,7 +328,7 @@ void bootstrap()
271
328
 
272
329
  Once active, every log entry automatically carries `traceId`, `spanId`, and `traceFlags`. Click the `traceId` in Grafana to jump directly to the correlated span in Tempo or Honeycomb.
273
330
 
274
- ### Step 6 — Context propagation with `LogContextService`
331
+ ### 7. Context propagation with `LogContextService`
275
332
 
276
333
  ```typescript
277
334
  // request-id.middleware.ts
@@ -300,17 +357,17 @@ Full options reference for `BymaxLoggerModule.forRoot(options)`:
300
357
 
301
358
  ### Top-level options
302
359
 
303
- | Option | Type | Default | Description |
304
- | ---------------------------- | -------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------ |
305
- | `service.name` | `string` | **Required** | Service name emitted in every log entry |
306
- | `service.version` | `string` | **Required** | Release version/SHA emitted in every log entry |
307
- | `level` | `LogLevel` | `'info'` | Minimum log level. One of `fatal \| error \| warn \| info \| debug \| trace` |
308
- | `isPretty` | `boolean` | `!isProduction` | ⚠️ Reserved — not auto-wired. For pretty output add `new PrettyDevDestination()` to `destinations` (needs `pino-pretty`) |
309
- | `redactPaths` | `string[]` | `[]` | Additional `fast-redact` paths merged with the defaults |
310
- | `shouldDisableDefaultRedact` | `boolean` | `false` | Skip the 113 default PII paths. ⚠️ Emits a bootstrap warning — document why |
311
- | `redactCensor` | `string \| function` | `'[REDACTED]'` | Replacement value for redacted fields. A function receives the raw value |
312
- | `maxEntrySizeBytes` | `number` | `65536` | Entries larger than this are replaced by a structured `LOGGER_ENTRY_TRUNCATED` warning |
313
- | `destinations` | `ILogDestination[]` | `[]` | Additional sinks (Loki, Postgres, rolling file, …) alongside default stdout |
360
+ | Option | Type | Default | Description |
361
+ | ---------------------------- | ------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------ |
362
+ | `service.name` | `string` | **Required** | Service name emitted in every log entry |
363
+ | `service.version` | `string` | **Required** | Release version/SHA emitted in every log entry |
364
+ | `level` | `LogLevel` | `'info'` | Minimum log level. One of `fatal \| error \| warn \| info \| debug \| trace` |
365
+ | `isPretty` | `boolean` | `!isProduction` | ⚠️ Reserved — not auto-wired. For pretty output add `new PrettyDevDestination()` to `destinations` (needs `pino-pretty`) |
366
+ | `redactPaths` | `string[]` | `[]` | Additional `fast-redact` paths merged with the defaults |
367
+ | `shouldDisableDefaultRedact` | `boolean` | `false` | Skip the 113 default PII paths. ⚠️ Emits a bootstrap warning — document why |
368
+ | `redactCensor` | `string` | `'[REDACTED]'` | Replacement value written in place of every redacted field |
369
+ | `maxEntrySizeBytes` | `number` | `65536` | UTF-8 byte ceiling per serialized field (`err` + any custom serializer); over it the value becomes a truncation envelope |
370
+ | `destinations` | `ILogDestination[]` | `[]` | Additional sinks (Loki, Postgres, rolling file, …) alongside default stdout |
314
371
 
315
372
  ### `http` options
316
373
 
@@ -331,54 +388,6 @@ Full options reference for `BymaxLoggerModule.forRoot(options)`:
331
388
 
332
389
  ---
333
390
 
334
- ## 🔐 PII Redaction
335
-
336
- ### Default paths
337
-
338
- The library ships **113 redact paths** compiled at initialization into a single `fast-redact` function (< 3% throughput impact). These cover:
339
-
340
- | Category | Fields |
341
- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
342
- | Passwords | `password`, `passwordHash`, `passwordConfirm`, `newPassword`, `oldPassword` |
343
- | Tokens | `token`, `accessToken`, `refreshToken`, `idToken`, `apiKey`, `apiSecret` |
344
- | MFA | `mfaSecret`, `mfaRecoveryCodes`, `totpSecret` |
345
- | Generic secrets | `secret`, `clientSecret`, `signingSecret`, `privateKey` |
346
- | Payment / PCI DSS | `cardNumber`, `cardCvv`, `cvv`, `cvc`, `cardExpiry` |
347
- | Personal documents (LGPD) | `cpf`, `cnpj`, `rg` |
348
- | Conservative PII | `email` |
349
- | HTTP headers (absolute) | `req.headers.authorization`, `req.headers.cookie`, `req.headers["x-api-key"]`, `req.headers["x-auth-token"]`, `res.headers["set-cookie"]` |
350
-
351
- Every field is listed at wildcard depths 1–4 (`*.field`, `*.*.field`, `*.*.*.field`, `*.*.*.*.field`) because `fast-redact`'s `*` matches a single level only — not recursive.
352
-
353
- ### Extending the defaults
354
-
355
- ```typescript
356
- BymaxLoggerModule.forRoot({
357
- service: { name: 'my-app', version: '1.0.0' },
358
- redactPaths: [
359
- '*.internalSecret', // depth-1 wildcard
360
- 'body.creditCard.*', // all fields inside a subobject
361
- 'payload.user.taxId' // exact path
362
- ]
363
- })
364
- ```
365
-
366
- The extra paths are **merged** with the defaults — never replacing them.
367
-
368
- ### Disabling defaults (not recommended)
369
-
370
- ```typescript
371
- BymaxLoggerModule.forRoot({
372
- service: { name: 'my-app', version: '1.0.0' },
373
- shouldDisableDefaultRedact: true, // ⚠️ emits LOGGER_BOOTSTRAP_WARNING
374
- redactPaths: ['*.password'] // you own the full list
375
- })
376
- ```
377
-
378
- A `LOGGER_BOOTSTRAP_WARNING` entry is emitted on startup so security reviews can audit when PII protection was intentionally reduced.
379
-
380
- ---
381
-
382
391
  ## 🔑 Log Key Convention
383
392
 
384
393
  All structured log calls must use the `MODULE_ACTION_RESULT` format:
@@ -587,12 +596,151 @@ Application Service
587
596
  └─────────────────────────────┘
588
597
  ```
589
598
 
590
- **Key design decisions:**
599
+ ### Design Principles
591
600
 
592
- - **Singleton scope, not `Scope.REQUEST`** — `AsyncLocalStorage` delivers per-request context at zero latency overhead. NestJS request scope adds ~5% latency on the injection graph; unacceptable on a logger that runs every request.
593
- - **One Pino mixin** — ALS context and OTel trace context are composed into a single mixin. Merge order is deterministic: ALS fields first, then OTel (OTel wins on name conflicts because an active span is the authoritative trace identity).
594
- - **Zero direct dependencies** — the library ships with `"dependencies": {}`. All packages arrive as peer dependencies so consumers control exact versions.
595
- - **`fast-redact` compiled at init** — 113 paths compile once at module bootstrap into a specialized JS function. No per-log regex matching.
601
+ | Principle | Description |
602
+ | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
603
+ | **🪶 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 |
604
+ | **🧬 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 |
605
+ | **⚡ Compiled Redaction** | 113 `fast-redact` paths compile once at module bootstrap into a specialized function; no per-log regex matching, under 3% throughput impact |
606
+ | **🔌 Interface-Driven Sinks** | `ILogDestination` is a contract — Loki, Postgres, rolling files, or anything else is a consumer implementation, never a dependency of this package |
607
+ | **🌳 Zero Runtime Deps** | `"dependencies": {}` — every package arrives as a peer dependency, so consumers pin exact versions and the supply-chain surface stays theirs |
608
+
609
+ ---
610
+
611
+ ## 🔐 Security Model
612
+
613
+ A logger sees every payload the application handles, so the security posture is about what **never** reaches the sink — and about a sink failure never reaching the application.
614
+
615
+ ### Redaction by default
616
+
617
+ The library ships **113 redact paths** compiled at initialization into a single `fast-redact` function (< 3% throughput impact). These cover:
618
+
619
+ | Category | Fields |
620
+ | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
621
+ | Passwords | `password`, `passwordHash`, `passwordConfirm`, `newPassword`, `oldPassword` |
622
+ | Tokens | `token`, `accessToken`, `refreshToken`, `idToken`, `apiKey`, `apiSecret` |
623
+ | MFA | `mfaSecret`, `mfaRecoveryCodes`, `totpSecret` |
624
+ | Generic secrets | `secret`, `clientSecret`, `signingSecret`, `privateKey` |
625
+ | Payment / PCI DSS | `cardNumber`, `cardCvv`, `cvv`, `cvc`, `cardExpiry` |
626
+ | Personal documents (LGPD) | `cpf`, `cnpj`, `rg` |
627
+ | Conservative PII | `email` |
628
+ | HTTP headers (absolute) | `req.headers.authorization`, `req.headers.cookie`, `req.headers["x-api-key"]`, `req.headers["x-auth-token"]`, `res.headers["set-cookie"]` |
629
+
630
+ Every field is listed at wildcard depths 1–4 (`*.field`, `*.*.field`, `*.*.*.field`, `*.*.*.*.field`) because `fast-redact`'s `*` matches a single level only — not recursive.
631
+
632
+ ### Extending the defaults
633
+
634
+ ```typescript
635
+ BymaxLoggerModule.forRoot({
636
+ service: { name: 'my-app', version: '1.0.0' },
637
+ redactPaths: [
638
+ '*.internalSecret', // depth-1 wildcard
639
+ 'body.creditCard.*', // all fields inside a subobject
640
+ 'payload.user.taxId' // exact path
641
+ ]
642
+ })
643
+ ```
644
+
645
+ The extra paths are **merged** with the defaults — never replacing them.
646
+
647
+ ### Disabling defaults (not recommended)
648
+
649
+ ```typescript
650
+ BymaxLoggerModule.forRoot({
651
+ service: { name: 'my-app', version: '1.0.0' },
652
+ shouldDisableDefaultRedact: true, // ⚠️ emits LOGGER_BOOTSTRAP_WARNING
653
+ redactPaths: ['*.password'] // you own the full list
654
+ })
655
+ ```
656
+
657
+ A `LOGGER_BOOTSTRAP_WARNING` entry is emitted on startup so security reviews can audit when PII protection was intentionally reduced.
658
+
659
+ ### An append-only default list
660
+
661
+ `DEFAULT_REDACT_PATHS` is append-only by contract: a path may be added in a minor release, but removing one requires a major version bump. A field that stopped being redacted silently is a leak that no consumer would notice — the version number is what makes it visible.
662
+
663
+ ### Validated trace identifiers
664
+
665
+ Trace context is never copied verbatim into a log entry. The OTel mixin reads the active span and passes its identifiers through `isValidTraceId` / `isValidSpanId` before injecting them, so a malformed or attacker-influenced value cannot be written into the correlation fields your dashboards and alerts key on.
666
+
667
+ ### Destination failures are contained
668
+
669
+ Every `write()` runs inside a try/catch. A destination that throws or rejects produces a `LOGGER_DESTINATION_WRITE_FAILED` line on `stderr` — never back through the logger, which would turn a broken sink into a write → log → write feedback loop — and the entry is dropped for that sink only. A destination whose `onInit()` rejects is reported as `LOGGER_DESTINATION_INIT_FAILED` and excluded from the shutdown sequence without blocking boot; it stays in the write fan-out, where the same fail-soft wrapper contains it. A logging backend going down degrades logging — it never takes the application with it.
670
+
671
+ ### Bounded field size
672
+
673
+ Every serializer — the default `err` one and any you supply — is wrapped so a field whose serialized JSON exceeds `maxEntrySizeBytes` (64 KB by default) is replaced by a compact envelope: `_truncated`, `_logKey: 'LOGGER_ENTRY_TRUNCATED'`, `_originalSize`, and a 200-character `_preview`. An accidentally logged webhook payload costs a bounded number of bytes and leaves a record that truncation happened, instead of flooding the sink.
674
+
675
+ ### Security Checklist
676
+
677
+ When integrating `@bymax-one/nest-logger` in production, verify each of the following:
678
+
679
+ - `shouldDisableDefaultRedact` stays `false` — if it is on, the `LOGGER_BOOTSTRAP_WARNING` must be justified in a security review
680
+ - Every custom field carrying a secret or personal document is added to `redactPaths` at the depths it actually appears
681
+ - `http.excludePaths` regexes are anchored and linear-time — an unbounded pattern runs on every request URL (ReDoS)
682
+ - Custom destinations never re-serialize the raw request object; they receive the already-redacted `LogEntry`
683
+ - Destination credentials (Loki, Postgres, OTLP) come from the environment, never from values written into module options in source control
684
+ - Log retention at the sink matches your data-retention policy — redaction bounds what is stored, not how long
685
+
686
+ ---
687
+
688
+ ## 🛡️ Security Table
689
+
690
+ | Layer | Implementation |
691
+ | ------------------- | ------------------------------------------------------------------------------------------------------------------ |
692
+ | PII Redaction | 113 `fast-redact` paths compiled once at bootstrap — no per-log regex matching |
693
+ | Credentials | `password`, `passwordHash`, `token`, `accessToken`, `refreshToken`, `apiKey`, `apiSecret`, `privateKey` |
694
+ | MFA Secrets | `mfaSecret`, `mfaRecoveryCodes`, `totpSecret` — redacted at wildcard depths 1–4 |
695
+ | PCI DSS | `cardNumber`, `cardCvv`, `cvv`, `cvc`, `cardExpiry` |
696
+ | LGPD Documents | `cpf`, `cnpj`, `rg`, plus `email` as a conservative default |
697
+ | HTTP Headers | `authorization`, `cookie`, `x-api-key`, `x-auth-token`, `set-cookie` — absolute paths on `req` / `res` |
698
+ | Redact List | `DEFAULT_REDACT_PATHS` is append-only; removal requires a major version |
699
+ | Trace Injection | `isValidTraceId` / `isValidSpanId` validated before any identifier is written |
700
+ | URL Cardinality | UUIDs and numeric IDs normalized to `:id` — bounds label cardinality and keeps raw identifiers out of the path |
701
+ | Destination Failure | Every `write()` try/catch-wrapped; a failure is reported on `stderr` and swallowed, never propagated to the caller |
702
+ | Field Size | Per-field UTF-8 cap at `maxEntrySizeBytes` (64 KB default); oversized values become a truncation envelope |
703
+ | Supply Chain | `"dependencies": {}` — no transitive runtime packages of the library's own choosing |
704
+
705
+ > [!IMPORTANT]
706
+ > Redaction protects the fields it knows about. Any new field carrying a secret or a personal document must be added to `redactPaths` — the default list covers the standard set, not your domain's.
707
+
708
+ ---
709
+
710
+ ## 🧱 Tech Stack
711
+
712
+ [![Pino](https://img.shields.io/badge/Pino-10-green?style=flat-square)](https://getpino.io)
713
+ [![NestJS](https://img.shields.io/badge/NestJS-11-E0234E?style=flat-square&logo=nestjs)](https://nestjs.com)
714
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-3178C6?style=flat-square&logo=typescript&logoColor=white)](https://www.typescriptlang.org)
715
+ [![Node.js](https://img.shields.io/badge/Node.js-24-339933?style=flat-square&logo=node.js&logoColor=white)](https://nodejs.org)
716
+ [![Jest](https://img.shields.io/badge/Jest-30-C21325?style=flat-square&logo=jest)](https://jestjs.io)
717
+ [![Stryker](https://img.shields.io/badge/Stryker-Mutation_Testing-red?style=flat-square)](https://stryker-mutator.io)
718
+ [![pnpm](https://img.shields.io/badge/pnpm-10-F69220?style=flat-square&logo=pnpm&logoColor=white)](https://pnpm.io)
719
+ [![tsup](https://img.shields.io/badge/tsup-8-orange?style=flat-square)](https://tsup.egoist.dev)
720
+
721
+ ---
722
+
723
+ ## 🧪 Testing & Quality
724
+
725
+ 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**.
726
+
727
+ - ✅ **100% line coverage** — statements, branches, functions, and lines, enforced as a release gate across unit + e2e
728
+ - ✅ **97.42% mutation score** — verified with [Stryker](https://stryker-mutator.io/) against a `break` threshold of 95; 97.42% is the theoretical maximum for this codebase
729
+ - ✅ **Every survivor documented** — the 10 remaining mutants are equivalents, each recorded in [docs/mutation_testing_results.md](./docs/mutation_testing_results.md) rather than silenced with an inline comment, so the score is an accounting rather than a number
730
+ - ✅ **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
731
+
732
+ ```bash
733
+ pnpm test # unit tests (Jest)
734
+ pnpm test:cov # coverage report
735
+ pnpm test:e2e # end-to-end tests (supertest)
736
+ pnpm test:cov:all # full coverage gate (100% statements/branches/functions/lines)
737
+ pnpm mutation # Stryker mutation testing (95% break gate)
738
+ pnpm typecheck # tsc strict check (all tsconfig variants)
739
+ pnpm lint # ESLint
740
+ ```
741
+
742
+ > [!NOTE]
743
+ > Line coverage proves a line _executed_ under test; mutation testing proves a test _would fail_ if that line were wrong. The full methodology and per-area breakdown are in [docs/mutation_testing_results.md](./docs/mutation_testing_results.md).
596
744
 
597
745
  ---
598
746
 
@@ -664,16 +812,19 @@ interface ILogDestination {
664
812
 
665
813
  ## 🚨 Error Code Catalog
666
814
 
667
- | Code | Severity | When | Action |
668
- | --------------------------------- | ----------------- | -------------------------------------------------------- | --------------------------------------------------------------------- |
669
- | `LOGGER_INVALID_OPTIONS` | Throws at init | `service.name` or `service.version` missing | Add the required fields |
670
- | `LOGGER_INVALID_LEVEL` | Throws at init | `level` is not a valid Pino value | Use `'fatal'\|'error'\|'warn'\|'info'\|'debug'\|'trace'` |
671
- | `LOGGER_PRETTY_UNAVAILABLE` | Warn on bootstrap | `isPretty: true` but `pino-pretty` not installed | Install `pino-pretty` or set `isPretty: false` |
672
- | `LOGGER_OTEL_API_UNAVAILABLE` | Info on bootstrap | OTel mixin active but `@opentelemetry/api` not installed | Install the package or set `otel.shouldAutoInjectTraceContext: false` |
673
- | `LOGGER_DESTINATION_INIT_FAILED` | Error (logged) | `destination.onInit()` rejects | Destination is removed; other destinations continue |
674
- | `LOGGER_DESTINATION_WRITE_FAILED` | Warn (logged) | `destination.write()` throws | Entry skipped for that destination; others continue |
675
- | `LOGGER_CONTEXT_OUT_OF_SCOPE` | Throws | `LogContextService.set()` called outside `run()` | Wrap in `logContext.run({ ... }, () => ...)` |
676
- | `LOGGER_ENTRY_TRUNCATED` | Warn (meta-log) | Entry exceeds `maxEntrySizeBytes` | Reduce metadata or raise the limit |
815
+ | Code | Surfaces as | When | Action |
816
+ | --------------------------------- | ---------------------------- | ----------------------------------------------------------------- | --------------------------------------------------------------------- |
817
+ | `LOGGER_INVALID_OPTIONS` | Thrown `Error` at init | `service`, `service.name`, or `service.version` missing or empty | Add the required fields |
818
+ | `LOGGER_INVALID_LEVEL` | Thrown `Error` at init | `level` is not a valid Pino value | Use `'fatal'\|'error'\|'warn'\|'info'\|'debug'\|'trace'` |
819
+ | `LOGGER_PRETTY_UNAVAILABLE` | Thrown `Error` at `onInit()` | `PrettyDevDestination` registered but `pino-pretty` not installed | Install `pino-pretty` or drop the destination |
820
+ | `LOGGER_OTEL_API_UNAVAILABLE` | Nothing emitted | `@opentelemetry/api` cannot be resolved | None — the trace mixin steps aside silently |
821
+ | `LOGGER_DESTINATION_INIT_FAILED` | Structured error log | `destination.onInit()` rejects | Sink is excluded from shutdown; boot continues, writes stay fail-soft |
822
+ | `LOGGER_DESTINATION_WRITE_FAILED` | NDJSON line on `stderr` | `destination.write()` throws or rejects | Entry dropped for that destination; others continue |
823
+ | `LOGGER_CONTEXT_OUT_OF_SCOPE` | Thrown `Error` | `LogContextService.set()` called outside `run()` | Wrap in `logContext.run({ ... }, () => ...)` |
824
+ | `LOGGER_ENTRY_TRUNCATED` | `_logKey` in the envelope | A serialized field exceeds `maxEntrySizeBytes` | Reduce the payload or raise the ceiling |
825
+
826
+ > [!NOTE]
827
+ > The codes above name the condition, not a machine-readable field. Thrown errors carry a human-readable message prefixed with the emitting class (e.g. `[BymaxLoggerModule] options.service is required`), not the code string.
677
828
 
678
829
  ---
679
830
 
@@ -690,35 +841,6 @@ interface ILogDestination {
690
841
 
691
842
  ---
692
843
 
693
- ## 🧪 Testing & Quality
694
-
695
- ```bash
696
- pnpm test # unit tests (Jest)
697
- pnpm test:cov # coverage report
698
- pnpm test:e2e # end-to-end tests (supertest)
699
- pnpm test:cov:all # full coverage gate (100% statements/branches/functions/lines)
700
- pnpm mutation # Stryker mutation testing (95% break gate)
701
- pnpm typecheck # tsc strict check (all tsconfig variants)
702
- pnpm lint # ESLint
703
- ```
704
-
705
- Coverage gate: **100%** on statements, branches, functions, and lines. Mutation: **97.42%** current vs the **95%** break gate (Stryker `thresholds.break: 95`); 97.42% is the theoretical maximum — all 10 surviving mutants are documented equivalents. See [mutation_testing_results.md](docs/mutation_testing_results.md).
706
-
707
- ---
708
-
709
- ## 🧱 Tech Stack
710
-
711
- [![Pino](https://img.shields.io/badge/Pino-10-green?style=flat-square)](https://getpino.io)
712
- [![NestJS](https://img.shields.io/badge/NestJS-11-E0234E?style=flat-square&logo=nestjs)](https://nestjs.com)
713
- [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-3178C6?style=flat-square&logo=typescript&logoColor=white)](https://www.typescriptlang.org)
714
- [![Node.js](https://img.shields.io/badge/Node.js-24-339933?style=flat-square&logo=node.js&logoColor=white)](https://nodejs.org)
715
- [![Jest](https://img.shields.io/badge/Jest-30-C21325?style=flat-square&logo=jest)](https://jestjs.io)
716
- [![Stryker](https://img.shields.io/badge/Stryker-Mutation_Testing-red?style=flat-square)](https://stryker-mutator.io)
717
- [![pnpm](https://img.shields.io/badge/pnpm-10-F69220?style=flat-square&logo=pnpm&logoColor=white)](https://pnpm.io)
718
- [![tsup](https://img.shields.io/badge/tsup-8-orange?style=flat-square)](https://tsup.egoist.dev)
719
-
720
- ---
721
-
722
844
  ## ⚡ Performance
723
845
 
724
846
  Pino official benchmarks (Node 24, 100k logs):
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bymax-one/nest-logger",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
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",
@@ -22,14 +22,35 @@
22
22
  ],
23
23
  "exports": {
24
24
  ".": {
25
- "types": "./dist/server/index.d.ts",
26
- "import": "./dist/server/index.mjs",
27
- "require": "./dist/server/index.cjs"
25
+ "import": {
26
+ "types": "./dist/server/index.d.ts",
27
+ "default": "./dist/server/index.mjs"
28
+ },
29
+ "require": {
30
+ "types": "./dist/server/index.d.cts",
31
+ "default": "./dist/server/index.cjs"
32
+ }
28
33
  },
29
34
  "./shared": {
30
- "types": "./dist/shared/index.d.ts",
31
- "import": "./dist/shared/index.mjs",
32
- "require": "./dist/shared/index.cjs"
35
+ "import": {
36
+ "types": "./dist/shared/index.d.ts",
37
+ "default": "./dist/shared/index.mjs"
38
+ },
39
+ "require": {
40
+ "types": "./dist/shared/index.d.cts",
41
+ "default": "./dist/shared/index.cjs"
42
+ }
43
+ },
44
+ "./package.json": "./package.json"
45
+ },
46
+ "main": "./dist/server/index.cjs",
47
+ "module": "./dist/server/index.mjs",
48
+ "types": "./dist/server/index.d.cts",
49
+ "typesVersions": {
50
+ "*": {
51
+ "shared": [
52
+ "./dist/shared/index.d.cts"
53
+ ]
33
54
  }
34
55
  },
35
56
  "lint-staged": {
@@ -45,11 +66,11 @@
45
66
  "peerDependencies": {
46
67
  "@nestjs/common": "^11.0.0",
47
68
  "@nestjs/core": "^11.0.0",
69
+ "@opentelemetry/api": "^1.9.0",
48
70
  "pino": "^10.0.0",
49
- "reflect-metadata": "^0.2.0",
50
- "rxjs": "^7.8.0",
51
71
  "pino-pretty": "^13.0.0",
52
- "@opentelemetry/api": "^1.9.0"
72
+ "reflect-metadata": "^0.2.0",
73
+ "rxjs": "^7.8.0"
53
74
  },
54
75
  "peerDependenciesMeta": {
55
76
  "pino-pretty": {
@@ -60,6 +81,7 @@
60
81
  }
61
82
  },
62
83
  "devDependencies": {
84
+ "@arethetypeswrong/cli": "^0.18.5",
63
85
  "@commitlint/cli": "^21.0.2",
64
86
  "@commitlint/config-conventional": "^21.0.2",
65
87
  "@eslint/js": "^9.39.4",
@@ -73,7 +95,7 @@
73
95
  "@stryker-mutator/typescript-checker": "^9",
74
96
  "@types/express": "^5.0.6",
75
97
  "@types/jest": "^30.0.0",
76
- "@types/node": "^25",
98
+ "@types/node": "^26",
77
99
  "@types/supertest": "^7.2.0",
78
100
  "@typescript-eslint/eslint-plugin": "^8.59.3",
79
101
  "@typescript-eslint/parser": "^8.59.3",
@@ -135,6 +157,7 @@
135
157
  "bench": "node --expose-gc --import tsx bench/throughput.bench.ts",
136
158
  "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.server.json",
137
159
  "size": "node scripts/check-size.mjs",
160
+ "check:exports": "attw --pack .",
138
161
  "clean": "rm -rf dist coverage",
139
162
  "release": "pnpm publish --provenance"
140
163
  }