@bymax-one/nest-core 1.3.2 → 1.5.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
@@ -1,5 +1,5 @@
1
1
  import * as _nestjs_common from '@nestjs/common';
2
- import { DynamicModule, ExceptionFilter, ArgumentsHost, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
2
+ import { NestModule, MiddlewareConsumer, DynamicModule, ExceptionFilter, ArgumentsHost, NestInterceptor, ExecutionContext, CallHandler, NestMiddleware } from '@nestjs/common';
3
3
  import { HttpAdapterHost } from '@nestjs/core';
4
4
  import { Observable } from 'rxjs';
5
5
 
@@ -106,6 +106,21 @@ type OpenApiOperationKey = `${OpenApiHttpMethod} /${string}`;
106
106
  * An empty array marks the operation public, overriding any document default.
107
107
  */
108
108
  type OperationSecurityMap = Readonly<Record<OpenApiOperationKey, readonly OpenApiSecurityRequirement[]>>;
109
+ /**
110
+ * Names the operation a route handler produced, in the generated document.
111
+ *
112
+ * Matches `@nestjs/swagger`'s own factory signature so a consumer's existing one
113
+ * can be handed over unchanged. This package installs a factory of its own to
114
+ * learn which handler produced which operation — a contract a library keys its
115
+ * contributed fragments against — and delegates the id string to this one when
116
+ * it is set, so nothing an application already generates is renamed.
117
+ *
118
+ * @param controllerKey - The controller class name.
119
+ * @param methodKey - The handler method name.
120
+ * @param version - The route's version, when the application versions routes.
121
+ * @returns The operation id to publish.
122
+ */
123
+ type OpenApiOperationIdFactory = (controllerKey: string, methodKey: string, version?: string) => string;
109
124
  /**
110
125
  * OpenAPI document configuration.
111
126
  *
@@ -171,6 +186,16 @@ interface OpenApiOptions {
171
186
  * }
172
187
  */
173
188
  operationSecurity?: OperationSecurityMap;
189
+ /**
190
+ * Name the operations in the generated document. Default: the format
191
+ * `@nestjs/swagger` itself produces, `<ControllerKey>_<methodKey>`.
192
+ *
193
+ * Set it to control the ids a client generator will use. This package always
194
+ * installs a factory of its own so it can learn which handler produced which
195
+ * operation, and delegates to this one when it is set — so supplying it
196
+ * changes the published ids, and leaving it unset changes nothing.
197
+ */
198
+ operationIdFactory?: OpenApiOperationIdFactory;
174
199
  /**
175
200
  * Contribute the schemas this package owns — the error envelope, the health
176
201
  * response, and the pagination shapes — to the document's components, and
@@ -304,6 +329,7 @@ interface ResolvedOpenApiOptions {
304
329
  securitySchemes: Readonly<Record<string, OpenApiSecurityScheme>>;
305
330
  security: readonly OpenApiSecurityRequirement[];
306
331
  operationSecurity: OperationSecurityMap;
332
+ operationIdFactory?: OpenApiOperationIdFactory;
307
333
  includeCoreSchemas: boolean;
308
334
  }
309
335
  /**
@@ -338,7 +364,69 @@ declare const ASYNC_OPTIONS_TYPE: _nestjs_common.ConfigurableModuleAsyncOptions<
338
364
  /**
339
365
  * `BymaxCoreModule`, the application foundation module for NestJS 11.
340
366
  */
341
- declare class BymaxCoreModule extends BymaxCoreModuleBase {
367
+ declare class BymaxCoreModule extends BymaxCoreModuleBase implements NestModule {
368
+ private readonly resolved;
369
+ private readonly adapterHost?;
370
+ /**
371
+ * @param options - The resolved snapshot, read to decide whether the timing
372
+ * middleware is applied at all.
373
+ */
374
+ constructor(resolved: ResolvedCoreOptions, adapterHost?: HttpAdapterHost | undefined);
375
+ /**
376
+ * Apply the request-timing middleware to every route.
377
+ *
378
+ * Middleware rather than the interceptor this replaced, because guards run
379
+ * before interceptors: a request rejected by an authentication, authorization
380
+ * or throttling guard never reached the interceptor, and one matching no
381
+ * route never reached a controller at all. A deployment could therefore be
382
+ * under a credential-stuffing run — a flood of 401s — with a flat error
383
+ * graph, which is why this is a security fix rather than a metrics
384
+ * improvement.
385
+ *
386
+ * Applied on both registration paths, and only when timing is enabled: the
387
+ * middleware is the sole recorder now, since a second one would count every
388
+ * matched request twice.
389
+ *
390
+ * There is no single pattern that covers both adapters, which is the whole
391
+ * reason this reads the adapter first. Measured, requesting the root, a
392
+ * parameterised route, a nested path and an unmatched path, with and without
393
+ * `setGlobalPrefix('api')`:
394
+ *
395
+ * | `forRoutes(...)` | Express | Fastify |
396
+ * | ---------------- | ---------------- | -------------------- |
397
+ * | `'*splat'` | skips the root | — |
398
+ * | `'{*splat}'` | skips `/api` | every path |
399
+ * | `'/'` | every path | matches `/` only |
400
+ *
401
+ * On Express `'/'` is a mount and matches everything beneath whatever prefix
402
+ * it is scoped to, while `'{*splat}'` — the form Nest 11's migration guide
403
+ * prescribes for "all routes" — stops matching the prefixed root once an
404
+ * application calls `setGlobalPrefix`. That was reported as nest#14520 and
405
+ * fixed by nest#14522, whose regression test covers Fastify; on
406
+ * `@nestjs/core` 11.1.28 with the Express adapter the prefixed root still
407
+ * reaches no middleware while resolving to `200`.
408
+ *
409
+ * On Fastify the same `'/'` is an exact match rather than a mount — one of
410
+ * three requests reached the middleware — so the wildcard is the only form
411
+ * that works there.
412
+ *
413
+ * A recorder that quietly omits one route is the same class of defect this
414
+ * middleware exists to fix, so each adapter gets the form that omits none.
415
+ *
416
+ * Fastify also needs {@link bridgeFastifyRouteMetadata}, because middie hands
417
+ * middleware the raw request, which carries no route metadata at all; see
418
+ * that file for why the label would otherwise be `<unmatched>` for every
419
+ * request.
420
+ *
421
+ * One limit stays and is documented in the README: Nest scopes module
422
+ * middleware to the global prefix, so with `setGlobalPrefix('api')` a request
423
+ * to `/nope` — outside the prefix entirely — reaches no middleware and is not
424
+ * recorded, while `/api/nope` is. No `forRoutes` argument changes that, and
425
+ * nothing this module can register reaches outside its own scope.
426
+ *
427
+ * @param consumer - Nest's middleware consumer.
428
+ */
429
+ configure(consumer: MiddlewareConsumer): void;
342
430
  /**
343
431
  * Register the module synchronously. Options are known now, so disabled
344
432
  * features are omitted from the providers and controllers arrays and the
@@ -428,7 +516,7 @@ declare const BYMAX_CORE_OPTIONS: unique symbol;
428
516
  */
429
517
  declare const BYMAX_CORRELATION_PROVIDER: unique symbol;
430
518
  /**
431
- * Provide the `ITimingSink` that receives one sample per completed request.
519
+ * Provide the `ITimingSink` that receives one sample per closed request.
432
520
  * Defaults to a no-op sink.
433
521
  */
434
522
  declare const BYMAX_TIMING_SINK: unique symbol;
@@ -745,9 +833,11 @@ interface MonotonicClock {
745
833
  }
746
834
 
747
835
  /**
748
- * @fileoverview Request-timing contracts. The timing interceptor emits one
749
- * {@link RequestTimingSample} per completed request to the bound
750
- * {@link ITimingSink}. The default sink is a no-op; consumers plug in a logger
836
+ * @fileoverview Request-timing contracts. `BymaxTimingMiddleware` emits one
837
+ * {@link RequestTimingSample} per **closed** request to the bound
838
+ * {@link ITimingSink} every request the server finished with, including the
839
+ * ones a guard rejected, the ones that matched no route, and the ones a client
840
+ * abandoned mid-flight. The default sink is a no-op; consumers plug in a logger
751
841
  * bridge or the metrics bridge through the `BYMAX_TIMING_SINK` token.
752
842
  * @layer Contract
753
843
  */
@@ -777,12 +867,16 @@ interface RequestTimingSample {
777
867
  }
778
868
  /**
779
869
  * Receive request-timing samples. Implementations must never throw: a sink
780
- * failure is caught and silenced by the interceptor so timing never breaks a
870
+ * failure is caught and silenced by the recorder so timing never breaks a
781
871
  * request.
782
872
  */
783
873
  interface ITimingSink {
784
874
  /**
785
- * Record one sample for a completed request.
875
+ * Record one sample for a closed request, however it ended.
876
+ *
877
+ * Called once per request the server closed — not only the ones a handler
878
+ * answered. A rejection issued by a guard, a request matching no route, and a
879
+ * client that hung up mid-response all arrive here.
786
880
  *
787
881
  * @param sample - The timing sample to record.
788
882
  */
@@ -790,10 +884,15 @@ interface ITimingSink {
790
884
  }
791
885
 
792
886
  /**
793
- * Request-timing interceptor. Registered as the `APP_INTERCEPTOR` when the
794
- * timing feature is enabled, on both the sync and async registration paths.
795
- * Non-HTTP execution contexts (GraphQL, RPC) pass through untouched: this
796
- * feature is HTTP-first, matching the exception filter's documented scope.
887
+ * Request-timing interceptor. No longer registered by `BymaxCoreModule`, which
888
+ * records through {@link BymaxTimingMiddleware} instead so that requests ended
889
+ * by a guard or by no route matching are counted too. Non-HTTP execution
890
+ * contexts (GraphQL, RPC) pass through untouched: this feature is HTTP-first,
891
+ * matching the exception filter's documented scope.
892
+ *
893
+ * @deprecated Since 1.4.0, superseded by `BymaxTimingMiddleware`, which the
894
+ * module registers automatically. Registering this interceptor as well
895
+ * records a second sample for every request that reaches a handler.
797
896
  */
798
897
  declare class TimingInterceptor implements NestInterceptor {
799
898
  private readonly options;
@@ -842,9 +941,9 @@ declare class TimingInterceptor implements NestInterceptor {
842
941
  */
843
942
  private readErrorStatus;
844
943
  /**
845
- * Build the sample, compute the slow flag, and deliver it to the sink inside
846
- * a try/catch that silences any failure: a throwing sink must never affect
847
- * the request it is observing.
944
+ * Build the sample and deliver it to the sink inside a try/catch that
945
+ * silences any failure: a throwing sink must never affect the request it is
946
+ * observing.
848
947
  *
849
948
  * @param method - HTTP method of the request.
850
949
  * @param route - Route template of the request.
@@ -854,6 +953,133 @@ declare class TimingInterceptor implements NestInterceptor {
854
953
  private recordSample;
855
954
  }
856
955
 
956
+ /**
957
+ * @fileoverview Neutral request-info accessor for request timing. Reads the
958
+ * HTTP method and the route template off a request without assuming Express or
959
+ * Fastify, so `RequestTimingSample.route` always carries a bounded-cardinality
960
+ * label instead of the raw URL.
961
+ *
962
+ * The bound is a security property, not a tidiness one. Requests that match no
963
+ * route are exactly the ones a scanner produces, each at a different path, so a
964
+ * label taken from the URL would mint one time series per probe — turning the
965
+ * scrape endpoint into the most expensive route in the service and the metric
966
+ * into the outage. Every unmatched request therefore shares one label.
967
+ *
968
+ * Dropping the raw-URL fallback bought a **second** guarantee that is worth
969
+ * stating because it was a side effect rather than the intent, and anyone
970
+ * weighing the fallback again would otherwise only re-weigh the cardinality
971
+ * argument. The recorder is middleware mounted at `'/'`, and Express gives
972
+ * mounted middleware a `req.url` relative to its mount point: under
973
+ * `setGlobalPrefix('api')` a request to `/api` arrives as `/`, so a path read
974
+ * there reports somewhere the caller never asked for. Nothing in this file
975
+ * reads `req.url` any more — the only `.url` left is Fastify's
976
+ * `routeOptions.url`, which is a template, not a path — so that class of bug
977
+ * has nowhere to land. Reintroducing a path-derived label brings both problems
978
+ * back, not just the cardinality one. If a raw path is ever genuinely needed,
979
+ * the correct read is `req.originalUrl ?? req.url`: `originalUrl` is Express's
980
+ * and carries the mount prefix, and the `??` covers an adapter that supplies
981
+ * only `url`, where no mount trimmed it in the first place.
982
+ * @layer Utility
983
+ */
984
+
985
+ /**
986
+ * The route label carried by every request that matched no route.
987
+ *
988
+ * A single constant rather than the request's own path, and exported rather
989
+ * than inlined: it is the value an alert rule matches on to see a scan, so it
990
+ * belongs to the contract and must not drift from what this package writes.
991
+ * Reading `route="<unmatched>"` in a dashboard says what happened; an empty
992
+ * label — which Prometheus treats as equivalent to no label at all — reads as
993
+ * missing data and gets scrolled past, which for a signal whose purpose is to
994
+ * be noticed is the same as not emitting it.
995
+ */
996
+ declare const UNMATCHED_ROUTE = "<unmatched>";
997
+ /** Structural shape of an Express request's route metadata. */
998
+ interface ExpressRouteShape {
999
+ route?: {
1000
+ path?: string;
1001
+ };
1002
+ baseUrl?: string;
1003
+ }
1004
+ /** Structural shape of a Fastify request's route metadata. */
1005
+ interface FastifyRouteShape {
1006
+ routeOptions?: {
1007
+ url?: string;
1008
+ };
1009
+ }
1010
+ /** Structural shape shared by both frameworks for the method. */
1011
+ interface RawRequestShape {
1012
+ method?: string;
1013
+ }
1014
+ /** The combined structural shape read off the request object. */
1015
+ type RequestShape = ExpressRouteShape & FastifyRouteShape & RawRequestShape;
1016
+
1017
+ /** The part of a response this middleware reads and listens on. */
1018
+ interface ResponseShape {
1019
+ /** Node's event emitter, used for the single `'close'` subscription. */
1020
+ on(event: 'close', listener: () => void): unknown;
1021
+ /**
1022
+ * Final status code. Optional and explicitly nullable: a connection can close
1023
+ * before anything settled one, and that case has to be expressible rather
1024
+ * than assumed away.
1025
+ */
1026
+ statusCode?: number | undefined;
1027
+ }
1028
+ /**
1029
+ * Records one timing sample per request, whatever ended it.
1030
+ *
1031
+ * Registered by `BymaxCoreModule` for every route when the timing feature is
1032
+ * enabled. It replaces the interceptor as the recorder rather than joining it:
1033
+ * two recorders would count every matched request twice, which is a worse
1034
+ * defect than the one being fixed because it is silent and plausible.
1035
+ */
1036
+ declare class BymaxTimingMiddleware implements NestMiddleware {
1037
+ private readonly options;
1038
+ private readonly clock;
1039
+ /** The bound sink, or an in-code no-op when nothing is bound. */
1040
+ private readonly sink;
1041
+ /** The bound trace reader, or an in-code no-op when nothing is bound. */
1042
+ private readonly traceContext;
1043
+ /**
1044
+ * @param options - The resolved options, read for the slow-request threshold.
1045
+ * @param sink - The bound timing sink; its failures are swallowed, because a
1046
+ * throwing sink must never affect the request it is observing.
1047
+ * @param clock - Monotonic clock seam, so a test advances time by controlled
1048
+ * amounts instead of sleeping.
1049
+ * @param traceContext - Reads the active span's identifiers. Optional: the
1050
+ * middleware stays constructible on its own, and a no-op resolves nothing.
1051
+ */
1052
+ constructor(options: ResolvedCoreOptions, sink: ITimingSink | undefined, clock?: MonotonicClock, traceContext?: ITraceContextProvider);
1053
+ /**
1054
+ * Start the measurement and arrange for the sample to be recorded once the
1055
+ * connection closes, then hand the request straight on.
1056
+ *
1057
+ * The route is read inside the listener rather than here: at middleware time
1058
+ * the router has not matched yet, so `req.route` is still empty. By the time
1059
+ * the connection closes it is populated — including for a request a guard
1060
+ * rejected, since matching happens before guards run.
1061
+ *
1062
+ * The trace lookup is tried in the live context first and in this moment's
1063
+ * captured context second; see this file's header for the measurements that
1064
+ * decided that order.
1065
+ *
1066
+ * @param request - The framework request object.
1067
+ * @param response - The framework response object.
1068
+ * @param next - Continues the chain; called synchronously and unconditionally.
1069
+ */
1070
+ use(request: RequestShape, response: ResponseShape, next: () => void): void;
1071
+ /**
1072
+ * Build the sample and hand it to the sink, guarding both steps.
1073
+ *
1074
+ * @param request - The framework request object.
1075
+ * @param response - The framework response object.
1076
+ * @param start - Monotonic timestamp captured before the chain ran.
1077
+ * @param trace - The span identifiers already resolved by the caller, which
1078
+ * owns the choice of which context to read them from.
1079
+ */
1080
+ private record;
1081
+ }
1082
+
857
1083
  /**
858
1084
  * @fileoverview Stable `BYMAX_*` error-code catalog and HTTP-status derivation.
859
1085
  * These codes are the machine-readable half of the error envelope contract and
@@ -907,4 +1133,4 @@ declare const BYMAX_GATEWAY_TIMEOUT = "BYMAX_GATEWAY_TIMEOUT";
907
1133
  */
908
1134
  declare function codeForStatus(status: number): string;
909
1135
 
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 };
1136
+ 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, BymaxTimingMiddleware, type EnvelopeOptions, type ErrorDetails, type ErrorEnvelope, type FilterErrorContext, type HealthOptions, type ICorrelationIdProvider, type ITimingSink, type ITraceContextProvider, type MetricsOptions, type OpenApiHttpMethod, type OpenApiOperationIdFactory, type OpenApiOperationKey, type OpenApiOptions, type OpenApiSecurityRequirement, type OpenApiSecurityScheme, type OpenApiServerDescriptor, type OperationSecurityMap, type RequestTimingSample, type ResolvedCoreOptions, type TelemetryOptions, TimingInterceptor, type TimingOptions, type TraceContext, UNMATCHED_ROUTE, buildErrorEnvelope, codeForStatus };