@bymax-one/nest-core 1.5.0 → 1.5.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/CHANGELOG.md CHANGED
@@ -11,6 +11,93 @@ heading here.
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [1.5.2] - 2026-08-15
15
+
16
+ The production guard read `NODE_ENV` and nothing else, and treated an unset
17
+ variable as production. An application that validates its own `APP_ENV` and
18
+ never sets `NODE_ENV` was therefore classified as production on evidence it
19
+ never gave — the OpenAPI document was refused in a development deployment, with
20
+ no way to answer back. Two independent consumers reported the same split.
21
+
22
+ **Apply to a derived backend:** nothing to change. A deployment that sets
23
+ `NODE_ENV` behaves exactly as before. If yours validates its own variable
24
+ instead, pass it as `environment` and the document is served where that variable
25
+ says `development` or `test`.
26
+
27
+ ### Added
28
+
29
+ - **`environment`, for applications that validate their own environment
30
+ variable.** The production guard read `NODE_ENV` and nothing else, and treated
31
+ an unset variable as production. An application that parses an `APP_ENV`
32
+ through its config schema and never sets `NODE_ENV` was therefore classified
33
+ as production on evidence it never gave — the OpenAPI document was refused in
34
+ a development deployment, with no way to answer back. Two independent
35
+ consumers reported the same split between the library's view of the
36
+ environment and their own validated one.
37
+
38
+ A top-level `environment` option is now consulted **where the process declares
39
+ nothing**: `NODE_ENV` unset, or set to whitespace. `NODE_ENV` wins whenever it
40
+ says anything at all, so no configured value can make a runtime that named
41
+ itself production serve the document — asserted in both guards rather than in
42
+ one. The declaration enters the same fail-closed classification, so an
43
+ unrecognized name is production like any other: this is a second source for
44
+ the value, never a second set of rules.
45
+
46
+ The narrowing is stated rather than buried. Both guards previously classified
47
+ from the process alone; now, in the single case where the process says
48
+ nothing, the snapshot a consumer bound decides the answer, because there is
49
+ nothing else to decide it with. Replacing a guess with a declaration is not an
50
+ override, but it is a real change to what the second guard depends on.
51
+
52
+ **Apply to a derived backend:** nothing to change. The option is optional and
53
+ every existing classification is unchanged — a deployment that sets `NODE_ENV`
54
+ behaves exactly as before.
55
+
56
+ ## [1.5.1] - 2026-08-15
57
+
58
+ Documentation only; no source change. The 1.5.0 warning's known-limit note told
59
+ readers that no tool could catch the one shape the warning cannot report, and
60
+ that rendering the document twice was therefore the only check. That is true
61
+ only of something reading the rendered document alone — a consumer's own suite
62
+ knows the intent and can assert it on every commit — so the note was arguing
63
+ against the better practice.
64
+
65
+ **Apply to a derived backend:** nothing to change in code. Read the revised
66
+ "Documenting authentication" section and write the assertion it now shows; it
67
+ costs one test and replaces a manual step nobody remembers to run.
68
+
69
+ ### Documentation
70
+
71
+ - **The credential-free warning's known limit no longer argues against the
72
+ practice that covers it.** The README said no tool could distinguish a
73
+ document that lost its requirements from one that never had any, and that
74
+ rendering the document twice was therefore the only check. The first half is
75
+ true only of something reading the rendered document alone; a consumer's own
76
+ suite knows which of the two it is and can assert it on every commit. Saying
77
+ otherwise did not merely overstate a limit — it told readers that the standing
78
+ check they should write does not exist. The section now attributes the limit
79
+ correctly, states why the warning cannot fire on that shape (it follows from
80
+ the trigger, in every version), gives the assertion as the practice, and
81
+ leaves render-and-diff the narrower job it is genuinely good at: seeing what
82
+ moved when you change something, so you can turn it into an assertion.
83
+
84
+ - **A contributed scheme's presence is documented as part of the contributor's
85
+ configuration.** Which security schemes a library contributes can depend on
86
+ how that library is configured — the names are stable, their presence is not.
87
+ A document-level default must therefore be derived from the same configuration
88
+ the contributor reads. A literal is correct only for the configuration it was
89
+ written against: elsewhere it either resolves while describing one of two
90
+ credentials a route accepts (quietly incomplete) or names a scheme nobody
91
+ declares (a failed document build). Guarding on whether the scheme exists
92
+ clears the loud case and ships the quiet one.
93
+
94
+ - **What a document-level default does not let you say** is now stated. Its
95
+ entries are alternatives applied to every operation that says nothing, so a
96
+ backend with two credential families _can_ list both and nothing rejects it —
97
+ the result asserts that either credential works for every inheriting route,
98
+ which is false in the permissive direction. The minority family belongs in
99
+ `openapi.operationSecurity`, which outranks the default.
100
+
14
101
  ## [1.5.0] - 2026-08-15
