@bymax-one/nest-core 1.4.0 → 1.5.1

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 CHANGED
@@ -11,6 +11,91 @@ heading here.
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [1.5.1] - 2026-08-15
15
+
16
+ Documentation only; no source change. The 1.5.0 warning's known-limit note told
17
+ readers that no tool could catch the one shape the warning cannot report, and
18
+ that rendering the document twice was therefore the only check. That is true
19
+ only of something reading the rendered document alone — a consumer's own suite
20
+ knows the intent and can assert it on every commit — so the note was arguing
21
+ against the better practice.
22
+
23
+ **Apply to a derived backend:** nothing to change in code. Read the revised
24
+ "Documenting authentication" section and write the assertion it now shows; it
25
+ costs one test and replaces a manual step nobody remembers to run.
26
+
27
+ ### Documentation
28
+
29
+ - **The credential-free warning's known limit no longer argues against the
30
+ practice that covers it.** The README said no tool could distinguish a
31
+ document that lost its requirements from one that never had any, and that
32
+ rendering the document twice was therefore the only check. The first half is
33
+ true only of something reading the rendered document alone; a consumer's own
34
+ suite knows which of the two it is and can assert it on every commit. Saying
35
+ otherwise did not merely overstate a limit — it told readers that the standing
36
+ check they should write does not exist. The section now attributes the limit
37
+ correctly, states why the warning cannot fire on that shape (it follows from
38
+ the trigger, in every version), gives the assertion as the practice, and
39
+ leaves render-and-diff the narrower job it is genuinely good at: seeing what
40
+ moved when you change something, so you can turn it into an assertion.
41
+
42
+ - **A contributed scheme's presence is documented as part of the contributor's
43
+ configuration.** Which security schemes a library contributes can depend on
44
+ how that library is configured — the names are stable, their presence is not.
45
+ A document-level default must therefore be derived from the same configuration
46
+ the contributor reads. A literal is correct only for the configuration it was
47
+ written against: elsewhere it either resolves while describing one of two
48
+ credentials a route accepts (quietly incomplete) or names a scheme nobody
49
+ declares (a failed document build). Guarding on whether the scheme exists
50
+ clears the loud case and ships the quiet one.
51
+
52
+ - **What a document-level default does not let you say** is now stated. Its
53
+ entries are alternatives applied to every operation that says nothing, so a
54
+ backend with two credential families _can_ list both and nothing rejects it —
55
+ the result asserts that either credential works for every inheriting route,
56
+ which is false in the permissive direction. The minority family belongs in
57
+ `openapi.operationSecurity`, which outranks the default.
58
+
59
+ ## [1.5.0] - 2026-08-15
60
+
61
+ An OpenAPI document could stop requiring credentials without anything saying
62
+ so. Deleting a document-level `security` default — typically alongside the
63
+ per-operation entries a library has taken over describing — leaves every route
64
+ the backend itself owns with no requirement from any source, and the document
65
+ stays valid, no requirement dangles, and the runtime still answers `401`. The
66
+ only observable change is that a client generated from the document stops
67
+ sending credentials.
68
+
69
+ **Apply to a derived backend:** bump the dependency. No code change is needed.
70
+ If the boot log now names operations, they are the ones a generated client will
71
+ call without credentials — set `openapi.security`, or mark each public with an
72
+ explicit `[]` in `openapi.operationSecurity`.
73
+
74
+ ### Added
75
+
76
+ - **The document build warns when an operation ends up requiring no credential
77
+ at all.** Deleting a document-level `security` default — typically alongside
78
+ the per-operation entries a library has taken over describing — leaves every
79
+ route the backend itself owns with no requirement from any source. Nothing
80
+ catches it today: the document is valid, no requirement dangles so
81
+ `assertSchemesDeclared` is satisfied, the runtime still answers `401` so a
82
+ status-code probe finds nothing, and a consumer's document test stays green if
83
+ it asserts only the operations it enumerated. The only observable change is
84
+ that a client generated from the document sends no credentials.
85
+ `applyBymaxOpenApi` now emits one warning per build naming the affected
86
+ operations, capped at ten with a count of the rest.
87
+
88
+ It warns and never throws — an API that is public on purpose is legitimate —
89
+ and the trigger is narrow so the line stays worth reading: only when the
90
+ document declares no top-level `security`, **and** at least one other
91
+ operation does state a requirement, **and** the operation is not one of the
92
+ three this package registers. An explicit `[]` — from `operationSecurity`, a
93
+ decorator, or a library's fragment — states the intent and stops the report.
94
+ The known limit is documented rather than closed: a document with nothing
95
+ explicit anywhere is indistinguishable from an API that is public on purpose,
96
+ so removing _every_ requirement at once is not warned. Render the document
97
+ with and without your libraries and diff the operations you mount.
98
+
14
99
  ## [1.4.0] - 2026-08-13
