@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/dist/index.d.ts 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
- * Default: `true`.
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 };
package/dist/index.mjs CHANGED
@@ -15,6 +15,10 @@ var __decorateClass = (decorators, target, key, kind) => {
15
15
  };
16
16
  var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
17
17
 
18
+ // src/route-defaults.ts
19
+ var DEFAULT_HEALTH_PATH = "health";
20
+ var DEFAULT_METRICS_PATH = "metrics";
21
+
18
22
  // src/runtime.environment.ts
19
23
  var NON_PRODUCTION_ENVIRONMENTS = /* @__PURE__ */ new Set(["development", "test"]);
20
24
  function isProductionRuntime(value = process.env["NODE_ENV"]) {
@@ -25,9 +29,7 @@ function isProductionRuntime(value = process.env["NODE_ENV"]) {
25
29
  }
26
30
 
27
31
  // src/core.options.ts
28
- var DEFAULT_HEALTH_PATH = "health";
29
32
  var DEFAULT_INDICATOR_TIMEOUT_MS = 5e3;
30
- var DEFAULT_METRICS_PATH = "metrics";
31
33
  var DEFAULT_OPENAPI_PATH = "docs";
32
34
  var DEFAULT_OPENAPI_JSON_PATH = "docs-json";
33
35
  var DEFAULT_OPENAPI_TITLE = "API";
@@ -103,6 +105,11 @@ function resolveOpenApi(raw) {
103
105
  // consumer-owned nested objects, and the deep-freeze below would otherwise
104
106
  // reach into them.
105
107
  securitySchemes: structuredClone(raw?.securitySchemes ?? {}),
108
+ // Cloned for the same reason as the schemes above: both are consumer-owned
109
+ // nested structures, and the deep-freeze applied to the snapshot would
110
+ // otherwise reach into objects the consumer still holds a reference to.
111
+ security: structuredClone(raw?.security ?? []),
112
+ operationSecurity: structuredClone(raw?.operationSecurity ?? {}),
106
113
  includeCoreSchemas: raw?.includeCoreSchemas ?? true
107
114
  };
108
115
  }
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var common = require('@nestjs/common');
4
+ var core = require('@nestjs/core');
4
5
 
5
6
  // src/openapi/openapi.bootstrap.ts
6
7
 
@@ -16,6 +17,10 @@ function isProductionRuntime(value = process.env["NODE_ENV"]) {
16
17
  return !NON_PRODUCTION_ENVIRONMENTS.has(value.trim().toLowerCase());
17
18
  }
18
19
 
20
+ // src/route-defaults.ts
21
+ var DEFAULT_HEALTH_PATH = "health";
22
+ var DEFAULT_METRICS_PATH = "metrics";
23
+
19
24
  // src/envelope/error-codes.ts
20
25
  var BYMAX_BAD_REQUEST = "BYMAX_BAD_REQUEST";
21
26
  var BYMAX_VALIDATION_FAILED = "BYMAX_VALIDATION_FAILED";
@@ -177,6 +182,19 @@ var CORE_PARAMETERS = {
177
182
  };
178
183
 
179
184
  // src/openapi/openapi.document.ts
