@lenne.tech/nest-server 11.42.10 → 11.42.12

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 (42) hide show
  1. package/.claude/rules/configurable-features.md +1 -1
  2. package/.claude/rules/package-management.md +12 -5
  3. package/.claude/rules/testing.md +10 -8
  4. package/CLAUDE.md +1 -1
  5. package/FRAMEWORK-API.md +1 -1
  6. package/dist/core/common/helpers/cookies.helper.d.ts +3 -2
  7. package/dist/core/common/helpers/cookies.helper.js +13 -4
  8. package/dist/core/common/helpers/cookies.helper.js.map +1 -1
  9. package/dist/core/common/helpers/outbound-url.helper.js +2 -5
  10. package/dist/core/common/helpers/outbound-url.helper.js.map +1 -1
  11. package/dist/core/modules/auth/core-auth.controller.js +2 -1
  12. package/dist/core/modules/auth/core-auth.controller.js.map +1 -1
  13. package/dist/core/modules/auth/core-auth.resolver.js +2 -1
  14. package/dist/core/modules/auth/core-auth.resolver.js.map +1 -1
  15. package/dist/core/modules/better-auth/core-better-auth-api.middleware.js +1 -0
  16. package/dist/core/modules/better-auth/core-better-auth-api.middleware.js.map +1 -1
  17. package/dist/core/modules/better-auth/core-better-auth-cookie.helper.d.ts +2 -0
  18. package/dist/core/modules/better-auth/core-better-auth-cookie.helper.js +2 -1
  19. package/dist/core/modules/better-auth/core-better-auth-cookie.helper.js.map +1 -1
  20. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js +7 -2
  21. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js.map +1 -1
  22. package/dist/core/modules/better-auth/core-better-auth.controller.js +1 -0
  23. package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -1
  24. package/dist/core/modules/better-auth/core-better-auth.service.d.ts +1 -0
  25. package/dist/core/modules/better-auth/core-better-auth.service.js +4 -0
  26. package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
  27. package/dist/tsconfig.build.tsbuildinfo +1 -1
  28. package/migration-guides/11.27.2-to-11.27.3.md +4 -2
  29. package/migration-guides/11.36.3-to-11.36.4.md +12 -7
  30. package/migration-guides/11.42.10-to-11.42.11.md +176 -0
  31. package/migration-guides/11.42.11-to-11.42.12.md +142 -0
  32. package/package.json +12 -12
  33. package/src/core/common/helpers/cookies.helper.ts +50 -6
  34. package/src/core/common/helpers/outbound-url.helper.ts +4 -10
  35. package/src/core/modules/auth/core-auth.controller.ts +2 -1
  36. package/src/core/modules/auth/core-auth.resolver.ts +2 -1
  37. package/src/core/modules/better-auth/README.md +1 -0
  38. package/src/core/modules/better-auth/core-better-auth-api.middleware.ts +1 -0
  39. package/src/core/modules/better-auth/core-better-auth-cookie.helper.ts +24 -5
  40. package/src/core/modules/better-auth/core-better-auth-email-verification.service.ts +20 -10
  41. package/src/core/modules/better-auth/core-better-auth.controller.ts +4 -0
  42. package/src/core/modules/better-auth/core-better-auth.service.ts +24 -0
@@ -155,8 +155,10 @@ some projects copied `scripts/check.mjs` from 11.27.1+:
155
155
  no longer wipe each other's users/sessions mid-flight. A new reporter
156
156
  (`tests/db-lifecycle.reporter.ts`) drops the DB after a successful run and
157
157
  collects stale leftovers; after a failed run the DB is kept for debugging
158
- until the next successful run. An externally set `MONGODB_URI` (CI) keeps
159
- the previous behavior.
158
+ until the next successful run. An externally set `NSC__MONGOOSE__URI` (CI)
159
+ keeps the previous behavior. (This line originally said `MONGODB_URI`, the
160
+ variable the framework's own test setup reads; a starter-derived project's
161
+ `tests/global-setup.ts` reads `NSC__MONGOOSE__URI`.)
160
162
 
161
163
  ---
162
164
 
@@ -35,10 +35,15 @@ fix reaches an existing project.
35
35
  1. Your project's slug contains `ci`, `test`, `e2e` or `acctest` **as a substring** — including
36
36
  inside an ordinary word: `ci` hides in so**ci**al, spe**ci**al, finan**ci**al, invoi**ci**ng,
37
37
  pri**ci**ng, muni**ci**pal; `test` hides in la**test**, con**test**, **test**imonials.
38
- 2. You use `lt dev up`, which exports `MONGODB_URI` pointing at `<slug>-local` — your **development**
39
- database.
38
+ 2. You use `lt dev up`, which exports `NSC__MONGOOSE__URI` pointing at `<slug>-local` — your
39
+ **development** database, and the variable your `tests/global-setup.ts` drops when it is set.
40
40
  3. You started the e2e suite from that same shell.
41
41
 
42
+ > **Correction (11.42.x):** earlier versions of this guide named `MONGODB_URI` throughout. That is
43
+ > the variable the framework's OWN test setup reads; `lt dev` never sets it. A starter-derived
44
+ > project's `tests/global-setup.ts` reads `NSC__MONGOOSE__URI`, which `lt dev` does export — so
45
+ > the steps below are right for your project, only the variable name was wrong.
46
+
42
47
  Then `tests/global-setup.ts` dropped that database. The guard meant to prevent exactly this accepted
43
48
  it, because it matched the marker anywhere in the name rather than as a delimited segment.
44
49
 
@@ -82,7 +87,7 @@ cannot:
82
87
 