15
102
 
16
103
  An OpenAPI document could stop requiring credentials without anything saying
@@ -690,4 +777,6 @@ have regressed from. They are kept because the reasoning is worth having.
690
777
  [1.2.0]: https://github.com/bymaxone/nest-core/compare/v1.1.1...v1.2.0
691
778
  [1.4.0]: https://github.com/bymaxone/nest-core/compare/v1.3.2...v1.4.0
692
779
  [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
780
+ [1.5.1]: https://github.com/bymaxone/nest-core/compare/v1.5.0...v1.5.1
781
+ [1.5.2]: https://github.com/bymaxone/nest-core/compare/v1.5.1...v1.5.2
782
+ [Unreleased]: https://github.com/bymaxone/nest-core/compare/v1.5.2...HEAD
package/README.md CHANGED
@@ -202,6 +202,20 @@ BymaxCoreModule.forRoot({ isGlobal: false })
202
202
  Every block is optional; an omitted block, or an omitted field within it,
203
203
  falls back to the documented default. Pass only what you want to change.
204
204
 
205
+ ### `environment`
206
+
207
+ The one top-level option rather than a block, because it describes the
208
+ deployment rather than a feature.
209
+
210
+ | Option | Type | Default | Description |
211
+ | ------------- | -------- | ------- | ------------------------------------------------------------------------------------- |
212
+ | `environment` | `string` | unset | The environment this deployment runs in, read only where `NODE_ENV` declares nothing. |
213
+
214
+ Set it when your application validates its own environment variable and does not
215
+ also set `NODE_ENV`. `NODE_ENV` wins whenever it says anything, so this can never
216
+ serve the OpenAPI document in a runtime that named itself production. Full rules
217
+ and the classification table: [Production is a closed door](#production-is-a-closed-door).
218
+
205
219
  ### `envelope`
206
220
 
207
221
  | Option | Type | Default | Description |
@@ -420,14 +434,84 @@ rather than by silencing output: `operationSecurity: { 'GET /examples': [] }`
420
434
  marks the operation public, and it stops being reported. A library can do the
421
435
  same for its own routes by contributing `security: []` in its fragment.
422
436
 
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.
437
+ **There is one shape this warning cannot report.** If you remove _every_
438
+ requirement at once no library describing anything, no decorator, no override,
439
+ no document default the second condition above is never met: nothing states a
440
+ requirement, so there are no bare operations sitting _beside_ described ones.
441
+ That follows from the trigger and therefore holds in every version; it is not a
442
+ gap a later release closes, and a reader who expects one is exactly the reader
443
+ who stops checking.
444
+
445
+ The limit belongs to **anything reading only the rendered document**. Such a
446
+ document is indistinguishable from that of an API which is public on purpose —
447
+ both are a set of operations asking for nothing — and this package cannot tell
448
+ them apart without also warning at every genuinely public API, which is how a
449
+ warning earns the right to be ignored.
450
+
451
+ **Your own suite has no such handicap, because you know which one you are.**
452
+ Assert it, and the check runs on every commit rather than when somebody
453
+ remembers to look:
454
+
455
+ ```ts
456
+ it('still requires a credential everywhere it should', () => {
457
+ const document = buildYourDocument()
458
+
459
+ // Assert the default you expect, not merely that one exists. `[]` is a
460
+ // defined value that requires nothing, so a "toBeDefined" check passes for
461
+ // a default that degraded to empty — which is the regression this test is
462
+ // here to catch, wearing the shape of a pass.
463
+ expect(document.security).toEqual([{ cookieAuth: [] }])
464
+
465
+ // An explicit `[]` is how an operation says "public". The set of operations
466
+ // saying it should be the set you meant — no more, no fewer.
467
+ expect(publicOperationsOf(document)).toEqual(['POST /auth/login'])
468
+ })
469
+ ```
470
+
471
+ Render-and-diff keeps a narrower job, and it is a good one: when you are
472
+ _changing_ something — adopting a library that describes its own routes, moving
473
+ a default — render with and without the change and compare the operations you
474
+ mount. It shows you what moved without your having to predict it. Then turn what
475
+ it showed you into an assertion, so the next change is caught rather than
476
+ inspected.
477
+
478
+ #### A contributed scheme's presence is part of the contributor's configuration
479
+
480
+ A library contributes security schemes, and **which** ones it contributes can
481
+ depend on how you configured it. Any of them, gated on any of its inputs, and
482
+ often on more than one — the scheme you have in mind may be the absent one, and
483
+ the setting you are thinking of may not be the only gate. The names are stable;
484
+ their presence is not.
485
+
486
+ That has one consequence worth stating as a rule, because getting it wrong
487
+ produces a failure at either end of the loudness scale:
488
+
489
+ > Derive a document-level default from **the same configuration the contributor
490
+ > reads**, never from the scheme names, and never from what the document
491
+ > happened to contain before you adopted the library.
492
+
493
+ Writing the name as a literal is correct only for the configuration you wrote it
494
+ against. Under a configuration that declares that scheme plus another, the
495
+ default still resolves but describes one of two credentials the route accepts —
496
+ **quietly incomplete**. Under one that declares it not at all, the name resolves
497
+ to nothing and the document build **fails** with the undeclared-scheme error
498
+ above. Guarding on whether the scheme exists is the tempting fix and it is the
499
+ wrong one: it clears the case that already announced itself and ships the one
500
+ that does not.
501
+
502
+ Note what a document-level default does not let you _say_. Its entries are
503
+ **alternatives** — any one of them satisfies an operation — and they apply to
504
+ every operation that states nothing. So a backend whose routes sit behind **two
505
+ different credential families** can certainly list both, and nothing rejects it:
506
+ the result is a document asserting that _either_ credential works for _every_
507
+ inheriting route. That is not an incomplete document, it is a false one, and it
508
+ is false in the permissive direction — it tells a client that a credential the
509
+ route will reject is one the route accepts.
510
+
511
+ Give the majority family the default and the minority explicit
512
+ `operationSecurity` entries. Those outrank the default, and an operation
513
+ carrying one is never named by the warning above, because it states a
514
+ requirement.
431
515
 
432
516
  ## 🔑 DI Tokens
433
517
 
@@ -889,16 +973,61 @@ can emit it once and never branch:
889
973
  ### Production is a closed door
890
974
 
891
975
  `NODE_ENV` decides, and the decision is fail-closed: only `development` and
892
- `test` are non-production. Any other value including an unset variable
893
- is production, and in production the document is never built and never mounted,
894
- whatever the configuration says. The guard runs twice, independently: the option
895
- resolver forces the feature off, and the bootstrap helper refuses again without
896
- trusting that resolution. There is no override.
976
+ `test` are non-production. Any other value is production, and in production the
977
+ document is never built and never mounted, whatever the configuration says. The
978
+ guard runs twice, independently: the option resolver forces the feature off, and
979
+ the bootstrap helper classifies the runtime again without trusting that
980
+ resolution.
981
+
982
+ **`NODE_ENV` cannot be overridden.** With it set to anything, no option serves
983
+ the document in a runtime it named production.
897
984
 
898
985
  Enabling it in production is not an error, it is a no-op with a warning naming
899
986
  the option that was ignored, so a single configuration can be shared across
900
987
  environments.
901
988
 
989
+ #### When your application validates its own environment variable
990
+
991
+ Plenty of applications parse an `APP_ENV` through a config schema and never set
992
+ `NODE_ENV` at all. Those deployments used to be classified as production —
993
+ absence was the only evidence available — so the document was refused in an
994
+ environment that never asked for the refusal, with no way to answer back.
995
+
996
+ Declare the environment and it is used **where the process declares nothing**:
997
+
998
+ ```typescript
999
+ BymaxCoreModule.forRootAsync({
1000
+ inject: [ConfigService],
1001
+ useFactory: (config: ConfigService) => ({
1002
+ // Your validated value, not a second copy of NODE_ENV.
1003
+ environment: config.get('APP_ENV'),
1004
+ openapi: { enabled: true }
1005
+ })
1006
+ })
1007
+ ```
1008
+
1009
+ | `NODE_ENV` | `environment` | Classified as |
1010
+ | --------------- | ------------- | -------------- |
1011
+ | `production` | `development` | **production** |
1012
+ | `development` | (anything) | development |
1013
+ | unset, or blank | `development` | development |
1014
+ | unset, or blank | `staging` | **production** |
1015
+ | unset, or blank | unset | **production** |
1016
+
1017
+ Two properties are worth reading off that table. A declaration never overrules a
1018
+ process that named its own environment — the first row is the one that matters,
1019
+ and it is asserted in both guards rather than in one. And the declaration enters
1020
+ the same fail-closed classification, so an unrecognized name is production like
1021
+ any other; this is a second **source** for the value, never a second set of
1022
+ rules.
1023
+
1024
+ The narrowing is deliberate and worth naming rather than burying: in the one
1025
+ case where the process declares nothing, the configuration a consumer bound does
1026
+ decide the answer, because there is nothing else to decide it with. Replacing a
1027
+ guess with a declaration is not the same as allowing an override — but it is a
1028
+ real change to what the second guard depends on, and you should know it before
1029
+ relying on either.
1030
+
902
1031
  ### Testing the enabled path under Jest
903
1032
 
904
1033
  `applyBymaxOpenApi` loads `@nestjs/swagger` through a dynamic `import()` — that
@@ -1289,9 +1418,11 @@ guard you would apply to any internal endpoint, or keep it off the public listen
1289
1418
  A published document is a map of every route, parameter and error shape an application has —
1290
1419
  useful to a developer, and just as useful to anyone probing the service. So unlike the metrics
1291
1420
  endpoint, it is not left to a guard: it is refused outright whenever the runtime is not
1292
- positively `development` or `test`, in two independent layers, with no option to override.
1293
- An unset `NODE_ENV` counts as production, because the deployment nobody configured is the one
1294
- most likely to be exposed.
1421
+ positively `development` or `test`, in two independent layers. **`NODE_ENV` cannot be
1422
+ overridden** with it set to anything, no option serves the document in a runtime it named
1423
+ production. A runtime that declares nothing is classified from the application's own
1424
+ [`environment`](#environment) when it supplied one, and counts as production otherwise, because
1425
+ the deployment nobody configured is the one most likely to be exposed.
1295
1426
 
1296
1427
  ---
1297
1428
 
package/dist/index.cjs CHANGED
@@ -30,6 +30,13 @@ function isProductionRuntime(value = process.env["NODE_ENV"]) {
30
30
  }
31
31
  return !NON_PRODUCTION_ENVIRONMENTS.has(value.trim().toLowerCase());
32
32
  }
33
+ function runtimeEnvironmentName(declared) {
34
+ const fromProcess = process.env["NODE_ENV"];
35
+ if (fromProcess !== void 0 && fromProcess.trim() !== "") {
36
+ return fromProcess;
37
+ }
38
+ return declared;
39
+ }
33
40
 
34
41
  // src/core.options.ts
35
42
  var DEFAULT_INDICATOR_TIMEOUT_MS = 5e3;
@@ -92,9 +99,9 @@ function cloneServers(raw) {
92
99
  (server) => server.description === void 0 ? { url: server.url } : { url: server.url, description: server.description }
93
100
  );
94
101
  }
95
- function resolveOpenApi(raw) {
102
+ function resolveOpenApi(raw, declaredEnvironment) {
96
103
  const requested = raw?.enabled ?? false;
97
- const production = isProductionRuntime();
104
+ const production = isProductionRuntime(runtimeEnvironmentName(declaredEnvironment));
98
105
  return {
99
106
  enabled: requested && !production,
100
107
  suppressedInProduction: requested && production,
@@ -131,8 +138,12 @@ function normalizeCoreOptions(raw) {
131
138
  timing: resolveTiming(raw?.timing),
132
139
  health: resolveHealth(raw?.health),
133
140
  metrics: resolveMetrics(raw?.metrics),
134
- openapi: resolveOpenApi(raw?.openapi),
135
- telemetry: resolveTelemetry(raw?.telemetry)
141
+ openapi: resolveOpenApi(raw?.openapi, raw?.environment),
142
+ telemetry: resolveTelemetry(raw?.telemetry),
143
+ // Spread rather than assigned so an application that declared nothing has
144
+ // no `environment` member at all, matching every other optional member on
145
+ // this snapshot under `exactOptionalPropertyTypes`.
146
+ ...raw?.environment === void 0 ? {} : { environment: raw.environment }
136
147
  });
137
148
  }
138
149
  normalizeCoreOptions();
package/dist/index.d.cts CHANGED
@@ -273,6 +273,28 @@ interface BymaxCoreModuleOptions {
273
273
  openapi?: OpenApiOptions;
274
274
  /** Trace correlation. Default: disabled. */
275
275
  telemetry?: TelemetryOptions;
276
+ /**
277
+ * The environment this deployment is running in, for the features that must
278
+ * never exist outside development — today, the OpenAPI document and its UI.
279
+ *
280
+ * **`NODE_ENV` always wins.** This is consulted only when the process
281
+ * declares nothing: `NODE_ENV` unset, or set to whitespace. It cannot make a
282
+ * runtime that identified itself as production serve the document, and no
283
+ * value here overrides one there.
284
+ *
285
+ * Set it when your application validates its own environment variable — an
286
+ * `APP_ENV` your config schema parses — and does not also set `NODE_ENV`.
287
+ * Without it, that deployment is classified as production because absence was
288
+ * the only evidence available, and the document is refused in an environment
289
+ * that never asked for the refusal.
290
+ *
291
+ * Recognized non-production values are `development` and `test`, compared
292
+ * case-insensitively and ignoring surrounding whitespace. Anything else,
293
+ * including an unrecognized name, is production.
294
+ *
295
+ * @example 'development'
296
+ */
297
+ environment?: string;
276
298
  }
277
299
  /** Fully-resolved envelope options. */
278
300
  interface ResolvedEnvelopeOptions {
@@ -334,9 +356,9 @@ interface ResolvedOpenApiOptions {
334
356
  }
335
357
  /**
336
358
  * The effective, defaults-applied configuration exposed under
337
- * `BYMAX_CORE_OPTIONS`. Fields with a documented default are always present;
338
- * the only optional field is `timing.slowRequestThresholdMs`, which has no
339
- * default and is absent unless the consumer sets it.
359
+ * `BYMAX_CORE_OPTIONS`. Fields with a documented default are always present.
360
+ * Two fields have no default and are absent unless the consumer supplies them:
361
+ * `timing.slowRequestThresholdMs`, and `environment`.
340
362
  */
341
363
  interface ResolvedCoreOptions {
342
364
  envelope: ResolvedEnvelopeOptions;
@@ -345,6 +367,12 @@ interface ResolvedCoreOptions {
345
367
  metrics: ResolvedMetricsOptions;
346
368
  openapi: ResolvedOpenApiOptions;
347
369
  telemetry: ResolvedTelemetryOptions;
370
+ /**
371
+ * The environment the application declared, carried through so the bootstrap
372
+ * helper classifies the runtime from the same two inputs the resolver did.
373
+ * Absent when the application declared none.
374
+ */
375
+ environment?: string;
348
376
  }
349
377
 
350
378
  /** Non-option extras accepted by `forRoot` / `forRootAsync`. */
package/dist/index.d.ts CHANGED
@@ -273,6 +273,28 @@ interface BymaxCoreModuleOptions {
273
273
  openapi?: OpenApiOptions;
274
274
  /** Trace correlation. Default: disabled. */
275
275
  telemetry?: TelemetryOptions;
276
+ /**
277
+ * The environment this deployment is running in, for the features that must
278
+ * never exist outside development — today, the OpenAPI document and its UI.
279
+ *
280
+ * **`NODE_ENV` always wins.** This is consulted only when the process
281
+ * declares nothing: `NODE_ENV` unset, or set to whitespace. It cannot make a
282
+ * runtime that identified itself as production serve the document, and no
283
+ * value here overrides one there.
284
+ *
285
+ * Set it when your application validates its own environment variable — an
286
+ * `APP_ENV` your config schema parses — and does not also set `NODE_ENV`.
287
+ * Without it, that deployment is classified as production because absence was
288
+ * the only evidence available, and the document is refused in an environment
289
+ * that never asked for the refusal.
290
+ *
291
+ * Recognized non-production values are `development` and `test`, compared
292
+ * case-insensitively and ignoring surrounding whitespace. Anything else,
293
+ * including an unrecognized name, is production.
294
+ *
295
+ * @example 'development'
296
+ */
297
+ environment?: string;
276
298
  }
277
299
  /** Fully-resolved envelope options. */
278
300
  interface ResolvedEnvelopeOptions {
@@ -334,9 +356,9 @@ interface ResolvedOpenApiOptions {
334
356
  }
335
357
  /**
336
358
  * The effective, defaults-applied configuration exposed under
337
- * `BYMAX_CORE_OPTIONS`. Fields with a documented default are always present;
338
- * the only optional field is `timing.slowRequestThresholdMs`, which has no
339
- * default and is absent unless the consumer sets it.
359
+ * `BYMAX_CORE_OPTIONS`. Fields with a documented default are always present.
360
+ * Two fields have no default and are absent unless the consumer supplies them:
361
+ * `timing.slowRequestThresholdMs`, and `environment`.
340
362
  */
341
363
  interface ResolvedCoreOptions {
342
364
  envelope: ResolvedEnvelopeOptions;
@@ -345,6 +367,12 @@ interface ResolvedCoreOptions {
345
367
  metrics: ResolvedMetricsOptions;
346
368
  openapi: ResolvedOpenApiOptions;
347
369
  telemetry: ResolvedTelemetryOptions;
370
+ /**
371
+ * The environment the application declared, carried through so the bootstrap
372
+ * helper classifies the runtime from the same two inputs the resolver did.
373
+ * Absent when the application declared none.
374
+ */
375
+ environment?: string;
348
376
  }
349
377
 
350
378
  /** Non-option extras accepted by `forRoot` / `forRootAsync`. */
package/dist/index.mjs CHANGED
@@ -28,6 +28,13 @@ function isProductionRuntime(value = process.env["NODE_ENV"]) {
28
28
  }
29
29
  return !NON_PRODUCTION_ENVIRONMENTS.has(value.trim().toLowerCase());
30
30
  }
31
+ function runtimeEnvironmentName(declared) {
32
+ const fromProcess = process.env["NODE_ENV"];
33
+ if (fromProcess !== void 0 && fromProcess.trim() !== "") {
34
+ return fromProcess;
35
+ }
36
+ return declared;
37
+ }
31
38
 
32
39
  // src/core.options.ts
33
40
  var DEFAULT_INDICATOR_TIMEOUT_MS = 5e3;
@@ -90,9 +97,9 @@ function cloneServers(raw) {
90
97
  (server) => server.description === void 0 ? { url: server.url } : { url: server.url, description: server.description }
91
98
  );
92
99
  }
93
- function resolveOpenApi(raw) {
100
+ function resolveOpenApi(raw, declaredEnvironment) {
94
101
  const requested = raw?.enabled ?? false;
95
- const production = isProductionRuntime();
102
+ const production = isProductionRuntime(runtimeEnvironmentName(declaredEnvironment));
96
103
  return {
97
104
  enabled: requested && !production,
98
105
  suppressedInProduction: requested && production,
@@ -129,8 +136,12 @@ function normalizeCoreOptions(raw) {
129
136
  timing: resolveTiming(raw?.timing),
130
137
  health: resolveHealth(raw?.health),
131
138
  metrics: resolveMetrics(raw?.metrics),
132
- openapi: resolveOpenApi(raw?.openapi),
133
- telemetry: resolveTelemetry(raw?.telemetry)
139
+ openapi: resolveOpenApi(raw?.openapi, raw?.environment),
140
+ telemetry: resolveTelemetry(raw?.telemetry),
141
+ // Spread rather than assigned so an application that declared nothing has
142
+ // no `environment` member at all, matching every other optional member on
143
+ // this snapshot under `exactOptionalPropertyTypes`.
144
+ ...raw?.environment === void 0 ? {} : { environment: raw.environment }
134
145
  });
135
146
  }
136
147
  normalizeCoreOptions();
@@ -16,6 +16,13 @@ function isProductionRuntime(value = process.env["NODE_ENV"]) {
16
16
  }
17
17
  return !NON_PRODUCTION_ENVIRONMENTS.has(value.trim().toLowerCase());
18
18
  }
19
+ function runtimeEnvironmentName(declared) {
20
+ const fromProcess = process.env["NODE_ENV"];
21
+ if (fromProcess !== void 0 && fromProcess.trim() !== "") {
22
+ return fromProcess;
23
+ }
24
+ return declared;
25
+ }
19
26
 
20
27
  // src/discovery.ts
21
28
  function labelFor(className, token) {
@@ -653,7 +660,7 @@ async function applyBymaxOpenApi(app) {
653
660
  const logger = new common.Logger("BymaxCoreModule");
654
661
  const resolved = resolveCoreOptions(app);
655
662
  const options = resolved.openapi;
656
- if (isProductionRuntime()) {
663
+ if (isProductionRuntime(runtimeEnvironmentName(resolved.environment))) {
657
664
  if (options.suppressedInProduction || options.enabled) {
658
665
  logger.warn(
659
666
  'openapi.enabled was requested but the OpenAPI document is never served in production. Set NODE_ENV to "development" or "test" to serve it.'
@@ -14,6 +14,13 @@ function isProductionRuntime(value = process.env["NODE_ENV"]) {
14
14
  }
15
15
  return !NON_PRODUCTION_ENVIRONMENTS.has(value.trim().toLowerCase());
16
16
  }
17
+ function runtimeEnvironmentName(declared) {
18
+ const fromProcess = process.env["NODE_ENV"];
19
+ if (fromProcess !== void 0 && fromProcess.trim() !== "") {
20
+ return fromProcess;
21
+ }
22
+ return declared;
23
+ }
17
24
 
18
25
  // src/discovery.ts
19
26
  function labelFor(className, token) {
@@ -651,7 +658,7 @@ async function applyBymaxOpenApi(app) {
651
658
  const logger = new Logger("BymaxCoreModule");
652
659
  const resolved = resolveCoreOptions(app);
653
660
  const options = resolved.openapi;
654
- if (isProductionRuntime()) {
661
+ if (isProductionRuntime(runtimeEnvironmentName(resolved.environment))) {
655
662
  if (options.suppressedInProduction || options.enabled) {
656
663
  logger.warn(
657
664
  'openapi.enabled was requested but the OpenAPI document is never served in production. Set NODE_ENV to "development" or "test" to serve it.'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bymax-one/nest-core",
3
- "version": "1.5.0",
3
+ "version": "1.5.2",
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",