@lenne.tech/nest-server 11.27.4 → 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 +2 -1
- package/bin/migrate.js +84 -25
- package/dist/config.env.js +1 -0
- package/dist/config.env.js.map +1 -1
- package/dist/core/common/helpers/cookies.helper.d.ts +19 -0
- package/dist/core/common/helpers/cookies.helper.js +98 -4
- package/dist/core/common/helpers/cookies.helper.js.map +1 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +1 -0
- package/dist/core/modules/better-auth/better-auth.config.js +20 -41
- 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 +22 -2
- package/migration-guides/11.27.4-to-11.27.5.md +241 -0
- package/migration-guides/11.27.5-to-11.27.6.md +359 -0
- package/package.json +15 -8
- package/src/config.env.ts +4 -0
- package/src/core/common/helpers/cookies.helper.ts +293 -12
- package/src/core/common/interfaces/server-options.interface.ts +57 -6
- package/src/core/modules/better-auth/README.md +10 -4
- package/src/core/modules/better-auth/better-auth.config.ts +93 -84
- package/src/core/modules/migrate/migration-runner.ts +16 -2
|
@@ -457,11 +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
|
+
| `cors: { deriveAppUrl: false }` | `appUrl` not derived from `baseUrl` | same | same |
|
|
463
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
|
+
|
|
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:
|
|
468
|
+
|
|
469
|
+
1. `appUrl` set explicitly → used as-is
|
|
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` |
|
|
483
|
+
|
|
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.
|
|
465
485
|
|
|
466
486
|
### NestJS Middleware Chain (CoreModule)
|
|
467
487
|
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# Migration Guide: 11.27.4 → 11.27.5
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
| Category | Details |
|
|
6
|
+
|----------|---------|
|
|
7
|
+
| **Breaking Changes** | None (API-compatible). One **behavior change**: a config that sets only `baseUrl` now also trusts the derived app origin in the REST/GraphQL CORS allowlist — see [Behavior Change](#behavior-change-cors-allowlist-derives-appurl). |
|
|
8
|
+
| **New Features** | `cors.deriveAppUrl` opt-out; exported `resolveServerUrls()` and `deriveAppUrlFromBaseUrl()` helpers. |
|
|
9
|
+
| **Bugfixes** | REST/GraphQL CORS blocked the frontend origin on `api.<host>` deployments and in `local`/`ci`/`e2e`; the `api.`-strip could produce a bare TLD or the opaque `null` origin; trailing-slash `baseUrl` produced a never-matching origin entry. |
|
|
10
|
+
| **Security Updates** | `better-auth` + `@better-auth/passkey` `1.6.11` → `1.6.23` (GHSA-86j7-9j95-vpqj, High); `@xhmikosr/decompress` pinned to `11.1.3` via override (GHSA-mp2f-45pm-3cg9, Critical). |
|
|
11
|
+
| **Migration Effort** | 0 minutes for most projects (`pnpm update`). ~2 minutes if your apex domain is untrusted — see below. |
|
|
12
|
+
|
|
13
|
+
The three CORS layers (GraphQL/Apollo, REST/Express, BetterAuth `trustedOrigins`)
|
|
14
|
+
previously each resolved `appUrl`/`baseUrl` on their own and had drifted. They now
|
|
15
|
+
share a single `resolveServerUrls()` helper, so they cannot disagree.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Quick Migration
|
|
20
|
+
|
|
21
|
+
No code changes required for the common case.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# Update package
|
|
25
|
+
pnpm add @lenne.tech/nest-server@11.27.5
|
|
26
|
+
|
|
27
|
+
# Verify build
|
|
28
|
+
pnpm run build
|
|
29
|
+
|
|
30
|
+
# Run tests
|
|
31
|
+
pnpm test
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## What's Fixed in 11.27.5
|
|
37
|
+
|
|
38
|
+
### 1. `api.<host>` deployments: the frontend origin was blocked
|
|
39
|
+
|
|
40
|
+
A deployment that sets only `baseUrl` (the typical `NSC__BASE_URL` /
|
|
41
|
+
`BASE_URL` setup) got a CORS allowlist containing just the API's own origin.
|
|
42
|
+
BetterAuth already derived `https://example.com` from
|
|
43
|
+
`https://api.example.com` for its `trustedOrigins`, but the REST and GraphQL
|
|
44
|
+
layers did not — so IAM endpoints worked cross-origin while every other
|
|
45
|
+
endpoint failed preflight.
|
|
46
|
+
|
|
47
|
+
Both layers now derive `appUrl` identically:
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
// config.env.ts — no appUrl needed
|
|
51
|
+
baseUrl: 'https://api.example.com',
|
|
52
|
+
// → CORS allowlist: ['https://example.com', 'https://api.example.com']
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 2. `local` / `ci` / `e2e`: localhost defaults were not applied to CORS
|
|
56
|
+
|
|
57
|
+
`resolveUrls()` in BetterAuth mapped these environments to
|
|
58
|
+
`appUrl = http://localhost:3001` (API on `:3000`, app on `:3001`).
|
|
59
|
+
`buildCorsConfig()` had no such branch, so the frontend was blocked. The
|
|
60
|
+
shared resolver now applies the same defaults on every layer.
|
|
61
|
+
|
|
62
|
+
> The framework's own `development` environment is **not** one of the
|
|
63
|
+
> localhost-default environments. `src/config.env.ts` now sets
|
|
64
|
+
> `appUrl: process.env.APP_URL || 'http://localhost:3001'` there explicitly.
|
|
65
|
+
> Projects that copied the `development` block from the starter and run their
|
|
66
|
+
> frontend on `:3001` should do the same.
|
|
67
|
+
|
|
68
|
+
### 3. Hardened `api.`-label stripping
|
|
69
|
+
|
|
70
|
+
| `baseUrl` | Before | After |
|
|
71
|
+
|-----------|--------|-------|
|
|
72
|
+
| `https://api.example.com` | `https://example.com` | unchanged |
|
|
73
|
+
| `https://api.dev.example.com` | `https://dev.example.com` | unchanged |
|
|
74
|
+
| `https://api.dev` | `https://dev` (bare TLD!) | `https://api.dev` (no strip) |
|
|
75
|
+
| `https://api.` | `https://api.` (silent no-op) | `https://api.` (explicit no-op) |
|
|
76
|
+
| `custom://api.example.com` | `"null"` (opaque origin!) | `custom://api.example.com` |
|
|
77
|
+
| `https://api.example.com/` | `https://example.com` + dead `…/` entry | both entries origin-normalized |
|
|
78
|
+
|
|
79
|
+
The `"null"` case was the most serious: `URL.origin` serializes opaque origins
|
|
80
|
+
to the literal string `"null"`, which is exactly the `Origin` header a
|
|
81
|
+
sandboxed iframe sends. In a `credentials: true` allowlist that would have
|
|
82
|
+
granted credentialed access to any site able to frame a sandboxed document.
|
|
83
|
+
Only `http:`/`https:` URLs are normalized to origins now.
|
|
84
|
+
|
|
85
|
+
### 4. Security updates in dependencies
|
|
86
|
+
|
|
87
|
+
| Package | Change | Advisory |
|
|
88
|
+
|---------|--------|----------|
|
|
89
|
+
| `better-auth` | `1.6.11` → `1.6.23` (direct) | [GHSA-86j7-9j95-vpqj](https://github.com/advisories/GHSA-86j7-9j95-vpqj) — High: stored XSS in the auth-server origin via a `javascript:` `redirect_uri` in `oidc-provider` and `mcp` |
|
|
90
|
+
| `@better-auth/passkey` | `1.6.11` → `1.6.23` (direct) | Kept in lockstep — peers on `better-auth@^1.6.23` |
|
|
91
|
+
| `@xhmikosr/decompress` | pinned to `11.1.3` via `pnpm.overrides` | [GHSA-mp2f-45pm-3cg9](https://github.com/advisories/GHSA-mp2f-45pm-3cg9) — Critical: archive extraction can create files/links outside the target directory |
|
|
92
|
+
|
|
93
|
+
`@xhmikosr/decompress` is transitive via `@swc/cli > @xhmikosr/bin-wrapper > @xhmikosr/downloader`.
|
|
94
|
+
`@swc/cli@0.8.1` is the latest release and still resolves the vulnerable range, so an override
|
|
95
|
+
with a fixed target is the only available fix.
|
|
96
|
+
|
|
97
|
+
Both `better-auth` bumps stay inside `1.6.x` — no BetterAuth API change. The full suite
|
|
98
|
+
(2019 tests) passes unchanged.
|
|
99
|
+
|
|
100
|
+
npm-mode consumers get all three fixes with a plain `pnpm update`. Vendor-mode consumers must
|
|
101
|
+
bump `better-auth`/`@better-auth/passkey` in their own `package.json` and copy the
|
|
102
|
+
`@xhmikosr/decompress` override — the vendored file set does not carry `package.json`.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Behavior Change: CORS allowlist derives `appUrl`
|
|
107
|
+
|
|
108
|
+
**Who is affected:** projects that configure `baseUrl` but neither `appUrl`
|
|
109
|
+
nor `cors.allowedOrigins`, and whose apex domain is not under their control.
|
|
110
|
+
|
|
111
|
+
Deriving `appUrl` means the apex origin (`https://example.com`) receives
|
|
112
|
+
**credentialed** cross-origin access (`Access-Control-Allow-Credentials: true`).
|
|
113
|
+
That is the intended, documented `appUrl` auto-detection and matches what
|
|
114
|
+
BetterAuth's `trustedOrigins` has always done — but it now also applies to the
|
|
115
|
+
REST and GraphQL layers.
|
|
116
|
+
|
|
117
|
+
If your apex is a third-party-hosted marketing site (WordPress, Webflow,
|
|
118
|
+
HubSpot) whose XSS surface you do not control, opt out:
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
// config.env.ts
|
|
122
|
+
{
|
|
123
|
+
baseUrl: 'https://api.example.com',
|
|
124
|
+
cors: {
|
|
125
|
+
// Do not trust https://example.com implicitly
|
|
126
|
+
deriveAppUrl: false,
|
|
127
|
+
// List the real frontend origin instead
|
|
128
|
+
allowedOrigins: ['https://app.example.com'],
|
|
129
|
+
},
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`deriveAppUrl: false` suppresses the derivation on **all three** layers
|
|
134
|
+
(GraphQL, REST, BetterAuth `trustedOrigins`). It has no effect when `appUrl`
|
|
135
|
+
is set explicitly, and it does not disable the `local`/`ci`/`e2e` localhost
|
|
136
|
+
defaults.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## What's New in 11.27.5
|
|
141
|
+
|
|
142
|
+
### `cors.deriveAppUrl`
|
|
143
|
+
|
|
144
|
+
| Value | Effect |
|
|
145
|
+
|-------|--------|
|
|
146
|
+
| `true` (default) | `appUrl` is derived from `baseUrl` by stripping a leading `api.` label |
|
|
147
|
+
| `false` | No derivation; configure `appUrl` / `cors.allowedOrigins` explicitly |
|
|
148
|
+
|
|
149
|
+
### Exported URL helpers
|
|
150
|
+
|
|
151
|
+
Both are exported from the package root (`cookies.helper.ts`):
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
import { deriveAppUrlFromBaseUrl, resolveServerUrls } from '@lenne.tech/nest-server';
|
|
155
|
+
|
|
156
|
+
deriveAppUrlFromBaseUrl('https://api.example.com'); // 'https://example.com'
|
|
157
|
+
|
|
158
|
+
resolveServerUrls({ baseUrl: 'https://api.example.com' });
|
|
159
|
+
// { appUrl: 'https://example.com', appUrlSource: 'derived',
|
|
160
|
+
// baseUrl: 'https://api.example.com', baseUrlSource: 'explicit' }
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
`appUrlSource` / `baseUrlSource` (`'explicit' | 'localhost-default' | 'derived' | 'none'`)
|
|
164
|
+
let callers emit accurate startup diagnostics without re-deriving the resolution logic.
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Breaking Changes
|
|
169
|
+
|
|
170
|
+
None. `ICorsConfig` gained an optional field; no signature, export, or Core
|
|
171
|
+
class changed.
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Compatibility Notes
|
|
176
|
+
|
|
177
|
+
- **npm-mode consumers:** `pnpm update` is sufficient. Review the
|
|
178
|
+
[behavior change](#behavior-change-cors-allowlist-derives-appurl) if your
|
|
179
|
+
apex domain is untrusted.
|
|
180
|
+
- **Vendor-mode consumers:** the change touches `src/core/common/helpers/cookies.helper.ts`,
|
|
181
|
+
`src/core/common/interfaces/server-options.interface.ts` and
|
|
182
|
+
`src/core/modules/better-auth/better-auth.config.ts` — all inside the vendored
|
|
183
|
+
`src/core/` file set. Sync via `/lt-dev:backend:update-nest-server-core`.
|
|
184
|
+
`better-auth.config.ts` now imports `resolveServerUrls` from
|
|
185
|
+
`../../common/helpers/cookies.helper` (still inside `src/core/`, so the
|
|
186
|
+
self-containment rule holds).
|
|
187
|
+
- **Projects that set `appUrl` explicitly:** unaffected. Explicit values are
|
|
188
|
+
never overridden by the derivation.
|
|
189
|
+
- **Projects that set `cors.allowAll: true`:** unaffected. `allowAll` short-circuits
|
|
190
|
+
before URL resolution.
|
|
191
|
+
- **Passkey / WebAuthn in `development`:** if you adopt the new
|
|
192
|
+
`appUrl: 'http://localhost:3001'` in the `development` block, the Passkey
|
|
193
|
+
`origin` becomes `http://localhost:3001` (where the browser actually runs)
|
|
194
|
+
instead of the API's `:3000`. This is a fix — WebAuthn ceremonies from the
|
|
195
|
+
frontend previously used the wrong origin.
|
|
196
|
+
- **A duplicate private `deriveAppUrlFromBaseUrl()` was removed** from
|
|
197
|
+
`better-auth.config.ts`. It was never exported; nothing to migrate.
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Troubleshooting
|
|
202
|
+
|
|
203
|
+
### My frontend is still blocked by CORS
|
|
204
|
+
|
|
205
|
+
Check, in order:
|
|
206
|
+
|
|
207
|
+
1. `cors.enabled` is not `false`.
|
|
208
|
+
2. `cookies` is not disabled — `buildCorsConfig()` returns `{}` when cookies
|
|
209
|
+
are off (REST then falls back to permissive `enableCors()` without credentials).
|
|
210
|
+
3. Your frontend origin is actually derivable. `baseUrl: 'https://my-api.example.com'`
|
|
211
|
+
has no `api.` label to strip, so `appUrl` resolves to the API's own origin.
|
|
212
|
+
Set `appUrl` explicitly.
|
|
213
|
+
4. `env` is one of `local`/`ci`/`e2e` if you rely on the `localhost:3001` default.
|
|
214
|
+
`development` is not — set `appUrl` there.
|
|
215
|
+
|
|
216
|
+
### My apex domain unexpectedly appears in the allowlist
|
|
217
|
+
|
|
218
|
+
That is the `appUrl` derivation. Set `cors.deriveAppUrl: false` and list the
|
|
219
|
+
frontend origin explicitly. See
|
|
220
|
+
[Behavior Change](#behavior-change-cors-allowlist-derives-appurl).
|
|
221
|
+
|
|
222
|
+
### `https://api.dev` no longer derives an app origin
|
|
223
|
+
|
|
224
|
+
Intended. Stripping would leave the bare TLD `dev`, which is not a deployable
|
|
225
|
+
host. Set `appUrl` explicitly for such domains.
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Module Documentation
|
|
230
|
+
|
|
231
|
+
- **Request lifecycle & CORS:** [docs/REQUEST-LIFECYCLE.md](../docs/REQUEST-LIFECYCLE.md) — section `0b. CORS`
|
|
232
|
+
- **Configurable features:** [.claude/rules/configurable-features.md](../.claude/rules/configurable-features.md) — `CORS` row
|
|
233
|
+
- **BetterAuth:** [src/core/modules/better-auth/README.md](../src/core/modules/better-auth/README.md)
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## References
|
|
238
|
+
|
|
239
|
+
- [Migration Guide 11.27.3 → 11.27.4](./11.27.3-to-11.27.4.md) — Previous release (test tooling)
|
|
240
|
+
- [Migration Guide 11.24.x → 11.25.0](./11.24.x-to-11.25.0.md) — Introduced the unified `cors` config
|
|
241
|
+
- [nest-server-starter](https://github.com/lenneTech/nest-server-starter) — reference implementation
|
|
@@ -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
|