@lenne.tech/nest-server 11.31.3 → 11.32.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/.claude/rules/versioning.md +5 -8
  2. package/CLAUDE.md +3 -3
  3. package/FRAMEWORK-API.md +4 -2
  4. package/README.md +1 -0
  5. package/dist/core/common/helpers/process-diagnostics.helper.d.ts +18 -0
  6. package/dist/core/common/helpers/process-diagnostics.helper.js +88 -0
  7. package/dist/core/common/helpers/process-diagnostics.helper.js.map +1 -0
  8. package/dist/core/common/interfaces/server-options.interface.d.ts +3 -0
  9. package/dist/core/common/services/brevo.service.d.ts +7 -1
  10. package/dist/core/common/services/brevo.service.js +37 -16
  11. package/dist/core/common/services/brevo.service.js.map +1 -1
  12. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js +9 -4
  13. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js.map +1 -1
  14. package/dist/core/modules/migrate/migration-runner.js +4 -0
  15. package/dist/core/modules/migrate/migration-runner.js.map +1 -1
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.js +1 -0
  18. package/dist/index.js.map +1 -1
  19. package/dist/main.js +5 -2
  20. package/dist/main.js.map +1 -1
  21. package/dist/tsconfig.build.tsbuildinfo +1 -1
  22. package/docs/REQUEST-LIFECYCLE.md +1 -0
  23. package/docs/brevo-manual-test.md +166 -0
  24. package/docs/security-overrides.md +90 -0
  25. package/migration-guides/11.31.3-to-11.32.0.md +254 -0
  26. package/migration-guides/11.32.0-to-11.32.1.md +84 -0
  27. package/package.json +15 -14
  28. package/src/core/common/helpers/process-diagnostics.helper.spec.ts +310 -0
  29. package/src/core/common/helpers/process-diagnostics.helper.ts +321 -0
  30. package/src/core/common/interfaces/server-options.interface.ts +32 -0
  31. package/src/core/common/services/brevo.service.spec.ts +266 -0
  32. package/src/core/common/services/brevo.service.ts +100 -17
  33. package/src/core/modules/better-auth/core-better-auth-email-verification.service.ts +14 -4
  34. package/src/core/modules/hub/helpers/hub-mermaid.helper.spec.ts +8 -1
  35. package/src/core/modules/migrate/migration-runner.ts +17 -0
  36. package/src/index.ts +1 -0
  37. package/src/main.ts +22 -3
@@ -214,6 +214,7 @@ JWT-based authentication for existing projects:
214
214
  | **Error Code Module** | Centralized error registry with unique IDs |
215
215
  | **Permissions Report** | Interactive HTML dashboard, JSON, and Markdown reports |
216
216
  | **Hub (Operator Cockpit)** | Build-free ADMIN-gated dashboard at `/hub` (config-gated per environment). Adds an optional HTTP trace middleware (registered by `CoreHubModule.configure()` only when traces are enabled), a chaining `Logger.overrideLogger()` delegate for the log buffer, an optional `EmailService` capture hook (`HUB_EMAIL_CAPTURE` token) for the mailbox, and — when the query profiler is enabled — opts the MongoDB driver into `monitorCommands` from `core.module.ts`. See `src/core/modules/hub/README.md`. |
217
+ | **Process Diagnostics** | Opt-in process-level exit diagnostics (`installProcessDiagnostics()` + `handleFatalBootstrapError`, `src/core/common/helpers/process-diagnostics.helper.ts`). Wired into `main.ts` — **NOT** into `CoreModule.forRoot()`, because it must run before `NestFactory.create()` and installs a `process.exit(1)` path that must never arm inside `Test.createTestingModule()`. Logs unhandled rejections without crashing (configurable), uncaught exceptions before the exit, non-zero exit codes, and labels SIGTERM/SIGINT/SIGHUP/SIGQUIT as external terminations. Pair with `server.enableShutdownHooks()` — see the helper docblock |
217
218
  | **System Setup Module** | Initial admin creation for fresh deployments |
218
219
  | **Cron Jobs** | `CoreCronJobsService` with timezone/UTC offset support |
219
220
  | **Model Documentation** | Auto-generated model docs via `ModelDocService` |
