@bymax-one/nest-core 1.3.2 → 1.4.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.
@@ -17,6 +17,116 @@ function isProductionRuntime(value = process.env["NODE_ENV"]) {
17
17
  return !NON_PRODUCTION_ENVIRONMENTS.has(value.trim().toLowerCase());
18
18
  }
19
19
 
20
+ // src/discovery.ts
21
+ function labelFor(className, token) {
22
+ return className === "" ? String(token) : className;
23
+ }
24
+ function findMarkedProviders(discovery, reflector, metadataKey) {
25
+ const marked = [];
26
+ for (const wrapper of discovery.getProviders()) {
27
+ const metatype = wrapper.metatype;
28
+ if (typeof metatype !== "function") {
29
+ continue;
30
+ }
31
+ if (reflector.get(metadataKey, metatype) !== true) {
32
+ continue;
33
+ }
34
+ marked.push({ instance: wrapper.instance, label: labelFor(metatype.name, wrapper.name) });
35
+ }
36
+ return marked;
37
+ }
38
+ var BYMAX_OPENAPI_CONTRACT_VERSION = 1;
39
+ var BYMAX_OPENAPI_CONTRIBUTOR_METADATA = "bymax-one:openapi-contributor";
40
+ function BymaxOpenApiContributor() {
41
+ return common.SetMetadata(BYMAX_OPENAPI_CONTRIBUTOR_METADATA, true);
42
+ }
43
+
44
+ // src/openapi/openapi.contribution.ts
45
+ function createHandlerIdMap() {
46
+ const ids = /* @__PURE__ */ new Map();
47
+ const versions = /* @__PURE__ */ new Map();
48
+ return {
49
+ record: (controllerKey, methodKey, version, id) => {
50
+ const handlerKey = `${controllerKey}.${methodKey}`;
51
+ const seen = versions.get(handlerKey) ?? /* @__PURE__ */ new Set();
52
+ if (seen.has(version)) {
53
+ throw new Error(
54
+ `[BymaxCoreModule] two route handlers in this application answer to "${handlerKey}", so an OpenAPI fragment addressing it would apply to both. Handler keys are "<ControllerClassName>.<methodName>"; rename one of the controller classes.`
55
+ );
56
+ }
57
+ seen.add(version);
58
+ versions.set(handlerKey, seen);
59
+ ids.set(handlerKey, [...ids.get(handlerKey) ?? [], id]);
60
+ },
61
+ idsFor: (handlerKey) => ids.get(handlerKey) ?? [],
62
+ keys: () => [...ids.keys()]
63
+ };
64
+ }
65
+ function isContributor(instance) {
66
+ return typeof instance?.contributeOpenApi === "function";
67
+ }
68
+ function callContributor(contributor, label) {
69
+ try {
70
+ return contributor.contributeOpenApi();
71
+ } catch (cause) {
72
+ const reason = cause instanceof Error ? cause.message : String(cause);
73
+ throw new Error(
74
+ `[BymaxCoreModule] "${label}" failed to contribute to the OpenAPI document: ${reason}`,
75
+ {
76
+ cause
77
+ }
78
+ );
79
+ }
80
+ }
81
+ function assertContractVersion(fragment, label) {
82
+ if (fragment.contractVersion === BYMAX_OPENAPI_CONTRACT_VERSION) {
83
+ return;
84
+ }
85
+ throw new Error(
86
+ `[BymaxCoreModule] "${label}" contributed a fragment written against OpenAPI contract version ${String(fragment.contractVersion)}, and this package speaks version ${String(BYMAX_OPENAPI_CONTRACT_VERSION)}. Upgrade whichever of the two is behind; the shapes are not interchangeable.`
87
+ );
88
+ }
89
+ function resolveOperations(fragment, label, handlers) {
90
+ const entries = Object.entries(fragment.operations ?? {});
91
+ const unmatched = entries.filter(([handlerKey]) => handlers.idsFor(handlerKey).length === 0);
92
+ if (unmatched.length > 0) {
93
+ const known = handlers.keys();
94
+ throw new Error(
95
+ `[BymaxCoreModule] "${label}" contributed fragments for ${unmatched.length} handler(s) this application does not have: ${unmatched.map(([key]) => key).join(", ")}. Keys are "<ControllerClassName>.<methodName>". The application has: ${known.length === 0 ? "(none)" : known.join(", ")}.`
96
+ );
97
+ }
98
+ return Object.fromEntries(
99
+ entries.flatMap(
100
+ ([handlerKey, operation]) => handlers.idsFor(handlerKey).map((id) => [id, operation])
101
+ )
102
+ );
103
+ }
104
+ function collectContributions(discovery, reflector, handlers) {
105
+ const marked = [...findMarkedProviders(discovery, reflector, BYMAX_OPENAPI_CONTRIBUTOR_METADATA)];
106
+ const labels = marked.map(({ label }) => label);
107
+ const duplicated = labels.filter((label, index) => labels.indexOf(label) !== index);
108
+ if (duplicated.length > 0) {
109
+ throw new Error(
110
+ `[BymaxCoreModule] more than one OpenAPI contributor is named "${[...new Set(duplicated)].join('", "')}", so the order they merge in would depend on the container rather than on anything stated. Rename one of the contributor classes.`
111
+ );
112
+ }
113
+ marked.sort((left, right) => left.label.localeCompare(right.label));
114
+ return marked.map(({ instance, label }) => {
115
+ if (!isContributor(instance)) {
116
+ throw new Error(
117
+ `[BymaxCoreModule] "${label}" is marked @BymaxOpenApiContributor() but does not implement IOpenApiContributor: it must expose a "contributeOpenApi" method.`
118
+ );
119
+ }
120
+ const fragment = callContributor(instance, label);
121
+ assertContractVersion(fragment, label);
122
+ return {
123
+ label,
124
+ operations: resolveOperations(fragment, label, handlers),
125
+ components: fragment.components ?? {}
126
+ };
127
+ });
128
+ }
129
+
20
130
  // src/route-defaults.ts