83
88
  **A second guard for the one drop site that has no other condition.** The startup sweep and the
84
89
  post-run collection both additionally require `isStaleTestDb()` — the name must belong to your own
85
- base. The externally-supplied-`MONGODB_URI` branch has no such backstop, and a project slug may
90
+ base. The externally-supplied-`NSC__MONGOOSE__URI` branch has no such backstop, and a project slug may
86
91
  legitimately carry a marker as a *whole word*: `ci` is the ordinary German abbreviation for
87
92
  *Corporate Identity*, `test` a product noun for an exam. `ci-portal-local` passes the segment rule.
88
93
 
@@ -100,7 +105,7 @@ Then use `isDroppableTestDb(db.databaseName)` in place of `SAFE_TEST_DB_PATTERN.
100
105
  database into a leaked one.
101
106
 
102
107
  **Refuse a URI that names no database.** `mongodb://127.0.0.1` makes the driver fall back to its
103
- default database, `test` — a name the guard accepts, so a truncated `MONGODB_URI` dropped the
108
+ default database, `test` — a name the guard accepts, so a truncated `NSC__MONGOOSE__URI` dropped the
104
109
  server's `test` database instead of failing. Note that this needs the `splitMongoUri()` fix too: the
105
110
  previous greedy form reported the **host** as the database name for such a URI, so the check could
106
111
  never fire.
@@ -125,7 +130,7 @@ refusal list *is* the specification, and reverting the anchoring turns 13 cases
125
130
  |---------|--------|
126
131
  | npm-mode projects | Unaffected by the package update; apply §1 to your own `tests/` |
127
132
  | Vendor-mode projects | Same — `tests/` is outside the vendor file set |
128
- | CI pipelines that pin `MONGODB_URI` | Unaffected, unless the pinned URI names no database or carries an environment suffix. A CI database called `myproject-ci` is fine; one called `myproject-ci-staging` is now refused |
133
+ | CI pipelines that pin `NSC__MONGOOSE__URI` | Unaffected, unless the pinned URI names no database or carries an environment suffix. A CI database called `myproject-ci` is fine; one called `myproject-ci-staging` is now refused |
129
134
  | Projects not generated from `nest-server-starter` | Nothing to do |
130
135
 
131
136
  ---
@@ -133,11 +138,11 @@ refusal list *is* the specification, and reverting the anchoring turns 13 cases
133
138
  ## Troubleshooting
134
139
 
135
140
  **`Refusing to dropDatabase("…"): not a recognized disposable test database`**
136
- Your `MONGODB_URI` points at something the guard will not delete. That is the guard working. Check
141
+ Your `NSC__MONGOOSE__URI` points at something the guard will not delete. That is the guard working. Check
137
142
  whether the URI is aimed at your development database — an `lt dev` shell exports it that way — and
138
143
  point it at a disposable test database instead. Do not loosen the pattern to get past this.
139
144
 
140
- **`Refusing to use MONGODB_URI="…": it names no database`**
145
+ **`Refusing to use NSC__MONGOOSE__URI="…": it names no database`**
141
146
  The URI has no path (`mongodb://host` rather than `mongodb://host/mydb`). Add the database name.
142
147
 
143
148
  **`N stale database(s) skipped by the safety guard`**
