@bymax-one/nest-core 1.3.0 → 1.3.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.
package/CHANGELOG.md CHANGED
@@ -11,6 +11,154 @@ heading here.
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [1.3.2] - 2026-08-11
15
+
16
+ A consumer audit of the served document found that it described the library's
17
+ promises rather than the deployment: routes of features that were switched off
18
+ were still listed, the contributed schemas were never referenced by any
19
+ operation, and nothing said which operations needed authentication. Everything
20
+ below is additive — no option changes meaning, no existing document loses an
21
+ entry it had.
22
+
23
+ **Apply to a derived backend:** bump the dependency. The document improves with
24
+ no code change; the two new options are opt-in.
25
+
26
+ ### Added
27
+
28
+ - **`openapi.security` and `openapi.operationSecurity`.** A document-level
29
+ default requirement, plus per-operation overrides keyed `"<METHOD> <path>"`.
30
+ An empty array marks an operation public, which is the specification's own way
31
+ of overriding the default — and it matters for generated clients, since an
32
+ operation with _absent_ security inherits the document default and a client
33
+ would attach credentials to a public registration endpoint.
34
+ - **The operation key is a documented contract**, with `OpenApiOperationKey` and
35
+ `OperationSecurityMap` exported as types so a sibling library can ship a
36
+ plain-data map of its own operations and have it checked at its own compile
37
+ time, with no runtime coupling. The path is written exactly as documented,
38
+ **including any global prefix** — `@nestjs/swagger` puts `setGlobalPrefix` into
39
+ the documented paths, so a library shipping such a map should expose a function
40
+ taking the prefix rather than a frozen constant.
41
+ - **A key addressing no operation fails the document build**, listing both the
42
+ keys that missed and the operations that exist. A stale key would otherwise
43
+ leave a route silently documented as authenticated when it is not, or the
44
+ reverse. Failing is safe here: the document is only ever built outside
45
+ production.
46
+ - **A requirement naming an undeclared security scheme fails the same way.** A
47
+ requirement is a reference, and a reference to nothing produces a document
48
+ whose security cannot be resolved — a client generator looks the name up in
49
+ `components.securitySchemes`, finds nothing, and either fails or emits an
50
+ unauthenticated client. Configuring the requirement and forgetting the scheme
51
+ is one edit apart. A scheme the document itself declares counts as declared,
52
+ and marking an operation public names no scheme, so it needs none.
53
+
54
+ ### Fixed
55
+
56
+ - **A disabled feature's routes are no longer documented.** With
57
+ `metrics: { enabled: false }` the runtime answers `GET /metrics` with a 404
58
+ envelope — on `forRootAsync` the controller is mounted unconditionally and
59
+ guards each request, because route metadata is fixed before the async options
60
+ resolve — while the document still advertised it. The filter reads the same
61
+ resolved snapshot the guard reads, so the two cannot drift. `@nestjs/swagger`
62
+ documents paths as the application serves them — the global prefix, and under
63
+ `enableVersioning({ type: URI })` the version segment that follows it, so
64
+ `/api/v1/metrics` — and both are **read from the application** rather than
65
+ inferred from the document. Versioning matters as much as the prefix: without
66
+ it, every versioned application kept advertising the routes of a feature it
67
+ had switched off, and its health probes lost the payload schema and the public
68
+ marking this package contributes. Inference is the trap: an application whose routes all sit under one
69
+ controller prefix would have that treated as the global one, and a consumer
70
+ route ending in `/health/live` deleted as though this package owned it. What
71
+ leaves is also the **operation**, not the path item — a method the consumer
72
+ mounted on the same path survives, and the path disappears only once nothing
73
+ is left under it.
74
+ - **The automatic security policy is stated for `GET` alone.** These controllers
75
+ expose no other method, so a consumer's `POST` on the same path is theirs and
76
+ no longer inherits a requirement written for ours.
77
+ - **The envelope response follows the envelope feature.** With
78
+ `envelope.enabled` off, errors are shaped by Nest or by the consumer's own
79
+ handler, so documenting this package's envelope described a body the
80
+ deployment never sends. The health response is a separate feature and is
81
+ unaffected.
82
+ - **A response written as a bare `$ref` is a declaration.** It carries no
83
+ `content`, so the placeholder rule would have overwritten it — discarding the
84
+ reference and leaving `$ref` beside sibling keys, which is not a valid
85
+ response object.
86
+ - **`BymaxMetricsAuth` is reserved while a scrape token is configured.** The
87
+ name was silently overwritten or silently lost depending on where the other
88
+ definition came from, and the losing case left the scrape operation pointing
89
+ at a scheme that is not the bearer token the runtime checks. It now fails the
90
+ document build with the collision named.
91
+ - **The contributed schemas are referenced by the operations that return them.**
92
+ They shipped orphaned: `components.schemas` carried the envelope, the health
93
+ response and the pagination shapes while no operation pointed at any of them,
94
+ so a generated client had no error type at all. Every operation now carries a
95
+ `default` response referencing `BymaxErrorEnvelope`, and the health endpoints
96
+ an explicit `200` referencing `BymaxHealthResponse`. Gated by
97
+ `includeCoreSchemas`, because referencing a schema that was not contributed
98
+ would leave a dangling `$ref`.
99
+ - **A response is judged by whether it declares a shape.** `@nestjs/swagger`
100
+ emits a placeholder `200` with a description and no content for every handler,
101
+ so a plain "existing always wins" rule would never have written a contributed
102
+ schema. A response carrying `content` is a real declaration and is untouched;
103
+ one without it is filled in, keeping any description already written.
104
+ - **The library documents the security of its own three routes.** The health
105
+ probes are marked public — an orchestrator polls them holding no credential —
106
+ and the scrape endpoint carries a bearer requirement, with its scheme, exactly
107
+ when `metrics.authToken` is set. The library owns both the routes and the
108
+ option, so no consumer should have to restate either.
109
+
110
+ ### Documentation
111
+
112
+ - The metrics naming rules are framed as an adoption guideline for sibling
113
+ libraries, with the reason the rules live here: a Prometheus registry is a flat
114
+ namespace, so two libraries picking the same metric name collide at the
115
+ _consumer's_ boot, in an application neither library's CI ever assembles. A
116
+ contributing library is asked to publish its own metric list; this package
117
+ deliberately keeps no central catalogue.
118
+ - `applyBymaxOpenApi` documents that testing its enabled path under Jest needs
119
+ `NODE_OPTIONS=--experimental-vm-modules`, because the optional peer is reached
120
+ through a dynamic `import()`. Only the enabled case fails without it, which is
121
+ what makes the omission confusing.
122
+
123
+ ## [1.3.1] - 2026-08-11
124
+
125
+ A patch fixing a defect that existed only in the published artifact: `applyBymaxOpenApi` threw on
126
+ every consumer boot, including consumers that never enabled the OpenAPI document. No API changed —
127
+ the type declarations are byte-identical to `1.3.0` apart from one added documentation comment.
128
+
129
+ **Apply to a derived backend:** `pnpm up @bymax-one/nest-core`. No code change on the consumer
130
+ side; the DI token identities are internal to the package.
131
+
132
+ ### Fixed
133
+
134
+ - **`applyBymaxOpenApi` resolves the options registered by `BymaxCoreModule` again.** The DI
135
+ tokens were minted with `Symbol()`. This package ships one bundle per published subpath with the
136
+ shared internals inlined into each, so `core.tokens` existed twice at runtime — once in
137
+ `dist/index.cjs`, once in `dist/openapi/index.cjs` — and `Symbol('X') !== Symbol('X')`. The
138
+ provider bound by the package root carried one identity and the `./openapi` helper looked up
139
+ another, so `app.get()` found nothing and the helper threw its "could not resolve
140
+ BYMAX_CORE_OPTIONS" error with `BymaxCoreModule` correctly registered. Under Nest's default
141
+ `abortOnError` that took down the process. The feature flag did not protect anyone: the helper
142
+ resolves the options before it reads `openapi.enabled`, so an application with the document
143
+ switched off failed exactly the same way. Every token is now minted with `Symbol.for` against the
144
+ runtime's global symbol registry, which is immune to bundle duplication by construction.
145
+ `./health`, `./metrics` and `./pagination` were audited and carry no DI token at all, so
146
+ `./openapi` was the only subpath where the defect could manifest; the remaining tokens are
147
+ converted anyway, so a future subpath that starts consuming one is safe before the fact.
148
+
149
+ ### Internal
150
+
151
+ - **The consumer load gate now boots a real application against the packed tarball.** It registers
152
+ `BymaxCoreModule.forRootAsync` from the package root, calls `applyBymaxOpenApi` from the
153
+ `./openapi` subpath, and asserts all three outcomes — disabled, mounted outside production, and
154
+ refused in production — in ESM and in CommonJS. The unit suite structurally could not catch this
155
+ class of defect: under ts-jest every module is loaded once, so tokens shared between two entries
156
+ are the same object however they were minted, and the bug only exists once the code is bundled.
157
+ The gate fails against the `1.3.0` artifact and passes against this one.
158
+ - The token specs assert that every exported token round-trips through `Symbol.for`, swept from the
159
+ module namespace rather than a hand-maintained list, so a token added later is covered without
160
+ anyone remembering to add it.
161
+
14
162
  ## [1.3.0] - 2026-08-11
