@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.
- package/.claude/rules/versioning.md +5 -8
- package/CLAUDE.md +3 -3
- package/FRAMEWORK-API.md +4 -2
- package/README.md +1 -0
- package/dist/core/common/helpers/process-diagnostics.helper.d.ts +18 -0
- package/dist/core/common/helpers/process-diagnostics.helper.js +88 -0
- package/dist/core/common/helpers/process-diagnostics.helper.js.map +1 -0
- package/dist/core/common/interfaces/server-options.interface.d.ts +3 -0
- package/dist/core/common/services/brevo.service.d.ts +7 -1
- package/dist/core/common/services/brevo.service.js +37 -16
- package/dist/core/common/services/brevo.service.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js +9 -4
- package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js.map +1 -1
- package/dist/core/modules/migrate/migration-runner.js +4 -0
- package/dist/core/modules/migrate/migration-runner.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/main.js +5 -2
- package/dist/main.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/REQUEST-LIFECYCLE.md +1 -0
- package/docs/brevo-manual-test.md +166 -0
- package/docs/security-overrides.md +90 -0
- package/migration-guides/11.31.3-to-11.32.0.md +254 -0
- package/migration-guides/11.32.0-to-11.32.1.md +84 -0
- package/package.json +15 -14
- package/src/core/common/helpers/process-diagnostics.helper.spec.ts +310 -0
- package/src/core/common/helpers/process-diagnostics.helper.ts +321 -0
- package/src/core/common/interfaces/server-options.interface.ts +32 -0
- package/src/core/common/services/brevo.service.spec.ts +266 -0
- package/src/core/common/services/brevo.service.ts +100 -17
- package/src/core/modules/better-auth/core-better-auth-email-verification.service.ts +14 -4
- package/src/core/modules/hub/helpers/hub-mermaid.helper.spec.ts +8 -1
- package/src/core/modules/migrate/migration-runner.ts +17 -0
- package/src/index.ts +1 -0
- 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 |
|