@lenne.tech/nest-server 11.27.5 → 11.27.6
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 +2 -2
- package/.claude/rules/testing.md +27 -9
- package/CLAUDE.md +4 -2
- package/FRAMEWORK-API.md +1 -1
- package/bin/migrate.js +84 -25
- package/dist/core/common/helpers/cookies.helper.d.ts +1 -0
- package/dist/core/common/helpers/cookies.helper.js +33 -16
- package/dist/core/common/helpers/cookies.helper.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.config.js +6 -8
- package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
- package/dist/core/modules/migrate/migration-runner.d.ts +1 -0
- package/dist/core/modules/migrate/migration-runner.js +3 -2
- package/dist/core/modules/migrate/migration-runner.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/REQUEST-LIFECYCLE.md +17 -4
- package/migration-guides/11.27.5-to-11.27.6.md +359 -0
- package/package.json +11 -6
- package/src/core/common/helpers/cookies.helper.ts +101 -26
- package/src/core/common/interfaces/server-options.interface.ts +35 -6
- package/src/core/modules/better-auth/README.md +10 -4
- package/src/core/modules/better-auth/better-auth.config.ts +74 -15
- package/src/core/modules/migrate/migration-runner.ts +16 -2
|
@@ -457,18 +457,31 @@ if (!isCorsDisabled(envConfig.cors)) {
|
|
|
457
457
|
|
|
458
458
|
| Config | REST (Express) | GraphQL (Apollo) | BetterAuth |
|
|
459
459
|
|--------|----------------|-------------------|------------|
|
|
460
|
-
| `cors: { allowAll: true }` | `origin: true` | `origin: true` | `trustedOrigins:
|
|
460
|
+
| `cors: { allowAll: true }` | `origin: true` | `origin: true` | `trustedOrigins: [appUrl] (+ passkey origins)` |
|
|
461
461
|
| `cors: { allowedOrigins: [...] }` | `origin: [merged list]` | `origin: [merged list]` | `trustedOrigins: [merged list]` |
|
|
462
462
|
| `cors: { enabled: false }` | No CORS headers | No CORS headers | `trustedOrigins: []` |
|
|
463
463
|
| `cors: { deriveAppUrl: false }` | `appUrl` not derived from `baseUrl` | same | same |
|
|
464
464
|
|
|
465
|
+
> **BetterAuth has no "allow all origins" mode (since v11.27.6).** `cors.allowAll` mirrors the request origin for REST/GraphQL, but BetterAuth's origin check is a security control with no meaningful "allow everything" setting. So `allowAll` yields the known-good origins (`appUrl` + any passkey origins), NOT `undefined` — returning nothing there would leave BetterAuth trusting only its own `baseURL` and silently answer `403 INVALID_ORIGIN` for a separately hosted frontend on `two-factor/enable`, passkey registration, etc. For the same reason `trustedOrigins: []` (the `enabled: false` row) does **not** switch the origin check off: BetterAuth always trusts its own `baseURL`, so `[]` and `undefined` behave identically. To accept arbitrary origins for auth, set `betterAuth.trustedOrigins` explicitly.
|
|
466
|
+
|
|
465
467
|
**URL resolution (since v11.27.5):** all three layers resolve `appUrl`/`baseUrl` through the single `resolveServerUrls()` helper in `cookies.helper.ts`, so they can no longer drift:
|
|
466
468
|
|
|
467
469
|
1. `appUrl` set explicitly → used as-is
|
|
468
|
-
2. `env: 'local' | 'ci' | 'e2e'` with a localhost `baseUrl` → `
|
|
469
|
-
3.
|
|
470
|
+
2. `env: 'local' | 'ci' | 'e2e'` with a localhost `baseUrl` that splits API and app by **host** → derived from `baseUrl` (see below)
|
|
471
|
+
3. `env: 'local' | 'ci' | 'e2e'` with any other localhost `baseUrl` → `appUrl` defaults to `http://localhost:3001`
|
|
472
|
+
4. otherwise derived from `baseUrl` by stripping a leading `api.` label (`https://api.example.com` → `https://example.com`), unless `cors.deriveAppUrl: false`
|
|
473
|
+
|
|
474
|
+
**Port split vs. host split (step 2 vs. 3, since v11.27.6).** The localhost defaults encode a *port split*: one host, API on `:3000`, app on `:3001`. `lt dev up` instead serves a *host split* behind Caddy — API on `https://api.<slug>.localhost`, app on `https://<slug>.localhost`. The two are told apart by what the `api.` label strips to, never by the port:
|
|
475
|
+
|
|
476
|
+
| `baseUrl` (`env: 'local'`) | Split | Resolved `appUrl` |
|
|
477
|
+
|---------------------------|-------|-------------------|
|
|
478
|
+
| `https://api.crm.localhost` | host | `https://crm.localhost` |
|
|
479
|
+
| `https://api.crm.localhost:8443` | host | `https://crm.localhost:8443` |
|
|
480
|
+
| `https://api.localhost` | port (strips to the bare host the API answers on) | `http://localhost:3001` |
|
|
481
|
+
| `http://api.localhost:3000` | port | `http://localhost:3001` |
|
|
482
|
+
| `http://localhost:3000` | port (no `api.` label) | `http://localhost:3001` |
|
|
470
483
|
|
|
471
|
-
> **Security:**
|
|
484
|
+
> **Security:** steps 2 and 4 grant the derived origin credentialed CORS. If the apex domain is not trusted (e.g. a third-party-hosted marketing site whose XSS surface you do not control), set `cors.deriveAppUrl: false` and list the frontend origin explicitly via `appUrl` or `cors.allowedOrigins`. The derivation never yields a bare TLD (`https://api.dev` stays unchanged) and never emits the opaque `null` origin. With `cors.deriveAppUrl: false`, a host-split localhost `baseUrl` falls back to the `http://localhost:3001` default.
|
|
472
485
|
|
|
473
486
|
### NestJS Middleware Chain (CoreModule)
|
|
474
487
|
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
# Migration Guide: 11.27.5 → 11.27.6
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
| Category | Details |
|
|
6
|
+
|----------|---------|
|
|
7
|
+
| **Breaking Changes** | None. No existing signature, export, or Core class was removed or changed shape. |
|
|
8
|
+
| **New Features** | Two additive exports: `DEFAULT_MIGRATION_FILE_PATTERN` (the migration runner's default file matcher) and `strippedApiHostname` (the `api.`-label strip helper). Purely additive — nothing existing changed. |
|
|
9
|
+
| **Bugfixes** | (1) `resolveServerUrls()` ignored a host-split localhost `baseUrl` in `local`/`ci`/`e2e`: `https://api.crm.localhost` resolved `appUrl` to the flat `http://localhost:3001` default instead of deriving `https://crm.localhost`, blocking the frontend origin in the CORS allowlist, BetterAuth's `trustedOrigins`, and the Passkey `rpId`/`origin` for every `lt dev up` project. (2) `buildTrustedOrigins()` returned `undefined` for `cors.allowAll`, silently breaking 2FA/passkey. (3) `MigrationRunner`'s default pattern loaded `*.d.ts` declaration files as migrations and threw. (4) `bin/migrate.js` failed to find the CLI from vendored layouts. (5) BetterAuth's native handlers (2FA enable/verify, passkey register/list, backup codes, `/token`) returned `401 UNAUTHORIZED` on an `https://` baseURL because BetterAuth auto-prefixed its session cookie with `__Secure-` while the nest-server cookie helper writes the unprefixed name — a split-brain where `GET /iam/get-session` was `200` but every sensitive native endpoint was `401`. (6) `deriveCookieDomainFromUrls()` collapsed `api.<bare-TLD>` (e.g. `api.dev`) to the browser-rejected public suffix `dev`, dropping every cross-subdomain session cookie. |
|
|
10
|
+
| **Behavior Changes** | Five consumer-visible shifts — see [Behavior changes in framework source](#behavior-changes-in-framework-source): the host-split `appUrl` derivation above (an `api.*.localhost` `baseUrl` with no explicit `appUrl` now derives the sibling host instead of `localhost:3001`); `cors.allowAll` now yields `[appUrl]` for BetterAuth `trustedOrigins`; the migration runner's default pattern now skips `*.d.ts`; BetterAuth's native session cookie loses its `__Secure-` prefix (the `Secure` attribute is preserved), opt-out via `betterAuth.options.advanced.useSecureCookies`; and the cross-subdomain cookie domain keeps `api.<bare-TLD>` as-is instead of collapsing to a public suffix. |
|
|
11
|
+
| **Security Updates** | None. The `allowAll` fix removes an availability bug (the origin check was never loosened), and the BetterAuth cookie-**name** change preserves the `Secure` attribute — `createBetterAuthInstance()` restores it via `advanced.defaultCookieAttributes` on an `https://` baseURL, so session-cookie confidentiality is unchanged on every path, including the native handlers that forward `Set-Cookie` verbatim. |
|
|
12
|
+
| **Migration Effort** | 0 minutes (`pnpm update`) for the vast majority. A few edge cases need a one-line config: see [Behavior changes in framework source](#behavior-changes-in-framework-source). Optional: adopt the updated `scripts/check.mjs` / vitest configs / `test` scripts — see [Repo-internal tooling](#repo-internal-tooling). |
|
|
13
|
+
|
|
14
|
+
This is a **follow-up bugfix to the URL unification shipped in 11.27.5**. The
|
|
15
|
+
`api.`-strip derivation was already implemented and unit-tested for
|
|
16
|
+
`api.<slug>.localhost` hosts, but `resolveServerUrls()` never reached it in the
|
|
17
|
+
environments where those hosts actually occur. It also folds in three smaller
|
|
18
|
+
framework-source fixes (BetterAuth `allowAll`, the migration file pattern, and
|
|
19
|
+
the `bin/migrate.js` layout resolution) that shipped in the same release.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Quick Migration
|
|
24
|
+
|
|
25
|
+
No code changes required.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Update package
|
|
29
|
+
pnpm add @lenne.tech/nest-server@11.27.6
|
|
30
|
+
|
|
31
|
+
# Verify build
|
|
32
|
+
pnpm run build
|
|
33
|
+
|
|
34
|
+
# Run tests
|
|
35
|
+
pnpm test
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## What's Fixed in 11.27.6
|
|
41
|
+
|
|
42
|
+
### `lt dev up` projects: the app origin was resolved to `localhost:3001`
|
|
43
|
+
|
|
44
|
+
`resolveServerUrls()` checked `usesLocalhostDefaults && isLocalhostUrl(baseUrl)`
|
|
45
|
+
**before** the derivation branch, and `isLocalhostUrl()` matches `*.localhost`
|
|
46
|
+
subdomains. Any localhost `baseUrl` therefore short-circuited to the flat
|
|
47
|
+
localhost default, and the derivation branch became unreachable in exactly the
|
|
48
|
+
`local`/`ci`/`e2e` environments where `.localhost` hosts are used.
|
|
49
|
+
|
|
50
|
+
That contradicted `strippedApiHostname()` (formerly `canStripApiLabel()`), which
|
|
51
|
+
explicitly supports `api.localhost → localhost` ("`localhost` is the one
|
|
52
|
+
legitimate single-label host"), and the existing unit test `should strip api. in
|
|
53
|
+
front of localhost (lt dev: api.<slug>.localhost)`. The helper did the right
|
|
54
|
+
thing; nothing called it on that path.
|
|
55
|
+
|
|
56
|
+
| `env` | `baseUrl` | `appUrl` before | `appUrl` after |
|
|
57
|
+
|-------|-----------|-----------------|----------------|
|
|
58
|
+
| `local` | `https://api.crm.localhost` | `http://localhost:3001` | `https://crm.localhost` |
|
|
59
|
+
| `local` | `https://api.crm.localhost:8443` | `http://localhost:3001` | `https://crm.localhost:8443` |
|
|
60
|
+
| `local` | `https://api.nest-server.localhost` | `http://localhost:3001` | `https://nest-server.localhost` |
|
|
61
|
+
| `local` | `https://api.localhost` | `http://localhost:3001` | unchanged |
|
|
62
|
+
| `local` | `http://api.localhost:3000` | `http://localhost:3001` | unchanged |
|
|
63
|
+
| `local` | `http://localhost:3000` | `http://localhost:3001` | unchanged |
|
|
64
|
+
| `local` | `http://127.0.0.1:3000` | `http://localhost:3001` | unchanged |
|
|
65
|
+
| `develop` | `https://api.example.com` | `https://example.com` | unchanged |
|
|
66
|
+
|
|
67
|
+
### Port split vs. host split
|
|
68
|
+
|
|
69
|
+
The localhost defaults encode a **port split**: one host, API on `:3000`, app on
|
|
70
|
+
`:3001`. `lt dev up` instead serves a **host split** behind Caddy — API on
|
|
71
|
+
`https://api.<slug>.localhost`, app on `https://<slug>.localhost`. Only the
|
|
72
|
+
second shape can name a distinct app origin, so only it takes precedence over
|
|
73
|
+
the localhost default.
|
|
74
|
+
|
|
75
|
+
The two are told apart by **what the `api.` label strips to**, never by the port:
|
|
76
|
+
|
|
77
|
+
- `api.crm.localhost` → `crm.localhost` — a sibling host the API never answers
|
|
78
|
+
on. **Host split**, derive it. This holds on any port, so
|
|
79
|
+
`https://api.crm.localhost:8443` derives `https://crm.localhost:8443`.
|
|
80
|
+
- `api.localhost` → `localhost` — the bare host the API already answers on. Only
|
|
81
|
+
the port tells app and API apart. **Port split**, keep `http://localhost:3001`.
|
|
82
|
+
This holds whether or not a port is written out, so both
|
|
83
|
+
`http://api.localhost:3000` and `https://api.localhost` keep the default.
|
|
84
|
+
|
|
85
|
+
> A port-based rule ("no explicit port ⇒ host split") looks equivalent on the two
|
|
86
|
+
> most common inputs and is wrong on both edges: it sends `api.crm.localhost:8443`
|
|
87
|
+
> back to `localhost:3001`, and it derives the unreachable `https://localhost`
|
|
88
|
+
> (port 443) from `https://api.localhost`.
|
|
89
|
+
|
|
90
|
+
### Impact per layer
|
|
91
|
+
|
|
92
|
+
All three CORS layers share `resolveServerUrls()` since 11.27.5, so all three
|
|
93
|
+
were affected identically:
|
|
94
|
+
|
|
95
|
+
- **REST / GraphQL CORS:** the allowlist contained `http://localhost:3001`
|
|
96
|
+
instead of `https://<slug>.localhost`, so the frontend was blocked by
|
|
97
|
+
preflight. Projects whose local config sets `cors.allowAll: true` did **not**
|
|
98
|
+
see this — `allowAll` short-circuits before URL resolution.
|
|
99
|
+
- **BetterAuth `trustedOrigins`:** taken straight from the resolved `appUrl`, so
|
|
100
|
+
the real frontend origin was not trusted.
|
|
101
|
+
- **Passkey `rpId` / `origin`:** both auto-detected from `appUrl`, so `rpId`
|
|
102
|
+
resolved to `localhost` and `origin` to `http://localhost:3001` instead of the
|
|
103
|
+
`lt dev` host. Projects that set `betterAuth.passkey.rpId`/`origin` explicitly
|
|
104
|
+
were unaffected.
|
|
105
|
+
|
|
106
|
+
**Not affected by this fix:** the `crossSubDomainCookies` domain. For the host-split
|
|
107
|
+
`appUrl` derivation, `deriveCookieDomainFromUrls()` skips an `appUrl` whose hostname is
|
|
108
|
+
exactly `localhost` and falls back to the `baseUrl` with its `api.` label stripped — so it
|
|
109
|
+
already resolved to `crm.localhost` before this fix. (The function itself **did** change this
|
|
110
|
+
release, but for a separate reason — the `api.<bare-TLD>` guard, [behavior change #5](#5-cross-subdomain-cookie-domain-keeps-apibare-tld-as-is) below.)
|
|
111
|
+
|
|
112
|
+
Projects that set `APP_URL` explicitly were never affected — `lt dev up` exports
|
|
113
|
+
both `BASE_URL` and `APP_URL`, and explicit values always win
|
|
114
|
+
(`appUrlSource: 'explicit'`). The bug surfaced only when `baseUrl` was set
|
|
115
|
+
without `appUrl`.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Behavior changes in framework source
|
|
120
|
+
|
|
121
|
+
All three ship inside the published `dist/` and reach every consumer via
|
|
122
|
+
`pnpm update`. None changes an existing signature — they change what existing
|
|
123
|
+
code *does* — so review this section even though the [Overview](#overview) lists
|
|
124
|
+
no breaking changes.
|
|
125
|
+
|
|
126
|
+
### 1. Host-split `appUrl` derivation (the primary fix)
|
|
127
|
+
|
|
128
|
+
For `env: 'local' | 'ci' | 'e2e'`, a `baseUrl` whose `api.` label strips to a
|
|
129
|
+
**sibling host** now derives that host instead of falling back to the flat
|
|
130
|
+
`http://localhost:3001` default:
|
|
131
|
+
|
|
132
|
+
| `env` | `baseUrl` (no `appUrl` set) | `appUrl` before | `appUrl` after |
|
|
133
|
+
|-------|----------------------------|-----------------|----------------|
|
|
134
|
+
| `local` | `https://api.crm.localhost` | `http://localhost:3001` | **`https://crm.localhost`** |
|
|
135
|
+
| `local` | `https://api.localhost` | `http://localhost:3001` | `http://localhost:3001` (unchanged) |
|
|
136
|
+
|
|
137
|
+
This is the intended fix for `lt dev up`, but it **is** a behavior change for any
|
|
138
|
+
project that previously relied on the `localhost:3001` default while setting an
|
|
139
|
+
`api.*.localhost` `baseUrl`. If your frontend really does run on
|
|
140
|
+
`http://localhost:3001` (e.g. a plain `pnpm dev`, not behind Caddy), set
|
|
141
|
+
`appUrl: 'http://localhost:3001'` or `cors.deriveAppUrl: false` — both suppress
|
|
142
|
+
the derivation. `lt dev up` projects need no action (they export `APP_URL`).
|
|
143
|
+
|
|
144
|
+
### 2. `cors.allowAll` no longer disables BetterAuth's origin check
|
|
145
|
+
|
|
146
|
+
`buildTrustedOrigins()` used to `return undefined` for `cors.allowAll`, in the
|
|
147
|
+
belief that BetterAuth then allowed all origins. It does not: without
|
|
148
|
+
`trustedOrigins`, BetterAuth trusts **only its own `baseURL`** (its
|
|
149
|
+
`getTrustedOrigins()` unconditionally pushes `new URL(baseURL).origin`), so the
|
|
150
|
+
separately hosted app origin was rejected and every origin-checked endpoint
|
|
151
|
+
(`two-factor/enable`, passkey registration) answered `403 INVALID_ORIGIN`. That
|
|
152
|
+
silently broke 2FA and passkeys in exactly the setups that run with `allowAll` —
|
|
153
|
+
local dev and CI.
|
|
154
|
+
|
|
155
|
+
`allowAll` now falls through to the known-good origins (`appUrl` + passkey), so
|
|
156
|
+
BetterAuth trusts the real app origin. An origin check has no meaningful "allow
|
|
157
|
+
everything" mode; projects that genuinely need to accept arbitrary origins for
|
|
158
|
+
auth must set `betterAuth.trustedOrigins` explicitly. **No action needed** unless
|
|
159
|
+
you depended on the (broken) old behavior.
|
|
160
|
+
|
|
161
|
+
### 3. Migration runner skips `*.d.ts`
|
|
162
|
+
|
|
163
|
+
`MigrationRunner`'s default file pattern changed from `/\.(ts|js)$/` to
|
|
164
|
+
`DEFAULT_MIGRATION_FILE_PATTERN` (`/(?:(?<!\.d)\.ts|\.js)$/`), which matches
|
|
165
|
+
`foo.ts` / `foo.js` but not `foo.d.ts`. A compiled migration directory ships a
|
|
166
|
+
declaration file next to each `.js`; the old pattern loaded it as a *second*
|
|
167
|
+
migration and threw (`export declare …` is not valid CommonJS). Projects that
|
|
168
|
+
passed an explicit `pattern` to `MigrationRunner` are unaffected. Projects on the
|
|
169
|
+
default now correctly ignore declaration files. The pattern is exported as
|
|
170
|
+
`DEFAULT_MIGRATION_FILE_PATTERN` if you need to extend rather than replace it.
|
|
171
|
+
|
|
172
|
+
> `bin/migrate.js` also gained multi-layout CLI resolution (npm package, vendored
|
|
173
|
+
> repo, vendored production image). This is transparent — it only widens where the
|
|
174
|
+
> shim looks for the compiled CLI — and needs no action.
|
|
175
|
+
|
|
176
|
+
### 4. BetterAuth native session cookie loses its `__Secure-` prefix (`Secure` preserved)
|
|
177
|
+
|
|
178
|
+
`createBetterAuthInstance()` now pins `advanced.useSecureCookies: false`. On an `https://`
|
|
179
|
+
baseURL Better-Auth otherwise auto-enables secure cookies **and** prefixes its session cookie
|
|
180
|
+
with `__Secure-`, so its native handlers (2FA enable/verify, passkey register/list, backup
|
|
181
|
+
codes, `/token`) look for `__Secure-<cookiePrefix>.session_token` — a cookie the nest-server
|
|
182
|
+
cookie helper never writes (it emits the unprefixed name) — and answer `401 UNAUTHORIZED`,
|
|
183
|
+
while `GET /iam/get-session` (reading the unprefixed cookie) still returns `200`. Pinning the
|
|
184
|
+
name realigns the native read path with the helper.
|
|
185
|
+
|
|
186
|
+
**The `Secure` attribute is NOT dropped.** `useSecureCookies: false` would by itself also strip
|
|
187
|
+
`Secure` from every cookie Better-Auth sets — including the ones its native handlers forward
|
|
188
|
+
**verbatim** (2FA verify, social callback, magic link, passkey) without passing through the
|
|
189
|
+
cookie helper. To avoid a transport downgrade, `createBetterAuthInstance()` restores the exact
|
|
190
|
+
`Secure` flag Better-Auth would have derived (an `https://` baseURL) via
|
|
191
|
+
`advanced.defaultCookieAttributes`, so on production HTTPS the session cookie still ships with
|
|
192
|
+
`Secure`.
|
|
193
|
+
|
|
194
|
+
**Opt-out.** Consumers who manage cookies entirely through Better-Auth (not the nest-server
|
|
195
|
+
helper) can re-enable the `__Secure-` prefix with
|
|
196
|
+
`betterAuth.options.advanced.useSecureCookies: true` (deep-merged). Most projects need no
|
|
197
|
+
action — `pnpm update` is sufficient.
|
|
198
|
+
|
|
199
|
+
### 5. Cross-subdomain cookie domain keeps `api.<bare-TLD>` as-is
|
|
200
|
+
|
|
201
|
+
`deriveCookieDomainFromUrls()` now strips the `api.` label through the shared
|
|
202
|
+
`strippedApiHostname()` guard. Previously a `baseUrl` like `https://api.dev` (with
|
|
203
|
+
`crossSubDomainCookies` enabled) derived the cookie domain `dev` — a public suffix browsers
|
|
204
|
+
reject outright, dropping every session cookie. It now keeps `api.dev`. Only projects with
|
|
205
|
+
`crossSubDomainCookies` on an `api.<single-label>` host are affected, and only for the better.
|
|
206
|
+
|
|
207
|
+
> Known limitation (fails closed): a **multi-label** public suffix (`api.co.uk` → `co.uk`) is
|
|
208
|
+
> still not recognised without a Public-Suffix-List check, which this security-critical module
|
|
209
|
+
> avoids adding as a dependency. For such apex domains set
|
|
210
|
+
> `betterAuth.crossSubDomainCookies.domain` explicitly.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Repo-internal tooling
|
|
215
|
+
|
|
216
|
+
These changes touch **no framework source** and ship no behavior change to
|
|
217
|
+
consuming projects. They are listed because `nest-server-starter` mirrors these
|
|
218
|
+
files, so projects that copied them can adopt the same fixes.
|
|
219
|
+
|
|
220
|
+
### Unit tests ran in the e2e runner
|
|
221
|
+
|
|
222
|
+
`vitest-e2e.config.ts` used `include: ['tests/**/*.ts']`, which matched
|
|
223
|
+
`tests/unit/**` as well. Every unit test therefore ran inside the e2e runner —
|
|
224
|
+
under `NODE_ENV=e2e`, the mongod `globalSetup` and `retry: 5`, none of which a
|
|
225
|
+
unit test needs. The include now names the suites that actually need a database:
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
include: ['tests/**/*.e2e-spec.ts', 'tests/stories/**/*.story.test.ts'],
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Story tests are matched explicitly — they are e2e-grade and must keep the
|
|
232
|
+
globalSetup. The pattern also stops matching helpers, `global-setup.ts`,
|
|
233
|
+
`setup.ts` and the DB reporter, so `exclude` could be dropped entirely (which
|
|
234
|
+
restores vitest's own defaults for `node_modules`, `dist`, … instead of
|
|
235
|
+
replacing them).
|
|
236
|
+
|
|
237
|
+
Because the unit tests no longer come along for the ride, `test` now runs both
|
|
238
|
+
runners:
|
|
239
|
+
|
|
240
|
+
```jsonc
|
|
241
|
+
"test": "pnpm run vitest:unit && pnpm run vitest",
|
|
242
|
+
"test:ci": "pnpm run vitest:unit && pnpm run vitest:ci",
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Three follow-on fixes were needed to make that split safe:
|
|
246
|
+
|
|
247
|
+
1. **`vitest.config.ts` gained `setupFiles: ['tests/setup.ts']`.** The e2e config
|
|
248
|
+
had it; without it the unit run loses `Logger.overrideLogger(['error','fatal'])`
|
|
249
|
+
and the `@UnifiedField` deprecation-warning filter, and drowns in expected
|
|
250
|
+
DEBUG/WARN output.
|
|
251
|
+
2. **`tests/unit/test-file-routing.spec.ts` guards the include patterns.** Narrow
|
|
252
|
+
patterns mean a file matching *neither* runner runs nowhere and still reports
|
|
253
|
+
green — `tests/stories/foo.test.ts` (missing `.story.`),
|
|
254
|
+
`tests/integration/bar.spec.ts`, `tests/unit/baz.test.ts`. The spec reads the
|
|
255
|
+
`include` patterns out of both configs and asserts every `*.spec.ts` /
|
|
256
|
+
`*.test.ts` in the repo is claimed by exactly one runner.
|
|
257
|
+
3. **Coverage covers both suites again.** `vitest:cov` only ever ran the e2e
|
|
258
|
+
config, which no longer sees the unit tests. New `vitest:unit:cov`, and
|
|
259
|
+
`test:cov` runs both. The two runners are separate vitest processes, so each
|
|
260
|
+
writes its own report (`coverage/unit`, `coverage/e2e`) rather than
|
|
261
|
+
overwriting the other.
|
|
262
|
+
|
|
263
|
+
### `check.mjs` under-reported multi-run test steps
|
|
264
|
+
|
|
265
|
+
`parseVitest()` used `String.match()` without the `g` flag and read only the
|
|
266
|
+
**first** vitest summary block in a step's output. With `test` now invoking
|
|
267
|
+
vitest twice, the report would have shown the unit run's `passed` count and
|
|
268
|
+
silently dropped the e2e run — and the test count is the only visible evidence in
|
|
269
|
+
the report that a suite ran at all. It now sums every summary block, is exported,
|
|
270
|
+
and is covered by `tests/unit/check-script-metrics.spec.ts`. The script only
|
|
271
|
+
executes its pipeline when invoked directly (`process.argv[1]` check), so
|
|
272
|
+
importing it for tests does not spawn a check run.
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## Behavior Change
|
|
277
|
+
|
|
278
|
+
Five consumer-visible behavior changes ship in this release — the host-split
|
|
279
|
+
`appUrl` derivation, the `cors.allowAll` origin handling, the migration file
|
|
280
|
+
pattern, the BetterAuth native-cookie name change (`Secure` preserved), and the
|
|
281
|
+
cross-subdomain `api.<bare-TLD>` cookie-domain guard. Each is described, with its
|
|
282
|
+
opt-out where one exists, under
|
|
283
|
+
[Behavior changes in framework source](#behavior-changes-in-framework-source).
|
|
284
|
+
|
|
285
|
+
Unchanged: `deriveAppUrl: false` still suppresses the derivation on all three
|
|
286
|
+
layers, and still does **not** disable the `local`/`ci`/`e2e` localhost defaults —
|
|
287
|
+
a host-split `baseUrl` with `deriveAppUrl: false` falls back to
|
|
288
|
+
`http://localhost:3001`, exactly as before.
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## Breaking Changes
|
|
293
|
+
|
|
294
|
+
None — every change is additive or a bugfix; no existing signature was removed or
|
|
295
|
+
altered. `separatesApiAndAppByHost()` and `toHttpUrl()` remain module-private.
|
|
296
|
+
`deriveAppUrlFromBaseUrl()` and `resolveServerUrls()` keep their signatures. Two
|
|
297
|
+
new named exports are added (both additive): `DEFAULT_MIGRATION_FILE_PATTERN` and
|
|
298
|
+
`strippedApiHostname()` (the latter promoted from module-private so BetterAuth's
|
|
299
|
+
cookie-domain derivation can share its bare-TLD guard). Both are free
|
|
300
|
+
helpers/constants, so they do not appear in `FRAMEWORK-API.md` (which tracks
|
|
301
|
+
interfaces, `CoreModule.forRoot()` overloads, and `CrudService` methods). The
|
|
302
|
+
tooling changes live in `scripts/` and the vitest configs, outside the published
|
|
303
|
+
`dist/`.
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## Compatibility Notes
|
|
308
|
+
|
|
309
|
+
- **npm-mode consumers:** `pnpm update` is sufficient.
|
|
310
|
+
- **Projects that copied `scripts/check.mjs` / `vitest-e2e.config.ts` from the
|
|
311
|
+
starter:** optionally adopt the updated files. Check first whether your
|
|
312
|
+
`vitest-e2e.config.ts` matches `tests/unit/**`; if it does, your unit tests are
|
|
313
|
+
running inside the e2e runner against a database they do not use.
|
|
314
|
+
- **Vendor-mode consumers:** the change touches
|
|
315
|
+
`src/core/common/helpers/cookies.helper.ts` and the `appUrl` JSDoc in
|
|
316
|
+
`src/core/common/interfaces/server-options.interface.ts` — both inside the
|
|
317
|
+
vendored `src/core/` file set. Sync via
|
|
318
|
+
`/lt-dev:backend:update-nest-server-core`.
|
|
319
|
+
- **Projects that set `appUrl` / `APP_URL` explicitly:** unaffected.
|
|
320
|
+
- **Projects that set `cors.allowAll: true` in local envs:** unaffected on the
|
|
321
|
+
CORS layer; the `trustedOrigins` and Passkey fixes still apply.
|
|
322
|
+
- **Deployed envs (`develop` / `test` / `production`):** unaffected. They are not
|
|
323
|
+
localhost-default environments, so the derivation branch was always reachable
|
|
324
|
+
there.
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
## Troubleshooting
|
|
329
|
+
|
|
330
|
+
### My `lt dev` frontend is still blocked by CORS
|
|
331
|
+
|
|
332
|
+
Check, in order:
|
|
333
|
+
|
|
334
|
+
1. You are on 11.27.6 or later.
|
|
335
|
+
2. `baseUrl` carries an `api.` label. `https://crm.localhost` has nothing to
|
|
336
|
+
strip — set `appUrl` explicitly.
|
|
337
|
+
3. The label strips to a **sibling host**, not to the bare `localhost`.
|
|
338
|
+
`https://api.localhost` is a port split and resolves to `http://localhost:3001`
|
|
339
|
+
by design.
|
|
340
|
+
4. `cors.deriveAppUrl` is not `false`.
|
|
341
|
+
|
|
342
|
+
### I rely on `http://localhost:3001` for an `api.<slug>.localhost` baseUrl
|
|
343
|
+
|
|
344
|
+
Set `appUrl: 'http://localhost:3001'` explicitly, or `cors.deriveAppUrl: false`.
|
|
345
|
+
Both suppress the derivation.
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## Module Documentation
|
|
350
|
+
|
|
351
|
+
- **Request lifecycle & CORS:** [docs/REQUEST-LIFECYCLE.md](../docs/REQUEST-LIFECYCLE.md) — section `0b. CORS`
|
|
352
|
+
- **BetterAuth:** [src/core/modules/better-auth/README.md](../src/core/modules/better-auth/README.md)
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
## References
|
|
357
|
+
|
|
358
|
+
- [Migration Guide 11.27.4 → 11.27.5](./11.27.4-to-11.27.5.md) — introduced `resolveServerUrls()`
|
|
359
|
+
- [nest-server-starter](https://github.com/lenneTech/nest-server-starter) — reference implementation
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lenne.tech/nest-server",
|
|
3
|
-
"version": "11.27.
|
|
3
|
+
"version": "11.27.6",
|
|
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",
|
|
@@ -48,8 +48,9 @@
|
|
|
48
48
|
"start:dev:swc": "nest start -b swc -w --type-check",
|
|
49
49
|
"start:local": "NODE_ENV=local nodemon",
|
|
50
50
|
"start:local:swc": "NODE_ENV=local nest start -b swc -w --type-check",
|
|
51
|
-
"test": "pnpm run vitest",
|
|
52
|
-
"test:ci": "pnpm run vitest:ci",
|
|
51
|
+
"test": "pnpm run vitest:unit && pnpm run vitest",
|
|
52
|
+
"test:ci": "pnpm run vitest:unit && pnpm run vitest:ci",
|
|
53
|
+
"test:cov": "pnpm run vitest:unit:cov && pnpm run vitest:cov",
|
|
53
54
|
"test:e2e": "pnpm run vitest",
|
|
54
55
|
"prepack": "pnpm run prestart:prod",
|
|
55
56
|
"prepublishOnly": "pnpm run lint && pnpm run test:ci",
|
|
@@ -59,6 +60,7 @@
|
|
|
59
60
|
"vitest:cov": "NODE_ENV=e2e vitest run --coverage --config vitest-e2e.config.ts",
|
|
60
61
|
"vitest:watch": "NODE_ENV=e2e vitest --config vitest-e2e.config.ts",
|
|
61
62
|
"vitest:unit": "vitest run --config vitest.config.ts",
|
|
63
|
+
"vitest:unit:cov": "vitest run --coverage --config vitest.config.ts",
|
|
62
64
|
"test:unit:watch": "vitest --config vitest.config.ts",
|
|
63
65
|
"test:types": "tsc --noEmit --skipLibCheck -p tests/types/tsconfig.json",
|
|
64
66
|
"test:cleanup": "find tests -type f \\( -name '*.txt' -o -name '*.bin' \\) -not -name '.gitkeep' -delete && echo 'Test artifacts cleaned up'",
|
|
@@ -72,7 +74,7 @@
|
|
|
72
74
|
"url": "https://github.com/lenneTech/nest-server/issues"
|
|
73
75
|
},
|
|
74
76
|
"engines": {
|
|
75
|
-
"node": ">=
|
|
77
|
+
"node": ">= 22"
|
|
76
78
|
},
|
|
77
79
|
"dependencies": {
|
|
78
80
|
"@apollo/server": "5.5.1",
|
|
@@ -211,13 +213,15 @@
|
|
|
211
213
|
"follow-redirects@<=1.15.11": "Security: Custom Authentication Headers leak on cross-domain redirect (GHSA-r4q5-vmmm-2653) - transitive via axios>@getbrevo/brevo and axios>node-mailjet",
|
|
212
214
|
"uuid@<14.0.0": "Security: Missing buffer bounds check in v3/v5/v6 (GHSA-w5hq-g745-h8pq) - transitive via @compodoc/compodoc and @compodoc/compodoc>@compodoc/live-server>http-auth",
|
|
213
215
|
"postcss@<8.5.10": "Security: XSS via Unescaped </style> in CSS Stringify Output (GHSA-qx2v-qp2m-jg93) - transitive via vite. Remove when vite ships with postcss>=8.5.10",
|
|
216
|
+
"esbuild@>=0.17.0 <0.28.1": "Security: esbuild dev-server responds to any request origin, letting any website the developer visits read files it serves (GHSA-67mh-4wv8-2f99, moderate); pinned to the latest patched release to keep esbuild aligned across the toolchain - dev-only, transitive via vite>vitest and @nestjs build tooling. Remove when all consumers resolve esbuild>=0.28.1",
|
|
214
217
|
"form-data@<4.0.6": "Security: CRLF injection via unescaped multipart field names/filenames (GHSA-hmw2-7cc7-3qxx) - transitive via @getbrevo/brevo>axios and node-mailjet>axios",
|
|
215
218
|
"vite@>=8.0.0 <8.0.16": "Security: fs.deny bypass on Windows alternate paths + file read CVEs - transitive via better-auth>vitest",
|
|
216
219
|
"hono@<4.12.25": "Security: multiple CVEs <4.12.25 (prototype pollution, bodyLimit/Vary bypass, JWT NumericDate) - transitive via @nestjs/terminus>prisma>@prisma/dev",
|
|
217
220
|
"nodemailer@<9.0.1": "Security: email/header injection CVEs <9.0.1 - direct dependency",
|
|
218
221
|
"multer@<2.2.0": "Security: unhandled multipart errors / DoS <2.2.0 - transitive via @nestjs/platform-express",
|
|
219
222
|
"js-yaml@<4.2.0": "Security: special-character handling / prototype pollution (patched in 4.2.0; 4.1.2 was never published) - transitive via @nestjs/swagger",
|
|
220
|
-
"@xhmikosr/decompress@<11.1.3": "Security: archive extraction can create files/links outside the target directory (GHSA-mp2f-45pm-3cg9, critical) - transitive via @swc/cli>@xhmikosr/bin-wrapper>@xhmikosr/downloader; @swc/cli 0.8.1 is already the latest release and still resolves the vulnerable range, so an override is the only fix"
|
|
223
|
+
"@xhmikosr/decompress@<11.1.3": "Security: archive extraction can create files/links outside the target directory (GHSA-mp2f-45pm-3cg9, critical) - transitive via @swc/cli>@xhmikosr/bin-wrapper>@xhmikosr/downloader; @swc/cli 0.8.1 is already the latest release and still resolves the vulnerable range, so an override is the only fix",
|
|
224
|
+
"morgan@<1.11.0": "Security: Log Forging via unneutralized control characters in :remote-user (GHSA-4vj7-5mj6-jm8m, moderate, patched in 1.11.0) - dev-only, transitive via @compodoc/compodoc>@compodoc/live-server>morgan"
|
|
221
225
|
},
|
|
222
226
|
"overrides": {
|
|
223
227
|
"axios@<1.16.0": "1.16.0",
|
|
@@ -254,7 +258,8 @@
|
|
|
254
258
|
"nodemailer@<9.0.1": "9.0.1",
|
|
255
259
|
"multer@<2.2.0": "2.2.0",
|
|
256
260
|
"js-yaml@<4.2.0": "4.2.0",
|
|
257
|
-
"@xhmikosr/decompress@<11.1.3": "11.1.3"
|
|
261
|
+
"@xhmikosr/decompress@<11.1.3": "11.1.3",
|
|
262
|
+
"morgan@<1.11.0": "1.11.0"
|
|
258
263
|
},
|
|
259
264
|
"//peerDependencyRules": "allowedVersions: deps lag behind our newer majors (graphql-upload wants @types/express@^4, the deprecated apollo playground plugin wants @apollo/server@^4) — both work with our v5. ignoreMissing: browser-only vis-network peers pulled in transitively via yuml-diagram (server-side UML generation never renders, so these are not needed).",
|
|
260
265
|
"peerDependencyRules": {
|