15
163
 
16
164
  Coordinated ecosystem release aligning every `@bymax-one/*` package after the ioredis 6 /
@@ -355,8 +503,10 @@ have regressed from. They are kept because the reasoning is worth having.
355
503
  [1.0.1]: https://github.com/bymaxone/nest-core/compare/v1.0.0...v1.0.1
356
504
  [1.0.0]: https://github.com/bymaxone/nest-core/releases/tag/v1.0.0
357
505
  [1.1.1]: https://github.com/bymaxone/nest-core/compare/v1.1.0...v1.1.1
506
+ [1.3.2]: https://github.com/bymaxone/nest-core/compare/v1.3.1...v1.3.2
507
+ [1.3.1]: https://github.com/bymaxone/nest-core/compare/v1.3.0...v1.3.1
358
508
  [1.3.0]: https://github.com/bymaxone/nest-core/compare/v1.2.2...v1.3.0
359
509
  [1.2.2]: https://github.com/bymaxone/nest-core/compare/v1.2.1...v1.2.2
360
510
  [1.2.1]: https://github.com/bymaxone/nest-core/compare/v1.2.0...v1.2.1
361
511
  [1.2.0]: https://github.com/bymaxone/nest-core/compare/v1.1.1...v1.2.0
362
- [Unreleased]: https://github.com/bymaxone/nest-core/compare/v1.3.0...HEAD
512
+ [Unreleased]: https://github.com/bymaxone/nest-core/compare/v1.3.2...HEAD
package/README.md CHANGED
@@ -282,12 +282,102 @@ BymaxCoreModule.forRoot({
282
282
  | `version` | `string` | `'1.0.0'` | Document version, independent of the package version. |
283
283
  | `servers` | `{ url, description? }[]` | `[]` | Servers advertised by the document. |
284
284
  | `securitySchemes` | `Record<string, object>` | `{}` | Security schemes copied into the document's components. |
285
- | `includeCoreSchemas` | `boolean` | `true` | Contributes this package's own schemas envelope, health, pagination. |
285
+ | `security` | `SecurityRequirement[]` | `[]` | The requirement every operation carries unless it says otherwise. |
286
+ | `operationSecurity` | `OperationSecurityMap` | `{}` | Per-operation overrides. An empty array marks that operation public. |
287
+ | `includeCoreSchemas` | `boolean` | `true` | Contributes this package's own schemas and references them from the responses. |
286
288
 
287
289
  Unlike `health` and `metrics`, this block behaves identically on `forRoot` and
288
290
  `forRootAsync`: the document is mounted from the bootstrap helper, after the
289
291
  options have resolved, so a custom `path` is honored on both registration paths.
290
292
 
293
+ #### Documenting authentication
294
+
295
+ Set the default on the document and mark the exceptions. `security` names
296
+ schemes declared in `securitySchemes`; `operationSecurity` overrides it for one
297
+ operation, and an **empty array is how the specification says "public"** — which
298
+ matters more than it looks, because an operation with _absent_ security inherits
299
+ the document default, so a generated client would attach credentials to your
300
+ registration endpoint.
301
+
302
+ ```ts
303
+ openapi: {
304
+ enabled: true,
305
+ securitySchemes: {
306
+ cookieAuth: { type: 'apiKey', in: 'cookie', name: 'access_token' },
307
+ refreshCookie: { type: 'apiKey', in: 'cookie', name: 'refresh_token' }
308
+ },
309
+ security: [{ cookieAuth: [] }],
310
+ operationSecurity: {
311
+ 'POST /auth/login': [],
312
+ 'POST /auth/register': [],
313
+ 'POST /auth/refresh': [{ refreshCookie: [] }]
314
+ }
315
+ }
316
+ ```
317
+
318
+ An operation that already declares its own requirement — because you decorated
319
+ the handler — is never overwritten, on either path.
320
+
321
+ #### The operation key is a contract
322
+
323
+ Keys are `"<METHOD> <path>"`, and the format is documented rather than
324
+ incidental: a sibling library can ship a plain-data map of its own operations
325
+ keyed this way, so you spread it in instead of restating which of its routes are
326
+ public. Import `OperationSecurityMap` to have that map checked at the library's
327
+ own compile time — it is a type-only export, so nothing couples at runtime.
328
+
329
+ - The method is **uppercase**, separated by exactly one space.
330
+ - The path is written **exactly as it appears in the generated document**:
331
+ leading slash, OpenAPI template braces (`/users/{id}`), no trailing slash, and
332
+ **including any global prefix**. `@nestjs/swagger` puts
333
+ `app.setGlobalPrefix('api')` into the documented paths, so the key becomes
334
+ `'POST /api/auth/login'` in an application that sets one.
335
+
336
+ Because of that last point, a library shipping such a map should expose a
337
+ **function taking the prefix**, not a frozen constant — the call site is the only
338
+ place that knows it:
339
+
340
+ ```ts
341
+ // in the library
342
+ export function authOperationSecurity(prefix = ''): OperationSecurityMap { /* … */ }
343
+
344
+ // in the application
345
+ operationSecurity: { ...authOperationSecurity('api'), ...myOwnOverrides }
346
+ ```
347
+
348
+ A requirement naming a scheme that is not declared **fails the document build**
349
+ too, listing the names that missed and the ones the document defines. A
350
+ requirement is a reference, and a reference to nothing yields a document whose
351
+ security cannot be resolved: a client generator looks the name up, finds
352
+ nothing, and either fails or emits an unauthenticated client.
353
+
354
+ A key matching no operation **fails the document build**, listing both the keys
355
+ that missed and the operations that exist. Silence would be worse: a route
356
+ renamed out from under a stale key would quietly inherit the document default
357
+ and be documented as authenticated when it is not, or the reverse. Failing is
358
+ safe here in a way it rarely is — the document is only ever built outside
359
+ production, so this can only stop a developer.
360
+
361
+ One consequence for conditionally-registered routes: the map is static wiring
362
+ while a route may not be. If an operation belongs to a feature you register per
363
+ environment — your own conditional module, or a library feature toggled off
364
+ somewhere — a key naming it fails the boot in whichever docs-enabled environment
365
+ lacks that route. That is the intended loud behavior, so build the map the same
366
+ way you build the modules: assemble it per feature and spread the fragments in,
367
+ rather than writing one flat literal that outlives the routes it names.
368
+
369
+ That last sentence cuts both ways, and the consequence is worth stating rather
370
+ than discovering. **These checks only run when the document is actually built.**
371
+ With `openapi.enabled` false, or in a production runtime where the feature is
372
+ forced off, nothing validates: a stale key, a renamed route, or a requirement
373
+ naming a scheme you deleted all sit there quietly until someone turns the
374
+ document on. That is deliberate — refusing to boot a production service over a
375
+ documentation setting it never serves would be the wrong trade — but it means
376
+ the errors surface on a developer's machine or in CI, **not** at the moment the
377
+ configuration went wrong. If you gate the document behind an environment flag,
378
+ make sure at least one environment that runs your tests has it on, or these
379
+ checks never fire.
380
+
291
381
  ## 🔑 DI Tokens
292
382
 
293
383
  Every token is a `Symbol`. `BYMAX_CORRELATION_PROVIDER` and
@@ -627,6 +717,28 @@ conventions are part of the contract:
627
717
  - Keep labels bounded. Route templates, never raw paths; status codes, never
628
718
  messages. **Never** a tenant, user, or request id — one unbounded label is
629
719
  enough to make a scrape endpoint the most expensive route in a service.
720
+ `tenantId` deserves naming twice: every library in this family is
721
+ tenant-aware, so it is the first label anyone reaches for and it is unbounded
722
+ by construction.
723
+ - **Publish the list.** A library that contributes metrics documents them in its
724
+ own README — name, type, labels. This package deliberately keeps no central
725
+ catalogue: a list of everyone else's metrics rots the moment a library ships a
726
+ new one. What it does require is that the list exists somewhere an operator
727
+ can find it.
728
+
729
+ **Why these rules live here.** A Prometheus registry is a flat namespace, and
730
+ `prom-client` rejects a duplicate metric name. If two libraries independently
731
+ pick `bymax_operations_total`, the collision surfaces at the **consumer's** boot
732
+ — in an application neither library's CI ever assembles, as a hard failure, in
733
+ front of whoever wired the app. Neither library can test for it. A namespace
734
+ rule is the only thing that prevents it, and it can only be arbitrated by the
735
+ dependency they share, which is this package.
736
+
737
+ The rules are documentation, not enforcement. This package could inspect the
738
+ registry around each contributor and reject an unprefixed name, but that would
739
+ need a per-contributor prefix on the contract, and it would wrongly reject the
740
+ case that matters most — an **application's own** contributor, which has no
741
+ business being pushed into a `bymax_` namespace.
630
742
 
631
743
  ## 📘 OpenAPI
632
744
 
@@ -683,6 +795,24 @@ Enabling it in production is not an error, it is a no-op with a warning naming
683
795
  the option that was ignored, so a single configuration can be shared across
684
796
  environments.
685
797
 
798
+ ### Testing the enabled path under Jest
799
+
800
+ `applyBymaxOpenApi` loads `@nestjs/swagger` through a dynamic `import()` — that
801
+ is what keeps the peer optional for everyone who never enables the document —
802
+ and Jest's module registry cannot service a dynamic import without a flag:
803
+
804
+ ```jsonc
805
+ // package.json
806
+ "scripts": {
807
+ "test:e2e": "NODE_OPTIONS=--experimental-vm-modules jest --config jest.e2e.config.ts"
808
+ }
809
+ ```
810
+
811
+ Without it, only the **enabled** case fails, with `dynamic import callback
812
+ invoked without --experimental-vm-modules`. The disabled and production cases
813
+ never reach the loader and pass either way, which is what makes the omission
814
+ confusing when you meet it.
815
+
686
816
  ### What the library contributes
687
817
 
688
818
  With `includeCoreSchemas` on, the document carries the contracts this package
@@ -704,6 +834,59 @@ that never enable the feature.
704
834
  A contributed entry never overwrites one the document already has: if you
705
835
  document your own `BymaxErrorEnvelope`, yours wins.
706
836
 
837
+ Contributing the schemas is only half of it — the operations **reference** them,
838
+ which is what a generated client actually reads:
839
+
840
+ - every operation gains a `default` response pointing at `BymaxErrorEnvelope`,
841
+ because every error path in this package answers with that envelope. It is
842
+ attached as `default` rather than guessed per status code: this package knows
843
+ what an error looks like and does not know which statuses your handler emits.
844
+ It follows the feature: with `envelope.enabled` off, errors are shaped by Nest
845
+ or by your own handler, so nothing is documented;
846
+ - the health endpoints gain an explicit `200` pointing at `BymaxHealthResponse`,
847
+ which this package _does_ know precisely, having registered them itself.
848
+
849
+ `@nestjs/swagger` emits a placeholder response for every handler — a `200` with
850
+ a description and no content — so "already documented" is judged on whether a
851
+ response declares a **shape**: one carrying `content` — or written as a bare
852
+ `$ref`, which points at a shape declared elsewhere — is yours and is left alone,
853
+ one without either gets filled in while keeping any description you wrote.
854
+
855
+ Both halves are the same switch. Referencing a schema that was not contributed
856
+ would leave a dangling `$ref`, and a document that resolves nowhere is worse
857
+ than one that says less — so `includeCoreSchemas: false` opts out of both.
858
+
859
+ ### The document describes _this_ deployment
860
+
861
+ A feature you turned off has its routes removed from the document. With
862
+ `metrics: { enabled: false }` the runtime answers `GET /metrics` with a 404
863
+ envelope — on `forRootAsync` the controller is mounted unconditionally and
864
+ guards each request, because route metadata is fixed before the async options
865
+ resolve — so a document still listing the route would describe something this
866
+ deployment does not serve. The filter reads the same resolved snapshot the
867
+ runtime guard reads, which is what keeps the two from drifting.
868
+
869
+ Your own routes are safe from it. `@nestjs/swagger` documents paths including
870
+ `app.setGlobalPrefix()`, so the match cannot be on equality — but a bare tail
871
+ match would treat `/tenants/{id}/health/live` as this package's probe and delete
872
+ it from your document. What separates the two is that a global prefix prefixes
873
+ _everything_: a tail match counts only when whatever precedes it also precedes
874
+ every other path in the document. `/api/v2` qualifies; `/tenants/{id}` does not,
875
+ because it does not prefix `/invoices`.
876
+
877
+ This package also documents the security of the three routes it owns, without
878
+ being asked:
879
+
880
+ | Route | Documented as |
881
+ | ----------------------------------- | ------------------------------------------------------- |
882
+ | `GET /health/live`, `/health/ready` | Public (`security: []`), when a document default exists |
883
+ | `GET /metrics` | Bearer-protected **iff** `metrics.authToken` is set |
884
+
885
+ The probes are polled by an orchestrator holding no credential, and the scrape
886
+ endpoint is protected exactly when you configured a token — this package owns
887
+ both the route and the option, so you should not have to restate either. Your
888
+ own `operationSecurity` entry still wins.
889
+
707
890
  ## 🧵 Trace correlation
708
891
 
709
892
  Off by default. Enabled, it reads the span your instrumentation already opened
@@ -966,21 +1149,22 @@ in the sections above.
966
1149
 
967
1150
  ### `.` (root)
968
1151
 
969
- | Export | Kind | Description |
970
- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ---------------------------------------------------------------------- |
971
- | `BymaxCoreModule` | class | The dynamic module: `forRoot` and `forRootAsync`. |
972
- | `BymaxCoreModuleOptions`, `EnvelopeOptions`, `TimingOptions`, `HealthOptions`, `MetricsOptions`, `TelemetryOptions`, `OpenApiOptions`, `OpenApiServerDescriptor`, `OpenApiSecurityScheme`, `ResolvedCoreOptions` | types | The options surface and its resolved shape. |
973
- | `BYMAX_CORE_OPTIONS`, `BYMAX_CORRELATION_PROVIDER`, `BYMAX_TIMING_SINK`, `BYMAX_HEALTH_INDICATORS`, `BYMAX_METRICS_REGISTRY` | tokens | The DI tokens; see the [token table](#-di-tokens). |
974
- | `ICorrelationIdProvider` | type | The correlation-provider contract. |
975
- | `ITraceContextProvider`, `TraceContext` | types | The trace-context contract and the identifiers it resolves. |
976
- | `BymaxExceptionFilter` | class | The envelope exception filter. |
977
- | `FilterErrorContext` | type | The neutral request context passed to the filter's observability seam. |
978
- | `buildErrorEnvelope` | function | Pure builder assembling an `ErrorEnvelope`. |
979
- | `ErrorEnvelope`, `ErrorDetails`, `BuildErrorEnvelopeInput` | types | The envelope contract and its builder input. |
980
- | `TimingInterceptor` | class | The request-timing interceptor. |
981
- | `ITimingSink`, `RequestTimingSample` | types | The timing-sink contract and its sample shape. |
982
- | `BYMAX_BAD_GATEWAY` `BYMAX_VALIDATION_FAILED` | constants | The full error-code catalog (see [Error envelope](#-error-envelope)). |
983
- | `codeForStatus` | function | Derives a catalog code from an HTTP status. |
1152
+ | Export | Kind | Description |
1153
+ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ---------------------------------------------------------------------------------- |
1154
+ | `BymaxCoreModule` | class | The dynamic module: `forRoot` and `forRootAsync`. |
1155
+ | `BymaxCoreModuleOptions`, `EnvelopeOptions`, `TimingOptions`, `HealthOptions`, `MetricsOptions`, `TelemetryOptions`, `OpenApiOptions`, `OpenApiServerDescriptor`, `OpenApiSecurityScheme`, `ResolvedCoreOptions` | types | The options surface and its resolved shape. |
1156
+ | `OpenApiSecurityRequirement`, `OpenApiHttpMethod`, `OpenApiOperationKey`, `OperationSecurityMap` | types | The operation-key contract a sibling library targets to ship its own security map. |
1157
+ | `BYMAX_CORE_OPTIONS`, `BYMAX_CORRELATION_PROVIDER`, `BYMAX_TIMING_SINK`, `BYMAX_HEALTH_INDICATORS`, `BYMAX_METRICS_REGISTRY` | tokens | The DI tokens; see the [token table](#-di-tokens). |
1158
+ | `ICorrelationIdProvider` | type | The correlation-provider contract. |
1159
+ | `ITraceContextProvider`, `TraceContext` | types | The trace-context contract and the identifiers it resolves. |
1160
+ | `BymaxExceptionFilter` | class | The envelope exception filter. |
1161
+ | `FilterErrorContext` | type | The neutral request context passed to the filter's observability seam. |
1162
+ | `buildErrorEnvelope` | function | Pure builder assembling an `ErrorEnvelope`. |
1163
+ | `ErrorEnvelope`, `ErrorDetails`, `BuildErrorEnvelopeInput` | types | The envelope contract and its builder input. |
1164
+ | `TimingInterceptor` | class | The request-timing interceptor. |
1165
+ | `ITimingSink`, `RequestTimingSample` | types | The timing-sink contract and its sample shape. |
1166
+ | `BYMAX_BAD_GATEWAY` … `BYMAX_VALIDATION_FAILED` | constants | The full error-code catalog (see [Error envelope](#-error-envelope)). |
1167
+ | `codeForStatus` | function | Derives a catalog code from an HTTP status. |
984
1168
 
985
1169
  ### `./pagination`
986
1170
 
package/dist/index.cjs CHANGED
@@ -17,6 +17,10 @@ var __decorateClass = (decorators, target, key, kind) => {
17
17
  };
18
18
  var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
19
19
 
20
+ // src/route-defaults.ts
21
+ var DEFAULT_HEALTH_PATH = "health";
22
+ var DEFAULT_METRICS_PATH = "metrics";
23
+
20
24
  // src/runtime.environment.ts
21
25
  var NON_PRODUCTION_ENVIRONMENTS = /* @__PURE__ */ new Set(["development", "test"]);
22
26
  function isProductionRuntime(value = process.env["NODE_ENV"]) {
@@ -27,9 +31,7 @@ function isProductionRuntime(value = process.env["NODE_ENV"]) {
27
31
  }
28
32
 
29
33
  // src/core.options.ts
30
- var DEFAULT_HEALTH_PATH = "health";
31
34
  var DEFAULT_INDICATOR_TIMEOUT_MS = 5e3;
32
- var DEFAULT_METRICS_PATH = "metrics";
33
35
  var DEFAULT_OPENAPI_PATH = "docs";
34
36
  var DEFAULT_OPENAPI_JSON_PATH = "docs-json";
35
37
  var DEFAULT_OPENAPI_TITLE = "API";
@@ -105,6 +107,11 @@ function resolveOpenApi(raw) {
105
107
  // consumer-owned nested objects, and the deep-freeze below would otherwise
106
108
  // reach into them.
107
109
  securitySchemes: structuredClone(raw?.securitySchemes ?? {}),
110
+ // Cloned for the same reason as the schemes above: both are consumer-owned
111
+ // nested structures, and the deep-freeze applied to the snapshot would
112
+ // otherwise reach into objects the consumer still holds a reference to.
113
+ security: structuredClone(raw?.security ?? []),
114
+ operationSecurity: structuredClone(raw?.operationSecurity ?? {}),
108
115
  includeCoreSchemas: raw?.includeCoreSchemas ?? true
109
116
  };
110
117
  }
@@ -127,12 +134,18 @@ function normalizeCoreOptions(raw) {
127
134
  normalizeCoreOptions();
128
135
 
129
136
  // src/core.tokens.ts
130
- var BYMAX_CORE_OPTIONS = /* @__PURE__ */ Symbol("BYMAX_CORE_OPTIONS");
131
- var BYMAX_CORRELATION_PROVIDER = /* @__PURE__ */ Symbol("BYMAX_CORRELATION_PROVIDER");
132
- var BYMAX_TIMING_SINK = /* @__PURE__ */ Symbol("BYMAX_TIMING_SINK");
133
- var BYMAX_HEALTH_INDICATORS = /* @__PURE__ */ Symbol("BYMAX_HEALTH_INDICATORS");
134
- var BYMAX_METRICS_REGISTRY = /* @__PURE__ */ Symbol("BYMAX_METRICS_REGISTRY");
135
- var BYMAX_TRACE_CONTEXT = /* @__PURE__ */ Symbol("BYMAX_TRACE_CONTEXT");
137
+ var BYMAX_CORE_OPTIONS = /* @__PURE__ */ Symbol.for("@bymax-one/nest-core:core-options");
138
+ var BYMAX_CORRELATION_PROVIDER = /* @__PURE__ */ Symbol.for(
139
+ "@bymax-one/nest-core:correlation-provider"
140
+ );
141
+ var BYMAX_TIMING_SINK = /* @__PURE__ */ Symbol.for("@bymax-one/nest-core:timing-sink");
142
+ var BYMAX_HEALTH_INDICATORS = /* @__PURE__ */ Symbol.for(
143
+ "@bymax-one/nest-core:health-indicators"
144
+ );
145
+ var BYMAX_METRICS_REGISTRY = /* @__PURE__ */ Symbol.for(
146
+ "@bymax-one/nest-core:metrics-registry"
147
+ );
148
+ var BYMAX_TRACE_CONTEXT = /* @__PURE__ */ Symbol.for("@bymax-one/nest-core:trace-context");
136
149
 
137
150
  // src/optional-peer.ts
138
151
  function isMissingModuleError(cause) {
@@ -201,7 +214,7 @@ async function resolveTraceContextProvider(options) {
201
214
  var DEFAULT_MONOTONIC_CLOCK = {
202
215
  now: () => performance.now()
203
216
  };
204
- var BYMAX_TIMING_CLOCK = /* @__PURE__ */ Symbol("BYMAX_TIMING_CLOCK");
217
+ var BYMAX_TIMING_CLOCK = /* @__PURE__ */ Symbol.for("@bymax-one/nest-core:timing-clock");
205
218
 
206
219
  // src/defaults.providers.ts
207
220
  var NoopCorrelationIdProvider = class {