15
100
 
16
101
  HTTP metrics were blind to every request that did not reach a handler. Nest runs
@@ -649,4 +734,6 @@ have regressed from. They are kept because the reasoning is worth having.
649
734
  [1.2.1]: https://github.com/bymaxone/nest-core/compare/v1.2.0...v1.2.1
650
735
  [1.2.0]: https://github.com/bymaxone/nest-core/compare/v1.1.1...v1.2.0
651
736
  [1.4.0]: https://github.com/bymaxone/nest-core/compare/v1.3.2...v1.4.0
652
- [Unreleased]: https://github.com/bymaxone/nest-core/compare/v1.4.0...HEAD
737
+ [1.5.0]: https://github.com/bymaxone/nest-core/compare/v1.4.0...v1.5.0
738
+ [1.5.1]: https://github.com/bymaxone/nest-core/compare/v1.5.0...v1.5.1
739
+ [Unreleased]: https://github.com/bymaxone/nest-core/compare/v1.5.1...HEAD
package/README.md CHANGED
@@ -379,6 +379,126 @@ configuration went wrong. If you gate the document behind an environment flag,
379
379
  make sure at least one environment that runs your tests has it on, or these
380
380
  checks never fire.
381
381
 
382
+ #### When an operation ends up requiring nothing
383
+
384
+ The checks above catch a requirement that points at nothing. The opposite
385
+ mistake — an operation that ends up pointing at _no requirement at all_ — breaks
386
+ nothing, and that is what makes it dangerous. It is a valid document. No name
387
+ dangles. The runtime still answers `401`. A document test that asserts only the
388
+ operations you enumerated stays green. The only thing that changed is that a
389
+ client generated from the document now sends no credentials.
390
+
391
+ The edit that produces it is ordinary: a library starts describing its own
392
+ routes, its adoption note tells you to delete the entries you had written for
393
+ them, and the document-level `security` default is sitting in the same options
394
+ block and goes with them. Every route _you_ own then reads as public.
395
+
396
+ So the document build says so, once, naming the operations:
397
+
398
+ ```
399
+ [BymaxCoreModule] a client generated from the OpenAPI document will send no
400
+ credentials to 2 operation(s): GET /examples, POST /orders. They state no
401
+ security requirement, the document declares no default, and other operations in
402
+ it do state one — so this is more often a missing openapi.security default than
403
+ a public API. Set openapi.security, or state the intent per operation with an
404
+ explicit [] in openapi.operationSecurity.
405
+ ```
406
+
407
+ It is a **warning, not a failure**: an API that is public on purpose is a
408
+ legitimate configuration, and failing a boot over one would be worse than the
409
+ silence it replaces. The trigger is deliberately narrow, so the line stays worth
410
+ reading:
411
+
412
+ | Condition | Why |
413
+ | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
414
+ | The document declares no top-level `security` | With a default present, a bare operation inherits it. An explicit `security: []` counts as an answer, not an omission. |
415
+ | At least one other operation _does_ state a requirement | Somebody described a posture on purpose, so the bare ones beside it are an omission rather than a public API. |
416
+ | The operation is not one of this package's own | A health probe carrying nothing is the correct description of a route an orchestrator polls without a credential. |
417
+
418
+ The escape hatch is to say what you mean, in the document's own vocabulary
419
+ rather than by silencing output: `operationSecurity: { 'GET /examples': [] }`
420
+ marks the operation public, and it stops being reported. A library can do the
421
+ same for its own routes by contributing `security: []` in its fragment.
422
+
423
+ **There is one shape this warning cannot report.** If you remove _every_
424
+ requirement at once — no library describing anything, no decorator, no override,
425
+ no document default — the second condition above is never met: nothing states a
426
+ requirement, so there are no bare operations sitting _beside_ described ones.
427
+ That follows from the trigger and therefore holds in every version; it is not a
428
+ gap a later release closes, and a reader who expects one is exactly the reader
429
+ who stops checking.
430
+
431
+ The limit belongs to **anything reading only the rendered document**. Such a
432
+ document is indistinguishable from that of an API which is public on purpose —
433
+ both are a set of operations asking for nothing — and this package cannot tell
434
+ them apart without also warning at every genuinely public API, which is how a
435
+ warning earns the right to be ignored.
436
+
437
+ **Your own suite has no such handicap, because you know which one you are.**
438
+ Assert it, and the check runs on every commit rather than when somebody
439
+ remembers to look:
440
+
441
+ ```ts
442
+ it('still requires a credential everywhere it should', () => {
443
+ const document = buildYourDocument()
444
+
445
+ // Assert the default you expect, not merely that one exists. `[]` is a
446
+ // defined value that requires nothing, so a "toBeDefined" check passes for
447
+ // a default that degraded to empty — which is the regression this test is
448
+ // here to catch, wearing the shape of a pass.
449
+ expect(document.security).toEqual([{ cookieAuth: [] }])
450
+
451
+ // An explicit `[]` is how an operation says "public". The set of operations
452
+ // saying it should be the set you meant — no more, no fewer.
453
+ expect(publicOperationsOf(document)).toEqual(['POST /auth/login'])
454
+ })
455
+ ```
456
+
457
+ Render-and-diff keeps a narrower job, and it is a good one: when you are
458
+ _changing_ something — adopting a library that describes its own routes, moving
459
+ a default — render with and without the change and compare the operations you
460
+ mount. It shows you what moved without your having to predict it. Then turn what
461
+ it showed you into an assertion, so the next change is caught rather than
462
+ inspected.
463
+
464
+ #### A contributed scheme's presence is part of the contributor's configuration
465
+
466
+ A library contributes security schemes, and **which** ones it contributes can
467
+ depend on how you configured it. Any of them, gated on any of its inputs, and
468
+ often on more than one — the scheme you have in mind may be the absent one, and
469
+ the setting you are thinking of may not be the only gate. The names are stable;
470
+ their presence is not.
471
+
472
+ That has one consequence worth stating as a rule, because getting it wrong
473
+ produces a failure at either end of the loudness scale:
474
+
475
+ > Derive a document-level default from **the same configuration the contributor
476
+ > reads**, never from the scheme names, and never from what the document
477
+ > happened to contain before you adopted the library.
478
+
479
+ Writing the name as a literal is correct only for the configuration you wrote it
480
+ against. Under a configuration that declares that scheme plus another, the
481
+ default still resolves but describes one of two credentials the route accepts —
482
+ **quietly incomplete**. Under one that declares it not at all, the name resolves
483
+ to nothing and the document build **fails** with the undeclared-scheme error
484
+ above. Guarding on whether the scheme exists is the tempting fix and it is the
485
+ wrong one: it clears the case that already announced itself and ships the one
486
+ that does not.
487
+
488
+ Note what a document-level default does not let you _say_. Its entries are
489
+ **alternatives** — any one of them satisfies an operation — and they apply to
490
+ every operation that states nothing. So a backend whose routes sit behind **two
491
+ different credential families** can certainly list both, and nothing rejects it:
492
+ the result is a document asserting that _either_ credential works for _every_
493
+ inheriting route. That is not an incomplete document, it is a false one, and it
494
+ is false in the permissive direction — it tells a client that a credential the
495
+ route will reject is one the route accepts.
496
+
497
+ Give the majority family the default and the minority explicit
498
+ `operationSecurity` entries. Those outrank the default, and an operation
499
+ carrying one is never named by the warning above, because it states a
500
+ requirement.
501
+
382
502
  ## 🔑 DI Tokens