185
+ var OPERATION_METHODS = [
186
+ "get",
187
+ "post",
188
+ "put",
189
+ "patch",
190
+ "delete",
191
+ "head",
192
+ "options",
193
+ "trace"
194
+ ];
195
+ var METRICS_SCHEME_NAME = "BymaxMetricsAuth";
196
+ var ERROR_ENVELOPE_SCHEMA = "BymaxErrorEnvelope";
197
+ var HEALTH_RESPONSE_SCHEMA = "BymaxHealthResponse";
180
198
  function asRecord(value) {
181
199
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
182
200
  return {};
@@ -186,21 +204,192 @@ function asRecord(value) {
186
204
  function mergeAbsent(existing, additions) {
187
205
  return { ...additions, ...existing };
188
206
  }
189
- function augmentDocument(document, options) {
207
+ function operationsOf(item) {
208
+ return Object.entries(asRecord(item)).filter(([key]) => OPERATION_METHODS.includes(key));
209
+ }
210
+ function mergeResponses(existing, additions) {
211
+ const merged = new Map(Object.entries(existing));
212
+ for (const [status, contributed] of Object.entries(additions)) {
213
+ const current = asRecord(merged.get(status));
214
+ if (current["content"] === void 0 && current["$ref"] === void 0) {
215
+ const described = current["description"] === void 0 || current["description"] === "" ? contributed["description"] : current["description"];
216
+ merged.set(status, { ...current, ...contributed, description: described });
217
+ }
218
+ }
219
+ return Object.fromEntries(merged);
220
+ }
221
+ function trimSlashes(segment) {
222
+ return segment.split("/").filter((part) => part !== "").join("/");
223
+ }
224
+ function routePath(prefix, suffix) {
225
+ return prefix === "" ? `/${suffix}` : `/${prefix}/${suffix}`;
226
+ }
227
+ function healthRoutes(options) {
228
+ const bases = /* @__PURE__ */ new Set([trimSlashes(options.health.path), DEFAULT_HEALTH_PATH]);
229
+ return [...bases].flatMap((base) => [`${base}/live`, `${base}/ready`]);
230
+ }
231
+ function metricsRoutes(options) {
232
+ return [.../* @__PURE__ */ new Set([trimSlashes(options.metrics.path), DEFAULT_METRICS_PATH])];
233
+ }
234
+ function indexOwnRoutes(options, prefixes) {
235
+ const normalized = prefixes.map(trimSlashes);
236
+ const expand = (suffixes) => normalized.flatMap((prefix) => suffixes.map((suffix) => routePath(prefix, suffix)));
237
+ const health = expand(healthRoutes(options));
238
+ const metrics = expand(metricsRoutes(options));
239
+ return {
240
+ isHealth: (path) => health.includes(path),
241
+ isMetrics: (path) => metrics.includes(path)
242
+ };
243
+ }
244
+ function withoutDisabledRoutes(paths, options, routes) {
245
+ const disabled = (path) => !options.health.enabled && routes.isHealth(path) || !options.metrics.enabled && routes.isMetrics(path);
246
+ const kept = Object.entries(paths).map(([path, item]) => {
247
+ if (!disabled(path)) {
248
+ return [path, item];
249
+ }
250
+ const remaining = Object.fromEntries(
251
+ Object.entries(asRecord(item)).filter(([key]) => key !== "get")
252
+ );
253
+ return [path, operationsOf(remaining).length === 0 ? void 0 : remaining];
254
+ });
255
+ return Object.fromEntries(kept.filter(([, item]) => item !== void 0));
256
+ }
257
+ function ownRouteSecurity(path, method, options, routes) {
258
+ if (method !== "get") {
259
+ return void 0;
260
+ }
261
+ if (options.metrics.authToken !== void 0 && routes.isMetrics(path)) {
262
+ return [{ [METRICS_SCHEME_NAME]: [] }];
263
+ }
264
+ if (options.openapi.security.length > 0 && routes.isHealth(path)) {
265
+ return [];
266
+ }
267
+ return void 0;
268
+ }
269
+ function operationKey(method, path) {
270
+ return `${method.toUpperCase()} ${path}`;
271
+ }
272
+ function coreResponses(path, options, routes) {
273
+ const responses = {};
274
+ if (options.envelope.enabled) {
275
+ responses["default"] = {
276
+ description: "Error envelope returned by every failing request.",
277
+ content: {
278
+ "application/json": { schema: { $ref: `#/components/schemas/${ERROR_ENVELOPE_SCHEMA}` } }
279
+ }
280
+ };
281
+ }
282
+ if (routes.isHealth(path)) {
283
+ responses["200"] = {
284
+ description: "Aggregated health report.",
285
+ content: {
286
+ "application/json": { schema: { $ref: `#/components/schemas/${HEALTH_RESPONSE_SCHEMA}` } }
287
+ }
288
+ };
289
+ }
290
+ return responses;
291
+ }
292
+ function augmentOperation(operation, path, method, options, routes) {
293
+ const result = { ...operation };
294
+ if (result["security"] === void 0) {
295
+ const override = options.openapi.operationSecurity[operationKey(method, path)];
296
+ const security = override ?? ownRouteSecurity(path, method, options, routes);
297
+ if (security !== void 0) {
298
+ result["security"] = security;
299
+ }
300
+ }
301
+ if (options.openapi.includeCoreSchemas) {
302
+ result["responses"] = mergeResponses(
303
+ asRecord(result["responses"]),
304
+ coreResponses(path, options, routes)
305
+ );
306
+ }
307
+ return result;
308
+ }
309
+ function assertSchemesDeclared(openapi, schemes) {
310
+ const required = [...openapi.security, ...Object.values(openapi.operationSecurity).flat()];
311
+ const named = [...new Set(required.flatMap((requirement) => Object.keys(requirement)))];
312
+ const declared = Object.keys(schemes);
313
+ const missing = named.filter((name) => !declared.includes(name));
314
+ if (missing.length === 0) {
315
+ return;
316
+ }
317
+ throw new Error(
318
+ `[BymaxCoreModule] openapi security names ${missing.length} scheme(s) that the document does not define: ${missing.join(", ")}. Declare them in openapi.securitySchemes, or drop the requirement. The document defines: ${declared.length === 0 ? "(none)" : declared.join(", ")}.`
319
+ );
320
+ }
321
+ function assertScrapeSchemeIsOurs(openapi, components, contributes) {
322
+ if (!contributes) {
323
+ return;
324
+ }
325
+ const declaredByConsumer = Object.keys(openapi.securitySchemes).includes(METRICS_SCHEME_NAME);
326
+ const declaredByDocument = Object.keys(asRecord(components["securitySchemes"])).includes(
327
+ METRICS_SCHEME_NAME
328
+ );
329
+ if (!declaredByConsumer && !declaredByDocument) {
330
+ return;
331
+ }
332
+ throw new Error(
333
+ `[BymaxCoreModule] the security scheme "${METRICS_SCHEME_NAME}" is reserved: this package contributes it to document the bearer token the scrape endpoint checks, and it is already defined ${declaredByConsumer ? "in openapi.securitySchemes" : "by the generated document"}. Rename yours, or unset metrics.authToken if the endpoint is not protected.`
334
+ );
335
+ }
336
+ function documentedOperationKeys(paths) {
337
+ return Object.entries(paths).flatMap(
338
+ ([path, item]) => operationsOf(item).map(([method]) => operationKey(method, path))
339
+ );
340
+ }
341
+ function assertOverridesMatch(paths, openapi) {
342
+ const configured = Object.keys(openapi.operationSecurity);
343
+ const documented = documentedOperationKeys(paths);
344
+ const unmatched = configured.filter((key) => !documented.includes(key));
345
+ if (unmatched.length === 0) {
346
+ return;
347
+ }
348
+ throw new Error(
349
+ `[BymaxCoreModule] openapi.operationSecurity addresses ${unmatched.length} operation(s) that the document does not contain: ${unmatched.join(", ")}. Keys are "<METHOD> <path>" with the path exactly as documented, including any global prefix. The document contains: ${documented.length === 0 ? "(none)" : documented.join(", ")}.`
350
+ );
351
+ }
352
+ function augmentPaths(paths, options, routes) {
353
+ return Object.fromEntries(
354
+ Object.entries(paths).map(([path, item]) => {
355
+ const augmented = operationsOf(item).map(([method, operation]) => [
356
+ method,
357
+ augmentOperation(asRecord(operation), path, method, options, routes)
358
+ ]);
359
+ return [path, { ...asRecord(item), ...Object.fromEntries(augmented) }];
360
+ })
361
+ );
362
+ }
363
+ function augmentDocument(document, options, pathPrefixes = [""]) {
364
+ const { openapi } = options;
190
365
  const components = asRecord(document.components);
191
366
  const merged = { ...components };
192
- if (options.includeCoreSchemas) {
367
+ if (openapi.includeCoreSchemas) {
193
368
  merged["schemas"] = mergeAbsent(asRecord(components["schemas"]), CORE_SCHEMAS);
194
369
  merged["parameters"] = mergeAbsent(asRecord(components["parameters"]), CORE_PARAMETERS);
195
370
  }
196
- const securitySchemeNames = Object.keys(options.securitySchemes);
197
- if (securitySchemeNames.length > 0) {
198
- merged["securitySchemes"] = mergeAbsent(
199
- asRecord(components["securitySchemes"]),
200
- options.securitySchemes
201
- );
371
+ const scrapeScheme = options.metrics.authToken === void 0 ? {} : {
372
+ [METRICS_SCHEME_NAME]: {
373
+ type: "http",
374
+ scheme: "bearer",
375
+ description: "Bearer token required by the metrics scrape endpoint."
376
+ }
377
+ };
378
+ assertScrapeSchemeIsOurs(openapi, components, options.metrics.authToken !== void 0);
379
+ const schemes = mergeAbsent(asRecord(components["securitySchemes"]), {
380
+ ...openapi.securitySchemes,
381
+ ...scrapeScheme
382
+ });
383
+ if (Object.keys(schemes).length > 0) {
384
+ merged["securitySchemes"] = schemes;
202
385
  }
203
- return { ...document, components: merged };
386
+ assertSchemesDeclared(openapi, schemes);
387
+ const routes = indexOwnRoutes(options, pathPrefixes);
388
+ const served = withoutDisabledRoutes(asRecord(document.paths), options, routes);
389
+ assertOverridesMatch(served, openapi);
390
+ const paths = document.paths === void 0 ? {} : { paths: augmentPaths(served, options, routes) };
391
+ const security = openapi.security.length > 0 && document.security === void 0 ? { security: openapi.security } : {};
392
+ return { ...document, components: merged, ...paths, ...security };
204
393
  }
205
394
 
206
395
  // src/optional-peer.ts
@@ -234,6 +423,35 @@ function resolveCoreOptions(app) {
234
423
  throw new Error(OPTIONS_UNRESOLVED_MESSAGE, { cause });
235
424
  }
236
425
  }
426
+ function readAppConfig(app) {
427
+ try {
428
+ return app.get(core.ApplicationConfig);
429
+ } catch {
430
+ return void 0;
431
+ }
432
+ }
433
+ function versionSegments(versioning) {
434
+ if (versioning === void 0 || versioning.type !== common.VersioningType.URI) {
435
+ return [""];
436
+ }
437
+ const prefix = versioning.prefix === false ? "" : versioning.prefix ?? "v";
438
+ const declared = versioning.defaultVersion;
439
+ if (declared === void 0) {
440
+ return [""];
441
+ }
442
+ const versions = Array.isArray(declared) ? declared : [declared];
443
+ return versions.map(
444
+ (version) => version === common.VERSION_NEUTRAL ? "" : `${prefix}${String(version)}`
445
+ );
446
+ }
447
+ function readPathPrefixes(app) {
448
+ const config = readAppConfig(app);
449
+ if (config === void 0) {
450
+ return [""];
451
+ }
452
+ const globalPrefix = config.getGlobalPrefix();
453
+ return versionSegments(config.getVersioning()).map((segment) => `${globalPrefix}/${segment}`);
454
+ }
237
455
  function buildConfig(builder, options) {
238
456
  builder.setTitle(options.title).setDescription(options.description).setVersion(options.version);
239
457
  for (const server of options.servers) {
@@ -243,7 +461,8 @@ function buildConfig(builder, options) {
243
461
  }
244
462
  async function applyBymaxOpenApi(app) {
245
463
  const logger = new common.Logger("BymaxCoreModule");
246
- const options = resolveCoreOptions(app).openapi;
464
+ const resolved = resolveCoreOptions(app);
465
+ const options = resolved.openapi;
247
466
  if (isProductionRuntime()) {
248
467
  if (options.suppressedInProduction || options.enabled) {
249
468
  logger.warn(
@@ -257,7 +476,11 @@ async function applyBymaxOpenApi(app) {
257
476
  }
258
477
  const swagger = await loadSwagger();
259
478
  const config = buildConfig(new swagger.DocumentBuilder(), options);
260
- const document = augmentDocument(swagger.SwaggerModule.createDocument(app, config), options);
479
+ const document = augmentDocument(
480
+ swagger.SwaggerModule.createDocument(app, config),
481
+ resolved,
482
+ readPathPrefixes(app)
483
+ );
261
484
  swagger.SwaggerModule.setup(options.path, app, document, {
262
485
  jsonDocumentUrl: options.jsonPath
263
486
  });
@@ -30,10 +30,20 @@ interface OpenApiMountOutcome {
30
30
  * It is safe to call unconditionally: with the feature disabled, or in
31
31
  * production, it mounts nothing, loads no optional peer, and returns why.
32
32
  *
33
+ * Testing this under Jest needs one flag. `@nestjs/swagger` is loaded through a
34
+ * dynamic `import()`, which is what keeps the peer optional for everyone who
35
+ * never enables the document — and Jest's module registry cannot service a
36
+ * dynamic import without `NODE_OPTIONS=--experimental-vm-modules`. Without it,
37
+ * only the *enabled* path fails, with `dynamic import callback invoked without
38
+ * --experimental-vm-modules`; the disabled and production paths never reach the
39
+ * loader and pass either way, which is what makes the omission confusing.
40
+ *
33
41
  * @param app - The created Nest application, not yet listening.
34
42
  * @returns What happened: mounted, or skipped with a reason.
35
- * @throws Error When `BymaxCoreModule` is not registered, or when the feature is
36
- * enabled and the optional peer `@nestjs/swagger` is not installed.
43
+ * @throws Error When `BymaxCoreModule` is not registered, when the feature is
44
+ * enabled and the optional peer `@nestjs/swagger` is not installed, or when
45
+ * `openapi.operationSecurity` addresses an operation the generated document
46
+ * does not contain.
37
47
  * @example
38
48
  * const app = await NestFactory.create(AppModule)
39
49
  * await applyBymaxOpenApi(app)
@@ -30,10 +30,20 @@ interface OpenApiMountOutcome {
30
30
  * It is safe to call unconditionally: with the feature disabled, or in
31
31
  * production, it mounts nothing, loads no optional peer, and returns why.
32
32
  *
33
+ * Testing this under Jest needs one flag. `@nestjs/swagger` is loaded through a
34
+ * dynamic `import()`, which is what keeps the peer optional for everyone who
35
+ * never enables the document — and Jest's module registry cannot service a
36
+ * dynamic import without `NODE_OPTIONS=--experimental-vm-modules`. Without it,
37
+ * only the *enabled* path fails, with `dynamic import callback invoked without
38
+ * --experimental-vm-modules`; the disabled and production paths never reach the
39
+ * loader and pass either way, which is what makes the omission confusing.
40
+ *
33
41
  * @param app - The created Nest application, not yet listening.
34
42
  * @returns What happened: mounted, or skipped with a reason.
35
- * @throws Error When `BymaxCoreModule` is not registered, or when the feature is
36
- * enabled and the optional peer `@nestjs/swagger` is not installed.
43
+ * @throws Error When `BymaxCoreModule` is not registered, when the feature is
44
+ * enabled and the optional peer `@nestjs/swagger` is not installed, or when
45
+ * `openapi.operationSecurity` addresses an operation the generated document
46
+ * does not contain.
37
47
  * @example
38
48
  * const app = await NestFactory.create(AppModule)
39
49
  * await applyBymaxOpenApi(app)