@@ -0,0 +1,176 @@
1
+ # Migration Guide: 11.42.10 → 11.42.11
2
+
3
+ ## Overview
4
+
5
+ | Category | Details |
6
+ | -------------------- | -------------------------------------------------------------------------------------------------------- |
7
+ | **Framework API** | No change |
8
+ | **Test tooling** | e2e low-resource cap fixed for Vitest 4, e2e run governor hardened, two `check.mjs` false greens closed (§1–§3) — reaches your project only through this guide |
9
+ | **Tests** | A race in tests that upload to a route a role guard refuses (§4); a red unit file in starter-derived monorepos (§7) |
10
+ | **Documentation** | `lt dev` exports `NSC__MONGOOSE__URI`, never `MONGODB_URI`; two older guides corrected (§5) |
11
+ | **Dependencies** | `nodemailer` 10.1.0 (minor, no API change); dev tooling (§6) |
12
+ | **Breaking Changes** | None |
13
+ | **Migration Effort** | None for the package. Starter-derived projects older than starter 11.42.10 should port §1 and §2 |
14
+
15
+ ## Quick Migration
16
+
17
+ ```bash
18
+ pnpm update @lenne.tech/nest-server
19
+ ```
20
+
21
+ Vendor-mode projects pick it up with the core update. No code or configuration change is required for the
22
+ framework itself.
23
+
24
+ **Why the rest of this guide exists:** `tests/`, `scripts/` and `vitest-e2e.config.ts` do not ship in the npm
25
+ tarball and are not part of the vendor file set. They are template files: `nest-server-starter` ships them,
26
+ and every project generated from it holds a private copy that no update channel touches. The sections below
27
+ say which starter version already carries each fix, so you can tell whether your copy needs it.
28
+
29
+ ## 1. e2e low-resource mode: `maxWorkers` instead of `poolOptions` (Vitest 4)
30
+
31
+ Already in projects generated from **starter 11.42.10 or later**.
32
+
33
+ Vitest 4 removed `test.poolOptions`. The low-resource mode in `vitest-e2e.config.ts` still set
34
+ `poolOptions.forks.maxForks`, so the worker cap was announced on stderr and then discarded — measured: a run
35
+ logging `maxForks=2` used 8 workers on a 12-core machine, the same run with `maxWorkers` used 2. Low-resource
36
+ mode only raised the timeouts, and overlapping e2e runs (several sessions, `lt dev test` next to `check`) kept
37
+ starving each other into spurious 401s and timeouts.
38
+
39
+ ```typescript
40
+ // Before
41
+ ...(LOW_RESOURCE ? { poolOptions: { forks: { maxForks: LOW_RESOURCE_FORKS, minForks: 1 } } } : {}),
42
+
43
+ // After
44
+ maxWorkers: LOW_RESOURCE ? LOW_RESOURCE_FORKS : undefined,
45
+ ```
46
+
47
+ `undefined` leaves Vitest's default in place for a normal run. `minWorkers` does not exist in Vitest 4 — drop
48
+ `minForks` rather than translating it. Also rename the option in the stderr announcement, so the one line that
49
+ reports the cap names an option that exists.
50
+
51
+ A type check does not catch this: under `module: commonjs`, Vitest's config type collapses to `any`, so a
52
+ removed option compiles cleanly. The starter pins it with `tests/unit/vitest-e2e-config.spec.ts`.
53
+
54
+ **Not for your project: the framework's GitHub-runner exception.** nest-server additionally skips the cap when
55
+ the load signal is the only trigger and `RUNNER_ENVIRONMENT === 'github-hosted'`: a GitHub-hosted runner runs
56
+ one job per VM, so the load it sees is the job's own unit step, and the cap measurably slowed every build
57
+ (133 s → 198 s mean e2e time). A shared GitLab runner sets `CI` but not `RUNNER_ENVIRONMENT`, and there the
58
+ load is real contention — keep the cap.
59
+
60
+ ## 2. e2e run governor (`tests/e2e-run-slots.ts`) hardened
61
+
62
+ Already in projects generated from **starter 11.42.10 or later**.
63
+
64
+ The slot directory under `os.tmpdir()` is shared by every lt project on the machine. Three fixes, each pinned
65
+ by a test in `tests/unit/e2e-run-slots.spec.ts`:
66
+
67
+ | Defect | Fix |
68
+ | ------ | --- |
69
+ | A slot whose PID was recycled by an unrelated long-lived process counted as an active run forever | Reclaim a slot older than six hours even when its PID answers |
70
+ | On a multi-user Linux host (`/tmp` world-writable) a planted `<pid>.slot` symlink made the write follow it | Create the directory `0o700`, unlink the own path first, write with `flag: 'wx', mode: 0o600` |
71
+ | An unwritable slot directory threw out of `acquireRunSlot()` and aborted the suite, despite the documented fail-open | Catch the claim; the run proceeds ungoverned |
72
+
73
+ The easiest port is to copy `tests/e2e-run-slots.ts` and its spec from the current starter; the file has no
74
+ project-specific content.
75
+
76
+ ## 3. `scripts/check.mjs`: two false greens
77
+
78
+ Already in projects generated from **starter 11.40.0 or later**. nest-server's own copy caught up in this
79
+ release.
80
+
81
+ - **The "Vulnerabilities" summary line tested `audit.counts` before `audit.degraded`.** A silent audit outage
82
+ (exit 0, all-zero JSON, advisory service unreachable) keeps `counts` set, so the summary printed
83
+ `critical 0 · high 0 · …` for a run that never reached the service. Test `degraded` first.
84
+ - **The CLI guard compared `resolve(process.argv[1])` with the module's own path.** Node resolves the main
85
+ module's symlinks for `import.meta.url` but keeps `argv[1]` as typed, so a start through a symlink — on macOS
86
+ any absolute path under `/tmp` — skipped the pipeline and exited 0. Compare both sides through
87
+ `realpathSync`, and exit 1 when the entry cannot be resolved.
88
+
89
+ ## 4. Tests that upload to a refused route: send no file
90
+
91
+ For the avatar upload test, already in projects generated from **starter 11.41.9 or later**. The two upload
92
+ refusals in `tests/modules/file-rest.e2e-spec.ts` (401 and 403 on `/files/upload`) still attached a file until
93
+ **starter 11.42.11**.
94
+
95
+ When a role guard refuses a multipart upload route, it answers before the file interceptor reads the body. A
96
+ test that still attaches a file races that answer: the 401/403 arrives while the client is writing the
97
+ multipart body, and the write fails with `EPIPE` (observed 1 run in 14 in this repository with retries off).
98
+ The file cannot change the guard's answer, so send none — an open route then answers 400 for the missing file,
99
+ so the test still fails if the guard is gone:
100
+
101
+ ```typescript
102
+ // Before
103
+ await testHelper.rest('/files/upload', { attachments: { file: local }, statusCode: 401 });
104
+
105
+ // After
106
+ await testHelper.rest('/files/upload', { method: 'POST', statusCode: 401 });
107
+ ```
108
+
109
+ Search your `tests/` for `attachments:` next to `statusCode: 401` or `403`.
110
+
111
+ ## 5. Documentation corrections: `NSC__MONGOOSE__URI`, not `MONGODB_URI`
112
+
113
+ `lt dev` exports `NSC__MONGOOSE__URI` (and `DATABASE_URL`); it has never set `MONGODB_URI`. The guides
114
+ [11.36.3 → 11.36.4](./11.36.3-to-11.36.4.md) and [11.27.2 → 11.27.3](./11.27.2-to-11.27.3.md) named
115
+ `MONGODB_URI` where a starter-derived project's `tests/global-setup.ts` reads `NSC__MONGOOSE__URI`. Both are
116
+ corrected. The steps in them were right for your project — only the variable name was wrong, so nothing needs
117
+ redoing unless you adapted a script to the wrong name.
118
+
119
+ ## 6. Dependency updates
120
+
121
+ - **`nodemailer` 10.0.16 → 10.1.0** — minor release: hardened message streams, SMTP connection and pool
122
+ handling, SMTP and encoding fixes. No API change; the framework's real-SMTP round-trip test passes. A project
123
+ that carries a lock-step `nodemailer` override (nest-server-starter does) should raise it:
124
+
125
+ ```yaml
126
+ # Before
127
+ overrides:
128
+ 'nodemailer@<10.0.16': '10.0.16'
129
+
130
+ # After
131
+ overrides:
132
+ 'nodemailer@<10.1.0': '10.1.0'
133
+ ```
134
+
135
+ - **Development tooling only:** `@aws-sdk/*` 3.1149.0, `bullmq` 6.3.12, `smtp-server` 3.19.19, `vite` 8.3.4,
136
+ `@types/node` 26.6.5, `@types/lodash` 4.17.26. If your project pins `@types/node` exactly, raise it to 26.6.5
137
+ so a fresh install does not keep two copies.
138
+
139
+ Unchanged on purpose: `express` stays 5.2.1, because `@nestjs/platform-express` 11.2.7 pins it exactly — a
140
+ direct bump would put two copies of express into your tree. `better-auth` stays on 1.7.7, the current release.
141
+
142
+ ## 7. Starter-derived monorepos: a red `audit-suppression-scope.spec.ts`
143
+
144
+ Fixed in **starter 11.42.11**; affects projects generated from **starter 11.41.7 or later** inside a
145
+ fullstack monorepo.
146
+
147
+ `tests/unit/audit-suppression-scope.spec.ts` read `pnpm-workspace.yaml` from the API project at COLLECTION
148
+ time. In a monorepo the API has no `pnpm-workspace.yaml` of its own (it lives at the workspace root), so the
149
+ whole file failed to load with `ENOENT` and the unit suite went red without a single test running. The
150
+ starter now reads the file inside the test and guards the monorepo layout with
151
+ `tests/unit/hoisted-layout.spec.ts`; copy both from the current starter.
152
+
153
+ Optional, also new in starter 11.42.11: its `scripts/check.mjs` ends a hanging `pnpm audit` after a timeout
154
+ (`CHECK_AUDIT_TIMEOUT`) and reports it as not checked, instead of stalling the whole `check`.
155
+
156
+ ## Compatibility Notes
157
+
158
+ | Pattern | Status |
159
+ | ------- | ------ |
160
+ | npm-mode and vendor-mode projects | Unaffected by the package update; §1–§4 apply to your own `tests/` and `scripts/` |
161
+ | Projects generated from starter ≥ 11.42.10 | §1–§4 already present |
162
+ | Projects not generated from `nest-server-starter` | Nothing to do |
163
+ | CI on a shared GitLab runner | Keep the low-resource cap; do not add a GitHub-runner exception |
164
+
165
+ ## Troubleshooting
166
+
167
+ **`[e2e] low-resource mode: … -> maxForks=…` in your log**
168
+ Your copy still sets `poolOptions` — the cap is not applied. Port §1.
169
+
170
+ **`[e2e-governor] could not claim a slot (…) — proceeding without the governor.`**
171
+ The slot directory is not writable (owned by another user, read-only tmpdir). The run continues, ungoverned.
172
+ Point `LT_E2E_SLOT_DIR` at a writable directory to get the governor back.
173
+
174
+ ## Module Documentation
175
+
176
+ - [`.claude/rules/testing.md`](../.claude/rules/testing.md) — run governor, low-resource mode, drop guard
@@ -0,0 +1,142 @@
1
+ # Migration Guide: 11.42.11 → 11.42.12
2
+
3
+ ## Overview
4
+
5
+ | Category | Details |
6
+ | -------------------- | -------------------------------------------------------------------------------------------------------- |
7
+ | **Security** | Password-reset and verification links are no longer printed on deployed `develop` / `test` stages (§1); auth cookies carry `Secure` on every https deployment, whatever the environment is called (§2) |
8
+ | **Framework API** | Additive only: `isLocalEnv()`, `CoreBetterAuthService.usesSecureCookies()`, optional `secureTransport` / `baseUrl` arguments on the cookie helpers |
9
+ | **Dependencies** | None for the package. The repository's own audit suppression for GHSA-ch52-4w7c-c8xp is gone (§3) |
10
+ | **Breaking Changes** | None |
11
+ | **Migration Effort** | None for most projects. Check §1 if you read auth links from a log outside `local` / `ci` / `e2e`, and §2 if you override a cookie-writing method |
12
+
13
+ ## Quick Migration
14
+
15
+ ```bash
16
+ pnpm update @lenne.tech/nest-server
17
+ ```
18
+
19
+ Vendor-mode projects pick it up with the core update. No code or configuration change is required unless one
20
+ of the checks in §1 or §2 applies to you.
21
+
22
+ ## 1. Auth links are printed only in local environments
23
+
24
+ `CoreBetterAuthEmailVerificationService.logAuthUrlForDevelopment()` prints the verification and password-reset
25
+ link to stdout so a developer can follow it without a mail server. The URL carries a live bearer token — a reset
26
+ link changes the password outright.
27
+
28
+ Until this release the link was printed everywhere except `production` and `staging`. The deployed `develop` and
29
+ `test` stages of a starter-based project are neither, so they wrote live reset links into their log pipeline,
30
+ where read access to the logs was enough to take over an account.
31
+
32
+ | Environment (`env`, else `NODE_ENV`) | Before | After |
33
+ | ------------------------------------ | ------ | ----- |
34
+ | `local`, `ci`, `e2e` | printed | printed |
35
+ | `develop`, `test`, `development`, any other name, none at all | printed | **not printed** |
36
+ | `production`, `staging`, `NODE_ENV=production` | not printed | not printed |
37
+
38
+ `LT_LOG_AUTH_URLS` gained a value:
39
+
40
+ | Value | Effect |
41
+ | ----- | ------ |
42
+ | unset | the table above |
43
+ | `1` | also print in an environment that is not in the list — for a developer machine with a different environment name |
44
+ | `0` | print nowhere, not even locally |
45
+
46
+ `production` and `staging` never print, whatever the variable says.
47
+
48
+ **What to check:**
49
+
50
+ - **Your local environment has another name** (e.g. `development`): set `LT_LOG_AUTH_URLS=1` in your local
51
+ `.env`. Never set it on a deployment.
52
+ - **A script or CI job reads `[EMAIL VERIFICATION]` / `[PASSWORD RESET]` from the API log** — fine as long as it
53
+ runs the API as `local`, `ci` or `e2e` (`lt dev up`, `lt dev test` and the starter's e2e scripts do). Against a
54
+ deployed stage, read the mail instead (Hub mailbox, Mailpit), which is what that stage should have been doing.
55
+
56
+ The decision uses the new exported helper `isLocalEnv(env)`, which reads the existing `LOCALHOST_URL_ENVS`
57
+ (`local`, `ci`, `e2e`). The SSRF guard's private-target allowance (`privateOutboundTargetsAllowed()`) now reads
58
+ the same helper; its behaviour is unchanged.
59
+
60
+ ## 2. Auth cookies carry `Secure` on every https deployment
61
+
62
+ The session cookie is written by two paths: Better-Auth's own handlers (2FA, social callback, passkey) and the
63
+ framework's cookie helper (sign-in and sign-up through `CoreBetterAuthController`, the API middleware, and the
64
+ legacy `token` / `refreshToken` cookies). Better-Auth sets `Secure` from an `https://` base URL. The framework
65
+ helper set it only for `production` / `staging` — so on an https `develop` or `test` stage the cookie that
66
+ establishes the session went out without `Secure`, while Better-Auth's handlers set it on the same deployment.
67
+
68
+ Now:
69
+
70
+ | Cookie | `Secure` when |
71
+ | ------ | ------------- |
72
+ | Better-Auth session cookie written by the framework | Better-Auth's own cookies are secure (read off the instance: `CoreBetterAuthService.usesSecureCookies()`), or the environment is production-like |
73
+ | Legacy `token` / `refreshToken` | `baseUrl` starts with `https://`, or the environment is production-like |
74
+
75
+ `Secure` is only ever added, never removed. A project that switched it off on purpose
76
+ (`betterAuth.options.advanced.defaultCookieAttributes.secure: false`) keeps that outside production and staging.
77
+
78
+ **What to check:**
79
+
80
+ - **A deployment whose `baseUrl` says `https://` but whose browsers reach the API over plain http** now loses the
81
+ framework-written session cookie too — browsers drop a `Secure` cookie received over http. Better-Auth's own
82
+ handlers already behaved this way, so such a deployment had broken 2FA and social sign-in before. Fix the
83
+ `baseUrl` or serve https.
84
+ - **You override a cookie-writing method.** The new inputs reach the helpers only if the caller passes them:
85
+
86
+ ```typescript
87
+ // A CoreAuthController / CoreAuthResolver subclass that overrides processCookies()
88
+ const baseUrl = this.configService.getFastButReadOnly<string>('baseUrl');
89
+ return setLegacyAuthCookies(res, result, cookiesConfig, env, baseUrl);
90
+
91
+ // Code that builds its own Better-Auth cookie helper
92
+ createCookieHelper(basePath, { ...options, secureTransport: this.betterAuthService.usesSecureCookies() });
93
+ ```
94
+
95
+ Without the argument the helper keeps the previous, environment-only rule — nothing breaks, the fix just does
96
+ not reach that path.
97
+ - **Your tests stub `CoreBetterAuthService`**: no change needed. The controller calls `usesSecureCookies()`
98
+ optionally, so a stub without it keeps the previous behaviour.
99
+
100
+ ## 3. Audit suppression GHSA-ch52-4w7c-c8xp removed (repository only)
101
+
102
+ This repository no longer lists GHSA-ch52-4w7c-c8xp (`http-cache-semantics <= 4.2.0`, dev tooling only:
103
+ `@swc/cli` > … > `cacheable-request`) in `auditConfig.ignoreGhsas`. Its lockfile now resolves 4.3.0, which
104
+ `cacheable-request`'s `^4.2.0` takes on any fresh resolve. Nothing here reaches the published package.
105
+
106
+ 4.3.0 is **not a fix**: its max-stale code is identical to 4.2.0's. It only lies outside the advisory's declared
107
+ range, because the advisory was written as "up to the newest release" and never followed. The maintainer disputes
108
+ the report (kornelski/http-cache-semantics#56); a withdrawal request is open (github/advisory-database#10139),
109
+ and an independent re-test there reproduces the behaviour through 4.3.0, so a range extension is possible.
110
+
111
+ **For starter-derived projects** that carry the same entry (nest-server-starter 11.41.7 – 11.42.11):
112
+
113
+ - Lockfile on 4.3.0 (`pnpm why http-cache-semantics`): remove the entry from `ignoreGhsas` together with its row in
114
+ `tests/unit/audit-suppression-scope.spec.ts`. On 4.3.0 the entry hides nothing and would swallow the advisory
115
+ unseen the day its range is extended; without it, that day turns `pnpm audit` red, which is the moment to
116
+ re-argue the dev-only case.
117
+ - Lockfile still on 4.2.0: `pnpm update http-cache-semantics` first, then as above. Keep the entry instead if
118
+ your resolve cannot move.
119
+
120
+ ## Compatibility Notes
121
+
122
+ | Pattern | Status |
123
+ | ------- | ------ |
124
+ | npm-mode and vendor-mode projects without cookie-related overrides | Nothing to do |
125
+ | `lt dev up` / `lt dev test` / starter e2e scripts reading auth links from the log | Unchanged — they run as `local` / `e2e` |
126
+ | Subclass overriding `processCookies()` (legacy) | Pass `baseUrl` (§2), otherwise unchanged behaviour |
127
+ | Custom `createCookieHelper()` call | Pass `secureTransport` (§2), otherwise unchanged behaviour |
128
+ | Test stubs of `CoreBetterAuthService` | Compatible |
129
+
130
+ ## Troubleshooting
131
+
132
+ **The `[PASSWORD RESET]` / `[EMAIL VERIFICATION]` line disappeared from your local log**
133
+ Your environment is not named `local`, `ci` or `e2e`. Set `LT_LOG_AUTH_URLS=1` locally (§1).
134
+
135
+ **After the update, sign-in succeeds but the next request is anonymous on a non-production stage**
136
+ The browser dropped the now-`Secure` session cookie because the page or API is served over http while `baseUrl`
137
+ says https. Serve https or correct `baseUrl` (§2).
138
+
139
+ ## Module Documentation
140
+
141
+ - [Better-Auth module README](../src/core/modules/better-auth/README.md) — `usesSecureCookies()`
142
+ - [Configurable features](../.claude/rules/configurable-features.md) — Cookies row, Outbound URL Guard row
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/nest-server",
3
- "version": "11.42.10",
3
+ "version": "11.42.12",
4
4
  "description": "Modern, fast, powerful Node.js web framework in TypeScript based on Nest with a GraphQL API and a connection to MongoDB (or other databases).",
5
5
  "keywords": [
6
6
  "node",
@@ -34,7 +34,7 @@
34
34
  "check:server-start": "cross-env NODE_ENV=local node scripts/check-server-start.mjs --entry=dist/main.js --port-env=NSC__PORT --ready=\"Server starte[dt] at\" --path=/",
35
35
  "check:swc-tdz": "nest build -b swc -p tsconfig.swc-tdz.json && node scripts/check-swc-tdz.mjs",
36
36
  "cnaf": "pnpm run check:naf",
37
- "docs": "pnpm run docs:ci && open http://127.0.0.1:8080/ && open ./public/index.html && compodoc -p tsconfig.json -s ",
37
+ "docs": "pnpm run docs:ci && node scripts/open.mjs http://127.0.0.1:8080/ ./public/index.html && compodoc -p tsconfig.json -s",
38
38
  "docs:bootstrap": "node extras/update-spectaql-version.mjs && node scripts/run-spectaql.mjs",
39
39
  "docs:ci": "ts-node ./scripts/init-server.ts && pnpm run docs:bootstrap && compodoc -p tsconfig.json",
40
40
  "format": "oxfmt --write src/ scripts/",
@@ -59,7 +59,7 @@
59
59
  "start:prod": "cross-env NODE_ENV=production node dist/main.js",
60
60
  "test": "pnpm run vitest:unit && pnpm run vitest",
61
61
  "test:ci": "pnpm run vitest:unit && pnpm run vitest:ci",
62
- "test:cleanup": "find tests -type f \\( -name '*.txt' -o -name '*.bin' -o -name '*.png' \\) -not -name '.gitkeep' -delete && echo 'Test artifacts cleaned up'",
62
+ "test:cleanup": "node scripts/clean-test-artifacts.mjs",
63
63
  "test:cov": "pnpm run vitest:unit:cov && pnpm run vitest:cov",
64
64
  "test:e2e": "pnpm run vitest",
65
65
  "test:infra": "node scripts/test-infra.mjs up",
@@ -129,7 +129,7 @@
129
129
  "mongoose": "9.10.4",
130
130
  "multer": "2.4.0",
131
131
  "node-mailjet": "6.0.11",
132
- "nodemailer": "10.0.16",
132
+ "nodemailer": "10.1.0",
133
133
  "passport": "0.7.0",
134
134
  "passport-jwt": "4.0.1",
135
135
  "reflect-metadata": "0.2.2",
@@ -182,9 +182,9 @@
182
182
  }
183
183
  },
