@bymax-one/nest-core 1.3.1 → 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 +111 -1
- package/README.md +200 -16
- package/dist/index.cjs +9 -2
- package/dist/index.d.cts +86 -3
- package/dist/index.d.ts +86 -3
- package/dist/index.mjs +9 -2
- package/dist/openapi/index.cjs +234 -11
- package/dist/openapi/index.d.cts +12 -2
- package/dist/openapi/index.d.ts +12 -2
- package/dist/openapi/index.mjs +235 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,115 @@ 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
|
+
|
|
14
123
|
## [1.3.1] - 2026-08-11
|
|
15
124
|
|
|
16
125
|
A patch fixing a defect that existed only in the published artifact: `applyBymaxOpenApi` threw on
|
|
@@ -394,9 +503,10 @@ have regressed from. They are kept because the reasoning is worth having.
|
|
|
394
503
|
[1.0.1]: https://github.com/bymaxone/nest-core/compare/v1.0.0...v1.0.1
|
|
395
504
|
[1.0.0]: https://github.com/bymaxone/nest-core/releases/tag/v1.0.0
|
|
396
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
|
|
397
507
|
[1.3.1]: https://github.com/bymaxone/nest-core/compare/v1.3.0...v1.3.1
|
|
398
508
|
[1.3.0]: https://github.com/bymaxone/nest-core/compare/v1.2.2...v1.3.0
|
|
399
509
|
[1.2.2]: https://github.com/bymaxone/nest-core/compare/v1.2.1...v1.2.2
|
|
400
510
|
[1.2.1]: https://github.com/bymaxone/nest-core/compare/v1.2.0...v1.2.1
|
|
401
511
|
[1.2.0]: https://github.com/bymaxone/nest-core/compare/v1.1.1...v1.2.0
|
|
402
|
-
[Unreleased]: https://github.com/bymaxone/nest-core/compare/v1.3.
|
|
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
|
-
| `
|
|
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
|
-
| `
|
|
974
|
-
| `
|
|
975
|
-
| `
|
|
976
|
-
| `
|
|
977
|
-
| `
|
|
978
|
-
| `
|
|
979
|
-
| `
|
|
980
|
-
| `
|
|
981
|
-
| `
|
|
982
|
-
| `
|
|
983
|
-
| `
|
|
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
|
}
|
package/dist/index.d.cts
CHANGED
|
@@ -68,6 +68,44 @@ interface OpenApiServerDescriptor {
|
|
|
68
68
|
* so the consumer's declaration reaches the UI unchanged.
|
|
69
69
|
*/
|
|
70
70
|
type OpenApiSecurityScheme = Readonly<Record<string, unknown>>;
|
|
71
|
+
/**
|
|
72
|
+
* One security requirement: scheme name to the scopes it needs, empty for a
|
|
73
|
+
* scheme that takes none. An operation's requirements are alternatives — any
|
|
74
|
+
* one of them satisfies it — so an empty *array* of requirements means the
|
|
75
|
+
* operation needs no authentication at all, which is how the specification
|
|
76
|
+
* expresses a public route that overrides a document-level default.
|
|
77
|
+
*/
|
|
78
|
+
type OpenApiSecurityRequirement = Readonly<Record<string, readonly string[]>>;
|
|
79
|
+
/** The HTTP methods an OpenAPI path item can carry an operation under. */
|
|
80
|
+
type OpenApiHttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS' | 'TRACE';
|
|
81
|
+
/**
|
|
82
|
+
* Addresses one operation in the generated document, as `"<METHOD> <path>"`.
|
|
83
|
+
*
|
|
84
|
+
* This format is a documented contract, not an implementation detail: a sibling
|
|
85
|
+
* library may ship a plain-data map of its own operations keyed this way, so a
|
|
86
|
+
* consumer spreads it into {@link OpenApiOptions.operationSecurity} instead of
|
|
87
|
+
* restating which of that library's routes are public. Import the type to get
|
|
88
|
+
* that map checked at the library's own compile time.
|
|
89
|
+
*
|
|
90
|
+
* The method is uppercase, one space separates the two parts, and the path is
|
|
91
|
+
* written **exactly as it appears in the generated document**: leading slash,
|
|
92
|
+
* OpenAPI template braces (`/users/{id}`), no trailing slash, and including any
|
|
93
|
+
* global prefix the application sets. That last part is the one that surprises:
|
|
94
|
+
* `@nestjs/swagger` includes `app.setGlobalPrefix('api')` in the documented
|
|
95
|
+
* paths, so the key is `'POST /api/auth/login'` in an application that sets one.
|
|
96
|
+
* A library shipping such a map should therefore expose a *function* taking the
|
|
97
|
+
* prefix rather than a frozen constant — the call site is the only place that
|
|
98
|
+
* knows it.
|
|
99
|
+
*
|
|
100
|
+
* @example 'GET /users/{id}'
|
|
101
|
+
* @example 'POST /api/auth/login'
|
|
102
|
+
*/
|
|
103
|
+
type OpenApiOperationKey = `${OpenApiHttpMethod} /${string}`;
|
|
104
|
+
/**
|
|
105
|
+
* Per-operation security requirements, keyed by {@link OpenApiOperationKey}.
|
|
106
|
+
* An empty array marks the operation public, overriding any document default.
|
|
107
|
+
*/
|
|
108
|
+
type OperationSecurityMap = Readonly<Record<OpenApiOperationKey, readonly OpenApiSecurityRequirement[]>>;
|
|
71
109
|
/**
|
|
72
110
|
* OpenAPI document configuration.
|
|
73
111
|
*
|
|
@@ -96,10 +134,53 @@ interface OpenApiOptions {
|
|
|
96
134
|
servers?: readonly OpenApiServerDescriptor[];
|
|
97
135
|
/** Security schemes added to the document's components. Default: `{}`. */
|
|
98
136
|
securitySchemes?: Readonly<Record<string, OpenApiSecurityScheme>>;
|
|
137
|
+
/**
|
|
138
|
+
* The requirement every operation carries unless it says otherwise, naming
|
|
139
|
+
* schemes declared in {@link OpenApiOptions.securitySchemes}. Default: `[]`,
|
|
140
|
+
* which documents nothing and leaves every operation as it was generated.
|
|
141
|
+
*
|
|
142
|
+
* Set this when most of the API is authenticated, and mark the exceptions
|
|
143
|
+
* public through {@link OpenApiOptions.operationSecurity}. An operation that
|
|
144
|
+
* already declares its own requirement is never overwritten.
|
|
145
|
+
*
|
|
146
|
+
* @example [{ cookieAuth: [] }]
|
|
147
|
+
*/
|
|
148
|
+
security?: readonly OpenApiSecurityRequirement[];
|
|
149
|
+
/**
|
|
150
|
+
* Per-operation overrides of {@link OpenApiOptions.security}, keyed by
|
|
151
|
+
* {@link OpenApiOperationKey}. An empty array marks that operation public.
|
|
152
|
+
* Default: `{}`.
|
|
153
|
+
*
|
|
154
|
+
* A key matching no operation in the generated document is a configuration
|
|
155
|
+
* error and fails the document build, naming the keys that do exist. Silence
|
|
156
|
+
* would be worse: a route renamed out from under a stale key would quietly
|
|
157
|
+
* inherit the document default and be documented as authenticated when it is
|
|
158
|
+
* not, or the reverse.
|
|
159
|
+
*
|
|
160
|
+
* That check runs only when the document is built. With
|
|
161
|
+
* {@link OpenApiOptions.enabled} false, or in a production runtime where the
|
|
162
|
+
* feature is forced off, a stale key is not reported — refusing to boot a
|
|
163
|
+
* service over a documentation setting it never serves would be the wrong
|
|
164
|
+
* trade. The cost is that the error waits for an environment that has the
|
|
165
|
+
* document switched on.
|
|
166
|
+
*
|
|
167
|
+
* @example
|
|
168
|
+
* {
|
|
169
|
+
* 'POST /auth/login': [],
|
|
170
|
+
* 'POST /auth/refresh': [{ refreshCookie: [] }]
|
|
171
|
+
* }
|
|
172
|
+
*/
|
|
173
|
+
operationSecurity?: OperationSecurityMap;
|
|
99
174
|
/**
|
|
100
175
|
* Contribute the schemas this package owns — the error envelope, the health
|
|
101
|
-
* response, and the pagination shapes — to the document's components
|
|
102
|
-
*
|
|
176
|
+
* response, and the pagination shapes — to the document's components, and
|
|
177
|
+
* reference them from the operations that return them: the error envelope as
|
|
178
|
+
* every operation's `default` response, and the health response on the health
|
|
179
|
+
* endpoints this package registers. Default: `true`.
|
|
180
|
+
*
|
|
181
|
+
* The two halves are one switch because they are one decision. Referencing a
|
|
182
|
+
* schema this package did not contribute would leave a dangling `$ref`, and a
|
|
183
|
+
* document that resolves nowhere is worse than one that says less.
|
|
103
184
|
*/
|
|
104
185
|
includeCoreSchemas?: boolean;
|
|
105
186
|
}
|
|
@@ -221,6 +302,8 @@ interface ResolvedOpenApiOptions {
|
|
|
221
302
|
version: string;
|
|
222
303
|
servers: readonly OpenApiServerDescriptor[];
|
|
223
304
|
securitySchemes: Readonly<Record<string, OpenApiSecurityScheme>>;
|
|
305
|
+
security: readonly OpenApiSecurityRequirement[];
|
|
306
|
+
operationSecurity: OperationSecurityMap;
|
|
224
307
|
includeCoreSchemas: boolean;
|
|
225
308
|
}
|
|
226
309
|
/**
|
|
@@ -824,4 +907,4 @@ declare const BYMAX_GATEWAY_TIMEOUT = "BYMAX_GATEWAY_TIMEOUT";
|
|
|
824
907
|
*/
|
|
825
908
|
declare function codeForStatus(status: number): string;
|
|
826
909
|
|
|
827
|
-
export { BYMAX_BAD_GATEWAY, BYMAX_BAD_REQUEST, BYMAX_CLIENT_ERROR, BYMAX_CONFLICT, BYMAX_CORE_OPTIONS, BYMAX_CORRELATION_PROVIDER, BYMAX_FORBIDDEN, BYMAX_GATEWAY_TIMEOUT, BYMAX_HEALTH_INDICATORS, BYMAX_INTERNAL_ERROR, BYMAX_METRICS_REGISTRY, BYMAX_NOT_FOUND, BYMAX_NOT_IMPLEMENTED, BYMAX_PAYLOAD_TOO_LARGE, BYMAX_SERVICE_UNAVAILABLE, BYMAX_TIMING_SINK, BYMAX_TOO_MANY_REQUESTS, BYMAX_TRACE_CONTEXT, BYMAX_UNAUTHORIZED, BYMAX_UNPROCESSABLE_ENTITY, BYMAX_UNSUPPORTED_MEDIA_TYPE, BYMAX_VALIDATION_FAILED, type BuildErrorEnvelopeInput, BymaxCoreModule, type BymaxCoreModuleOptions, BymaxExceptionFilter, type EnvelopeOptions, type ErrorDetails, type ErrorEnvelope, type FilterErrorContext, type HealthOptions, type ICorrelationIdProvider, type ITimingSink, type ITraceContextProvider, type MetricsOptions, type OpenApiOptions, type OpenApiSecurityScheme, type OpenApiServerDescriptor, type RequestTimingSample, type ResolvedCoreOptions, type TelemetryOptions, TimingInterceptor, type TimingOptions, type TraceContext, buildErrorEnvelope, codeForStatus };
|
|
910
|
+
export { BYMAX_BAD_GATEWAY, BYMAX_BAD_REQUEST, BYMAX_CLIENT_ERROR, BYMAX_CONFLICT, BYMAX_CORE_OPTIONS, BYMAX_CORRELATION_PROVIDER, BYMAX_FORBIDDEN, BYMAX_GATEWAY_TIMEOUT, BYMAX_HEALTH_INDICATORS, BYMAX_INTERNAL_ERROR, BYMAX_METRICS_REGISTRY, BYMAX_NOT_FOUND, BYMAX_NOT_IMPLEMENTED, BYMAX_PAYLOAD_TOO_LARGE, BYMAX_SERVICE_UNAVAILABLE, BYMAX_TIMING_SINK, BYMAX_TOO_MANY_REQUESTS, BYMAX_TRACE_CONTEXT, BYMAX_UNAUTHORIZED, BYMAX_UNPROCESSABLE_ENTITY, BYMAX_UNSUPPORTED_MEDIA_TYPE, BYMAX_VALIDATION_FAILED, type BuildErrorEnvelopeInput, BymaxCoreModule, type BymaxCoreModuleOptions, BymaxExceptionFilter, type EnvelopeOptions, type ErrorDetails, type ErrorEnvelope, type FilterErrorContext, type HealthOptions, type ICorrelationIdProvider, type ITimingSink, type ITraceContextProvider, type MetricsOptions, type OpenApiHttpMethod, type OpenApiOperationKey, type OpenApiOptions, type OpenApiSecurityRequirement, type OpenApiSecurityScheme, type OpenApiServerDescriptor, type OperationSecurityMap, type RequestTimingSample, type ResolvedCoreOptions, type TelemetryOptions, TimingInterceptor, type TimingOptions, type TraceContext, buildErrorEnvelope, codeForStatus };
|