@bymax-one/nest-core 1.0.0 → 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.ts 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,104 @@ 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;
43
+ /**
44
+ * Include the failing indicator's message in the readiness response under
45
+ * `details.error`. Never enable in production. Default: `false`.
46
+ *
47
+ * Readiness is typically unauthenticated and reachable by whatever probes it,
48
+ * and an indicator usually does not author its own failure message — it lets a
49
+ * driver's error propagate, and driver errors carry hosts, ports and sometimes
50
+ * credentials. With this off, the response names which indicator is down and
51
+ * nothing else; the message goes to the logger, where access is already
52
+ * controlled.
53
+ */
54
+ exposeIndicatorErrors?: boolean;
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;
38
129
  }
39
130
  /** Prometheus metrics endpoint configuration. */
40
131
  interface MetricsOptions {
@@ -60,6 +151,10 @@ interface BymaxCoreModuleOptions {
60
151
  health?: HealthOptions;
61
152
  /** Prometheus metrics endpoint. Default: disabled. */
62
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;
63
158
  }
64
159
  /** Fully-resolved envelope options. */
65
160
  interface ResolvedEnvelopeOptions {
@@ -76,6 +171,13 @@ interface ResolvedHealthOptions {
76
171
  enabled: boolean;
77
172
  path: string;
78
173
  indicatorTimeoutMs: number;
174
+ exposeIndicatorErrors: boolean;
175
+ autoDiscover: boolean;
176
+ }
177
+ /** Fully-resolved telemetry options. */
178
+ interface ResolvedTelemetryOptions {
179
+ enabled: boolean;
180
+ exposeTraceId: boolean;
79
181
  }
80
182
  /** Fully-resolved metrics options. */
81
183
  interface ResolvedMetricsOptions {
@@ -84,6 +186,30 @@ interface ResolvedMetricsOptions {
84
186
  collectDefaultMetrics: boolean;
85
187
  defaultLabels: Record<string, string>;
86
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
+ }
87
213
  /**
88
214
  * The effective, defaults-applied configuration exposed under
89
215
  * `BYMAX_CORE_OPTIONS`. Fields with a documented default are always present;
@@ -95,6 +221,8 @@ interface ResolvedCoreOptions {
95
221
  timing: ResolvedTimingOptions;
96
222
  health: ResolvedHealthOptions;
97
223
  metrics: ResolvedMetricsOptions;
224
+ openapi: ResolvedOpenApiOptions;
225
+ telemetry: ResolvedTelemetryOptions;
98
226
  }
99
227
 
100
228
  /** Non-option extras accepted by `forRoot` / `forRootAsync`. */
@@ -181,6 +309,12 @@ declare const BYMAX_HEALTH_INDICATORS: unique symbol;
181
309
  * lazily and only when the metrics feature is enabled.
182
310
  */
183
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;
184
318
 
185
319
  /**
186
320
  * @fileoverview Correlation-id contract consumed by the error envelope. The
@@ -202,10 +336,51 @@ interface ICorrelationIdProvider {
202
336
  getCorrelationId(): string | undefined;
203
337
  }
204
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
+
205
379
  /**
206
380
  * Neutral view of the current request handed to {@link BymaxExceptionFilter}
207
381
  * mappers and to the {@link BymaxExceptionFilter.onUnexpectedError} seam. It
208
- * exposes only the framework-agnostic surface (path, method, correlation id).
382
+ * exposes only the framework-agnostic surface (path, method, correlation id,
383
+ * trace id).
209
384
  */
210
385
  interface FilterErrorContext {
211
386
  /** HTTP method, read through the adapter (Express and Fastify neutral). */
@@ -214,6 +389,13 @@ interface FilterErrorContext {
214
389
  readonly path: string;
215
390
  /** Correlation id for the current request; absent when no provider resolves one. */
216
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;
217
399
  }
218
400
  /**
219
401
  * Global exception filter emitting the stable error envelope. Registered as the
@@ -227,6 +409,8 @@ declare class BymaxExceptionFilter implements ExceptionFilter {
227
409
  private readonly now;
228
410
  /** The resolved correlation provider, or the no-op fallback when none is bound. */
229
411
  private readonly correlation;
412
+ /** The resolved trace-context provider, or the no-op fallback when none is bound. */
413
+ private readonly traceContext;
230
414
  /**
231
415
  * @param options - Resolved core options; drives the `exposeInternals` switch.
232
416
  * @param correlation - Provider resolving the current request's correlation id.
@@ -236,7 +420,7 @@ declare class BymaxExceptionFilter implements ExceptionFilter {
236
420
  * nothing is bound, this falls back to a no-op that omits `correlationId`.
237
421
  * @param adapterHost - Host of the live HTTP adapter, resolved lazily per catch.
238
422
  */
239
- constructor(options: ResolvedCoreOptions, correlation: ICorrelationIdProvider | undefined, adapterHost: HttpAdapterHost);
423
+ constructor(options: ResolvedCoreOptions, correlation: ICorrelationIdProvider | undefined, adapterHost: HttpAdapterHost, traceContext?: ITraceContextProvider);
240
424
  /**
241
425
  * Format the exception into the stable envelope and reply with it.
242
426
  *
@@ -246,6 +430,28 @@ declare class BymaxExceptionFilter implements ExceptionFilter {
246
430
  * @param exception - The error that escaped the handler.
247
431
  * @param host - The arguments host for the current execution context.
248
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;
249
455
  catch(exception: unknown, host: ArgumentsHost): void;
250
456
  /**
251
457
  * Select the mapping rule for the exception and build its envelope. An
@@ -326,6 +532,8 @@ type ErrorDetails = readonly unknown[] | Readonly<Record<string, unknown>>;
326
532
  * - `details` is present only when structured context exists (validation issues
327
533
  * or, in development, the collapsed internal error).
328
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.
329
537
  */
330
538
  interface ErrorEnvelope {
331
539
  /** HTTP status code of the response. Always present. */
@@ -338,6 +546,8 @@ interface ErrorEnvelope {
338
546
  readonly details?: ErrorDetails;
339
547
  /** Correlation id for the current request. Present only when a provider resolves one. */
340
548
  readonly correlationId?: string;
549
+ /** Trace this request ran under. Present only when publishing it was opted into. */
550
+ readonly traceId?: string;
341
551
  /** ISO 8601 instant the error was formatted. Always present. */
342
552
  readonly timestamp: string;
343
553
  /** Request URL path. Always present. */
@@ -359,6 +569,8 @@ interface BuildErrorEnvelopeInput {
359
569
  readonly details?: ErrorDetails;
360
570
  /** Correlation id. Omit when none is bound; never pass `undefined`. */
361
571
  readonly correlationId?: string;
572
+ /** Trace id. Omit when absent or not opted into; never pass `undefined`. */
573
+ readonly traceId?: string;
362
574
  /** Request URL path. */
363
575
  readonly path: string;
364
576
  /** Injectable clock; called once to stamp the ISO 8601 timestamp. */
@@ -420,6 +632,14 @@ interface RequestTimingSample {
420
632
  durationMs: number;
421
633
  /** Whether the sample exceeded the configured slow-request threshold. */
422
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;
423
643
  }
424
644
  /**
425
645
  * Receive request-timing samples. Implementations must never throw: a sink
@@ -446,6 +666,8 @@ declare class TimingInterceptor implements NestInterceptor {
446
666
  private readonly clock;
447
667
  /** The bound timing sink, or the no-op fallback when none resolves. */
448
668
  private readonly sink;
669
+ /** The bound trace-context provider, or the no-op fallback when none resolves. */
670
+ private readonly traceContext;
449
671
  /**
450
672
  * @param options - Resolved core options; supplies `slowRequestThresholdMs`.
451
673
  * @param sink - The bound timing sink; its `record` failures are swallowed.
@@ -456,8 +678,12 @@ declare class TimingInterceptor implements NestInterceptor {
456
678
  * @param clock - Monotonic clock seam; defaults to `performance.now()`, and
457
679
  * is bound explicitly through {@link BYMAX_TIMING_CLOCK} so tests inject a
458
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.
459
685
  */
460
- constructor(options: ResolvedCoreOptions, sink: ITimingSink | undefined, clock?: MonotonicClock);
686
+ constructor(options: ResolvedCoreOptions, sink: ITimingSink | undefined, clock?: MonotonicClock, traceContext?: ITraceContextProvider);
461
687
  /**
462
688
  * Measure the handler chain and record exactly one sample per completed
463
689
  * request, on the success path and on the error path alike.
@@ -547,4 +773,4 @@ declare const BYMAX_GATEWAY_TIMEOUT = "BYMAX_GATEWAY_TIMEOUT";
547
773
  */
548
774
  declare function codeForStatus(status: number): string;
549
775
 
550
- 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 };