@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.
@@ -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.1)
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; }; mailjet?: MailjetOption...` — SMTP and template configuration for sending emails
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
@@ -315,6 +315,7 @@ export interface IServerOptions {
315
315
  database?: false | IDatabaseConfig | string;
316
316
  debugProcessInput?: boolean;
317
317
  email?: {
318
+ defaultReplyTo?: string;
318
319
  defaultSender?: {
319
320
  email?: string;
320
321
  name?: string;
@@ -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":"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"}
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.1",
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,