184
184
  "devDependencies": {
185
- "@aws-sdk/client-s3": "3.1147.0",
186
- "@aws-sdk/lib-storage": "3.1147.0",
187
- "@aws-sdk/s3-request-presigner": "3.1147.0",
185
+ "@aws-sdk/client-s3": "3.1149.0",
186
+ "@aws-sdk/lib-storage": "3.1149.0",
187
+ "@aws-sdk/s3-request-presigner": "3.1149.0",
188
188
  "@better-auth/core": "1.7.7",
189
189
  "@better-auth/passkey": "1.7.7",
190
190
  "@compodoc/compodoc": "2.0.0",
@@ -198,13 +198,13 @@
198
198
  "@types/cookie-parser": "1.4.10",
199
199
  "@types/ejs": "3.1.5",
200
200
  "@types/express": "5.0.6",
201
- "@types/lodash": "4.17.25",
201
+ "@types/lodash": "4.17.26",
202
202
  "@types/multer": "2.3.0",
203
- "@types/node": "26.6.4",
203
+ "@types/node": "26.6.5",
204
204
  "@types/passport": "1.0.17",
205
205
  "@vitest/coverage-v8": "4.1.11",
206
206
  "better-auth": "1.7.7",
207
- "bullmq": "6.3.11",
207
+ "bullmq": "6.3.12",
208
208
  "cross-env": "10.1.0",
209
209
  "ioredis": "6.0.0",
210
210
  "nodemon": "3.1.14",
@@ -213,14 +213,14 @@
213
213
  "oxfmt": "0.71.0",
214
214
  "oxlint": "1.87.0",
215
215
  "rimraf": "6.1.3",
216
- "smtp-server": "3.19.17",
216
+ "smtp-server": "3.19.19",
217
217
  "ts-node": "10.9.2",
218
218
  "tsconfig-paths": "4.2.0",
219
219
  "tsx": "4.23.15",
220
220
  "tus-js-client": "4.3.1",
221
221
  "typescript": "5.9.3",
222
222
  "unplugin-swc": "2.0.0",
223
- "vite": "8.3.3",
223
+ "vite": "8.3.4",
224
224
  "vite-plugin-node": "8.0.0",
225
225
  "vitest": "4.1.11"
226
226
  },