@@ -0,0 +1,166 @@
1
+ # Brevo: manual test against the real API
2
+
3
+ > **Framework repo only.** This page ships inside the npm package (because `docs/**` is in
4
+ > `package.json` `files`), but the things it tells you to run do **not**: `scripts/` and
5
+ > `.env.example` are excluded from the tarball, and `src/config.env.ts` is a path that only exists
6
+ > in this repository. If you are reading this from
7
+ > `node_modules/@lenne.tech/nest-server/docs/`, clone
8
+ > [lenneTech/nest-server](https://github.com/lenneTech/nest-server) to follow along. For configuring
9
+ > Brevo in *your* project, see the `brevo` block in `FRAMEWORK-API.md` instead.
10
+
11
+ The unit spec (`src/core/common/services/brevo.service.spec.ts`) covers `BrevoService` against a
12
+ mocked SDK. That proves the logic, not the wire format. This guide is the one-off manual check
13
+ against the **live** Brevo endpoint — worth running once after an SDK upgrade, and any time you
14
+ suspect a credential or sender problem.
15
+
16
+ > Every send here costs one transactional email from your Brevo quota and really lands in a mailbox.
17
+
18
+ ## 1. Get the credentials
19
+
20
+ | What | Where to get it |
21
+ | ------------------- | -------------------------------------------------------------------------------------------------------- |
22
+ | **API key** | <https://app.brevo.com/settings/keys/api> → *Generate a new API key*. Starts with `xkeysib-`. |
23
+ | **Verified sender** | <https://app.brevo.com/senders/list> → *Add a sender*, then confirm the mail Brevo sends to that address. |
24
+ | **Template ID** | *(optional)* <https://app.brevo.com/templates/listing> → open a template, the numeric ID is in the URL. |
25
+
26
+ Two pitfalls, both of which produce a confusing failure:
27
+
28
+ - **Use the API v3 key, not the SMTP key.** Brevo issues two kinds of credential on neighbouring
29
+ settings pages. The REST API — which `BrevoService` talks to — only accepts a key starting with
30
+ **`xkeysib-`**. A key starting with `xsmtpsib-` is the SMTP password for `smtp-relay.brevo.com`
31
+ and is rejected by every `api.brevo.com/v3/*` endpoint with `401 {"message":"Key not found"}`.
32
+ - **The sender address must be verified in Brevo.** An unverified sender is the most common cause
33
+ of a send that fails with HTTP 400 despite a valid key.
34
+
35
+ To check a key in isolation before spending a send, hit an endpoint that takes no payload:
36
+
37
+ ```bash
38
+ curl -s -w '\nHTTP %{http_code}\n' -H "api-key: $BREVO_API_KEY" https://api.brevo.com/v3/account
39
+ ```
40
+
41
+ `HTTP 200` plus your account JSON means the key is good; `401 "Key not found"` means it is the
42
+ wrong kind of key or belongs to a deleted account.
43
+
44
+ ## 2. Put them in `.env`
45
+
46
+ `src/config.env.ts` calls `dotenv.config()` at import time, so a `.env` in the repo root is picked
47
+ up by both the server and the smoke script. There is no `.env` in the repo — create one from the
48
+ template:
49
+
50
+ ```bash
51
+ cp .env.example .env
52
+ ```
53
+
54
+ Then set these four keys in `.env`. `EMAIL_DEFAULT_SENDER` is already an **active** line in the
55
+ template; `EMAIL_DEFAULT_SENDER_NAME`, `BREVO_API_KEY`, `BREVO_SMOKE_TO` and
56
+ `BREVO_SMOKE_TEMPLATE_ID` are there as comments — uncomment and fill them in:
57
+
58
+ ```dotenv
59
+ # API key from app.brevo.com/settings/keys/api
60
+ BREVO_API_KEY=xkeysib-your-real-key
61
+
62
+ # Sender - must be a VERIFIED sender in Brevo
63
+ EMAIL_DEFAULT_SENDER=noreply@your-verified-domain.tld
64
+ EMAIL_DEFAULT_SENDER_NAME=Nest Server Manual Test
65
+
66
+ # Recipient for the smoke script - a mailbox you can actually open
67
+ BREVO_SMOKE_TO=you@your-mailbox.tld
68
+
69
+ # Optional: only needed for the template send in step 3b
70
+ # BREVO_SMOKE_TEMPLATE_ID=12
71
+ ```
72
+
73
+ `.env` is git-ignored — never commit real keys.
74
+
75
+ Config-wise this is all that is needed: `config.env.ts` only builds the `brevo` block **when
76
+ `BREVO_API_KEY` is set** (`...(process.env.BREVO_API_KEY ? { brevo: {...} } : {})`). Without the
77
+ key there is no `brevo` config, `BrevoService` refuses to construct, and everything falls back to
78
+ SMTP.
79
+
80
+ ## 3. Run the smoke script
81
+
82
+ ```bash
83
+ pnpm exec tsx scripts/brevo-smoke.ts
84
+ ```
85
+
86
+ It builds a real `ConfigService` + `BrevoService` (no mocks, no Nest bootstrap) and sends through
87
+ the same code path the server uses.
88
+
89
+ ### 3a. What you should see
90
+
91
+ ```
92
+ [brevo-smoke] sender: Nest Server Manual Test <noreply@your-verified-domain.tld>
93
+ [brevo-smoke] recipient: you@your-mailbox.tld
94
+ [brevo-smoke] sending HTML mail via sendHtmlMail() ...
95
+ [brevo-smoke] sendHtmlMail result: { messageId: '<2026...@smtp-relay.mailin.fr>' }
96
+ [brevo-smoke] OK - check the recipient mailbox and https://app.brevo.com/transactional/email/logs.
97
+ ```
98
+
99
+ A `messageId` means Brevo accepted the mail. Confirm it twice:
100
+
101
+ 1. The mail arrives in the recipient mailbox (check spam on the first run).
102
+ 2. It shows up under **Transactional → Logs**: <https://app.brevo.com/transactional/email/logs>.
103
+
104
+ ### 3b. Also test a template (optional)
105
+
106
+ Templates are what `sendMail()` uses in production (e.g. BetterAuth email verification via
107
+ `betterAuth.emailVerification.brevoTemplateId`). Set `BREVO_SMOKE_TEMPLATE_ID` in `.env` and re-run
108
+ — the script then additionally calls `sendMail(to, templateId, { smokeTestStamp })`. Put
109
+ `{{ params.smokeTestStamp }}` somewhere in the template body to see the parameter substitution
110
+ land.
111
+
112
+ Note: template parameters only substitute when the template uses Brevo's **New Template Language**.
113
+ Old-language templates silently ignore `params`.
114
+
115
+ ### 3c. Reading a failure
116
+
117
+ `BrevoService` catches SDK errors, logs them and returns `null` — so the script prints the full
118
+ error object above a `result: null`. The status code tells you which knob is wrong:
119
+
120
+ | Status | Cause |
121
+ | ---------------------------- | ---------------------------------------------------------------------------- |
122
+ | **401 Unauthorized** | `BREVO_API_KEY` wrong, revoked, or from a different Brevo account. |
123
+ | **400 Bad Request** | Usually an unverified sender, or an invalid/foreign `templateId`. |
124
+ | **402 Payment Required** | Free-plan daily quota exhausted. |
125
+ | **`result: 'TEST_USER!'`** | The recipient matched `config.brevo.exclude` — nothing was sent (see step 5). |
126
+
127
+ A quick way to confirm the wiring without spending a send: run it with a bogus key. You should get
128
+ a clean 401 from `https://api.brevo.com/v3/smtp/email` — which proves client construction, request
129
+ build and error handling all work:
130
+
131
+ ```bash
132
+ BREVO_API_KEY=xkeysib-invalid BREVO_SMOKE_TO=smoke@test.com \
133
+ EMAIL_DEFAULT_SENDER=noreply@test.com pnpm exec tsx scripts/brevo-smoke.ts
134
+ ```
135
+
136
+ ## 4. End-to-end through the server (optional)
137
+
138
+ To test the path a real feature takes rather than the service in isolation, use BetterAuth's email
139
+ verification, which is the framework's built-in `BrevoService` consumer
140
+ (`core-better-auth-email-verification.service.ts:180`):
141
+
142
+ 1. In `src/config.env.ts`, set `betterAuth.emailVerification.brevoTemplateId` to your template ID
143
+ for the `local` environment.
144
+ 2. Start the server: `pnpm run start:local`.
145
+ 3. Sign up a user with an address you can read — the verification mail is sent through Brevo
146
+ instead of SMTP.
147
+
148
+ If `brevoTemplateId` is unset or `BrevoService` is unavailable, the flow silently uses the SMTP
149
+ transport instead — so seeing an SMTP mail is the expected signal that the Brevo overlay is *not*
150
+ active.
151
+
152
+ ## 5. The `exclude` guard
153
+
154
+ `config.brevo.exclude` is a `RegExp` that suppresses sends to matching addresses (returns the
155
+ string `'TEST_USER!'` instead). It is unset by default and the smoke script deliberately does not
156
+ set one. If you add one to `config.env.ts` for local work, remember the recipient you are testing
157
+ with must **not** match it, or nothing will be sent.
158
+
159
+ Note for maintainers: the guard is read from `configService.config`, never from
160
+ `configFastButReadOnly`. A deep-frozen `RegExp` carrying the `g` flag throws on `.test()` because
161
+ the call assigns `lastIndex`. The unit spec pins this behaviour.
162
+
163
+ ## 6. Clean up
164
+
165
+ Remove the real key from `.env` when you are done, or delete the file. Revoke the key in Brevo if
166
+ it was created only for this test.
@@ -0,0 +1,90 @@
1
+ # Security Overrides — and why yours are not inherited
2
+
3
+ ## The problem in one sentence
4
+
5
+ **pnpm `overrides:` apply only to the ROOT project of an install.** The overrides declared in
6
+ `@lenne.tech/nest-server`'s own `pnpm-workspace.yaml` therefore do **not** travel with the published
7
+ package. Your project resolves its own dependency tree with *your* overrides — which, by default,
8
+ are none.
9
+
10
+ A green `pnpm audit` inside the framework repo says nothing about your tree.
11
+
12
+ ## What this concretely means for you
13
+
14
+ The framework pulls in two transitive packages that resolve to a **vulnerable** version unless you
15
+ override them yourself:
16
+
17
+ | Package | Advisory | Why it cannot resolve forward on its own |
18
+ |---------|----------|------------------------------------------|
19
+ | `ws` | [GHSA-96hv-2xvq-fx4p](https://github.com/advisories/GHSA-96hv-2xvq-fx4p) — high: memory-exhaustion DoS + uninitialized memory disclosure. Patched `>=8.21.0` | `@nestjs/graphql` declares `"ws": "8.20.1"` — an **exact pin**, not a caret. No amount of updating moves it |
20
+ | `@hono/node-server` | [GHSA-frvp-7c67-39w9](https://github.com/advisories/GHSA-frvp-7c67-39w9) (static-file path traversal) + [GHSA-9mqv-5hh9-4cgg](https://github.com/advisories/GHSA-9mqv-5hh9-4cgg) (unauthenticated memory leak). Patched `>=2.0.10` | `@modelcontextprotocol/sdk` declares `^1.19.9` and ships **no 1.x fix line**, so the fix is only available across a major |
21
+
22
+ `@nestjs/graphql` is a plain `dependencies` entry, so `ws` is installed even when you run with
23
+ `graphQl: false`. Neither package is optional in practice.
24
+
25
+ ## The fix
26
+
27
+ Add this to your project's `pnpm-workspace.yaml` (pnpm 11+; in pnpm 10 and earlier it lives under
28
+ `pnpm.overrides` in `package.json`):
29
+
30
+ ```yaml
31
+ overrides:
32
+ # @nestjs/graphql exact-pins ws@8.20.1 (GHSA-96hv-2xvq-fx4p, high, patched >=8.21.0).
33
+ # An exact pin cannot resolve forward — the override is the only fix.
34
+ # Remove once @nestjs/graphql stops pinning it.
35
+ 'ws@>=8.0.0 <8.21.0': '8.21.1'
36
+
37
+ # @modelcontextprotocol/sdk declares @hono/node-server ^1.19.9 with no 1.x fix line
38
+ # (GHSA-frvp-7c67-39w9, GHSA-9mqv-5hh9-4cgg). Deliberately a CROSS-MAJOR override.
39
+ # Verified safe: the SDK's only consumed symbol is `getRequestListener`, whose signature
40
+ # `(fetchCallback, options?)` is unchanged in 2.x. Engines >=20 and peer hono@^4 both fit.
41
+ # Remove once @modelcontextprotocol/sdk moves its own range to ^2.
42
+ '@hono/node-server@<2.0.10': '2.0.11'
43
+ ```
44
+
45
+ Then:
46
+
47
+ ```bash
48
+ pnpm install
49
+ pnpm audit # must report no known vulnerabilities
50
+ pnpm test # nothing should regress
51
+ ```
52
+
53
+ Commit `package.json`/`pnpm-workspace.yaml` **and** `pnpm-lock.yaml` together.
54
+
55
+ > Projects generated from `nest-server-starter` or `lt-monorepo` already carry both entries. This
56
+ > page is for projects that predate that, or that were assembled by hand.
57
+
58
+ ## Rules for writing your own overrides
59
+
60
+ These are the same rules the framework applies to itself
61
+ (`.claude/rules/package-management.md`):
62
+
63
+ 1. **The target must be a fixed version.** Never `>=x`, `^x`, `~x`, `*`. An unbounded target lets
64
+ pnpm install the newest match, which can silently cross a major version. This is not
65
+ hypothetical — an override written as `'vite@>=7.0.0 <=7.3.1': '>=7.3.2'` resolved to `vite@8.0.8`
66
+ and broke peer dependencies across three packages.
67
+
68
+ 2. **Prefer a bounded key.** `'pkg@>=2.0.0 <2.1.2': '2.1.2'` leaves non-vulnerable versions alone
69
+ and limits the blast radius. `'pkg': '2.1.2'` replaces *every* version in the tree.
70
+
71
+ 3. **Floor the key inside the major you mean.** A key like `'brace-expansion@>=3.0.0 <5.0.7'` also
72
+ matches a future 3.x or 4.x dependency and would force it across two majors. Write `>=5.0.0`.
73
+
74
+ 4. **An override is a hard pin, not a floor.** pnpm replaces the whole matched spec with your
75
+ target. `'hono@>=4.0.0 <4.12.27': '4.12.27'` does not mean "at least 4.12.27" — it pins to
76
+ exactly that, holding the package back from later patches. Remove an override as soon as the
77
+ package resolves patched on its own; otherwise today's fix is tomorrow's downgrade-lock.
78
+
79
+ 5. **Document each entry**: the advisory, which package pulls the vulnerable version in, and the
80
+ condition under which the entry can be deleted.
81
+
82
+ 6. **Verify it is load-bearing.** Resolve a lockfile with the entry removed. If the package still
83
+ lands on a patched version, the override is inert — keep it only deliberately, and re-check it
84
+ every maintenance run.
85
+
86
+ ## Related
87
+
88
+ - [`.claude/rules/package-management.md`](../.claude/rules/package-management.md) — fixed-version
89
+ policy, the pnpm pin contract, and the incident behind rule 1
90
+ - `pnpm-workspace.yaml` in this package — the framework's own annotated override block
@@ -0,0 +1,254 @@
1
+ # Migration Guide: 11.31.3 → 11.32.0
2
+
3
+ ## Overview
4
+
5
+ | Category | Change | Effort |
6
+ |----------|--------|--------|
7
+ | **Breaking** | `@getbrevo/brevo` upgraded 3.x → 6.x — three public SDK symbols no longer exist | 5 min *(only if you import them)* |
8
+ | **Breaking** | `BrevoService` now logs failures via the Nest `Logger` instead of `console.error` | 0 min *(unless you assert on stdout)* |
9
+ | **Security** | pnpm `overrides:` do **not** reach your project — two entries you must add yourself | 5 min |
10
+ | **New** | `installProcessDiagnostics()` / `handleFatalBootstrapError()` — make a silent crash diagnosable | 5 min *(opt-in, two lines in `main.ts`)* |
11
+ | **New** | `server.enableShutdownHooks()` recommendation — without it a container hangs on `docker stop` | 1 min |
12
+ | **New** | `brevo.maxRetries`, `brevo.timeoutInSeconds`, `brevo.throwOnError` config | 0 min *(safe defaults)* |
13
+ | **Changed** | The Brevo SDK is now imported lazily — faster cold start for everyone not using Brevo | 0 min |
14
+
15
+ **Why MINOR and not PATCH:** `@getbrevo/brevo` is a direct `dependencies` entry of this package, so
16
+ `pnpm update` pulls v6 into your tree. Its v3 symbols are gone. Per
17
+ [`.claude/rules/versioning.md`](../.claude/rules/versioning.md), a breaking change increments MINOR.
18
+
19
+ ## Quick Migration
20
+
21
+ ```bash
22
+ pnpm update @lenne.tech/nest-server
23
+ pnpm run build # surfaces the Brevo symbol removals, if any apply to you
24
+ ```
25
+
26
+ If the build is clean and you do not use `BrevoService`, you are done — but please still read
27
+ [Security: add two overrides](#security-add-two-overrides), which applies to every project.
28
+
29
+ ---
30
+
31
+ ## Breaking Changes
32
+
33
+ ### 1. `@getbrevo/brevo` 3.x → 6.x: three symbols removed
34
+
35
+ v6 is a full rewrite around a `BrevoClient` facade. These no longer exist:
36
+
37
+ | Removed in v6 | Replacement |
38
+ |---------------|-------------|
39
+ | `TransactionalEmailsApi` | `BrevoClient` (`client.transactionalEmails.*`) |
40
+ | `TransactionalEmailsApiApiKeys` | `new BrevoClient({ apiKey })` |
41
+ | `SendSmtpEmail` | `Brevo.SendTransacEmailRequest` |
42
+
43
+ **Before**
44
+
45
+ ```typescript
46
+ import { SendSmtpEmail, TransactionalEmailsApi, TransactionalEmailsApiApiKeys } from '@getbrevo/brevo';
47
+
48
+ const api = new TransactionalEmailsApi();
49
+ api.setApiKey(TransactionalEmailsApiApiKeys.apiKey, apiKey);
50
+
51
+ const mail: SendSmtpEmail = { templateId, to: [{ email }] };
52
+ const result = await api.sendTransacEmail(mail);
53
+ return result.body; // <- v3 wrapped the payload in an envelope
54
+ ```
55
+
56
+ **After**
57
+
58
+ ```typescript
59
+ import { BrevoClient } from '@getbrevo/brevo';
60
+ import type { Brevo } from '@getbrevo/brevo';
61
+
62
+ const client = new BrevoClient({ apiKey });
63
+
64
+ const request: Brevo.SendTransacEmailRequest = { templateId, to: [{ email }] };
65
+ return await client.transactionalEmails.sendTransacEmail(request); // <- payload directly
66
+ ```
67
+
68
+ **The awaited data shape is unchanged.** v3's `.body` was `{ messageId?, messageIds? }`; v6's
69
+ awaited result is `SendTransacEmailResponse`, which is the same shape. Only the envelope is gone.
70
+ If you need the raw HTTP response, v6 offers `.withRawResponse()`.
71
+
72
+ **The thrown error type changed** from an axios-shaped `{ response, body }` to
73
+ `BrevoError { statusCode, body, rawResponse, requestId }`. Adjust any `catch` block that reached
74
+ into `error.response`.
75
+
76
+ > If you only ever call `BrevoService`, nothing changes for you — its method signatures and return
77
+ > values are unchanged.
78
+
79
+ ### 2. `BrevoService` failures now go through the Nest `Logger`
80
+
81
+ `console.error(error)` was replaced with `this.logger.error(...)`. The return contract is
82
+ unchanged, but the output now carries the `[BrevoService]` context and participates in whatever log
83
+ transport you configured. Only relevant if you assert on raw stdout in tests.
84
+
85
+ ---
86
+
87
+ ## Security: add two overrides
88
+
89
+ **This applies to every project, whether or not you use Brevo.**
90
+
91
+ pnpm `overrides:` are honoured only for the **root** project of an install. The overrides declared
92
+ inside `@lenne.tech/nest-server` therefore do **not** travel with the published package — your
93
+ project resolves its own tree, with your overrides. Two transitive dependencies land on a
94
+ known-vulnerable version without help:
95
+
96
+ ```yaml
97
+ # pnpm-workspace.yaml (pnpm 11+; pnpm.overrides in package.json for pnpm 10 and earlier)
98
+ overrides:
99
+ # @nestjs/graphql exact-pins ws@8.20.1 (GHSA-96hv-2xvq-fx4p, high). An exact pin
100
+ # cannot resolve forward — the override is the only fix.
101
+ 'ws@>=8.0.0 <8.21.0': '8.21.1'
102
+
103
+ # @modelcontextprotocol/sdk declares @hono/node-server ^1.19.9 with no 1.x fix line
104
+ # (GHSA-frvp-7c67-39w9, GHSA-9mqv-5hh9-4cgg). Deliberately cross-major; verified safe
105
+ # (the SDK's only consumed symbol, `getRequestListener`, is unchanged in 2.x).
106
+ '@hono/node-server@<2.0.10': '2.0.11'
107
+ ```
108
+
109
+ Then `pnpm install && pnpm audit` — it must report no known vulnerabilities.
110
+
111
+ Projects generated from `nest-server-starter` or `lt-monorepo` already carry these.
112
+ Full rationale and the rules for writing your own:
113
+ [`docs/security-overrides.md`](../docs/security-overrides.md).
114
+
115
+ ---
116
+
117
+ ## What's New
118
+
119
+ ### Process diagnostics (opt-in, two lines)
120
+
121
+ A Node API can die *silently*: the dev runner prints only `app crashed` with no stacktrace, and in a
122
+ container the process just vanishes. A missing stacktrace means a SIGNAL killed it, not a throw.
123
+
124
+ `installProcessDiagnostics()` makes the reason visible — unhandled rejections are logged without
125
+ killing the server, uncaught exceptions are logged before the exit, non-zero exit codes are
126
+ reported, and SIGTERM/SIGINT/SIGHUP/SIGQUIT are labelled as *external* terminations so they stop
127
+ masquerading as in-process crashes.
128
+
129
+ **Consumers own their own `main.ts`, so this does not arrive automatically.** Add it:
130
+
131
+ ```typescript
132
+ // src/main.ts
133
+ import { handleFatalBootstrapError, installProcessDiagnostics } from '@lenne.tech/nest-server';
134
+
135
+ async function bootstrap() {
136
+ // FIRST statement — it must run before NestFactory.create() to catch module-construction failures
137
+ installProcessDiagnostics();
138
+
139
+ const server = await NestFactory.create<NestExpressApplication>(ServerModule);
140
+
141
+ // ... your configuration ...
142
+
143
+ server.enableShutdownHooks(); // see below — required for a clean container stop
144
+ await server.listen(envConfig.port);
145
+ }
146
+
147
+ // A rejection here is a fatal startup failure (EADDRINUSE, DB unreachable). Without the .catch it
148
+ // becomes a mere unhandledRejection and leaves a zombie process, "alive" but listening on nothing.
149
+ bootstrap().catch(handleFatalBootstrapError);
150
+ ```
151
+
152
+ Do **not** call it from a module. It must run before `NestFactory.create()`, and it installs a
153
+ `process.exit(1)` path that would otherwise arm inside `Test.createTestingModule()` and kill your
154
+ whole test runner instead of failing one spec.
155
+
156
+ Options:
157
+
158
+ ```typescript
159
+ installProcessDiagnostics({
160
+ onUnhandledRejection: 'exit', // default 'log' — 'exit' restores Node's own fail-fast
161
+ shutdownTimeoutMs: 30_000, // watchdog when another handler owns the shutdown; 0 disables
162
+ });
163
+ ```
164
+
165
+ ### `server.enableShutdownHooks()` — fixes a container that will not stop
166
+
167
+ Independent of the diagnostics, and worth doing even if you skip them.
168
+
169
+ In a container Node usually runs as **PID 1**. A PID-namespace init is `SIGNAL_UNKILLABLE`: a signal
170
+ with the default disposition is silently discarded by the kernel. The listening HTTP server keeps
171
+ the event loop non-empty, so nothing exits on its own — `docker stop` waits out its **full grace
172
+ period** and then SIGKILLs. In-flight requests are dropped and every `onModuleDestroy()` is skipped.
173
+
174
+ `enableShutdownHooks()` closes the app and drains the loop, so the process exits normally.
175
+ Belt-and-braces: run a real init in the image (`docker run --init`, or `tini` as `ENTRYPOINT`).
176
+
177
+ ### New `brevo` config
178
+
179
+ ```typescript
180
+ brevo: {
181
+ apiKey: '...',
182
+ sender: { email: '...', name: '...' },
183
+ maxRetries: 0, // default 0 — see below
184
+ timeoutInSeconds: 10, // default 10
185
+ throwOnError: false, // default false
186
+ }
187
+ ```
188
+
189
+ The v6 SDK introduced a retry policy v3 did not have: 2 retries on 408/429/5xx, honouring
190
+ `Retry-After` with a cap of **60 seconds per attempt**, and no request timeout. Since
191
+ `BrevoService.sendMail()` is awaited inside request handlers (e.g. the BetterAuth
192
+ email-verification hook), those defaults would let a rate-limited Brevo park a user-facing HTTP
193
+ request for roughly two minutes. This framework therefore defaults to `maxRetries: 0` and
194
+ `timeoutInSeconds: 10`. Raise them deliberately if you want SDK-level retrying.
195
+
196
+ `throwOnError: true` makes a failed send throw instead of resolving to `null` — useful on
197
+ security-critical delivery paths.
198
+
199
+ ### Brevo SDK is now lazily imported
200
+
201
+ `@getbrevo/brevo` is ~580 CommonJS modules and `BrevoService` is re-exported from the package
202
+ barrel, so every consumer paid that cold-start cost whether or not Brevo was configured. The SDK is
203
+ now loaded on first send. No API change.
204
+
205
+ ---
206
+
207
+ ## Compatibility Notes
208
+
209
+ | Pattern | Status | Notes |
210
+ |---------|:------:|-------|
211
+ | Calling `BrevoService.sendMail()` / `sendHtmlMail()` | Compatible | Signatures and return values unchanged |
212
+ | Extending `BrevoService` | Compatible | `getClient()`, `handleSendError()`, `buildIdempotencyHeaders()` are `protected` and overridable |
213
+ | Importing `TransactionalEmailsApi` / `SendSmtpEmail` / `TransactionalEmailsApiApiKeys` | **Breaking** | Removed in v6 — see above |
214
+ | Reading `result.body` from a direct SDK call | **Breaking** | v6 resolves the payload directly |
215
+ | Not using Brevo at all | Compatible | Cold start improves |
216
+ | Existing `main.ts` without the diagnostics | Compatible | Purely opt-in; nothing regresses |
217
+
218
+ ### Checking whether a failed send was silently swallowed
219
+
220
+ `BrevoService` resolves to `null` on failure rather than throwing (unless `throwOnError` is set).
221
+ If you call it directly, check for it — "did not throw" is not "was delivered":
222
+
223
+ ```typescript
224
+ const result = await this.brevoService.sendMail(email, templateId, params);
225
+ if (result === null) {
226
+ // log, fall back to SMTP, retry — but do NOT record this as a successful send
227
+ }
228
+ ```
229
+
230
+ The framework's own email-verification path was fixed to do exactly this: it now falls through to
231
+ the SMTP fallback instead of marking the address as mailed.
232
+
233
+ ---
234
+
235
+ ## Troubleshooting
236
+
237
+ | Symptom | Cause | Fix |
238
+ |---------|-------|-----|
239
+ | `Module '"@getbrevo/brevo"' has no exported member 'TransactionalEmailsApi'` | v6 removed it | Use `BrevoClient` — see [Breaking Changes](#1-getbrevobrevo-3x--6x-three-symbols-removed) |
240
+ | `pnpm audit` reports `ws` / `@hono/node-server` | Overrides are not inherited | [Add the two entries](#security-add-two-overrides) |
241
+ | `docker stop` takes the full grace period, then kills the container | No `enableShutdownHooks()`, node is PID 1 | Add the call; optionally `--init` / `tini` |
242
+ | Sign-up request hangs ~2 minutes when Brevo is rate-limited | v6 retry defaults | Already fixed by the `maxRetries: 0` default — verify you did not override it |
243
+ | Verification mail silently never arrives, logs say "sent" | Pre-11.32.0 swallow bug | Fixed; on custom callers, check for `null` |
244
+ | `[exit] process exiting with code 0` no longer logged | Intentional | A clean exit is not a diagnostic; only non-zero codes are logged |
245
+ | Process warnings no longer double-printed | Intentional | Node prints them itself, with more detail — use `--trace-warnings` for a stack |
246
+
247
+ ---
248
+
249
+ ## Module Documentation
250
+
251
+ - [`docs/security-overrides.md`](../docs/security-overrides.md) — why overrides are not inherited, and the rules for writing them
252
+ - [`docs/REQUEST-LIFECYCLE.md`](../docs/REQUEST-LIFECYCLE.md) — Process Diagnostics row under "Development & Operations"
253
+ - `src/core/common/helpers/process-diagnostics.helper.ts` — full rationale in the module docblock
254
+ - [`.claude/rules/package-management.md`](../.claude/rules/package-management.md) — fixed-version policy for overrides
@@ -0,0 +1,84 @@
1
+ # Migration Guide: 11.32.0 → 11.32.1
2
+
3
+ ## Overview
4
+
5
+ | Category | Change | Effort |
6
+ |----------|--------|--------|
7
+ | **Bugfix** | A missing `migrations/` directory no longer blocks the server boot | None — automatic |
8
+ | **Bugfix** | A spec in `src/core/` no longer breaks vendor-mode projects | None — automatic |
9
+
10
+ **No code changes required.** Both fixes are internal.
11
+
12
+ ## Quick Migration
13
+
14
+ ```bash
15
+ pnpm update @lenne.tech/nest-server
16
+ ```
17
+
18
+ Vendor-mode projects: `/lt-dev:backend:update-nest-server-core`.
19
+
20
+ ---
21
+
22
+ ## What was fixed
23
+
24
+ ### A missing `migrations/` directory no longer blocks the boot
25
+
26
+ `MigrationRunner.loadMigrationFiles()` called `fs.readdirSync()` without checking that the
27
+ directory exists. A missing directory threw `ENOENT` — and because the standard start script is
28
+ `migrate:up && start:local`, the `&&` turned that into a server that never starts, with an error
29
+ that does not point at the cause.
30
+
31
+ This is a state people produce routinely: *"delete all migrations"* reads to most as *"throw the
32
+ folder away"*.
33
+
34
+ A missing directory now means the same thing as an empty one — there are no migrations:
35
+
36
+ ```
37
+ [migrate] migrations directory not found — treating as empty: ./migrations
38
+ ```
39
+
40
+ `up()` and `status()` continue and exit 0. **`down()` is unchanged and still fails hard** —
41
+ rollback is an explicit operator action, never a boot path, and an exit 0 with no rollback
42
+ performed would mislead scripted rollbacks. `NSC__MIGRATE__STRICT=true` still turns a
43
+ recorded-but-missing migration into a hard error, so the integrity guard is intact.
44
+
45
+ If your project kept a `migrations/.gitkeep` purely to work around this, you can drop it — though
46
+ keeping it does no harm.
47
+
48
+ > Tracked as DEV-2634.
49
+
50
+ ### A `src/core/` spec no longer breaks vendor-mode projects
51
+
52
+ `hub-mermaid.helper.spec.ts` mixed a value and a type import in one statement
53
+ (`import { buildErDiagram, type HubModelDescriptor } from …`). The lt CLI's vendor conversion drops
54
+ the inline `type` specifier, so the file arrived in vendored projects as
55
+ `import { buildErDiagram }` and failed to compile with `TS2304: Cannot find name
56
+ 'HubModelDescriptor'` — breaking `pnpm run check` in every freshly generated vendor-mode project.
57
+
58
+ The import is now split into two statements, which survives the conversion. Only vendor-mode
59
+ projects were affected; npm-mode consumers never saw it.
60
+
61
+ > The underlying CLI defect is tracked separately. Splitting the import keeps `src/core/`
62
+ > vendor-safe regardless.
63
+
64
+ ---
65
+
66
+ ## Compatibility Notes
67
+
68
+ | Pattern | Status |
69
+ |---------|:------:|
70
+ | Everything from 11.32.0 | Unchanged |
71
+ | `MigrationRunner` public API | Unchanged |
72
+ | `down()` behaviour on a missing rollback file | Unchanged (still hard error) |
73
+ | `NSC__MIGRATE__STRICT` | Unchanged |
74
+
75
+ Still relevant from the previous release: **[the two security overrides your project needs](11.31.3-to-11.32.0.md#security-add-two-overrides)** — pnpm `overrides:` are not inherited from this package. If you skipped that step, do it now.
76
+
77
+ ---
78
+
79
+ ## Troubleshooting
80
+
81
+ | Symptom | Cause | Fix |
82
+ |---------|-------|-----|
83
+ | `ENOENT: no such file or directory, scandir './migrations'` on start | Pre-11.32.1 | Update; no code change needed |
84
+ | `TS2304: Cannot find name 'HubModelDescriptor'` in a vendored core | Pre-11.32.1 vendored copy | Re-sync the core, or split the import in that one spec by hand |