@lenne.tech/nest-server 11.31.2 → 11.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/.claude/rules/architecture.md +2 -0
  2. package/.claude/rules/configurable-features.md +2 -0
  3. package/.claude/rules/versioning.md +5 -8
  4. package/CLAUDE.md +31 -4
  5. package/FRAMEWORK-API.md +7 -2
  6. package/README.md +1 -0
  7. package/dist/config.env.d.ts +4 -0
  8. package/dist/config.env.js +32 -2
  9. package/dist/config.env.js.map +1 -1
  10. package/dist/core/common/helpers/logging.helper.d.ts +1 -0
  11. package/dist/core/common/helpers/logging.helper.js +12 -0
  12. package/dist/core/common/helpers/logging.helper.js.map +1 -1
  13. package/dist/core/common/helpers/meta.helper.d.ts +1 -0
  14. package/dist/core/common/helpers/meta.helper.js +32 -1
  15. package/dist/core/common/helpers/meta.helper.js.map +1 -1
  16. package/dist/core/common/helpers/process-diagnostics.helper.d.ts +18 -0
  17. package/dist/core/common/helpers/process-diagnostics.helper.js +88 -0
  18. package/dist/core/common/helpers/process-diagnostics.helper.js.map +1 -0
  19. package/dist/core/common/interfaces/server-options.interface.d.ts +12 -0
  20. package/dist/core/common/services/brevo.service.d.ts +7 -1
  21. package/dist/core/common/services/brevo.service.js +37 -16
  22. package/dist/core/common/services/brevo.service.js.map +1 -1
  23. package/dist/core/common/services/email.service.d.ts +3 -1
  24. package/dist/core/common/services/email.service.js +33 -2
  25. package/dist/core/common/services/email.service.js.map +1 -1
  26. package/dist/core/common/services/template.service.js +9 -4
  27. package/dist/core/common/services/template.service.js.map +1 -1
  28. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js +9 -4
  29. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js.map +1 -1
  30. package/dist/core/modules/hub/core-hub-actions.controller.d.ts +22 -0
  31. package/dist/core/modules/hub/core-hub-actions.controller.js +141 -0
  32. package/dist/core/modules/hub/core-hub-actions.controller.js.map +1 -0
  33. package/dist/core/modules/hub/core-hub-html.service.d.ts +8 -0
  34. package/dist/core/modules/hub/core-hub-html.service.js +123 -0
  35. package/dist/core/modules/hub/core-hub-html.service.js.map +1 -0
  36. package/dist/core/modules/hub/core-hub.controller.d.ts +56 -0
  37. package/dist/core/modules/hub/core-hub.controller.js +398 -0
  38. package/dist/core/modules/hub/core-hub.controller.js.map +1 -0
  39. package/dist/core/modules/hub/core-hub.module.d.ts +17 -0
  40. package/dist/core/modules/hub/core-hub.module.js +109 -0
  41. package/dist/core/modules/hub/core-hub.module.js.map +1 -0
  42. package/dist/core/modules/hub/core-hub.service.d.ts +18 -0
  43. package/dist/core/modules/hub/core-hub.service.js +153 -0
  44. package/dist/core/modules/hub/core-hub.service.js.map +1 -0
  45. package/dist/core/modules/hub/helpers/hub-client-js.helper.d.ts +1 -0
  46. package/dist/core/modules/hub/helpers/hub-client-js.helper.js +755 -0
  47. package/dist/core/modules/hub/helpers/hub-client-js.helper.js.map +1 -0
  48. package/dist/core/modules/hub/helpers/hub-command-shape.helper.d.ts +1 -0
  49. package/dist/core/modules/hub/helpers/hub-command-shape.helper.js +33 -0
  50. package/dist/core/modules/hub/helpers/hub-command-shape.helper.js.map +1 -0
  51. package/dist/core/modules/hub/helpers/hub-mask.helper.d.ts +1 -0
  52. package/dist/core/modules/hub/helpers/hub-mask.helper.js +46 -0
  53. package/dist/core/modules/hub/helpers/hub-mask.helper.js.map +1 -0
  54. package/dist/core/modules/hub/helpers/hub-mermaid.helper.d.ts +10 -0
  55. package/dist/core/modules/hub/helpers/hub-mermaid.helper.js +35 -0
  56. package/dist/core/modules/hub/helpers/hub-mermaid.helper.js.map +1 -0
  57. package/dist/core/modules/hub/helpers/hub-shell.helper.d.ts +9 -0
  58. package/dist/core/modules/hub/helpers/hub-shell.helper.js +62 -0
  59. package/dist/core/modules/hub/helpers/hub-shell.helper.js.map +1 -0
  60. package/dist/core/modules/hub/hub-action-messages.d.ts +17 -0
  61. package/dist/core/modules/hub/hub-action-messages.js +21 -0
  62. package/dist/core/modules/hub/hub-action-messages.js.map +1 -0
  63. package/dist/core/modules/hub/hub-config.helper.d.ts +11 -0
  64. package/dist/core/modules/hub/hub-config.helper.js +164 -0
  65. package/dist/core/modules/hub/hub-config.helper.js.map +1 -0
  66. package/dist/core/modules/hub/hub-nav.d.ts +12 -0
  67. package/dist/core/modules/hub/hub-nav.js +43 -0
  68. package/dist/core/modules/hub/hub-nav.js.map +1 -0
  69. package/dist/core/modules/hub/hub-ring-buffer.d.ts +22 -0
  70. package/dist/core/modules/hub/hub-ring-buffer.js +59 -0
  71. package/dist/core/modules/hub/hub-ring-buffer.js.map +1 -0
  72. package/dist/core/modules/hub/hub.constants.d.ts +18 -0
  73. package/dist/core/modules/hub/hub.constants.js +22 -0
  74. package/dist/core/modules/hub/hub.constants.js.map +1 -0
  75. package/dist/core/modules/hub/index.d.ts +25 -0
  76. package/dist/core/modules/hub/index.js +42 -0
  77. package/dist/core/modules/hub/index.js.map +1 -0
  78. package/dist/core/modules/hub/interfaces/hub-config.interface.d.ts +115 -0
  79. package/dist/core/modules/hub/interfaces/hub-config.interface.js +3 -0
  80. package/dist/core/modules/hub/interfaces/hub-config.interface.js.map +1 -0
  81. package/dist/core/modules/hub/interfaces/hub-panels.interface.d.ts +231 -0
  82. package/dist/core/modules/hub/interfaces/hub-panels.interface.js +3 -0
  83. package/dist/core/modules/hub/interfaces/hub-panels.interface.js.map +1 -0
  84. package/dist/core/modules/hub/middleware/hub-trace.middleware.d.ts +8 -0
  85. package/dist/core/modules/hub/middleware/hub-trace.middleware.js +50 -0
  86. package/dist/core/modules/hub/middleware/hub-trace.middleware.js.map +1 -0
  87. package/dist/core/modules/hub/services/core-hub-actions.service.d.ts +48 -0
  88. package/dist/core/modules/hub/services/core-hub-actions.service.js +136 -0
  89. package/dist/core/modules/hub/services/core-hub-actions.service.js.map +1 -0
  90. package/dist/core/modules/hub/services/core-hub-db.service.d.ts +19 -0
  91. package/dist/core/modules/hub/services/core-hub-db.service.js +180 -0
  92. package/dist/core/modules/hub/services/core-hub-db.service.js.map +1 -0
  93. package/dist/core/modules/hub/services/core-hub-email.service.d.ts +24 -0
  94. package/dist/core/modules/hub/services/core-hub-email.service.js +148 -0
  95. package/dist/core/modules/hub/services/core-hub-email.service.js.map +1 -0
  96. package/dist/core/modules/hub/services/core-hub-mailbox.service.d.ts +20 -0
  97. package/dist/core/modules/hub/services/core-hub-mailbox.service.js +103 -0
  98. package/dist/core/modules/hub/services/core-hub-mailbox.service.js.map +1 -0
  99. package/dist/core/modules/hub/services/core-hub-migrations.service.d.ts +25 -0
  100. package/dist/core/modules/hub/services/core-hub-migrations.service.js +115 -0
  101. package/dist/core/modules/hub/services/core-hub-migrations.service.js.map +1 -0
  102. package/dist/core/modules/hub/services/core-hub-sources.service.d.ts +28 -0
  103. package/dist/core/modules/hub/services/core-hub-sources.service.js +187 -0
  104. package/dist/core/modules/hub/services/core-hub-sources.service.js.map +1 -0
  105. package/dist/core/modules/hub/services/hub-log-buffer.service.d.ts +24 -0
  106. package/dist/core/modules/hub/services/hub-log-buffer.service.js +210 -0
  107. package/dist/core/modules/hub/services/hub-log-buffer.service.js.map +1 -0
  108. package/dist/core/modules/hub/services/hub-query-profiler.service.d.ts +38 -0
  109. package/dist/core/modules/hub/services/hub-query-profiler.service.js +235 -0
  110. package/dist/core/modules/hub/services/hub-query-profiler.service.js.map +1 -0
  111. package/dist/core/modules/hub/services/hub-trace-buffer.service.d.ts +18 -0
  112. package/dist/core/modules/hub/services/hub-trace-buffer.service.js +123 -0
  113. package/dist/core/modules/hub/services/hub-trace-buffer.service.js.map +1 -0
  114. package/dist/core.module.js +42 -1
  115. package/dist/core.module.js.map +1 -1
  116. package/dist/index.d.ts +2 -0
  117. package/dist/index.js +2 -0
  118. package/dist/index.js.map +1 -1
  119. package/dist/main.js +5 -2
  120. package/dist/main.js.map +1 -1
  121. package/dist/tsconfig.build.tsbuildinfo +1 -1
  122. package/docs/REQUEST-LIFECYCLE.md +2 -0
  123. package/docs/brevo-manual-test.md +166 -0
  124. package/docs/security-overrides.md +90 -0
  125. package/migration-guides/11.31.2-to-11.31.3.md +135 -0
  126. package/migration-guides/11.31.3-to-11.32.0.md +254 -0
  127. package/package.json +16 -14
  128. package/src/config.env.ts +75 -2
  129. package/src/core/common/helpers/logging.helper.spec.ts +61 -0
  130. package/src/core/common/helpers/logging.helper.ts +48 -0
  131. package/src/core/common/helpers/meta.helper.ts +46 -1
  132. package/src/core/common/helpers/process-diagnostics.helper.spec.ts +310 -0
  133. package/src/core/common/helpers/process-diagnostics.helper.ts +321 -0
  134. package/src/core/common/interfaces/server-options.interface.ts +78 -0
  135. package/src/core/common/services/brevo.service.spec.ts +266 -0
  136. package/src/core/common/services/brevo.service.ts +100 -17
  137. package/src/core/common/services/email.service.ts +33 -1
  138. package/src/core/common/services/template.service.ts +21 -16
  139. package/src/core/modules/better-auth/core-better-auth-email-verification.service.ts +14 -4
  140. package/src/core/modules/hub/INTEGRATION-CHECKLIST.md +64 -0
  141. package/src/core/modules/hub/README.md +159 -0
  142. package/src/core/modules/hub/core-hub-actions.controller.ts +137 -0
  143. package/src/core/modules/hub/core-hub-html.service.ts +135 -0
  144. package/src/core/modules/hub/core-hub.controller.ts +286 -0
  145. package/src/core/modules/hub/core-hub.module.spec.ts +108 -0
  146. package/src/core/modules/hub/core-hub.module.ts +159 -0
  147. package/src/core/modules/hub/core-hub.service.ts +169 -0
  148. package/src/core/modules/hub/helpers/hub-client-js.helper.ts +768 -0
  149. package/src/core/modules/hub/helpers/hub-command-shape.helper.spec.ts +48 -0
  150. package/src/core/modules/hub/helpers/hub-command-shape.helper.ts +47 -0
  151. package/src/core/modules/hub/helpers/hub-mask.helper.spec.ts +67 -0
  152. package/src/core/modules/hub/helpers/hub-mask.helper.ts +78 -0
  153. package/src/core/modules/hub/helpers/hub-mermaid.helper.spec.ts +54 -0
  154. package/src/core/modules/hub/helpers/hub-mermaid.helper.ts +62 -0
  155. package/src/core/modules/hub/helpers/hub-shell.helper.spec.ts +106 -0
  156. package/src/core/modules/hub/helpers/hub-shell.helper.ts +90 -0
  157. package/src/core/modules/hub/hub-action-messages.ts +47 -0
  158. package/src/core/modules/hub/hub-config.helper.spec.ts +108 -0
  159. package/src/core/modules/hub/hub-config.helper.ts +233 -0
  160. package/src/core/modules/hub/hub-nav.ts +66 -0
  161. package/src/core/modules/hub/hub-ring-buffer.spec.ts +95 -0
  162. package/src/core/modules/hub/hub-ring-buffer.ts +101 -0
  163. package/src/core/modules/hub/hub.constants.ts +84 -0
  164. package/src/core/modules/hub/index.ts +25 -0
  165. package/src/core/modules/hub/interfaces/hub-config.interface.ts +265 -0
  166. package/src/core/modules/hub/interfaces/hub-panels.interface.ts +186 -0
  167. package/src/core/modules/hub/middleware/hub-trace.middleware.ts +45 -0
  168. package/src/core/modules/hub/services/core-hub-actions.service.ts +133 -0
  169. package/src/core/modules/hub/services/core-hub-db.service.ts +185 -0
  170. package/src/core/modules/hub/services/core-hub-email.service.ts +158 -0
  171. package/src/core/modules/hub/services/core-hub-mailbox.service.spec.ts +116 -0
  172. package/src/core/modules/hub/services/core-hub-mailbox.service.ts +121 -0
  173. package/src/core/modules/hub/services/core-hub-migrations.service.ts +112 -0
  174. package/src/core/modules/hub/services/core-hub-sources.service.ts +194 -0
  175. package/src/core/modules/hub/services/hub-log-buffer.service.ts +252 -0
  176. package/src/core/modules/hub/services/hub-query-profiler.service.ts +274 -0
  177. package/src/core/modules/hub/services/hub-trace-buffer.service.spec.ts +112 -0
  178. package/src/core/modules/hub/services/hub-trace-buffer.service.ts +134 -0
  179. package/src/core.module.ts +61 -1
  180. package/src/index.ts +7 -0
  181. package/src/main.ts +22 -3
