@nage-api/core 1.0.0-beta.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.
Files changed (81) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +141 -0
  3. package/dist/bootstrap/bootstrap.d.ts +48 -0
  4. package/dist/bootstrap/bootstrap.js +255 -0
  5. package/dist/bootstrap/drain.d.ts +48 -0
  6. package/dist/bootstrap/drain.js +113 -0
  7. package/dist/bootstrap/lifecycle.d.ts +30 -0
  8. package/dist/bootstrap/lifecycle.js +64 -0
  9. package/dist/bootstrap/process-guards.d.ts +42 -0
  10. package/dist/bootstrap/process-guards.js +103 -0
  11. package/dist/bootstrap/query-parser.d.ts +34 -0
  12. package/dist/bootstrap/query-parser.js +37 -0
  13. package/dist/bootstrap/shutdown.d.ts +55 -0
  14. package/dist/bootstrap/shutdown.js +182 -0
  15. package/dist/constants.d.ts +32 -0
  16. package/dist/constants.js +48 -0
  17. package/dist/context/active-context.d.ts +23 -0
  18. package/dist/context/active-context.js +34 -0
  19. package/dist/context/request-context.middleware.d.ts +31 -0
  20. package/dist/context/request-context.middleware.js +95 -0
  21. package/dist/context/request-context.service.d.ts +29 -0
  22. package/dist/context/request-context.service.js +67 -0
  23. package/dist/decorators/owner.decorator.d.ts +18 -0
  24. package/dist/decorators/owner.decorator.js +31 -0
  25. package/dist/decorators/public.decorator.d.ts +13 -0
  26. package/dist/decorators/public.decorator.js +23 -0
  27. package/dist/decorators/version.decorators.d.ts +34 -0
  28. package/dist/decorators/version.decorators.js +40 -0
  29. package/dist/errors/catalog.d.ts +149 -0
  30. package/dist/errors/catalog.js +289 -0
  31. package/dist/errors/index.d.ts +3 -0
  32. package/dist/errors/index.js +22 -0
  33. package/dist/errors/nage.error.d.ts +43 -0
  34. package/dist/errors/nage.error.js +45 -0
  35. package/dist/guards/api-version.guard.d.ts +20 -0
  36. package/dist/guards/api-version.guard.js +73 -0
  37. package/dist/http/all-exceptions.filter.d.ts +25 -0
  38. package/dist/http/all-exceptions.filter.js +256 -0
  39. package/dist/http/envelope.d.ts +25 -0
  40. package/dist/http/envelope.js +44 -0
  41. package/dist/http/no-envelope.decorator.d.ts +11 -0
  42. package/dist/http/no-envelope.decorator.js +16 -0
  43. package/dist/http/request-timeout.decorators.d.ts +23 -0
  44. package/dist/http/request-timeout.decorators.js +29 -0
  45. package/dist/http/request-timeout.interceptor.d.ts +28 -0
  46. package/dist/http/request-timeout.interceptor.js +75 -0
  47. package/dist/http/response.interceptor.d.ts +19 -0
  48. package/dist/http/response.interceptor.js +73 -0
  49. package/dist/index.d.ts +38 -0
  50. package/dist/index.js +135 -0
  51. package/dist/job/job.factory.d.ts +29 -0
  52. package/dist/job/job.factory.js +50 -0
  53. package/dist/logging/json.logger.d.ts +23 -0
  54. package/dist/logging/json.logger.js +136 -0
  55. package/dist/logging/nest-logger.adapter.d.ts +20 -0
  56. package/dist/logging/nest-logger.adapter.js +46 -0
  57. package/dist/module/core.module.d.ts +40 -0
  58. package/dist/module/core.module.js +112 -0
  59. package/dist/security/audit.d.ts +42 -0
  60. package/dist/security/audit.js +399 -0
  61. package/dist/security/index.d.ts +15 -0
  62. package/dist/security/index.js +50 -0
  63. package/dist/security/legacy-scan.d.ts +24 -0
  64. package/dist/security/legacy-scan.js +98 -0
  65. package/dist/security/random.d.ts +40 -0
  66. package/dist/security/random.js +87 -0
  67. package/dist/security/rate-limit.decorators.d.ts +24 -0
  68. package/dist/security/rate-limit.decorators.js +25 -0
  69. package/dist/security/rate-limit.guard.d.ts +44 -0
  70. package/dist/security/rate-limit.guard.js +130 -0
  71. package/dist/security/rate-limit.store.d.ts +30 -0
  72. package/dist/security/rate-limit.store.js +63 -0
  73. package/dist/security/redaction.d.ts +54 -0
  74. package/dist/security/redaction.js +146 -0
  75. package/dist/security/tls.d.ts +29 -0
  76. package/dist/security/tls.js +48 -0
  77. package/dist/tokens.d.ts +60 -0
  78. package/dist/tokens.js +89 -0
  79. package/dist/version.d.ts +5 -0
  80. package/dist/version.js +8 -0
  81. package/package.json +77 -0
