@lenne.tech/nest-server 12.3.1 → 12.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/rules/configurable-features.md +1 -0
- package/FRAMEWORK-API.md +2 -2
- package/dist/core/common/interfaces/server-options.interface.d.ts +1 -0
- package/dist/core/common/services/email.service.d.ts +3 -0
- package/dist/core/common/services/email.service.js +20 -0
- package/dist/core/common/services/email.service.js.map +1 -1
- package/migration-guides/12.3.1-to-12.3.2.md +52 -0
- package/package.json +1 -1
- package/src/core/common/interfaces/server-options.interface.ts +11 -0
- package/src/core/common/services/email.service.ts +33 -1
|
@@ -300,6 +300,7 @@ This pattern is currently applied to:
|
|
|
300
300
|
| Password-Reset Token Lifetime | `auth.passwordReset.tokenExpiresInMinutes` | **Numeric Sentinel — Family C** (see above) | **60 minutes since 11.38.0. Before that the legacy reset token NEVER expired** — `resetPassword()` looked it up by value and compared no time, while the exception it threw read "Invalid or expired password reset token", claiming a check that did not exist. A reset link takes over an EXISTING account, so an unbounded one means a mail in an archive or a restored backup opens that account years later. The IAM half already expired after an hour (Better-Auth's `resetPasswordTokenExpiresIn`); only the legacy half had nothing. `0` opts out; an invalid value falls back to 60, never to unbounded. **A token minted before 11.38.0 carries no timestamp and is treated as EXPIRED** — reading "no timestamp" as "valid forever" would preserve the defect permanently rather than for a migration window. An expired token is answered exactly like an unknown one (404, same message) and deleted on sight. Model field `passwordResetTokenExpiresAt`, `@Restricted(S_NO_ONE)`. Implementation: `core-user.service.ts` → `passwordResetTokenExpiryMinutes()` / `isPasswordResetTokenExpired()` |
|
|
301
301
|
| Legacy Password-Reset Link | `email.passwordResetLink` | Explicit Value with derived default | **`<appUrl>/auth/reset-password?token={token}` since 11.38.0.** Two conventions live here and which applies depends on whether the option is set AT ALL: unset yields `?token=`, the same string written out yields a PATH segment. That is deliberate — a project that configures such a value has a page built for a path parameter, and moving it silently would break the flow this release repairs — and because it is not guessable, `CoreUserService` **warns once at boot** when a configured value carries no `{token}`. The warning names a precondition: `{token}` is substituted by `buildPasswordResetLink()` and nothing else, so a caller still concatenating by hand must switch FIRST or it mails the placeholder verbatim. Returns `null` rather than a string containing `undefined`; resolves `appUrl` through `resolveServerUrls` (localhost defaults, host-split `baseUrl`) and honours `cors.deriveAppUrl` |
|
|
302
302
|
| SMTP TLS Mode | `email.smtp.secure` + `SMTP_REQUIRE_TLS` | Derived from the port | **`secure` follows `SMTP_PORT` unless set to `'true'`/`'false'`.** It is not an independent setting: 465 negotiates TLS immediately, everything else upgrades via STARTTLS. The production profile shipped 587 + `secure: true` — a pair that cannot connect — so a deployment configuring only host and credentials sent NO mail, invisibly, because authentication mail is not awaited. Only the two canonical values override; anything else defers to the port, which is the only rule under which no input produces an unconnectable pair. `requireTLS` defaults to **true**: `secure: false` alone means opportunistic STARTTLS, and an on-path attacker stripping the capability line gets credentials and a live reset link in plaintext. `EmailService` warns once when the resolved pair cannot connect — reported, not overruled. Implementation: `config.helper.ts` → `resolveSmtpSecure()` / `isImpossibleSmtpTlsCombination()` |
|
|
303
|
+
| Mail Reply-To | `email.defaultReplyTo` | Explicit Value — absent = no header | **Absent → no `Reply-To` header, exactly as before (12.3.2+).** A string (an address, or `"Name" <address>`) becomes the Reply-To of every mail `EmailService.sendMail()` sends; a call overrides it with `replyTo`, and `replyTo: false` (or `''`) sends that one mail without it. Env: `NSC__EMAIL__DEFAULT_REPLY_TO`. A value that is not a string is ignored with one warning — a misconfigured default must not stop password-reset or verification mail. SMTP path only (`email.smtp`, nodemailer, Hub capture unaffected): Brevo and Mailjet templates carry their own reply-to. Pinned by `tests/unit/email-smtp-roundtrip.spec.ts` (Reply-To block, real SMTP). Implementation: `common/services/email.service.ts` → `resolveReplyTo()` |
|
|
303
304
|
| Legacy Auth Rate Limiting | `auth.rateLimit` | Presence Implies Enabled | `max: 10`, `windowSeconds: 60`. Counters live in a `RateLimitStore`: `RedisRateLimitStore` when `redis` is configured (limit enforced EXACTLY across replicas instead of `max × replicas`), else `InMemoryRateLimitStore` (previous behavior). On a Redis outage it degrades to the in-memory counter and logs once per transition — never a 500, never "allowed". `check()` / `reset()` / `clear()` are ASYNC since 11.33.0; `getStats().activeEntries` is `-1` on the Redis store |
|
|
304
305
|
| BetterAuth Rate Limiting | `betterAuth.rateLimit` | Presence Implies Enabled | `max: 10`, `windowSeconds: 60`. Same `RateLimitStore` selection, degradation and async signatures as the Legacy Auth row (namespace `better-auth`) |
|
|
305
306
|
| BetterAuth JWT Plugin | `betterAuth.jwt` | Boolean Shorthand | `expiresIn: '15m'` |
|
package/FRAMEWORK-API.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @lenne.tech/nest-server — Framework API Reference
|
|
2
2
|
|
|
3
|
-
> Auto-generated from source code as of 2026-11-04 (v12.3.
|
|
3
|
+
> Auto-generated from source code as of 2026-11-04 (v12.3.2)
|
|
4
4
|
> File: `FRAMEWORK-API.md` — compact, machine-readable API surface for Claude Code
|
|
5
5
|
|
|
6
6
|
## CoreModule.forRoot()
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
- `cronJobs?`: `Record<string, string | false | 0 | CronJobConfigWithTimeZone<null, null> | C...` — Cron jobs configuration object with the name of the cron job function as key
|
|
28
28
|
- `database?`: `string | false | IDatabaseConfig` (default: `undefined (the `mongoose` configuration)`) — Data sources: where models are stored.
|
|
29
29
|
- `debugProcessInput?`: `boolean` (default: `false`) — When true, logs a debug message when prepareInput() changes the input type during process().
|
|
30
|
-
- `email?`: `{ defaultSender?: { email?: string; name?: string; }
|
|
30
|
+
- `email?`: `{ defaultReplyTo?: string; defaultSender?: { email?: string; name?: string; }...` — SMTP and template configuration for sending emails
|
|
31
31
|
- `env?`: `string` — Environment
|
|
32
32
|
- `version?`: `string` — Semantic version of the running build (e.g. from package.json / meta.json).
|
|
33
33
|
- `errorCode?`: `IErrorCode` — Configuration for the error code module
|
|
@@ -12,13 +12,16 @@ export declare class EmailService implements OnModuleDestroy {
|
|
|
12
12
|
private cachedSmtpConfig;
|
|
13
13
|
protected readonly emailServiceLogger: Logger;
|
|
14
14
|
private smtpTlsWarningEmitted;
|
|
15
|
+
private replyToWarningEmitted;
|
|
15
16
|
constructor(configService: ConfigService, templateService: TemplateService, emailCapture?: IHubEmailCapture);
|
|
16
17
|
protected warnOnImpossibleSmtpTlsCombination(smtp: unknown): void;
|
|
18
|
+
protected resolveReplyTo(replyTo: false | string | undefined): string | undefined;
|
|
17
19
|
onModuleDestroy(): void;
|
|
18
20
|
sendMail(recipients: string | string[], subject: string, config: {
|
|
19
21
|
attachments?: Attachment[];
|
|
20
22
|
html?: string;
|
|
21
23
|
htmlTemplate?: string;
|
|
24
|
+
replyTo?: false | string;
|
|
22
25
|
senderEmail?: string;
|
|
23
26
|
senderName?: string;
|
|
24
27
|
smtp?: MailTransportOptions;
|
|
@@ -27,6 +27,7 @@ let EmailService = EmailService_1 = class EmailService {
|
|
|
27
27
|
cachedSmtpConfig = null;
|
|
28
28
|
emailServiceLogger = new Logger(EmailService_1.name);
|
|
29
29
|
smtpTlsWarningEmitted = false;
|
|
30
|
+
replyToWarningEmitted = false;
|
|
30
31
|
constructor(configService, templateService, emailCapture) {
|
|
31
32
|
this.configService = configService;
|
|
32
33
|
this.templateService = templateService;
|
|
@@ -50,6 +51,23 @@ let EmailService = EmailService_1 = class EmailService {
|
|
|
50
51
|
'"wrong version number" from OpenSSL and NO outgoing mail — silently, because authentication mail ' +
|
|
51
52
|
'is not awaited. Set SMTP_SECURE=false for port 587, or SMTP_PORT=465 to keep implicit TLS.');
|
|
52
53
|
}
|
|
54
|
+
resolveReplyTo(replyTo) {
|
|
55
|
+
if (replyTo === false) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
const value = replyTo ?? this.configService.getFastButReadOnly('email.defaultReplyTo');
|
|
59
|
+
if (value === undefined || value === null) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
if (typeof value === 'string') {
|
|
63
|
+
return value.trim() || undefined;
|
|
64
|
+
}
|
|
65
|
+
if (!this.replyToWarningEmitted) {
|
|
66
|
+
this.replyToWarningEmitted = true;
|
|
67
|
+
this.emailServiceLogger.warn(`Reply-To ignored: expected an address string (email.defaultReplyTo / replyTo), got ${typeof value}.`);
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
53
71
|
onModuleDestroy() {
|
|
54
72
|
if (this.cachedTransporter) {
|
|
55
73
|
this.cachedTransporter.close();
|
|
@@ -118,10 +136,12 @@ let EmailService = EmailService_1 = class EmailService {
|
|
|
118
136
|
this.cachedTransporter = createTransport(smtp);
|
|
119
137
|
this.cachedSmtpConfig = smtpKey;
|
|
120
138
|
}
|
|
139
|
+
const replyTo = this.resolveReplyTo(config.replyTo);
|
|
121
140
|
return this.cachedTransporter.sendMail({
|
|
122
141
|
attachments,
|
|
123
142
|
from: `"${senderName}" <${senderEmail}>`,
|
|
124
143
|
html,
|
|
144
|
+
...(replyTo ? { replyTo } : {}),
|
|
125
145
|
subject,
|
|
126
146
|
text,
|
|
127
147
|
to: recipients,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"email.service.js","sourceRoot":"","sources":["../../../../src/core/common/services/email.service.ts"],"names":[],"mappings":";;;;;;;;;;;;;AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACpC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAmB,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AACvF,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAI7C,OAAO,EAAE,8BAA8B,EAAE,MAAM,6BAA6B,CAAC;AAC7E,OAAO,EAAE,gBAAgB,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAEnF,OAAO,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAOjD,IAAM,YAAY,oBAAlB,MAAM,YAAY;
|
|
1
|
+
{"version":3,"file":"email.service.js","sourceRoot":"","sources":["../../../../src/core/common/services/email.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Migration Guide: 12.3.1 → 12.3.2
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
| Category | Details |
|
|
6
|
+
| -------------------- | ------------------------------------------------------------------------------------------------------- |
|
|
7
|
+
| **New Features** | Optional Reply-To for outgoing mail: `email.defaultReplyTo` and a per-call `replyTo` (§1) |
|
|
8
|
+
| **Breaking Changes** | None |
|
|
9
|
+
| **Migration Effort** | None. Set `email.defaultReplyTo` only if answers to your mail should go to another address (§1) |
|
|
10
|
+
|
|
11
|
+
## Quick Migration
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm add @lenne.tech/nest-server@12.3.2
|
|
15
|
+
pnpm run update # nest-server-starter based projects
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Vendor-mode projects pick it up with the core update (`/lt-dev:backend:update-nest-server-core`).
|
|
19
|
+
|
|
20
|
+
## 1. Reply-To for outgoing mail
|
|
21
|
+
|
|
22
|
+
`EmailService.sendMail()` can set a `Reply-To` header, so a recipient's answer goes to another address than the
|
|
23
|
+
sender, e.g. a shared support mailbox while mail goes out from `noreply@…`. Nothing changes until you set it.
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
// config.env.ts
|
|
27
|
+
email: {
|
|
28
|
+
defaultSender: { email: 'noreply@example.com', name: 'Example' },
|
|
29
|
+
defaultReplyTo: 'support@example.com', // or '"Example Support" <support@example.com>'
|
|
30
|
+
},
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
As an environment variable:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
NSC__EMAIL__DEFAULT_REPLY_TO=support@example.com
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
A call can override it, or send one mail without it:
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
await this.emailService.sendMail(to, subject, { replyTo: 'sales@example.com', htmlTemplate, templateData });
|
|
43
|
+
await this.emailService.sendMail(to, subject, { replyTo: false, htmlTemplate, templateData });
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The default applies to every mail the service sends over SMTP, Better-Auth's verification and password-reset mail
|
|
47
|
+
included. Mail sent through Brevo (`brevo.apiKey`) or Mailjet is not affected: their templates carry their own
|
|
48
|
+
reply-to. A value that is not a string is ignored with one warning rather than stopping the mail.
|
|
49
|
+
|
|
50
|
+
## Module Documentation
|
|
51
|
+
|
|
52
|
+
- Configuration reference: `email.defaultReplyTo` in `FRAMEWORK-API.md` (`IServerOptions.email`)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lenne.tech/nest-server",
|
|
3
|
-
"version": "12.3.
|
|
3
|
+
"version": "12.3.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Server framework in TypeScript on top of NestJS: GraphQL and REST APIs on MongoDB, PostgreSQL, MySQL or MariaDB, with authentication, field-level permissions, multi-tenancy, offline sync and an AI assistant.",
|
|
6
6
|
"keywords": [
|
|
@@ -2687,6 +2687,17 @@ export interface IServerOptions {
|
|
|
2687
2687
|
* SMTP and template configuration for sending emails
|
|
2688
2688
|
*/
|
|
2689
2689
|
email?: {
|
|
2690
|
+
/**
|
|
2691
|
+
* Default Reply-To of every mail `EmailService.sendMail()` sends (12.3.2+): where a recipient's answer goes when it
|
|
2692
|
+
* should not reach the sender address, e.g. a shared support mailbox. An address, or `"Name" <address>`.
|
|
2693
|
+
*
|
|
2694
|
+
* Absent (the default) → no Reply-To header, exactly as before. A call overrides it with `replyTo`, and
|
|
2695
|
+
* `replyTo: false` sends that one mail without it. Set it through `NSC__EMAIL__DEFAULT_REPLY_TO`. Applies to the
|
|
2696
|
+
* SMTP path (nodemailer, `email.smtp`): Brevo and Mailjet templates carry their own reply-to.
|
|
2697
|
+
* @default undefined
|
|
2698
|
+
*/
|
|
2699
|
+
defaultReplyTo?: string;
|
|
2700
|
+
|
|
2690
2701
|
/**
|
|
2691
2702
|
* Data for default sender
|
|
2692
2703
|
*/
|
|
@@ -29,6 +29,9 @@ export class EmailService implements OnModuleDestroy {
|
|
|
29
29
|
/** Once-per-process latch for the SMTP TLS warning — a per-send warning would be noise. */
|
|
30
30
|
private smtpTlsWarningEmitted = false;
|
|
31
31
|
|
|
32
|
+
/** Once-per-process latch for a Reply-To that is not a string. */
|
|
33
|
+
private replyToWarningEmitted = false;
|
|
34
|
+
|
|
32
35
|
/**
|
|
33
36
|
* Inject services
|
|
34
37
|
*/
|
|
@@ -77,6 +80,31 @@ export class EmailService implements OnModuleDestroy {
|
|
|
77
80
|
);
|
|
78
81
|
}
|
|
79
82
|
|
|
83
|
+
/**
|
|
84
|
+
* The Reply-To of one mail (12.3.2+): the call's `replyTo`, else `email.defaultReplyTo`, else none. `false` (or an
|
|
85
|
+
* empty string) sends this mail without one. A value that is not a string is dropped with a warning instead of
|
|
86
|
+
* failing the mail: a misconfigured default must not stop password-reset or verification mail.
|
|
87
|
+
*/
|
|
88
|
+
protected resolveReplyTo(replyTo: false | string | undefined): string | undefined {
|
|
89
|
+
if (replyTo === false) {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
const value: unknown = replyTo ?? this.configService.getFastButReadOnly('email.defaultReplyTo');
|
|
93
|
+
if (value === undefined || value === null) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
if (typeof value === 'string') {
|
|
97
|
+
return value.trim() || undefined;
|
|
98
|
+
}
|
|
99
|
+
if (!this.replyToWarningEmitted) {
|
|
100
|
+
this.replyToWarningEmitted = true;
|
|
101
|
+
this.emailServiceLogger.warn(
|
|
102
|
+
`Reply-To ignored: expected an address string (email.defaultReplyTo / replyTo), got ${typeof value}.`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
80
108
|
onModuleDestroy(): void {
|
|
81
109
|
if (this.cachedTransporter) {
|
|
82
110
|
this.cachedTransporter.close();
|
|
@@ -95,6 +123,8 @@ export class EmailService implements OnModuleDestroy {
|
|
|
95
123
|
attachments?: Attachment[];
|
|
96
124
|
html?: string;
|
|
97
125
|
htmlTemplate?: string;
|
|
126
|
+
/** Reply-To of this mail; overrides `email.defaultReplyTo`, `false` sends it without one (12.3.2+) */
|
|
127
|
+
replyTo?: false | string;
|
|
98
128
|
senderEmail?: string;
|
|
99
129
|
senderName?: string;
|
|
100
130
|
smtp?: MailTransportOptions;
|
|
@@ -190,11 +220,13 @@ export class EmailService implements OnModuleDestroy {
|
|
|
190
220
|
this.cachedSmtpConfig = smtpKey;
|
|
191
221
|
}
|
|
192
222
|
|
|
193
|
-
// Send mail
|
|
223
|
+
// Send mail; without a Reply-To the header is left out entirely
|
|
224
|
+
const replyTo = this.resolveReplyTo(config.replyTo);
|
|
194
225
|
return this.cachedTransporter.sendMail({
|
|
195
226
|
attachments,
|
|
196
227
|
from: `"${senderName}" <${senderEmail}>`,
|
|
197
228
|
html,
|
|
229
|
+
...(replyTo ? { replyTo } : {}),
|
|
198
230
|
subject,
|
|
199
231
|
text,
|
|
200
232
|
to: recipients,
|