383
503
 
384
504
  Every token is a `Symbol`. `BYMAX_CORRELATION_PROVIDER` and
@@ -131,6 +131,34 @@ function collectContributions(discovery, reflector, handlers) {
131
131
  var DEFAULT_HEALTH_PATH = "health";
132
132
  var DEFAULT_METRICS_PATH = "metrics";
133
133
 
134
+ // src/openapi/openapi.routes.ts
135
+ function trimSlashes(segment) {
136
+ return segment.split("/").filter((part) => part !== "").join("/");
137
+ }
138
+ function routePath(prefix, suffix) {
139
+ return prefix === "" ? `/${suffix}` : `/${prefix}/${suffix}`;
140
+ }
141
+ function healthRoutes(options) {
142
+ const bases = /* @__PURE__ */ new Set([trimSlashes(options.health.path), DEFAULT_HEALTH_PATH]);
143
+ return [...bases].flatMap((base) => [`${base}/live`, `${base}/ready`]);
144
+ }
145
+ function metricsRoutes(options) {
146
+ return [.../* @__PURE__ */ new Set([trimSlashes(options.metrics.path), DEFAULT_METRICS_PATH])];
147
+ }
148
+ function indexOwnRoutes(options, prefixes) {
149
+ const normalized = prefixes.map(trimSlashes);
150
+ const expand = (suffixes) => normalized.flatMap((prefix) => suffixes.map((suffix) => routePath(prefix, suffix)));
151
+ const health = expand(healthRoutes(options));
152
+ const metrics = expand(metricsRoutes(options));
153
+ return {
154
+ isHealth: (path) => health.includes(path),
155
+ isMetrics: (path) => metrics.includes(path)
156
+ };
157
+ }
158
+ function isOwnRoute(path, method, routes) {
159
+ return method === "get" && (routes.isHealth(path) || routes.isMetrics(path));
160
+ }
161
+
134
162
  // src/envelope/error-codes.ts
135
163
  var BYMAX_BAD_REQUEST = "BYMAX_BAD_REQUEST";
136
164
  var BYMAX_VALIDATION_FAILED = "BYMAX_VALIDATION_FAILED";
@@ -329,29 +357,6 @@ function mergeResponses(existing, additions) {
329
357
  }
330
358
  return Object.fromEntries(merged);
331
359
  }