@@ -213,6 +213,8 @@ JWT-based authentication for existing projects:
213
213
  | **Health Check Module** | `GET /health` + GraphQL `healthCheck` query |
214
214
  | **Error Code Module** | Centralized error registry with unique IDs |
215
215
  | **Permissions Report** | Interactive HTML dashboard, JSON, and Markdown reports |
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 |
216
218
  | **System Setup Module** | Initial admin creation for fresh deployments |
217
219
  | **Cron Jobs** | `CoreCronJobsService` with timezone/UTC offset support |
218
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,135 @@
1
+ # Migration Guide: 11.31.2 → 11.31.3
2
+
3
+ ## Overview
4
+
5
+ | Category | Effort | Required? |
6
+ |----------|--------|-----------|
7
+ | **New Feature** | Opt-in | No — purely additive |
8
+ | Breaking Changes | None | — |
9
+ | Config Changes | Optional | Only if you enable the Hub |
10
+
11
+ This release adds the **Hub** — a build-free, ADMIN-gated operator cockpit served directly by the
12
+ framework at `/hub`. It is **disabled by default** and must be switched on per environment, so
13
+ existing projects are unaffected until they opt in. No code changes are required to update.
14
+
15
+ It also includes a **security patch**: three moderate `axios` advisories (recursion DoS + prototype
16
+ pollution, transitive via the Brevo and Mailjet mail SDKs) are closed by pinning `axios` to `1.18.1`.
17
+ Nothing to do on your side — just update.
18
+
19
+ ## Quick Migration
20
+
21
+ ```bash
22
+ # Update package
23
+ pnpm update @lenne.tech/nest-server
24
+
25
+ # Verify build
26
+ pnpm run build
27
+
28
+ # Run tests
29
+ pnpm test
30
+ ```
31
+
32
+ Nothing else is required — the Hub stays dormant (every `/hub` route answers 404) until you add a
33
+ `hub` block to a config environment.
34
+
35
+ ## What's New in 11.31.3
36
+
37
+ ### Hub — admin area / operator cockpit
38
+
39
+ A dependency-free dashboard of runtime information and admin tools, rendered as server-side HTML +
40
+ a vanilla-JS SPA (no React, no build step), so it works identically in npm- and vendor-mode.
41
+
42
+ **16 panels:** Dashboard, Diagnostics, Logs, Request Traces, Query Performance, Cron Jobs, Database,
43
+ Models/ERD, Migrations, Files (GridFS), Config (secrets masked), Auth Migration (Legacy→IAM), Error
44
+ Codes, Email Preview, Mailbox, AI. Each has a `*.json` sidecar (the stable data contract) the client
45
+ polls; optional sources degrade to an "unavailable" state instead of erroring.
46
+
47
+ **3 runtime collectors** (in-memory ring buffers, per-app-instance, parallel-test-safe):
48
+ - **Logs** — a chaining `Logger.overrideLogger()` delegate (no main.ts change), secrets redacted.
49
+ - **Traces** — an Express middleware bound only when the collector is enabled (zero cost otherwise).
50
+ - **Queries** — MongoDB driver command monitoring; records value-free query SHAPES (N+1 templates),
51
+ never values. Enabling it opts the driver into `monitorCommands`.
52
+
53
+ **Built-in Mailbox** — a Mailpit-style capture of outgoing mail hooked into `EmailService`. In
54
+ `mode: 'capture'` mail is recorded and NOT sent (dev/test); in `mode: 'copy'` it is sent AND a copy
55
+ is recorded (with tokens/links redacted in the stored copy). `capture` throws at startup in
56
+ `production`/`staging` so it can never silently swallow real mail.
57
+
58
+ **Admin actions** (opt-out via `hub.actions: false`) — migrations run/rollback, GridFS file delete,
59
+ cron start/stop/trigger, collector-buffer clear, test mail. Every mutating request requires the
60
+ `X-Hub-Request: 1` header (CSRF defense) and destructive ones a server-validated type-to-confirm
61
+ keyword; each writes a `[HUB-ACTION] <action> by user <id>` audit line.
62
+
63
+ #### Enable it
64
+
65
+ The Hub is **never enabled implicitly** — switch it on per environment in `config.env.ts`:
66
+
67
+ ```typescript
68
+ // config.env.ts (development / local)
69
+ hub: {
70
+ collectors: { queries: true }, // opt-in query profiler
71
+ mailbox: { mode: 'capture' }, // capture outgoing mail locally (Mailpit replacement)
72
+ },
73
+
74
+ // production — usually omit `hub` entirely. If you do want it (still ADMIN-gated),
75
+ // never use mailbox capture mode there:
76
+ // hub: { mailbox: false },
77
+ ```
78
+
79
+ Boolean shorthand `hub: true` enables it at `/hub`, admin-only, with default collectors. See
80
+ `IHubConfig` for every option. Env-var equivalents exist via `NSC__HUB__*` (see `.env.example`).
81
+
82
+ Then sign in as a user with `RoleEnum.ADMIN` and open `/hub`. The shell is self-sufficient: if you
83
+ are not authenticated it shows a login form that posts to `loginEndpoint` (default
84
+ `/iam/sign-in/email`), so an admin can sign in directly at the API without the frontend. In a
85
+ fullstack setup, a cross-subdomain session cookie from the app login already authenticates `/hub`.
86
+
87
+ #### Customization (optional)
88
+
89
+ Replace any Hub controller/service with a project subclass via `CoreModule.forRoot`:
90
+
91
+ ```typescript
92
+ CoreModule.forRoot(envConfig, {
93
+ hub: {
94
+ controller: MyHubController, // extends CoreHubController
95
+ actionsController: MyHubActionsController, // extends CoreHubActionsController
96
+ service: MyHubService, // extends CoreHubService
97
+ htmlService: MyHubHtmlService, // extends CoreHubHtmlService
98
+ actionsService: MyHubActionsService, // extends CoreHubActionsService
99
+ },
100
+ });
101
+ ```
102
+
103
+ ## Breaking Changes
104
+
105
+ None. The Hub is opt-in and disabled by default.
106
+
107
+ ## Compatibility Notes
108
+
109
+ | Pattern | Status | Notes |
110
+ |---------|--------|-------|
111
+ | Projects that never set `hub` | Unaffected | Every `/hub` route answers 404 |
112
+ | Custom `EmailService` subclass | Compatible | The mailbox capture hook is injected optionally (`@Optional()`) — absent when the Hub/mailbox is off, so there is zero cost and no behavior change |
113
+ | No auth system enabled | ⚠️ Action required | The Hub relies on the app-wide `RolesGuard`/`BetterAuthRolesGuard` to enforce `@Roles(ADMIN)`. With BetterAuth and legacy auth both disabled, no guard runs and the gate is inert — keep an auth module enabled or leave the Hub disabled |
114
+ | `queries` collector enabled | Compatible | Opts the MongoDB driver into `monitorCommands` from `core.module.ts` when the consumer has not set it |
115
+
116
+ ## Troubleshooting
117
+
118
+ | Issue | Fix |
119
+ |-------|-----|
120
+ | `/hub` → 404 everywhere | `hub` is not set in that environment's config block — add `hub: true` (or an options object) |
121
+ | Startup error mentioning mailbox `capture` | `mailbox.mode: 'capture'` is forbidden in `production`/`staging` — use `copy` or disable the mailbox there |
122
+ | Query panel empty | The queries collector is opt-in — set `hub.collectors.queries: true` |
123
+ | Cron panel empty | Import `ScheduleModule.forRoot()` in your ServerModule |
124
+ | Hub reachable without login | You disabled all auth systems, so no roles guard runs — see the Compatibility note above |
125
+
126
+ ## Module Documentation
127
+
128
+ - `src/core/modules/hub/README.md` — full panel/action/collector/security reference
129
+ - `src/core/modules/hub/INTEGRATION-CHECKLIST.md` — config-only setup + common mistakes
130
+ - `.claude/rules/configurable-features.md` — Hub row in the configurable-features table
131
+
132
+ ## References
133
+
134
+ - Configurable-features pattern: `.claude/rules/configurable-features.md`
135
+ - Request lifecycle (middleware + logger override + email hook): `docs/REQUEST-LIFECYCLE.md`
@@ -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