@@ -0,0 +1,48 @@
1
+ /**
2
+ * In-flight request accounting, so shutdown can wait for the requests it has
3
+ * (PLAN.md §21).
4
+ *
5
+ * `app.close()` alone cannot do this. Nest runs `onModuleDestroy` *before* it
6
+ * closes the HTTP server, so a handler still executing when the signal arrives
7
+ * finds its connection pool already shut — measured, not assumed: a 600ms
8
+ * handler signalled at 200ms returned `pool is closed` with HTTP 200. Knowing
9
+ * the number of live requests is what lets the sequence be put the right way
10
+ * round.
11
+ *
12
+ * Counted in middleware rather than in an interceptor because an interceptor
13
+ * only sees requests that reach a matched route: a request that 404s, or that
14
+ * dies in body parsing, holds a connection just the same.
15
+ */
16
+ /** The subset of a Node/Express response the tracker touches. */
17
+ export interface DrainResponseLike {
18
+ once(event: 'close', listener: () => void): unknown;
19
+ setHeader?: (name: string, value: string) => void;
20
+ readonly headersSent?: boolean;
21
+ }
22
+ export type DrainMiddleware = (request: unknown, response: DrainResponseLike, next: () => void) => void;
23
+ export interface RequestDrainOptions {
24
+ /** Consulted per request; `true` once a shutdown signal has been seen. */
25
+ readonly isDraining?: () => boolean;
26
+ }
27
+ export declare class RequestDrain {
28
+ #private;
29
+ constructor(options?: RequestDrainOptions);
30
+ /** Requests that have been accepted and not yet had their response closed. */
31
+ get active(): number;
32
+ /**
33
+ * The middleware. Install it first, ahead of helmet and the body parser, so
34
+ * the count covers the whole time the connection is held.
35
+ */
36
+ middleware(): DrainMiddleware;
37
+ /**
38
+ * Resolve when no request is in flight, or on the deadline.
39
+ *
40
+ * Returns whether it drained; the caller logs the difference, because "we cut
41
+ * off 3 requests" and "we finished cleanly" must not look the same in a deploy
42
+ * log.
43
+ */
44
+ whenIdle(timeoutMs: number): Promise<boolean>;
45
+ }
46
+ export declare function registerRequestDrain(app: object, drain: RequestDrain): void;
47
+ export declare function requestDrainFor(app: object): RequestDrain | undefined;
48
+ //# sourceMappingURL=drain.d.ts.map
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ /**
3
+ * In-flight request accounting, so shutdown can wait for the requests it has
4
+ * (PLAN.md §21).
5
+ *
6
+ * `app.close()` alone cannot do this. Nest runs `onModuleDestroy` *before* it
7
+ * closes the HTTP server, so a handler still executing when the signal arrives
8
+ * finds its connection pool already shut — measured, not assumed: a 600ms
9
+ * handler signalled at 200ms returned `pool is closed` with HTTP 200. Knowing
10
+ * the number of live requests is what lets the sequence be put the right way
11
+ * round.
12
+ *
13
+ * Counted in middleware rather than in an interceptor because an interceptor
14
+ * only sees requests that reach a matched route: a request that 404s, or that
15
+ * dies in body parsing, holds a connection just the same.
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.RequestDrain = void 0;
19
+ exports.registerRequestDrain = registerRequestDrain;
20
+ exports.requestDrainFor = requestDrainFor;
21
+ class RequestDrain {
22
+ #active = 0;
23
+ #idle = new Set();
24
+ #isDraining;
25
+ constructor(options = {}) {
26
+ this.#isDraining = options.isDraining ?? (() => false);
27
+ }
28
+ /** Requests that have been accepted and not yet had their response closed. */
29
+ get active() {
30
+ return this.#active;
31
+ }
32
+ /**
33
+ * The middleware. Install it first, ahead of helmet and the body parser, so
34
+ * the count covers the whole time the connection is held.
35
+ */
36
+ middleware() {
37
+ return (_request, response, next) => {
38
+ this.#active += 1;
39
+ // A draining process keeps serving — the orchestrator may still be
40
+ // routing to it — but tells each client not to reuse the socket, so the
41
+ // connection is released at the end of the response instead of being cut
42
+ // when the listener closes. Requests are *not* refused here: a blanket 503
43
+ // would also refuse `/health/live`, and a liveness probe failing during a
44
+ // drain is what turns a graceful shutdown into a `SIGKILL`.
45
+ if (this.#isDraining() && response.headersSent !== true) {
46
+ response.setHeader?.('connection', 'close');
47
+ }
48
+ // `close` rather than `finish`: an aborted response never finishes, and a
49
+ // counter that only ever goes up makes the drain wait out its full
50
+ // deadline on every deploy.
51
+ let settled = false;
52
+ response.once('close', () => {
53
+ if (settled)
54
+ return;
55
+ settled = true;
56
+ this.#active -= 1;
57
+ if (this.#active <= 0)
58
+ this.#releaseIdleWaiters();
59
+ });
60
+ next();
61
+ };
62
+ }
63
+ /**
64
+ * Resolve when no request is in flight, or on the deadline.
65
+ *
66
+ * Returns whether it drained; the caller logs the difference, because "we cut
67
+ * off 3 requests" and "we finished cleanly" must not look the same in a deploy
68
+ * log.
69
+ */
70
+ async whenIdle(timeoutMs) {
71
+ if (this.#active <= 0)
72
+ return true;
73
+ if (timeoutMs <= 0)
74
+ return false;
75
+ return new Promise((resolve) => {
76
+ const timer = setTimeout(() => {
77
+ this.#idle.delete(waiter);
78
+ resolve(false);
79
+ }, timeoutMs);
80
+ // Nothing about a shutdown deadline should itself hold the event loop
81
+ // open; the awaited promise is what keeps the sequence alive.
82
+ timer.unref();
83
+ const waiter = () => {
84
+ clearTimeout(timer);
85
+ resolve(true);
86
+ };
87
+ this.#idle.add(waiter);
88
+ });
89
+ }
90
+ #releaseIdleWaiters() {
91
+ const waiters = [...this.#idle];
92
+ this.#idle.clear();
93
+ for (const waiter of waiters)
94
+ waiter();
95
+ }
96
+ }
97
+ exports.RequestDrain = RequestDrain;
98
+ /**
99
+ * The tracker belongs to one application instance.
100
+ *
101
+ * A `WeakMap` rather than a provider: the middleware has to be installed by
102
+ * `createApplication` (before helmet, outside the DI-managed middleware chain),
103
+ * and a module-level singleton would let one test suite's counter leak into the
104
+ * next application built in the same process.
105
+ */
106
+ const drains = new WeakMap();
107
+ function registerRequestDrain(app, drain) {
108
+ drains.set(app, drain);
109
+ }
110
+ function requestDrainFor(app) {
111
+ return drains.get(app);
112
+ }
113
+ //# sourceMappingURL=drain.js.map
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Where the process is in its own life (PLAN.md §21).
3
+ *
4
+ * Readiness and shutdown are two views of one fact, and before this they were
5
+ * two facts: `/health/ready` answered from the dependency probes alone, so a
6
+ * process that had received `SIGTERM` and was draining still told the load
7
+ * balancer to send it traffic. Every request routed in that window is a request
8
+ * whose connection is about to be closed.
9
+ *
10
+ * Liveness deliberately does **not** read this. A draining process is healthy;
11
+ * failing liveness while it drains asks the orchestrator to `SIGKILL` the very
12
+ * process that is trying to finish its work.
13
+ */
14
+ export type LifecyclePhase = 'starting' | 'live' | 'draining';
15
+ export declare class LifecycleState {
16
+ #private;
17
+ get phase(): LifecyclePhase;
18
+ /** True from the moment a shutdown signal is observed. */
19
+ get draining(): boolean;
20
+ markLive(): void;
21
+ markDraining(): void;
22
+ /**
23
+ * Back to `starting`. A deployed process never does this — a signal is not
24
+ * something to undo — but a test runner that builds several applications in one
25
+ * process needs the phase not to leak from one to the next.
26
+ */
27
+ reset(): void;
28
+ }
29
+ export declare function processLifecycle(): LifecycleState;
30
+ //# sourceMappingURL=lifecycle.d.ts.map
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ /**
3
+ * Where the process is in its own life (PLAN.md §21).
4
+ *
5
+ * Readiness and shutdown are two views of one fact, and before this they were
6
+ * two facts: `/health/ready` answered from the dependency probes alone, so a
7
+ * process that had received `SIGTERM` and was draining still told the load
8
+ * balancer to send it traffic. Every request routed in that window is a request
9
+ * whose connection is about to be closed.
10
+ *
11
+ * Liveness deliberately does **not** read this. A draining process is healthy;
12
+ * failing liveness while it drains asks the orchestrator to `SIGKILL` the very
13
+ * process that is trying to finish its work.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.LifecycleState = void 0;
17
+ exports.processLifecycle = processLifecycle;
18
+ class LifecycleState {
19
+ #phase = 'starting';
20
+ get phase() {
21
+ return this.#phase;
22
+ }
23
+ /** True from the moment a shutdown signal is observed. */
24
+ get draining() {
25
+ return this.#phase === 'draining';
26
+ }
27
+ markLive() {
28
+ // A late `markLive()` — a listener callback that fires after a signal
29
+ // already arrived — must not resurrect a draining process.
30
+ if (this.#phase === 'starting')
31
+ this.#phase = 'live';
32
+ }
33
+ markDraining() {
34
+ this.#phase = 'draining';
35
+ }
36
+ /**
37
+ * Back to `starting`. A deployed process never does this — a signal is not
38
+ * something to undo — but a test runner that builds several applications in one
39
+ * process needs the phase not to leak from one to the next.
40
+ */
41
+ reset() {
42
+ this.#phase = 'starting';
43
+ }
44
+ }
45
+ exports.LifecycleState = LifecycleState;
46
+ /**
47
+ * The phase belongs to the **process**, not to an application object.
48
+ *
49
+ * A signal arrives at the process: if it hosted two applications, both are
50
+ * draining, and there is no meaningful state in which one of them is not. Holding
51
+ * it here rather than resolving it through DI also avoids a trap that cost a
52
+ * boot: `NestFactory.create` returns a Proxy that wraps every method in
53
+ * `ExceptionsZone`, whose default teardown is `process.exit(1)`. So
54
+ * `app.get(TOKEN)` for a token an application has not registered does not throw
55
+ * for a caller to catch — it kills the process. Measured: `createApplication` on a
56
+ * module that does not import `NageCoreModule` exited 1 before the listener was
57
+ * bound.
58
+ */
59
+ let processState;
60
+ function processLifecycle() {
61
+ processState ??= new LifecycleState();
62
+ return processState;
63
+ }
64
+ //# sourceMappingURL=lifecycle.js.map
@@ -0,0 +1,42 @@
1
+ /**
2
+ * What happens when an error escapes everything (PLAN.md §21, §18).
3
+ *
4
+ * Node's own policy is nearly right and reports nothing useful. An
5
+ * `uncaughtException` or an unhandled rejection prints a human-readable stack to
6
+ * stderr and exits 1 — so the one line describing why production died is the
7
+ * only line of the day that a JSON log shipper cannot parse, it carries no
8
+ * correlation id, and the pools are dropped rather than closed.
9
+ *
10
+ * The policy here keeps the crash and fixes the reporting:
11
+ *
12
+ * - **Both events are fatal.** Nothing is swallowed. A process that has thrown
13
+ * from outside a request has unknown state, and serving the next request from
14
+ * unknown state is how a bug becomes a data defect.
15
+ * - **One structured `fatal` line first**, through the framework logger, so the
16
+ * reason survives in the same stream as everything else.
17
+ * - **Then a bounded graceful shutdown** (`onFatal`), so in-flight responses get
18
+ * a chance to finish and pools get closed. Bounded because the handler cannot
19
+ * trust the very machinery it is shutting down.
20
+ *
21
+ * Installing a listener for these events also takes them off Node's default
22
+ * path, which is why the exit is explicit: a handler that logs and returns turns
23
+ * a crash into a zombie.
24
+ */
25
+ import type { LoggerPort } from '@nage-api/contracts';
26
+ export type FatalKind = 'uncaughtException' | 'unhandledRejection';
27
+ export interface ProcessGuardOptions {
28
+ readonly logger: LoggerPort;
29
+ /**
30
+ * Graceful shutdown to attempt before exiting — normally the
31
+ * `ShutdownHandle.run` returned by `installShutdown`.
32
+ */
33
+ readonly onFatal?: (kind: FatalKind, error: unknown) => unknown;
34
+ /** How long `onFatal` may take before the process exits anyway. */
35
+ readonly gracePeriodMs?: number;
36
+ /** Injected so a test can assert the exit code without exiting the runner. */
37
+ readonly exit?: (code: number) => void;
38
+ }
39
+ /** Removes the listeners this installed. */
40
+ export type DisposeProcessGuards = () => void;
41
+ export declare function installProcessGuards(options: ProcessGuardOptions): DisposeProcessGuards;
42
+ //# sourceMappingURL=process-guards.d.ts.map
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ /**
3
+ * What happens when an error escapes everything (PLAN.md §21, §18).
4
+ *
5
+ * Node's own policy is nearly right and reports nothing useful. An
6
+ * `uncaughtException` or an unhandled rejection prints a human-readable stack to
7
+ * stderr and exits 1 — so the one line describing why production died is the
8
+ * only line of the day that a JSON log shipper cannot parse, it carries no
9
+ * correlation id, and the pools are dropped rather than closed.
10
+ *
11
+ * The policy here keeps the crash and fixes the reporting:
12
+ *
13
+ * - **Both events are fatal.** Nothing is swallowed. A process that has thrown
14
+ * from outside a request has unknown state, and serving the next request from
15
+ * unknown state is how a bug becomes a data defect.
16
+ * - **One structured `fatal` line first**, through the framework logger, so the
17
+ * reason survives in the same stream as everything else.
18
+ * - **Then a bounded graceful shutdown** (`onFatal`), so in-flight responses get
19
+ * a chance to finish and pools get closed. Bounded because the handler cannot
20
+ * trust the very machinery it is shutting down.
21
+ *
22
+ * Installing a listener for these events also takes them off Node's default
23
+ * path, which is why the exit is explicit: a handler that logs and returns turns
24
+ * a crash into a zombie.
25
+ */
26
+ Object.defineProperty(exports, "__esModule", { value: true });
27
+ exports.installProcessGuards = installProcessGuards;
28
+ const DEFAULT_GRACE_PERIOD_MS = 5000;
29
+ function installProcessGuards(options) {
30
+ const { logger } = options;
31
+ const exit = options.exit ?? ((code) => process.exit(code));
32
+ const gracePeriodMs = options.gracePeriodMs ?? DEFAULT_GRACE_PERIOD_MS;
33
+ let handling = false;
34
+ const handle = (kind, error) => {
35
+ // A rejection thrown from inside the fatal handler must not restart it.
36
+ if (handling)
37
+ return;
38
+ handling = true;
39
+ logger.fatal('Fatal error; the process will exit', {
40
+ kind,
41
+ error: error instanceof Error ? error.message : String(error),
42
+ stack: error instanceof Error ? error.stack : undefined,
43
+ // A rejection with a non-Error reason (`reject('nope')`, or a rejected
44
+ // fetch response) is the case where the stack is absent and the value is
45
+ // the only evidence there is.
46
+ ...(error instanceof Error ? {} : { value: safeValue(error) }),
47
+ });
48
+ // Set now, so that even if the event loop empties before either the shutdown
49
+ // or the grace period finishes, the process still reports a failure.
50
+ process.exitCode = 1;
51
+ let timer;
52
+ const grace = new Promise((resolve) => {
53
+ // Not `unref`ed: the grace period is what keeps the process alive long
54
+ // enough to close its pools, and an unreferenced timer would let it exit
55
+ // the moment the last request's socket closed.
56
+ timer = setTimeout(resolve, gracePeriodMs);
57
+ });
58
+ const attempt = (async () => {
59
+ await options.onFatal?.(kind, error);
60
+ })();
61
+ void Promise.race([attempt, grace]).then(() => {
62
+ if (timer !== undefined)
63
+ clearTimeout(timer);
64
+ exit(1);
65
+ }, () => {
66
+ // The shutdown itself failed. Still exit: this path exists to guarantee
67
+ // the process does not keep serving.
68
+ if (timer !== undefined)
69
+ clearTimeout(timer);
70
+ exit(1);
71
+ });
72
+ };
73
+ const onUncaught = (error) => {
74
+ handle('uncaughtException', error);
75
+ };
76
+ const onRejection = (reason) => {
77
+ handle('unhandledRejection', reason);
78
+ };
79
+ process.on('uncaughtException', onUncaught);
80
+ process.on('unhandledRejection', onRejection);
81
+ return () => {
82
+ process.removeListener('uncaughtException', onUncaught);
83
+ process.removeListener('unhandledRejection', onRejection);
84
+ };
85
+ }
86
+ /** A rejection reason can be anything, including something that will not stringify. */
87
+ function safeValue(value) {
88
+ // `Promise.reject()` with no reason is a real case, and `JSON.stringify` returns
89
+ // `undefined` for it however the types describe that.
90
+ if (value === undefined)
91
+ return 'undefined';
92
+ if (typeof value === 'string')
93
+ return value;
94
+ try {
95
+ return JSON.stringify(value);
96
+ }
97
+ catch {
98
+ // A cyclic or getter-throwing reason must not turn the fatal handler into a
99
+ // second fatal error.
100
+ return '[unserializable]';
101
+ }
102
+ }
103
+ //# sourceMappingURL=process-guards.js.map
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The query parser the URL query DSL needs (PLAN.md §14.2).
3
+ *
4
+ * Express 5 changed its default `query parser` from `extended` to `simple`.
5
+ * Under `simple`, `?where[status]=active` does not become
6
+ * `{ where: { status: 'active' } }` — it becomes the single flat key
7
+ * `'where[status]'`. Nothing errors: `parseQuery` sees a top-level key it does
8
+ * not recognise, ignores it, and the request returns **the whole collection with
9
+ * a 200**. A client that asked to filter silently gets everything, which is the
10
+ * same class of failure as a missing allow-list.
11
+ *
12
+ * So the setting is applied explicitly rather than inherited. It is a named
13
+ * export as well as part of `bootstrap`, because an application built with
14
+ * `Test.createTestingModule(...).createNestApplication()` never goes through
15
+ * `bootstrap` and would otherwise test a differently-configured app than it ships.
16
+ */
17
+ /** The Nest-shaped slice needed to reach that instance. */
18
+ interface AppWithHttpAdapter {
19
+ getHttpAdapter: () => {
20
+ getInstance: () => unknown;
21
+ };
22
+ }
23
+ /**
24
+ * Enable bracketed query parsing on an application.
25
+ *
26
+ * A no-op on a platform whose instance has no `set` — Fastify parses brackets
27
+ * through its own querystring parser, so there is nothing to switch on and
28
+ * nothing to warn about.
29
+ *
30
+ * @returns whether the setting was applied
31
+ */
32
+ export declare function enableQueryDsl(app: AppWithHttpAdapter): boolean;
33
+ export {};
34
+ //# sourceMappingURL=query-parser.d.ts.map
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ /**
3
+ * The query parser the URL query DSL needs (PLAN.md §14.2).
4
+ *
5
+ * Express 5 changed its default `query parser` from `extended` to `simple`.
6
+ * Under `simple`, `?where[status]=active` does not become
7
+ * `{ where: { status: 'active' } }` — it becomes the single flat key
8
+ * `'where[status]'`. Nothing errors: `parseQuery` sees a top-level key it does
9
+ * not recognise, ignores it, and the request returns **the whole collection with
10
+ * a 200**. A client that asked to filter silently gets everything, which is the
11
+ * same class of failure as a missing allow-list.
12
+ *
13
+ * So the setting is applied explicitly rather than inherited. It is a named
14
+ * export as well as part of `bootstrap`, because an application built with
15
+ * `Test.createTestingModule(...).createNestApplication()` never goes through
16
+ * `bootstrap` and would otherwise test a differently-configured app than it ships.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.enableQueryDsl = enableQueryDsl;
20
+ /**
21
+ * Enable bracketed query parsing on an application.
22
+ *
23
+ * A no-op on a platform whose instance has no `set` — Fastify parses brackets
24
+ * through its own querystring parser, so there is nothing to switch on and
25
+ * nothing to warn about.
26
+ *
27
+ * @returns whether the setting was applied
28
+ */
29
+ function enableQueryDsl(app) {
30
+ const instance = app.getHttpAdapter().getInstance();
31
+ const settable = instance;
32
+ if (typeof settable.set !== 'function')
33
+ return false;
34
+ settable.set('query parser', 'extended');
35
+ return true;
36
+ }
37
+ //# sourceMappingURL=query-parser.js.map
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Graceful shutdown, in the order the stages actually have to happen
3
+ * (PLAN.md §21).
4
+ *
5
+ * `app.enableShutdownHooks()` is not this. Nest 11's signal handler runs
6
+ * `onModuleDestroy` → `beforeApplicationShutdown` → close the HTTP server →
7
+ * `onApplicationShutdown`, so **modules destroy their pools while requests are
8
+ * still running**. Measured against a 600ms handler signalled at 200ms: the
9
+ * handler's query came back `pool is closed` and the client got that as a 200.
10
+ * A second measurement: one handler that never returns kept the process alive
11
+ * past 8 seconds, because nothing in that sequence has a deadline — the
12
+ * container survives until the orchestrator's `SIGKILL`.
13
+ *
14
+ * This runs the stages in the order that makes each one meaningful:
15
+ *
16
+ * 1. **Fail readiness.** The load balancer needs to stop choosing this
17
+ * instance, and endpoint propagation is not instant (`readinessDelayMs`).
18
+ * 2. **Stop accepting connections**, and release keep-alive sockets that have
19
+ * no request on them. Requests already in flight keep their sockets.
20
+ * 3. **Wait for those requests**, bounded by `drainTimeoutMs`. Past the
21
+ * deadline the remaining sockets are cut, because a client that has hung is
22
+ * not a reason to abandon the pools.
23
+ * 4. **`app.close()`** — only now do modules close pools, queues and Redis.
24
+ * 5. **Exit.** Zero if the sequence finished, non-zero if `forceExitAfterMs`
25
+ * expired, so a deploy can tell a clean drain from a broken one.
26
+ */
27
+ import type { LoggerPort, NageCoreConfig } from '@nage-api/contracts';
28
+ import { type LifecycleState } from './lifecycle.js';
29
+ import { RequestDrain } from './drain.js';
30
+ /** The subset of `INestApplication` this needs; keeps tests free of a real app. */
31
+ export interface ClosableApplication {
32
+ close(): Promise<void>;
33
+ getHttpServer?: () => unknown;
34
+ }
35
+ export interface ShutdownOptions {
36
+ readonly config?: NageCoreConfig;
37
+ readonly logger?: LoggerPort;
38
+ /** Defaults to `SIGTERM` and `SIGINT`. */
39
+ readonly signals?: readonly NodeJS.Signals[];
40
+ /** Injected so a test can assert the exit code without exiting the runner. */
41
+ readonly exit?: (code: number) => void;
42
+ readonly lifecycle?: LifecycleState;
43
+ readonly drain?: RequestDrain;
44
+ }
45
+ export interface ShutdownHandle {
46
+ readonly draining: boolean;
47
+ /** Run the sequence. Idempotent: a second call awaits the first. */
48
+ run(reason: string): Promise<number>;
49
+ /** Remove the signal listeners. Tests and embedded apps need this. */
50
+ dispose(): void;
51
+ }
52
+ export declare function installShutdown(app: ClosableApplication, options?: ShutdownOptions): ShutdownHandle;
53
+ export declare function registerShutdownHandle(app: object, handle: ShutdownHandle): void;
54
+ export declare function shutdownHandleFor(app: object): ShutdownHandle | undefined;
55
+ //# sourceMappingURL=shutdown.d.ts.map