@bymax-one/nest-core 1.4.0 → 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/CHANGELOG.md +42 -1
- package/README.md +50 -0
- package/dist/openapi/index.cjs +61 -29
- package/dist/openapi/index.mjs +61 -29
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,46 @@ heading here.
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [1.5.0] - 2026-08-15
|
|
15
|
+
|
|
16
|
+
An OpenAPI document could stop requiring credentials without anything saying
|
|
17
|
+
so. Deleting a document-level `security` default — typically alongside the
|
|
18
|
+
per-operation entries a library has taken over describing — leaves every route
|
|
19
|
+
the backend itself owns with no requirement from any source, and the document
|
|
20
|
+
stays valid, no requirement dangles, and the runtime still answers `401`. The
|
|
21
|
+
only observable change is that a client generated from the document stops
|
|
22
|
+
sending credentials.
|
|
23
|
+
|
|
24
|
+
**Apply to a derived backend:** bump the dependency. No code change is needed.
|
|
25
|
+
If the boot log now names operations, they are the ones a generated client will
|
|
26
|
+
call without credentials — set `openapi.security`, or mark each public with an
|
|
27
|
+
explicit `[]` in `openapi.operationSecurity`.
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- **The document build warns when an operation ends up requiring no credential
|
|
32
|
+
at all.** Deleting a document-level `security` default — typically alongside
|
|
33
|
+
the per-operation entries a library has taken over describing — leaves every
|
|
34
|
+
route the backend itself owns with no requirement from any source. Nothing
|
|
35
|
+
catches it today: the document is valid, no requirement dangles so
|
|
36
|
+
`assertSchemesDeclared` is satisfied, the runtime still answers `401` so a
|
|
37
|
+
status-code probe finds nothing, and a consumer's document test stays green if
|
|
38
|
+
it asserts only the operations it enumerated. The only observable change is
|
|
39
|
+
that a client generated from the document sends no credentials.
|
|
40
|
+
`applyBymaxOpenApi` now emits one warning per build naming the affected
|
|
41
|
+
operations, capped at ten with a count of the rest.
|
|
42
|
+
|
|
43
|
+
It warns and never throws — an API that is public on purpose is legitimate —
|
|
44
|
+
and the trigger is narrow so the line stays worth reading: only when the
|
|
45
|
+
document declares no top-level `security`, **and** at least one other
|
|
46
|
+
operation does state a requirement, **and** the operation is not one of the
|
|
47
|
+
three this package registers. An explicit `[]` — from `operationSecurity`, a
|
|
48
|
+
decorator, or a library's fragment — states the intent and stops the report.
|
|
49
|
+
The known limit is documented rather than closed: a document with nothing
|
|
50
|
+
explicit anywhere is indistinguishable from an API that is public on purpose,
|
|
51
|
+
so removing _every_ requirement at once is not warned. Render the document
|
|
52
|
+
with and without your libraries and diff the operations you mount.
|
|
53
|
+
|
|
14
54
|
## [1.4.0] - 2026-08-13
|
|
15
55
|
|
|
16
56
|
HTTP metrics were blind to every request that did not reach a handler. Nest runs
|
|
@@ -649,4 +689,5 @@ have regressed from. They are kept because the reasoning is worth having.
|
|
|
649
689
|
[1.2.1]: https://github.com/bymaxone/nest-core/compare/v1.2.0...v1.2.1
|
|
650
690
|
[1.2.0]: https://github.com/bymaxone/nest-core/compare/v1.1.1...v1.2.0
|
|
651
691
|
[1.4.0]: https://github.com/bymaxone/nest-core/compare/v1.3.2...v1.4.0
|
|
652
|
-
[
|
|
692
|
+
[1.5.0]: https://github.com/bymaxone/nest-core/compare/v1.4.0...v1.5.0
|
|
693
|
+
[Unreleased]: https://github.com/bymaxone/nest-core/compare/v1.5.0...HEAD
|
package/README.md
CHANGED
|
@@ -379,6 +379,56 @@ 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
|
+
**And there is one shape nothing can warn you about, so the diff is the only
|
|
424
|
+
check that covers it.** If you remove _every_ requirement at once — no library
|
|
425
|
+
describing anything, no decorator, no override, no document default — what
|
|
426
|
+
remains is indistinguishable from the document of an API that is public on
|
|
427
|
+
purpose. Both are a set of operations that ask for nothing. No tool can separate
|
|
428
|
+
the two without also shouting at every genuinely public API, which is how a
|
|
429
|
+
warning earns the right to be ignored. Render the document twice and compare; it
|
|
430
|
+
is the one step that does not depend on somebody having anticipated your case.
|
|
431
|
+
|
|
382
432
|
## 🔑 DI Tokens
|
|
383
433
|
|
|
384
434
|
Every token is a `Symbol`. `BYMAX_CORRELATION_PROVIDER` and
|
package/dist/openapi/index.cjs
CHANGED
|
@@ -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
|
|
639
|
-
|
|
640
|
-
|
|
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
|
});
|
package/dist/openapi/index.mjs
CHANGED
|
@@ -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
|
|
637
|
-
|
|
638
|
-
|
|
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.
|
|
3
|
+
"version": "1.5.0",
|
|
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",
|