21
131
  var DEFAULT_HEALTH_PATH = "health";
22
132
  var DEFAULT_METRICS_PATH = "metrics";
@@ -209,7 +319,8 @@ function operationsOf(item) {
209
319
  }
210
320
  function mergeResponses(existing, additions) {
211
321
  const merged = new Map(Object.entries(existing));
212
- for (const [status, contributed] of Object.entries(additions)) {
322
+ for (const [status, value] of Object.entries(additions)) {
323
+ const contributed = asRecord(value);
213
324
  const current = asRecord(merged.get(status));
214
325
  if (current["content"] === void 0 && current["$ref"] === void 0) {
215
326
  const described = current["description"] === void 0 || current["description"] === "" ? contributed["description"] : current["description"];
@@ -289,11 +400,29 @@ function coreResponses(path, options, routes) {
289
400
  }
290
401
  return responses;
291
402
  }
292
- function augmentOperation(operation, path, method, options, routes) {
293
- const result = { ...operation };
294
- if (result["security"] === void 0) {
403
+ function fragmentsFor(operationId, contributions) {
404
+ return contributions.flatMap(
405
+ (contribution) => Object.entries(contribution.operations).filter(([id]) => id === operationId).map(([, fragment]) => fragment)
406
+ );
407
+ }
408
+ function mergeFragment(operation, fragment) {
409
+ const { responses, ...members } = fragment;
410
+ const merged = { ...members, ...operation };
411
+ if (responses !== void 0) {
412
+ merged["responses"] = mergeResponses(asRecord(operation["responses"]), asRecord(responses));
413
+ }
414
+ return merged;
415
+ }
416
+ function augmentOperation(operation, path, method, options, routes, contributions) {
417
+ const declaredByDocument = operation["security"] !== void 0;
418
+ let result = { ...operation };
419
+ for (const fragment of fragmentsFor(result["operationId"], contributions)) {
420
+ result = { ...mergeFragment(result, fragment) };
421
+ }
422
+ if (!declaredByDocument) {
295
423
  const override = options.openapi.operationSecurity[operationKey(method, path)];
296
- const security = override ?? ownRouteSecurity(path, method, options, routes);
424
+ const describedByLibrary = result["security"] !== void 0;
425
+ const security = override ?? (describedByLibrary ? void 0 : ownRouteSecurity(path, method, options, routes));
297
426
  if (security !== void 0) {
298
427
  result["security"] = security;
299
428
  }
@@ -349,24 +478,24 @@ function assertOverridesMatch(paths, openapi) {
349
478
  `[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
479
  );
351
480
  }
352
- function augmentPaths(paths, options, routes) {
481
+ function augmentPaths(paths, options, routes, contributions) {
353
482
  return Object.fromEntries(
354
483
  Object.entries(paths).map(([path, item]) => {
355
484
  const augmented = operationsOf(item).map(([method, operation]) => [
356
485
  method,
357
- augmentOperation(asRecord(operation), path, method, options, routes)
486
+ augmentOperation(asRecord(operation), path, method, options, routes, contributions)
358
487
  ]);
359
488
  return [path, { ...asRecord(item), ...Object.fromEntries(augmented) }];
360
489
  })
361
490
  );
362
491
  }
363
- function augmentDocument(document, options, pathPrefixes = [""]) {
492
+ function augmentDocument(document, options, pathPrefixes = [""], contributions = []) {
364
493
  const { openapi } = options;
365
494
  const components = asRecord(document.components);
366
- const merged = { ...components };
495
+ const merged = new Map(Object.entries(components));
367
496
  if (openapi.includeCoreSchemas) {
368
- merged["schemas"] = mergeAbsent(asRecord(components["schemas"]), CORE_SCHEMAS);
369
- merged["parameters"] = mergeAbsent(asRecord(components["parameters"]), CORE_PARAMETERS);
497
+ merged.set("schemas", mergeAbsent(asRecord(components["schemas"]), CORE_SCHEMAS));
498
+ merged.set("parameters", mergeAbsent(asRecord(components["parameters"]), CORE_PARAMETERS));
370
499
  }
371
500
  const scrapeScheme = options.metrics.authToken === void 0 ? {} : {
372
501
  [METRICS_SCHEME_NAME]: {
@@ -376,20 +505,25 @@ function augmentDocument(document, options, pathPrefixes = [""]) {
376
505
  }
377
506
  };
378
507
  assertScrapeSchemeIsOurs(openapi, components, options.metrics.authToken !== void 0);
379
- const schemes = mergeAbsent(asRecord(components["securitySchemes"]), {
508
+ const declaredSchemes = mergeAbsent(asRecord(components["securitySchemes"]), {
380
509
  ...openapi.securitySchemes,
381
510
  ...scrapeScheme
382
511
  });
383
- if (Object.keys(schemes).length > 0) {
384
- merged["securitySchemes"] = schemes;
512
+ if (Object.keys(declaredSchemes).length > 0) {
513
+ merged.set("securitySchemes", declaredSchemes);
514
+ }
515
+ for (const contribution of contributions) {
516
+ for (const [member, entries] of Object.entries(contribution.components)) {
517
+ merged.set(member, mergeAbsent(asRecord(merged.get(member)), entries));
518
+ }
385
519
  }
386
- assertSchemesDeclared(openapi, schemes);
520
+ assertSchemesDeclared(openapi, asRecord(merged.get("securitySchemes")));
387
521
  const routes = indexOwnRoutes(options, pathPrefixes);
388
522
  const served = withoutDisabledRoutes(asRecord(document.paths), options, routes);
389
523
  assertOverridesMatch(served, openapi);
390
- const paths = document.paths === void 0 ? {} : { paths: augmentPaths(served, options, routes) };
524
+ const paths = document.paths === void 0 ? {} : { paths: augmentPaths(served, options, routes, contributions) };
391
525
  const security = openapi.security.length > 0 && document.security === void 0 ? { security: openapi.security } : {};
392
- return { ...document, components: merged, ...paths, ...security };
526
+ return { ...document, components: Object.fromEntries(merged), ...paths, ...security };
393
527
  }
394
528
 
395
529
  // src/optional-peer.ts
@@ -452,6 +586,27 @@ function readPathPrefixes(app) {
452
586
  const globalPrefix = config.getGlobalPrefix();
453
587
  return versionSegments(config.getVersioning()).map((segment) => `${globalPrefix}/${segment}`);
454
588
  }
589
+ function recordingOperationIdFactory(handlers, configured) {
590
+ return (controllerKey, methodKey, version) => {
591
+ const id = configured === void 0 ? defaultOperationId(controllerKey, methodKey, version) : configured(controllerKey, methodKey, version);
592
+ handlers.record(controllerKey, methodKey, version, id);
593
+ return id;
594
+ };
595
+ }
596
+ function defaultOperationId(controllerKey, methodKey, version) {
597
+ return version === void 0 ? `${controllerKey}_${methodKey}` : `${controllerKey}_${methodKey}_${version}`;
598
+ }
599
+ function readContributions(app, handlers) {
600
+ let discovery;
601
+ let reflector;
602
+ try {
603
+ discovery = app.get(core.DiscoveryService);
604
+ reflector = app.get(core.Reflector);
605
+ } catch {
606
+ return [];
607
+ }
608
+ return collectContributions(discovery, reflector, handlers);
609
+ }
455
610
  function buildConfig(builder, options) {
456
611
  builder.setTitle(options.title).setDescription(options.description).setVersion(options.version);
457
612
  for (const server of options.servers) {
@@ -476,10 +631,15 @@ async function applyBymaxOpenApi(app) {
476
631
  }
477
632
  const swagger = await loadSwagger();
478
633
  const config = buildConfig(new swagger.DocumentBuilder(), options);
634
+ const handlers = createHandlerIdMap();
635
+ const generated = swagger.SwaggerModule.createDocument(app, config, {
636
+ operationIdFactory: recordingOperationIdFactory(handlers, options.operationIdFactory)
637
+ });
479
638
  const document = augmentDocument(
480
- swagger.SwaggerModule.createDocument(app, config),
639
+ generated,
481
640
  resolved,
482
- readPathPrefixes(app)
641
+ readPathPrefixes(app),
642
+ readContributions(app, handlers)
483
643
  );
484
644
  swagger.SwaggerModule.setup(options.path, app, document, {
485
645
  jsonDocumentUrl: options.jsonPath
@@ -488,4 +648,7 @@ async function applyBymaxOpenApi(app) {
488
648
  return { mounted: true, path: options.path };
489
649
  }
490
650
 
651
+ exports.BYMAX_OPENAPI_CONTRACT_VERSION = BYMAX_OPENAPI_CONTRACT_VERSION;
652
+ exports.BYMAX_OPENAPI_CONTRIBUTOR_METADATA = BYMAX_OPENAPI_CONTRIBUTOR_METADATA;
653
+ exports.BymaxOpenApiContributor = BymaxOpenApiContributor;
491
654
  exports.applyBymaxOpenApi = applyBymaxOpenApi;
@@ -1,4 +1,4 @@
1
- import { INestApplication } from '@nestjs/common';
1
+ import { INestApplication, CustomDecorator } from '@nestjs/common';
2
2
 
3
3
  /** Why the document was not mounted. */
4
4
  type OpenApiSkipReason =
@@ -51,4 +51,149 @@ interface OpenApiMountOutcome {
51
51
  */
52
52
  declare function applyBymaxOpenApi(app: INestApplication): Promise<OpenApiMountOutcome>;
53
53
 
54
- export { type OpenApiMountOutcome, type OpenApiSkipReason, applyBymaxOpenApi };
54
+ /**
55
+ * Addresses one route handler, as `'<ControllerClassName>.<methodName>'`.
56
+ *
57
+ * Controller handlers only. A route the application mounts some other way — raw
58
+ * middleware, a router the framework never scanned — produces no operation in
59
+ * the generated document and therefore no handler to address; a fragment naming
60
+ * one fails the build rather than being quietly dropped.
61
+ *
62
+ * This is the key a library writes its fragments against, and it is a contract:
63
+ * `@nestjs/swagger` hands the same pair to the operation-id factory this package
64
+ * installs, so a fragment can be matched to the operation the scan produced
65
+ * without either side reconstructing a path.
66
+ *
67
+ * Deliberately not the operation id itself. The id is whatever the ecosystem
68
+ * already produces — `AuthController_login` by default — and a library keying on
69
+ * it would break the moment a consumer supplied their own naming, while a
70
+ * consumer changing that naming would silently unmatch every fragment.
71
+ *
72
+ * @example 'AuthController.login'
73
+ * @example 'PasswordResetController.resetPassword'
74
+ */
75
+ type OpenApiHandlerKey = `${string}.${string}`;
76
+ /**
77
+ * A single OpenAPI object, copied into the document and never interpreted.
78
+ * Modelled as an open record for the same reason the contributed schemas are: a
79
+ * partial local model of the specification would be a second contract to keep in
80
+ * sync with the real one.
81
+ */
82
+ type OpenApiFragmentObject = Readonly<Record<string, unknown>>;
83
+ /**
84
+ * What a contributor hands over: operation fragments addressed by handler, and
85
+ * the components those fragments reference.
86
+ *
87
+ * The dialect is **OpenAPI 3.0**, which is what `@nestjs/swagger` produces —
88
+ * measured, not assumed: its `DocumentBuilder` writes `openapi: '3.0.0'` and a
89
+ * served document reports the same. A 3.1 fragment merged into a 3.0 document
90
+ * produces one that validates as neither, so nullability belongs in `nullable`
91
+ * rather than in a union type.
92
+ */
93
+ interface OpenApiFragment {
94
+ /**
95
+ * Which revision of this contract the fragment is written against. Always
96
+ * {@link BYMAX_OPENAPI_CONTRACT_VERSION}.
97
+ *
98
+ * A fragment is data crossing a boundary between independently released
99
+ * packages, and on that boundary compile-time types protect nothing: a
100
+ * library compiled against one revision of this contract runs inside an
101
+ * application that installed another, each having type-checked against its
102
+ * own copy. Only the value travelling at runtime can say which shape it is.
103
+ * So it self-describes, and a revision this package does not know fails the
104
+ * document build naming both — the same reason a Kubernetes object carries
105
+ * `apiVersion` rather than trusting that client and server agree.
106
+ *
107
+ * A plain integer rather than a semver range: this is the shape of the
108
+ * exchange, not the version of any package, and it changes only when the
109
+ * shape does.
110
+ */
111
+ readonly contractVersion: typeof BYMAX_OPENAPI_CONTRACT_VERSION;
112
+ /**
113
+ * Operation objects to merge, keyed by the handler that produced the
114
+ * operation. A key addressing a handler the document does not contain fails
115
+ * the build naming the contributor: a fragment for a renamed handler is a
116
+ * check that stopped running, which is worse than one that fails.
117
+ */
118
+ readonly operations?: Readonly<Record<OpenApiHandlerKey, OpenApiFragmentObject>>;
119
+ /**
120
+ * Entries to merge under the document's `components`, keyed by member —
121
+ * `schemas`, `securitySchemes`, `responses`, and so on. Additive: an entry the
122
+ * document already defines under that name is kept.
123
+ */
124
+ readonly components?: Readonly<Record<string, Readonly<Record<string, OpenApiFragmentObject>>>>;
125
+ }
126
+ /**
127
+ * A library that describes its own routes in the document.
128
+ *
129
+ * Called once, while the document is being built, and only when the OpenAPI
130
+ * feature is enabled — so a library implementing it costs an application that
131
+ * never builds a document nothing beyond one metadata entry.
132
+ */
133
+ interface IOpenApiContributor {
134
+ /**
135
+ * Produce this library's fragments.
136
+ *
137
+ * Called after the application's options have resolved, so a contributor may
138
+ * derive its contribution from its own configuration — which is the case that
139
+ * makes this contract necessary rather than convenient. A library whose
140
+ * credential names or transport are configurable cannot state its security
141
+ * statically; only it can say what its resolved options mean.
142
+ *
143
+ * @returns The fragments to merge. Return an empty object to contribute
144
+ * nothing; throwing fails the document build with the contributor named.
145
+ */
146
+ contributeOpenApi(): OpenApiFragment;
147
+ }
148
+ /**
149
+ * The revision of the fragment exchange this package speaks.
150
+ *
151
+ * Bumped only when the shape of {@link OpenApiFragment} changes in a way a
152
+ * previous revision's fragment would be misread under — not when a member is
153
+ * added, which older fragments simply do not carry.
154
+ */
155
+ declare const BYMAX_OPENAPI_CONTRACT_VERSION = 1;
156
+ /**
157
+ * Reflect metadata key carrying the contributor marker.
158
+ *
159
+ * A literal string rather than a key from `DiscoveryService.createDecorator()`,
160
+ * which mints a random key per module load: this package ships one bundle per
161
+ * subpath, so a library decorating its class through `./openapi` would get a
162
+ * different key than the scan running from the package root and nothing would
163
+ * ever be discovered. The same reasoning governs the health and metrics markers.
164
+ * Namespaced so it cannot collide with a consumer's own metadata.
165
+ */
166
+ declare const BYMAX_OPENAPI_CONTRIBUTOR_METADATA = "bymax-one:openapi-contributor";
167
+ /**
168
+ * Mark a provider class as an OpenAPI contributor, so `applyBymaxOpenApi` finds
169
+ * it without the application wiring anything.
170
+ *
171
+ * The class must implement {@link IOpenApiContributor}; a marked provider that
172
+ * does not fails the document build with a message naming it, rather than being
173
+ * skipped silently.
174
+ *
175
+ * @returns The class decorator carrying the marker.
176
+ * @example
177
+ * \@BymaxOpenApiContributor()
178
+ * \@Injectable()
179
+ * export class AuthOpenApi implements IOpenApiContributor {
180
+ * constructor(private readonly options: ResolvedAuthOptions) {}
181
+ * contributeOpenApi(): OpenApiFragment {
182
+ * return {
183
+ * components: {
184
+ * securitySchemes: {
185
+ * authCookie: { type: 'apiKey', in: 'cookie', name: this.options.cookies.accessTokenName },
186
+ * refreshCookie: { type: 'apiKey', in: 'cookie', name: this.options.cookies.refreshTokenName }
187
+ * }
188
+ * },
189
+ * operations: {
190
+ * 'AuthController.login': { security: [] },
191
+ * 'AuthController.refresh': { security: [{ refreshCookie: [] }] }
192
+ * }
193
+ * }
194
+ * }
195
+ * }
196
+ */
197
+ declare function BymaxOpenApiContributor(): CustomDecorator<string>;
198
+
199
+ export { BYMAX_OPENAPI_CONTRACT_VERSION, BYMAX_OPENAPI_CONTRIBUTOR_METADATA, BymaxOpenApiContributor, type IOpenApiContributor, type OpenApiFragment, type OpenApiFragmentObject, type OpenApiHandlerKey, type OpenApiMountOutcome, type OpenApiSkipReason, applyBymaxOpenApi };
@@ -1,4 +1,4 @@
1
- import { INestApplication } from '@nestjs/common';
1
+ import { INestApplication, CustomDecorator } from '@nestjs/common';
2
2
 
3
3
  /** Why the document was not mounted. */
4
4
  type OpenApiSkipReason =
@@ -51,4 +51,149 @@ interface OpenApiMountOutcome {
51
51
  */
52
52
  declare function applyBymaxOpenApi(app: INestApplication): Promise<OpenApiMountOutcome>;
53
53
 
54
- export { type OpenApiMountOutcome, type OpenApiSkipReason, applyBymaxOpenApi };
54
+ /**
55
+ * Addresses one route handler, as `'<ControllerClassName>.<methodName>'`.
56
+ *
57
+ * Controller handlers only. A route the application mounts some other way — raw
58
+ * middleware, a router the framework never scanned — produces no operation in
59
+ * the generated document and therefore no handler to address; a fragment naming
60
+ * one fails the build rather than being quietly dropped.
61
+ *
62
+ * This is the key a library writes its fragments against, and it is a contract:
63
+ * `@nestjs/swagger` hands the same pair to the operation-id factory this package
64
+ * installs, so a fragment can be matched to the operation the scan produced
65
+ * without either side reconstructing a path.
66
+ *
67
+ * Deliberately not the operation id itself. The id is whatever the ecosystem
68
+ * already produces — `AuthController_login` by default — and a library keying on
69
+ * it would break the moment a consumer supplied their own naming, while a
70
+ * consumer changing that naming would silently unmatch every fragment.
71
+ *
72
+ * @example 'AuthController.login'
73
+ * @example 'PasswordResetController.resetPassword'
74
+ */
75
+ type OpenApiHandlerKey = `${string}.${string}`;
76
+ /**
77
+ * A single OpenAPI object, copied into the document and never interpreted.
78
+ * Modelled as an open record for the same reason the contributed schemas are: a
79
+ * partial local model of the specification would be a second contract to keep in
80
+ * sync with the real one.
81
+ */
82
+ type OpenApiFragmentObject = Readonly<Record<string, unknown>>;
83
+ /**
84
+ * What a contributor hands over: operation fragments addressed by handler, and
85
+ * the components those fragments reference.
86
+ *
87
+ * The dialect is **OpenAPI 3.0**, which is what `@nestjs/swagger` produces —
88
+ * measured, not assumed: its `DocumentBuilder` writes `openapi: '3.0.0'` and a
89
+ * served document reports the same. A 3.1 fragment merged into a 3.0 document
90
+ * produces one that validates as neither, so nullability belongs in `nullable`
91
+ * rather than in a union type.
92
+ */
93
+ interface OpenApiFragment {
94
+ /**
95
+ * Which revision of this contract the fragment is written against. Always
96
+ * {@link BYMAX_OPENAPI_CONTRACT_VERSION}.
97
+ *
98
+ * A fragment is data crossing a boundary between independently released
99
+ * packages, and on that boundary compile-time types protect nothing: a
100
+ * library compiled against one revision of this contract runs inside an
101
+ * application that installed another, each having type-checked against its
102
+ * own copy. Only the value travelling at runtime can say which shape it is.
103
+ * So it self-describes, and a revision this package does not know fails the
104
+ * document build naming both — the same reason a Kubernetes object carries
105
+ * `apiVersion` rather than trusting that client and server agree.
106
+ *
107
+ * A plain integer rather than a semver range: this is the shape of the
108
+ * exchange, not the version of any package, and it changes only when the
109
+ * shape does.
110
+ */
111
+ readonly contractVersion: typeof BYMAX_OPENAPI_CONTRACT_VERSION;
112
+ /**
113
+ * Operation objects to merge, keyed by the handler that produced the
114
+ * operation. A key addressing a handler the document does not contain fails
115
+ * the build naming the contributor: a fragment for a renamed handler is a
116
+ * check that stopped running, which is worse than one that fails.
117
+ */
118
+ readonly operations?: Readonly<Record<OpenApiHandlerKey, OpenApiFragmentObject>>;
119
+ /**
120
+ * Entries to merge under the document's `components`, keyed by member —
121
+ * `schemas`, `securitySchemes`, `responses`, and so on. Additive: an entry the
122
+ * document already defines under that name is kept.
123
+ */
124
+ readonly components?: Readonly<Record<string, Readonly<Record<string, OpenApiFragmentObject>>>>;
125
+ }
126
+ /**
127
+ * A library that describes its own routes in the document.
128
+ *
129
+ * Called once, while the document is being built, and only when the OpenAPI
130
+ * feature is enabled — so a library implementing it costs an application that
131
+ * never builds a document nothing beyond one metadata entry.
132
+ */
133
+ interface IOpenApiContributor {
134
+ /**
135
+ * Produce this library's fragments.
136
+ *
137
+ * Called after the application's options have resolved, so a contributor may
138
+ * derive its contribution from its own configuration — which is the case that
139
+ * makes this contract necessary rather than convenient. A library whose
140
+ * credential names or transport are configurable cannot state its security
141
+ * statically; only it can say what its resolved options mean.
142
+ *
143
+ * @returns The fragments to merge. Return an empty object to contribute
144
+ * nothing; throwing fails the document build with the contributor named.
145
+ */
146
+ contributeOpenApi(): OpenApiFragment;
147
+ }
148
+ /**
149
+ * The revision of the fragment exchange this package speaks.
150
+ *
151
+ * Bumped only when the shape of {@link OpenApiFragment} changes in a way a
152
+ * previous revision's fragment would be misread under — not when a member is
153
+ * added, which older fragments simply do not carry.
154
+ */
155
+ declare const BYMAX_OPENAPI_CONTRACT_VERSION = 1;
156
+ /**
157
+ * Reflect metadata key carrying the contributor marker.
158
+ *
159
+ * A literal string rather than a key from `DiscoveryService.createDecorator()`,
160
+ * which mints a random key per module load: this package ships one bundle per
161
+ * subpath, so a library decorating its class through `./openapi` would get a
162
+ * different key than the scan running from the package root and nothing would
163
+ * ever be discovered. The same reasoning governs the health and metrics markers.
164
+ * Namespaced so it cannot collide with a consumer's own metadata.
165
+ */
166
+ declare const BYMAX_OPENAPI_CONTRIBUTOR_METADATA = "bymax-one:openapi-contributor";
167
+ /**
168
+ * Mark a provider class as an OpenAPI contributor, so `applyBymaxOpenApi` finds
169
+ * it without the application wiring anything.
170
+ *
171
+ * The class must implement {@link IOpenApiContributor}; a marked provider that
172
+ * does not fails the document build with a message naming it, rather than being
173
+ * skipped silently.
174
+ *
175
+ * @returns The class decorator carrying the marker.
176
+ * @example
177
+ * \@BymaxOpenApiContributor()
178
+ * \@Injectable()
179
+ * export class AuthOpenApi implements IOpenApiContributor {
180
+ * constructor(private readonly options: ResolvedAuthOptions) {}
181
+ * contributeOpenApi(): OpenApiFragment {
182
+ * return {
183
+ * components: {
184
+ * securitySchemes: {
185
+ * authCookie: { type: 'apiKey', in: 'cookie', name: this.options.cookies.accessTokenName },
186
+ * refreshCookie: { type: 'apiKey', in: 'cookie', name: this.options.cookies.refreshTokenName }
187
+ * }
188
+ * },
189
+ * operations: {
190
+ * 'AuthController.login': { security: [] },
191
+ * 'AuthController.refresh': { security: [{ refreshCookie: [] }] }
192
+ * }
193
+ * }
194
+ * }
195
+ * }
196
+ */
197
+ declare function BymaxOpenApiContributor(): CustomDecorator<string>;
198
+
199
+ export { BYMAX_OPENAPI_CONTRACT_VERSION, BYMAX_OPENAPI_CONTRIBUTOR_METADATA, BymaxOpenApiContributor, type IOpenApiContributor, type OpenApiFragment, type OpenApiFragmentObject, type OpenApiHandlerKey, type OpenApiMountOutcome, type OpenApiSkipReason, applyBymaxOpenApi };