@bymax-one/nest-core 1.0.1 → 1.1.0

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/dist/index.d.cts CHANGED
@@ -3,13 +3,6 @@ import { DynamicModule, ExceptionFilter, ArgumentsHost, NestInterceptor, Executi
3
3
  import { HttpAdapterHost } from '@nestjs/core';
4
4
  import { Observable } from 'rxjs';
5
5
 
6
- /**
7
- * @fileoverview Public configuration surface for `BymaxCoreModule` plus the
8
- * resolution pipeline that merges consumer options over the documented defaults
9
- * and deep-freezes the result. Every feature reads its effective configuration
10
- * from the resolved snapshot, never from the raw consumer input.
11
- * @layer Config
12
- */
13
6
  /** Error-envelope exception-filter configuration. */
14
7
  interface EnvelopeOptions {
15
8
  /** Register the global exception filter. Default: `true`. */
@@ -35,6 +28,18 @@ interface HealthOptions {
35
28
  path?: string;
36
29
  /** Per-indicator timeout before a check is reported as down. Default: `5000`. */
37
30
  indicatorTimeoutMs?: number;
31
+ /**
32
+ * Aggregate every provider marked with `@BymaxHealthIndicator()`, anywhere in
33
+ * the application, in addition to those registered under
34
+ * `BYMAX_HEALTH_INDICATORS`. Default: `false`.
35
+ *
36
+ * Off by default because it changes which failures can take an application out
37
+ * of rotation: a library the application merely imports gains the ability to
38
+ * fail its readiness probe. That is exactly the point once it is on — the
39
+ * dependency understands its own health better than the application does — but
40
+ * it is a decision the application makes, not one it inherits.
41
+ */
42
+ autoDiscover?: boolean;
38
43
  /**
39
44
  * Include the failing indicator's message in the readiness response under
40
45
  * `details.error`. Never enable in production. Default: `false`.
@@ -48,6 +53,80 @@ interface HealthOptions {
48
53
  */
49
54
  exposeIndicatorErrors?: boolean;
50
55
  }
56
+ /** One entry of the OpenAPI document's `servers` list. */
57
+ interface OpenApiServerDescriptor {
58
+ /** Absolute base URL the API is served from. */
59
+ url: string;
60
+ /** Human-readable label for the server, shown in the UI's selector. */
61
+ description?: string;
62
+ }
63
+ /**
64
+ * A single OpenAPI security scheme, kept as an open record rather than a closed
65
+ * union. The specification allows several shapes (HTTP, API key, OAuth2, OpenID
66
+ * Connect), each with its own required fields, and this package neither
67
+ * validates nor interprets them: it copies them into the document's components
68
+ * so the consumer's declaration reaches the UI unchanged.
69
+ */
70
+ type OpenApiSecurityScheme = Readonly<Record<string, unknown>>;
71
+ /**
72
+ * OpenAPI document configuration.
73
+ *
74
+ * The document and its UI are development-only. Enabling this in a production
75
+ * runtime does not serve them: the resolver forces the feature off and records
76
+ * why, and the bootstrap helper refuses to mount independently. See
77
+ * {@link ResolvedOpenApiOptions.suppressedInProduction}.
78
+ */
79
+ interface OpenApiOptions {
80
+ /**
81
+ * Build and serve the OpenAPI document. Default: `false`. Ignored in a
82
+ * production runtime, where the feature is always off.
83
+ */
84
+ enabled?: boolean;
85
+ /** Route the interactive UI is served from. Default: `'docs'`. */
86
+ path?: string;
87
+ /** Route the raw JSON document is served from. Default: `'docs-json'`. */
88
+ jsonPath?: string;
89
+ /** Document title. Default: `'API'`. */
90
+ title?: string;
91
+ /** Document description. Default: `''`. */
92
+ description?: string;
93
+ /** Document version, independent of the package version. Default: `'1.0.0'`. */
94
+ version?: string;
95
+ /** Servers advertised by the document. Default: `[]`. */
96
+ servers?: readonly OpenApiServerDescriptor[];
97
+ /** Security schemes added to the document's components. Default: `{}`. */
98
+ securitySchemes?: Readonly<Record<string, OpenApiSecurityScheme>>;
99
+ /**
100
+ * Contribute the schemas this package owns — the error envelope, the health
101
+ * response, and the pagination shapes — to the document's components.
102
+ * Default: `true`.
103
+ */
104
+ includeCoreSchemas?: boolean;
105
+ }
106
+ /** Trace-correlation configuration. */
107
+ interface TelemetryOptions {
108
+ /**
109
+ * Read the active OpenTelemetry span and carry its identifiers into the
110
+ * request-timing sample and, when {@link TelemetryOptions.exposeTraceId} is
111
+ * set, the error envelope. Default: `false`.
112
+ *
113
+ * This package never creates a span, configures an SDK, or installs an
114
+ * exporter: it reads what the instrumentation already running produces.
115
+ */
116
+ enabled?: boolean;
117
+ /**
118
+ * Include `traceId` in the error-envelope body served to the client.
119
+ * Default: `false`.
120
+ *
121
+ * A trace id is not a secret, but it is internal: published in a response it
122
+ * tells a caller that a tracing backend exists and gives them an identifier
123
+ * that correlates their request with everything else in that trace. Support
124
+ * teams often want exactly that; the default is off so it is a decision rather
125
+ * than a side effect. With this off, the identifiers still reach the timing
126
+ * sample and, through it, the logs.
127
+ */
128
+ exposeTraceId?: boolean;
129
+ }
51
130
  /** Prometheus metrics endpoint configuration. */
52
131
  interface MetricsOptions {
53
132
  /** Register the metrics controller. Default: `false`. */
@@ -72,6 +151,10 @@ interface BymaxCoreModuleOptions {
72
151
  health?: HealthOptions;
73
152
  /** Prometheus metrics endpoint. Default: disabled. */
74
153
  metrics?: MetricsOptions;
154
+ /** OpenAPI document and UI. Default: disabled, and never served in production. */
155
+ openapi?: OpenApiOptions;
156
+ /** Trace correlation. Default: disabled. */
157
+ telemetry?: TelemetryOptions;
75
158
  }
76
159
  /** Fully-resolved envelope options. */
77
160
  interface ResolvedEnvelopeOptions {
@@ -89,6 +172,12 @@ interface ResolvedHealthOptions {
89
172
  path: string;
90
173
  indicatorTimeoutMs: number;
91
174
  exposeIndicatorErrors: boolean;
175
+ autoDiscover: boolean;
176
+ }
177
+ /** Fully-resolved telemetry options. */
178
+ interface ResolvedTelemetryOptions {
179
+ enabled: boolean;
180
+ exposeTraceId: boolean;
92
181
  }
93
182
  /** Fully-resolved metrics options. */
94
183
  interface ResolvedMetricsOptions {
@@ -97,6 +186,30 @@ interface ResolvedMetricsOptions {
97
186
  collectDefaultMetrics: boolean;
98
187
  defaultLabels: Record<string, string>;
99
188
  }
189
+ /** Fully-resolved OpenAPI options. */
190
+ interface ResolvedOpenApiOptions {
191
+ /**
192
+ * Whether the document is actually served. This is the consumer's request
193
+ * intersected with the runtime: it is always `false` in production, whatever
194
+ * the consumer asked for.
195
+ */
196
+ enabled: boolean;
197
+ /**
198
+ * `true` when the consumer asked for the document and the production guard
199
+ * refused it. Carried in the snapshot so the bootstrap helper can tell "the
200
+ * operator never wanted this" apart from "the operator wanted this and we
201
+ * declined", and warn only in the second case.
202
+ */
203
+ suppressedInProduction: boolean;
204
+ path: string;
205
+ jsonPath: string;
206
+ title: string;
207
+ description: string;
208
+ version: string;
209
+ servers: readonly OpenApiServerDescriptor[];
210
+ securitySchemes: Readonly<Record<string, OpenApiSecurityScheme>>;
211
+ includeCoreSchemas: boolean;
212
+ }
100
213
  /**
101
214
  * The effective, defaults-applied configuration exposed under
102
215
  * `BYMAX_CORE_OPTIONS`. Fields with a documented default are always present;
@@ -108,6 +221,8 @@ interface ResolvedCoreOptions {
108
221
  timing: ResolvedTimingOptions;
109
222
  health: ResolvedHealthOptions;
110
223
  metrics: ResolvedMetricsOptions;
224
+ openapi: ResolvedOpenApiOptions;
225
+ telemetry: ResolvedTelemetryOptions;
111
226
  }
112
227
 
113
228
  /** Non-option extras accepted by `forRoot` / `forRootAsync`. */
@@ -194,6 +309,12 @@ declare const BYMAX_HEALTH_INDICATORS: unique symbol;
194
309
  * lazily and only when the metrics feature is enabled.
195
310
  */
196
311
  declare const BYMAX_METRICS_REGISTRY: unique symbol;
312
+ /**
313
+ * Provide the `ITraceContextProvider` that reads the active span's identifiers.
314
+ * Bound on every path: the real reader when telemetry is enabled, a no-op that
315
+ * resolves nothing otherwise.
316
+ */
317
+ declare const BYMAX_TRACE_CONTEXT: unique symbol;
197
318
 
198
319
  /**
199
320
  * @fileoverview Correlation-id contract consumed by the error envelope. The
@@ -215,10 +336,51 @@ interface ICorrelationIdProvider {
215
336
  getCorrelationId(): string | undefined;
216
337
  }
217
338
 
339
+ /**
340
+ * @fileoverview Reading the active trace, and nothing else.
341
+ *
342
+ * When a tracer is running, every log line, error response and timing sample of
343
+ * a request can carry the same identifiers, which is what turns three separate
344
+ * signals into one story. This package reads those identifiers; it never starts
345
+ * a span, never configures an SDK, and never installs an exporter. That belongs
346
+ * to whatever instrumentation the operator already runs, and duplicating it here
347
+ * would produce two spans per request.
348
+ *
349
+ * `@opentelemetry/api` is an optional peer, but unlike the other two it is read
350
+ * on every request, so it cannot be imported lazily at the point of use. It is
351
+ * loaded once while the module resolves and then held: the dynamic import runs
352
+ * during bootstrap, and only when the feature is enabled.
353
+ * @layer Provider
354
+ */
355
+
356
+ /** The identifiers of the span a request is currently running under. */
357
+ interface TraceContext {
358
+ /** The trace this request belongs to, as a 32-character hex string. */
359
+ readonly traceId: string;
360
+ /** The span currently active, as a 16-character hex string. */
361
+ readonly spanId: string;
362
+ }
363
+ /**
364
+ * Resolves the current request's trace identifiers.
365
+ *
366
+ * Bound under `BYMAX_TRACE_CONTEXT`. Implementations must be cheap and must
367
+ * never throw: they run on the error path and on the timing path, where a
368
+ * failure would replace a real error with a telemetry one.
369
+ */
370
+ interface ITraceContextProvider {
371
+ /**
372
+ * Resolve the identifiers of the currently active span.
373
+ *
374
+ * @returns The active trace context, or `undefined` when nothing is traced.
375
+ */
376
+ getTraceContext(): TraceContext | undefined;
377
+ }
378
+
218
379
  /**
219
380
  * Neutral view of the current request handed to {@link BymaxExceptionFilter}
220
381
  * mappers and to the {@link BymaxExceptionFilter.onUnexpectedError} seam. It
221
- * exposes only the framework-agnostic surface (path, method, correlation id).
382
+ * exposes only the framework-agnostic surface (path, method, correlation id,
383
+ * trace id).
222
384
  */
223
385
  interface FilterErrorContext {
224
386
  /** HTTP method, read through the adapter (Express and Fastify neutral). */
@@ -227,6 +389,13 @@ interface FilterErrorContext {
227
389
  readonly path: string;
228
390
  /** Correlation id for the current request; absent when no provider resolves one. */
229
391
  readonly correlationId?: string;
392
+ /**
393
+ * Trace the request ran under; absent when telemetry is off or nothing was
394
+ * recording. Present here whatever `telemetry.exposeTraceId` says: the seam
395
+ * feeds a logging pipeline, where the id is exactly what makes an error
396
+ * findable, and only the response body is gated by that option.
397
+ */
398
+ readonly traceId?: string;
230
399
  }
231
400
  /**
232
401
  * Global exception filter emitting the stable error envelope. Registered as the
@@ -240,6 +409,8 @@ declare class BymaxExceptionFilter implements ExceptionFilter {
240
409
  private readonly now;
241
410
  /** The resolved correlation provider, or the no-op fallback when none is bound. */
242
411
  private readonly correlation;
412
+ /** The resolved trace-context provider, or the no-op fallback when none is bound. */
413
+ private readonly traceContext;
243
414
  /**
244
415
  * @param options - Resolved core options; drives the `exposeInternals` switch.
245
416
  * @param correlation - Provider resolving the current request's correlation id.
@@ -249,7 +420,7 @@ declare class BymaxExceptionFilter implements ExceptionFilter {
249
420
  * nothing is bound, this falls back to a no-op that omits `correlationId`.
250
421
  * @param adapterHost - Host of the live HTTP adapter, resolved lazily per catch.
251
422
  */
252
- constructor(options: ResolvedCoreOptions, correlation: ICorrelationIdProvider | undefined, adapterHost: HttpAdapterHost);
423
+ constructor(options: ResolvedCoreOptions, correlation: ICorrelationIdProvider | undefined, adapterHost: HttpAdapterHost, traceContext?: ITraceContextProvider);
253
424
  /**
254
425
  * Format the exception into the stable envelope and reply with it.
255
426
  *
@@ -259,6 +430,28 @@ declare class BymaxExceptionFilter implements ExceptionFilter {
259
430
  * @param exception - The error that escaped the handler.
260
431
  * @param host - The arguments host for the current execution context.
261
432
  */
433
+ /**
434
+ * Resolve one optional annotation for the envelope, treating any failure as
435
+ * "absent".
436
+ *
437
+ * Both annotations this filter attaches — the correlation id and the trace id
438
+ * — come from providers it does not own: one is supplied by the consumer, the
439
+ * other reads a third-party API. Their contracts say they do not throw, but
440
+ * this filter is the last thing standing between an error and the client, and
441
+ * a guarantee that depends on someone else's good behavior is not one. A
442
+ * failed lookup costs an optional field; an unguarded one would cost the whole
443
+ * response.
444
+ *
445
+ * The failure is deliberately silent, and the same reasoning applies as for
446
+ * the {@link BymaxExceptionFilter.onUnexpectedError} seam a few lines below:
447
+ * this runs while an error is already being formatted, so reporting a
448
+ * telemetry failure here would replace the failure the caller actually needs
449
+ * to see.
450
+ *
451
+ * @param read - The lookup to attempt.
452
+ * @returns The resolved value, or `undefined` when absent or on failure.
453
+ */
454
+ private readAnnotation;
262
455
  catch(exception: unknown, host: ArgumentsHost): void;
263
456
  /**
264
457
  * Select the mapping rule for the exception and build its envelope. An
@@ -339,6 +532,8 @@ type ErrorDetails = readonly unknown[] | Readonly<Record<string, unknown>>;
339
532
  * - `details` is present only when structured context exists (validation issues
340
533
  * or, in development, the collapsed internal error).
341
534
  * - `correlationId` is present only when a correlation provider resolves an id.
535
+ * - `traceId` is present only when telemetry is enabled, a span was recording,
536
+ * and `telemetry.exposeTraceId` opted into publishing it.
342
537
  */
343
538
  interface ErrorEnvelope {
344
539
  /** HTTP status code of the response. Always present. */
@@ -351,6 +546,8 @@ interface ErrorEnvelope {
351
546
  readonly details?: ErrorDetails;
352
547
  /** Correlation id for the current request. Present only when a provider resolves one. */
353
548
  readonly correlationId?: string;
549
+ /** Trace this request ran under. Present only when publishing it was opted into. */
550
+ readonly traceId?: string;
354
551
  /** ISO 8601 instant the error was formatted. Always present. */
355
552
  readonly timestamp: string;
356
553
  /** Request URL path. Always present. */
@@ -372,6 +569,8 @@ interface BuildErrorEnvelopeInput {
372
569
  readonly details?: ErrorDetails;
373
570
  /** Correlation id. Omit when none is bound; never pass `undefined`. */
374
571
  readonly correlationId?: string;
572
+ /** Trace id. Omit when absent or not opted into; never pass `undefined`. */
573
+ readonly traceId?: string;
375
574
  /** Request URL path. */
376
575
  readonly path: string;
377
576
  /** Injectable clock; called once to stamp the ISO 8601 timestamp. */
@@ -433,6 +632,14 @@ interface RequestTimingSample {
433
632
  durationMs: number;
434
633
  /** Whether the sample exceeded the configured slow-request threshold. */
435
634
  slow: boolean;
635
+ /**
636
+ * Trace this request ran under. Present only when telemetry is enabled and a
637
+ * span was recording, so a sink can correlate the sample with the trace
638
+ * without deciding what "no trace" looks like.
639
+ */
640
+ traceId?: string;
641
+ /** Span active when the request completed. Present under the same conditions. */
642
+ spanId?: string;
436
643
  }
437
644
  /**
438
645
  * Receive request-timing samples. Implementations must never throw: a sink
@@ -459,6 +666,8 @@ declare class TimingInterceptor implements NestInterceptor {
459
666
  private readonly clock;
460
667
  /** The bound timing sink, or the no-op fallback when none resolves. */
461
668
  private readonly sink;
669
+ /** The bound trace-context provider, or the no-op fallback when none resolves. */
670
+ private readonly traceContext;
462
671
  /**
463
672
  * @param options - Resolved core options; supplies `slowRequestThresholdMs`.
464
673
  * @param sink - The bound timing sink; its `record` failures are swallowed.
@@ -469,8 +678,12 @@ declare class TimingInterceptor implements NestInterceptor {
469
678
  * @param clock - Monotonic clock seam; defaults to `performance.now()`, and
470
679
  * is bound explicitly through {@link BYMAX_TIMING_CLOCK} so tests inject a
471
680
  * stub advancing by controlled amounts.
681
+ * @param traceContext - Reads the active span's identifiers. Injected with
682
+ * `@Optional()` so this interceptor stays constructible on its own; when
683
+ * nothing resolves, a no-op resolves no trace and the sample simply omits
684
+ * the fields.
472
685
  */
473
- constructor(options: ResolvedCoreOptions, sink: ITimingSink | undefined, clock?: MonotonicClock);
686
+ constructor(options: ResolvedCoreOptions, sink: ITimingSink | undefined, clock?: MonotonicClock, traceContext?: ITraceContextProvider);
474
687
  /**
475
688
  * Measure the handler chain and record exactly one sample per completed
476
689
  * request, on the success path and on the error path alike.
@@ -560,4 +773,4 @@ declare const BYMAX_GATEWAY_TIMEOUT = "BYMAX_GATEWAY_TIMEOUT";
560
773
  */
561
774
  declare function codeForStatus(status: number): string;
562
775
 
563
- 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_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 MetricsOptions, type RequestTimingSample, type ResolvedCoreOptions, TimingInterceptor, type TimingOptions, buildErrorEnvelope, codeForStatus };
776
+ 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 };