332
- function trimSlashes(segment) {
333
- return segment.split("/").filter((part) => part !== "").join("/");
334
- }
335
- function routePath(prefix, suffix) {
336
- return prefix === "" ? `/${suffix}` : `/${prefix}/${suffix}`;
337
- }
338
- function healthRoutes(options) {
339
- const bases = /* @__PURE__ */ new Set([trimSlashes(options.health.path), DEFAULT_HEALTH_PATH]);
340
- return [...bases].flatMap((base) => [`${base}/live`, `${base}/ready`]);
341
- }
342
- function metricsRoutes(options) {
343
- return [.../* @__PURE__ */ new Set([trimSlashes(options.metrics.path), DEFAULT_METRICS_PATH])];
344
- }
345
- function indexOwnRoutes(options, prefixes) {
346
- const normalized = prefixes.map(trimSlashes);
347
- const expand = (suffixes) => normalized.flatMap((prefix) => suffixes.map((suffix) => routePath(prefix, suffix)));
348
- const health = expand(healthRoutes(options));
349
- const metrics = expand(metricsRoutes(options));
350
- return {
351
- isHealth: (path) => health.includes(path),
352
- isMetrics: (path) => metrics.includes(path)
353
- };
354
- }
355
360
  function withoutDisabledRoutes(paths, options, routes) {
356
361
  const disabled = (path) => !options.health.enabled && routes.isHealth(path) || !options.metrics.enabled && routes.isMetrics(path);
357
362
  const kept = Object.entries(paths).map(([path, item]) => {
@@ -435,6 +440,25 @@ function augmentOperation(operation, path, method, options, routes, contribution
435
440
  }
436
441
  return result;
437
442
  }
443
+ function consumerOperations(paths, routes) {
444
+ return Object.entries(paths).flatMap(
445
+ ([path, item]) => operationsOf(item).filter(([method]) => !isOwnRoute(path, method, routes)).map(([method, operation]) => ({
446
+ key: operationKey(method, path),
447
+ operation: asRecord(operation)
448
+ }))
449
+ );
450
+ }
451
+ function unsecuredOperations(document, options, pathPrefixes = [""]) {
452
+ if (document.security !== void 0) {
453
+ return [];
454
+ }
455
+ const routes = indexOwnRoutes(options, pathPrefixes);
456
+ const candidates = consumerOperations(asRecord(document.paths), routes);
457
+ if (!candidates.some(({ operation }) => operation["security"] !== void 0)) {
458
+ return [];
459
+ }
460
+ return candidates.filter(({ operation }) => operation["security"] === void 0).map(({ key }) => key);
461
+ }
438
462
  function assertSchemesDeclared(openapi, schemes) {
439
463
  const required = [...openapi.security, ...Object.values(openapi.operationSecurity).flat()];
440
464
  const named = [...new Set(required.flatMap((requirement) => Object.keys(requirement)))];
@@ -549,6 +573,7 @@ async function loadSwagger() {
549
573
  }
550
574
 
551
575
  // src/openapi/openapi.bootstrap.ts
576
+ var MAX_WARNED_OPERATIONS = 10;
552
577
  var OPTIONS_UNRESOLVED_MESSAGE = "[BymaxCoreModule] applyBymaxOpenApi could not resolve BYMAX_CORE_OPTIONS from the application. Register BymaxCoreModule (forRoot or forRootAsync) before calling it, and keep the module global or import it into the module you bootstrap.";
553
578
  function resolveCoreOptions(app) {
554
579
  try {
@@ -607,6 +632,16 @@ function readContributions(app, handlers) {
607
632
  }
608
633
  return collectContributions(discovery, reflector, handlers);
609
634
  }
635
+ function warnUnsecuredOperations(logger, keys) {
636
+ if (keys.length === 0) {
637
+ return;
638
+ }
639
+ const elided = keys.length - MAX_WARNED_OPERATIONS;
640
+ const listed = keys.slice(0, MAX_WARNED_OPERATIONS).join(", ");
641
+ logger.warn(
642
+ `a client generated from the OpenAPI document will send no credentials to ${keys.length} operation(s): ${listed}${elided > 0 ? `, and ${elided} more` : ""}. They state no security requirement, the document declares no default, and other operations in it do state one \u2014 so this is more often a missing openapi.security default than a public API. Set openapi.security, or state the intent per operation with an explicit [] in openapi.operationSecurity.`
643
+ );
644
+ }
610
645
  function buildConfig(builder, options) {
611
646
  builder.setTitle(options.title).setDescription(options.description).setVersion(options.version);
612
647
  for (const server of options.servers) {
@@ -635,12 +670,9 @@ async function applyBymaxOpenApi(app) {
635
670
  const generated = swagger.SwaggerModule.createDocument(app, config, {
636
671
  operationIdFactory: recordingOperationIdFactory(handlers, options.operationIdFactory)
637
672
  });
638
- const document = augmentDocument(
639
- generated,
640
- resolved,
641
- readPathPrefixes(app),
642
- readContributions(app, handlers)
643
- );
673
+ const prefixes = readPathPrefixes(app);
674
+ const document = augmentDocument(generated, resolved, prefixes, readContributions(app, handlers));
675
+ warnUnsecuredOperations(logger, unsecuredOperations(document, resolved, prefixes));
644
676
  swagger.SwaggerModule.setup(options.path, app, document, {
645
677
  jsonDocumentUrl: options.jsonPath
646
678
  });
@@ -129,6 +129,34 @@ function collectContributions(discovery, reflector, handlers) {
129
129
  var DEFAULT_HEALTH_PATH = "health";
130
130
  var DEFAULT_METRICS_PATH = "metrics";
131
131
 
132
+ // src/openapi/openapi.routes.ts
133
+ function trimSlashes(segment) {
134
+ return segment.split("/").filter((part) => part !== "").join("/");
135
+ }
136
+ function routePath(prefix, suffix) {
137
+ return prefix === "" ? `/${suffix}` : `/${prefix}/${suffix}`;
138
+ }
139
+ function healthRoutes(options) {
140
+ const bases = /* @__PURE__ */ new Set([trimSlashes(options.health.path), DEFAULT_HEALTH_PATH]);
141
+ return [...bases].flatMap((base) => [`${base}/live`, `${base}/ready`]);
142
+ }
143
+ function metricsRoutes(options) {
144
+ return [.../* @__PURE__ */ new Set([trimSlashes(options.metrics.path), DEFAULT_METRICS_PATH])];
145
+ }
146
+ function indexOwnRoutes(options, prefixes) {
147
+ const normalized = prefixes.map(trimSlashes);
148
+ const expand = (suffixes) => normalized.flatMap((prefix) => suffixes.map((suffix) => routePath(prefix, suffix)));
149
+ const health = expand(healthRoutes(options));
150
+ const metrics = expand(metricsRoutes(options));
151
+ return {
152
+ isHealth: (path) => health.includes(path),
153
+ isMetrics: (path) => metrics.includes(path)
154
+ };
155
+ }
156
+ function isOwnRoute(path, method, routes) {
157
+ return method === "get" && (routes.isHealth(path) || routes.isMetrics(path));
158
+ }
159
+
132
160
  // src/envelope/error-codes.ts
133
161
  var BYMAX_BAD_REQUEST = "BYMAX_BAD_REQUEST";
134
162
  var BYMAX_VALIDATION_FAILED = "BYMAX_VALIDATION_FAILED";
@@ -327,29 +355,6 @@ function mergeResponses(existing, additions) {
327
355
  }
328
356
  return Object.fromEntries(merged);
329
357
  }
330
- function trimSlashes(segment) {
331
- return segment.split("/").filter((part) => part !== "").join("/");
332
- }
333
- function routePath(prefix, suffix) {
334
- return prefix === "" ? `/${suffix}` : `/${prefix}/${suffix}`;
335
- }
336
- function healthRoutes(options) {
337
- const bases = /* @__PURE__ */ new Set([trimSlashes(options.health.path), DEFAULT_HEALTH_PATH]);
338
- return [...bases].flatMap((base) => [`${base}/live`, `${base}/ready`]);
339
- }
340
- function metricsRoutes(options) {
341
- return [.../* @__PURE__ */ new Set([trimSlashes(options.metrics.path), DEFAULT_METRICS_PATH])];
342
- }
343
- function indexOwnRoutes(options, prefixes) {
344
- const normalized = prefixes.map(trimSlashes);
345
- const expand = (suffixes) => normalized.flatMap((prefix) => suffixes.map((suffix) => routePath(prefix, suffix)));
346
- const health = expand(healthRoutes(options));
347
- const metrics = expand(metricsRoutes(options));
348
- return {
349
- isHealth: (path) => health.includes(path),
350
- isMetrics: (path) => metrics.includes(path)
351
- };
352
- }
353
358
  function withoutDisabledRoutes(paths, options, routes) {
354
359
  const disabled = (path) => !options.health.enabled && routes.isHealth(path) || !options.metrics.enabled && routes.isMetrics(path);
355
360
  const kept = Object.entries(paths).map(([path, item]) => {
@@ -433,6 +438,25 @@ function augmentOperation(operation, path, method, options, routes, contribution
433
438
  }
434
439
  return result;
435
440
  }
441
+ function consumerOperations(paths, routes) {
442
+ return Object.entries(paths).flatMap(
443
+ ([path, item]) => operationsOf(item).filter(([method]) => !isOwnRoute(path, method, routes)).map(([method, operation]) => ({
444
+ key: operationKey(method, path),
445
+ operation: asRecord(operation)
446
+ }))
447
+ );
448
+ }
449
+ function unsecuredOperations(document, options, pathPrefixes = [""]) {
450
+ if (document.security !== void 0) {
451
+ return [];
452
+ }
453
+ const routes = indexOwnRoutes(options, pathPrefixes);
454
+ const candidates = consumerOperations(asRecord(document.paths), routes);
455
+ if (!candidates.some(({ operation }) => operation["security"] !== void 0)) {
456
+ return [];
457
+ }
458
+ return candidates.filter(({ operation }) => operation["security"] === void 0).map(({ key }) => key);
459
+ }
436
460
  function assertSchemesDeclared(openapi, schemes) {
437
461
  const required = [...openapi.security, ...Object.values(openapi.operationSecurity).flat()];
438
462
  const named = [...new Set(required.flatMap((requirement) => Object.keys(requirement)))];
@@ -547,6 +571,7 @@ async function loadSwagger() {
547
571
  }
548
572
 
549
573
  // src/openapi/openapi.bootstrap.ts
574
+ var MAX_WARNED_OPERATIONS = 10;
550
575
  var OPTIONS_UNRESOLVED_MESSAGE = "[BymaxCoreModule] applyBymaxOpenApi could not resolve BYMAX_CORE_OPTIONS from the application. Register BymaxCoreModule (forRoot or forRootAsync) before calling it, and keep the module global or import it into the module you bootstrap.";
551
576
  function resolveCoreOptions(app) {
552
577
  try {
@@ -605,6 +630,16 @@ function readContributions(app, handlers) {
605
630
  }
606
631
  return collectContributions(discovery, reflector, handlers);
607
632
  }
633
+ function warnUnsecuredOperations(logger, keys) {
634
+ if (keys.length === 0) {
635
+ return;
636
+ }
637
+ const elided = keys.length - MAX_WARNED_OPERATIONS;
638
+ const listed = keys.slice(0, MAX_WARNED_OPERATIONS).join(", ");
639
+ logger.warn(
640
+ `a client generated from the OpenAPI document will send no credentials to ${keys.length} operation(s): ${listed}${elided > 0 ? `, and ${elided} more` : ""}. They state no security requirement, the document declares no default, and other operations in it do state one \u2014 so this is more often a missing openapi.security default than a public API. Set openapi.security, or state the intent per operation with an explicit [] in openapi.operationSecurity.`
641
+ );
642
+ }
608
643
  function buildConfig(builder, options) {
609
644
  builder.setTitle(options.title).setDescription(options.description).setVersion(options.version);
610
645
  for (const server of options.servers) {
@@ -633,12 +668,9 @@ async function applyBymaxOpenApi(app) {
633
668
  const generated = swagger.SwaggerModule.createDocument(app, config, {
634
669
  operationIdFactory: recordingOperationIdFactory(handlers, options.operationIdFactory)
635
670
  });
636
- const document = augmentDocument(
637
- generated,
638
- resolved,
639
- readPathPrefixes(app),
640
- readContributions(app, handlers)
641
- );
671
+ const prefixes = readPathPrefixes(app);
672
+ const document = augmentDocument(generated, resolved, prefixes, readContributions(app, handlers));
673
+ warnUnsecuredOperations(logger, unsecuredOperations(document, resolved, prefixes));
642
674
  swagger.SwaggerModule.setup(options.path, app, document, {
643
675
  jsonDocumentUrl: options.jsonPath
644
676
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bymax-one/nest-core",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
4
4
  "description": "Zero-dependency NestJS 11 application foundation kit: error-envelope exception filter, request-timing interceptor, pagination helpers, health endpoints with indicator discovery, an optional Prometheus metrics endpoint with a contribution contract, OpenAPI documents in development, and OpenTelemetry trace correlation.",
5
5
  "author": "Bymax One <support@bymax.one>",
6
6
  "license": "MIT",