@@ -51,23 +51,37 @@ export interface AuthCookieDefaultOptions {
51
51
  *
52
52
  * - `httpOnly: true` — prevents JavaScript access (XSS mitigation)
53
53
  * - `sameSite: 'lax'` — CSRF mitigation
54
- * - `secure: isProductionLikeEnv(env)` — HTTPS-only in production/staging
54
+ * - `secure` — HTTPS-only on an `https://` `baseUrl`, and always in production/staging
55
55
  *
56
- * Used by both Legacy Auth and BetterAuth cookie helpers for consistent security.
56
+ * Used by the Legacy Auth cookie helper. (The BetterAuth helper decides `secure` the same way, but
57
+ * reads the https half off the Better-Auth instance.)
57
58
  *
58
59
  * @param env - App-level environment from `IServerOptions.env` (e.g. `'production'`, `'staging'`, `'ci'`).
59
60
  * Falls back to `process.env.NODE_ENV === 'production'` if absent.
61
+ * @param baseUrl - The server's `IServerOptions.baseUrl`. An `https://` value sets `secure` whatever
62
+ * the environment is called — since 11.42.12, because the deployed `develop` / `test`
63
+ * stages of a starter-based project are not production-like and got cookies without it.
60
64
  *
61
65
  * @since 11.25.0
62
66
  */
63
- export function getDefaultAuthCookieOptions(env?: string): AuthCookieDefaultOptions {
67
+ export function getDefaultAuthCookieOptions(env?: string, baseUrl?: string): AuthCookieDefaultOptions {
64
68
  return {
65
69
  httpOnly: true,
66
70
  sameSite: 'lax',
67
- secure: isProductionLikeEnv(env),
71
+ secure: isProductionLikeEnv(env) || isHttpsUrl(baseUrl),
68
72
  };
69
73
  }
70
74
 
75
+ /**
76
+ * Whether a URL uses https, the transport signal for the `Secure` cookie attribute (Better-Auth
77
+ * derives its own the same way). Trimmed and case-insensitive; anything that is not a string is not.
78
+ *
79
+ * @since 11.42.12
80
+ */
81
+ function isHttpsUrl(url: unknown): boolean {
82
+ return typeof url === 'string' && url.trim().toLowerCase().startsWith('https://');
83
+ }
84
+
71
85
  /**
72
86
  * Asserts that the cookies configuration is safe for production/staging environments.
73
87
  *
@@ -114,6 +128,7 @@ export function assertCookiesProductionSafe(
114
128
  * @param result - Auth result containing `token` and `refreshToken` (modified in place)
115
129
  * @param cookies - The `cookies` config value from IServerOptions
116
130
  * @param env - App-level env from `IServerOptions.env` (used for `secure` flag derivation)
131
+ * @param baseUrl - `IServerOptions.baseUrl`; an `https://` value sets `secure` too (since 11.42.12)
117
132
  * @returns The (possibly modified) result object
118
133
  *
119
134
  * @since 11.25.0
@@ -123,12 +138,13 @@ export function setLegacyAuthCookies<T extends { refreshToken?: string; token?:
123
138
  result: T,
124
139
  cookies: boolean | ICookiesConfig | undefined,
125
140
  env?: string,
141
+ baseUrl?: string,
126
142
  ): T {
127
143
  if (!isCookiesEnabled(cookies)) {
128
144
  return result;
129
145
  }
130
146
 
131
- const cookieOptions = getDefaultAuthCookieOptions(env);
147
+ const cookieOptions = getDefaultAuthCookieOptions(env, baseUrl);
132
148
 
133
149
  // If result is absent or not an object, clear any existing cookies (logout path)
134
150
  if (!result || typeof result !== 'object') {
@@ -261,12 +277,40 @@ export const LOCALHOST_URL_DEFAULTS = {
261
277
  } as const;
262
278
 
263
279
  /**
264
- * Environments that fall back to {@link LOCALHOST_URL_DEFAULTS} when no URLs are configured.
280
+ * The LOCAL environments: they run on a developer machine or in a test job, never as a deployment.
281
+ *
282
+ * They fall back to {@link LOCALHOST_URL_DEFAULTS} when no URLs are configured, and
283
+ * {@link isLocalEnv} reads this list for every other "local only" decision (the SSRF guard's
284
+ * private targets, the auth-link log). One list, so the decisions cannot drift apart.
285
+ *
286
+ * Deliberately NOT `development` or `test`: in projects built from nest-server-starter `test` is a
287
+ * DEPLOYED stage (often the customer's QA stage), and `develop`/`development` name a deployed dev
288
+ * stage as often as a laptop.
265
289
  *
266
290
  * @since 11.27.5
267
291
  */
268
292
  export const LOCALHOST_URL_ENVS: readonly string[] = ['ci', 'e2e', 'local'];
269
293
 
294
+ /**
295
+ * Checks whether the given environment is local ({@link LOCALHOST_URL_ENVS}: `local`, `ci`, `e2e`),
296
+ * matched trimmed and case-insensitively.
297
+ *
298
+ * Not the complement of {@link isProductionLikeEnv}. That one answers "must this be hardened?" and
299
+ * names only the environments that obviously hold real users; everything it does not name counts
300
+ * as non-production, including the deployed `develop` and `test` stages of a starter-based project.
301
+ * Use this one when the question is the opposite — "may this run without protection?" — so that an
302
+ * environment nobody listed stays protected. An unknown or missing name answers `false`.
303
+ *
304
+ * @since 11.42.12
305
+ */
306
+ export function isLocalEnv(env?: string): boolean {
307
+ return LOCALHOST_URL_ENVS.includes(
308
+ String(env ?? '')
309
+ .trim()
310
+ .toLowerCase(),
311
+ );
312
+ }
313
+
270
314
  /**
271
315
  * The hostname label stripped from `baseUrl` to derive `appUrl`.
272
316
  */
@@ -3,6 +3,7 @@ import { BlockList, isIP } from 'node:net';
3
3
  import { Agent, buildConnector, fetch as undiciFetch } from 'undici';
4
4
 
5
5
  import { ConfigService } from '../services/config.service';
6
+ import { isLocalEnv } from './cookies.helper';
6
7
 
7
8
  /**
8
9
  * Requests to a URL a USER entered — a webhook, an export target, an ERP address.
@@ -53,12 +54,6 @@ for (const [network, prefix] of [
53
54
  blocked.addSubnet(network, prefix, 'ipv6');
54
55
  }
55
56
 
56
- /**
57
- * Environments in which internal targets are allowed unless configured otherwise. Deliberately NOT
58
- * `test`: in projects built from nest-server-starter that is a DEPLOYED stage.
59
- */
60
- const LOCAL_ENVIRONMENTS = new Set(['ci', 'e2e', 'local']);
61
-
62
57
  /** Thrown for a target that is, or resolves to, an internal address. */
63
58
  export class OutboundUrlBlockedError extends Error {
64
59
  constructor(
@@ -93,10 +88,9 @@ export function privateOutboundTargetsAllowed(): boolean {
93
88
  if (flag === false || flag === 'false') {
94
89
  return false;
95
90
  }
96
- const env = String(config?.env ?? process.env.NODE_ENV ?? '')
97
- .trim()
98
- .toLowerCase();
99
- return LOCAL_ENVIRONMENTS.has(env);
91
+ // Internal targets are allowed only in the local environments (`isLocalEnv`), deliberately not
92
+ // in `test`: in projects built from nest-server-starter that is a DEPLOYED stage.
93
+ return isLocalEnv(config?.env ?? process.env.NODE_ENV);
100
94
  }
101
95
 
102
96
  /** Whether an IP address is internal. A hostname (not an IP) answers false — resolve it first. */
@@ -192,6 +192,7 @@ export class CoreAuthController {
192
192
  protected processCookies(res: ResponseType, result: any) {
193
193
  const cookiesConfig = this.configService.getFastButReadOnly<boolean | ICookiesConfig>('cookies');
194
194
  const env = this.configService.getFastButReadOnly<string>('env');
195
- return setLegacyAuthCookies(res, result, cookiesConfig, env);
195
+ const baseUrl = this.configService.getFastButReadOnly<string>('baseUrl');
196
+ return setLegacyAuthCookies(res, result, cookiesConfig, env, baseUrl);
196
197
  }
197
198
  }
@@ -174,6 +174,7 @@ export class CoreAuthResolver {
174
174
  protected processCookies(ctx: { res: ResponseType }, result: any) {
175
175
  const cookiesConfig = this.configService.getFastButReadOnly<boolean | ICookiesConfig>('cookies');
176
176
  const env = this.configService.getFastButReadOnly<string>('env');
177
- return setLegacyAuthCookies(ctx.res, result, cookiesConfig, env);
177
+ const baseUrl = this.configService.getFastButReadOnly<string>('baseUrl');
178
+ return setLegacyAuthCookies(ctx.res, result, cookiesConfig, env, baseUrl);
178
179
  }
179
180
  }
@@ -1505,6 +1505,7 @@ export class MyService {
1505
1505
  | `getBasePath()` | Get the base path for endpoints |
1506
1506
  | `getBaseUrl()` | Get the base URL |
1507
1507
  | `getCookieDomain()` | Get resolved cross-subdomain cookie domain |
1508
+ | `usesSecureCookies()` | Whether Better-Auth's cookies carry Secure |
1508
1509
  | `getSession(req)` | Get current session from request |
1509
1510
  | `revokeSession(token)` | Revoke a session (logout) |
1510
1511
  | `isSessionExpiringSoon(session, t?)` | Check if session is expiring soon |
@@ -172,6 +172,7 @@ export class CoreBetterAuthApiMiddleware implements NestMiddleware {
172
172
  env,
173
173
  legacyCookieEnabled: false, // Middleware doesn't need legacy cookie
174
174
  secret: config?.secret, // Required for cookie signing
175
+ secureTransport: this.betterAuthService.usesSecureCookies?.() === true, // Same `Secure` rule as Better-Auth
175
176
  },
176
177
  this.logger,
177
178
  );