@lenne.tech/nest-server 11.24.4 → 11.25.1

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 (64) hide show
  1. package/.claude/rules/configurable-features.md +2 -0
  2. package/CLAUDE.md +13 -0
  3. package/FRAMEWORK-API.md +14 -2
  4. package/README.md +15 -0
  5. package/dist/config.env.js +100 -81
  6. package/dist/config.env.js.map +1 -1
  7. package/dist/core/common/helpers/cookies.helper.d.ts +19 -0
  8. package/dist/core/common/helpers/cookies.helper.js +109 -0
  9. package/dist/core/common/helpers/cookies.helper.js.map +1 -0
  10. package/dist/core/common/interfaces/server-options.interface.d.ts +11 -1
  11. package/dist/core/modules/auth/core-auth.controller.js +4 -16
  12. package/dist/core/modules/auth/core-auth.controller.js.map +1 -1
  13. package/dist/core/modules/auth/core-auth.resolver.js +4 -16
  14. package/dist/core/modules/auth/core-auth.resolver.js.map +1 -1
  15. package/dist/core/modules/auth/tokens.decorator.d.ts +1 -1
  16. package/dist/core/modules/better-auth/better-auth.config.d.ts +24 -1
  17. package/dist/core/modules/better-auth/better-auth.config.js +22 -2
  18. package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
  19. package/dist/core/modules/better-auth/core-better-auth-api.middleware.js +3 -0
  20. package/dist/core/modules/better-auth/core-better-auth-api.middleware.js.map +1 -1
  21. package/dist/core/modules/better-auth/core-better-auth-cookie.helper.d.ts +3 -1
  22. package/dist/core/modules/better-auth/core-better-auth-cookie.helper.js +7 -3
  23. package/dist/core/modules/better-auth/core-better-auth-cookie.helper.js.map +1 -1
  24. package/dist/core/modules/better-auth/core-better-auth.controller.js +7 -3
  25. package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -1
  26. package/dist/core/modules/better-auth/core-better-auth.module.d.ts +2 -1
  27. package/dist/core/modules/better-auth/core-better-auth.module.js +4 -1
  28. package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
  29. package/dist/core/modules/better-auth/core-better-auth.service.js +5 -4
  30. package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
  31. package/dist/core/modules/migrate/templates/migration-project.template.ts +16 -2
  32. package/dist/core.module.d.ts +3 -1
  33. package/dist/core.module.js +10 -7
  34. package/dist/core.module.js.map +1 -1
  35. package/dist/index.d.ts +1 -0
  36. package/dist/index.js +1 -0
  37. package/dist/index.js.map +1 -1
  38. package/dist/main.js +17 -3
  39. package/dist/main.js.map +1 -1
  40. package/dist/test/test.helper.d.ts +1 -0
  41. package/dist/test/test.helper.js +2 -1
  42. package/dist/test/test.helper.js.map +1 -1
  43. package/dist/tsconfig.build.tsbuildinfo +1 -1
  44. package/docs/REQUEST-LIFECYCLE.md +78 -3
  45. package/migration-guides/11.24.x-to-11.25.0.md +438 -0
  46. package/package.json +23 -21
  47. package/src/config.env.ts +116 -111
  48. package/src/core/common/helpers/cookies.helper.ts +298 -0
  49. package/src/core/common/interfaces/server-options.interface.ts +141 -2
  50. package/src/core/modules/auth/core-auth.controller.ts +11 -23
  51. package/src/core/modules/auth/core-auth.resolver.ts +11 -23
  52. package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +18 -0
  53. package/src/core/modules/better-auth/README.md +7 -0
  54. package/src/core/modules/better-auth/better-auth.config.ts +53 -15
  55. package/src/core/modules/better-auth/core-better-auth-api.middleware.ts +6 -3
  56. package/src/core/modules/better-auth/core-better-auth-cookie.helper.ts +33 -7
  57. package/src/core/modules/better-auth/core-better-auth.controller.ts +12 -3
  58. package/src/core/modules/better-auth/core-better-auth.module.ts +16 -1
  59. package/src/core/modules/better-auth/core-better-auth.service.ts +26 -10
  60. package/src/core/modules/migrate/templates/migration-project.template.ts +16 -2
  61. package/src/core.module.ts +40 -12
  62. package/src/index.ts +1 -0
  63. package/src/main.ts +32 -5
  64. package/src/test/test.helper.ts +15 -1
@@ -0,0 +1,298 @@
1
+ import type { Response } from 'express';
2
+
3
+ import type { ICookiesConfig, ICorsConfig, IServerOptions } from '../interfaces/server-options.interface';
4
+
5
+ /**
6
+ * Module-scoped cache of `process.env.NODE_ENV === 'production'`.
7
+ *
8
+ * `process.env` reads are cheap but not free; the auth hot path calls cookie helpers
9
+ * on every sign-in/sign-up/refresh. Since `NODE_ENV` is established at process start
10
+ * and immutable for the server lifetime, we read it once at import time.
11
+ *
12
+ * @since 11.25.0
13
+ */
14
+ const IS_PRODUCTION_NODE_ENV = process.env.NODE_ENV === 'production';
15
+
16
+ /**
17
+ * Checks whether the given environment should be treated as production-like.
18
+ *
19
+ * Production-like means auth cookies must be set with `secure: true`
20
+ * (HTTPS-only). Triggers when EITHER:
21
+ * - `env === 'production'` or `env === 'staging'` (app-level `config.env`), OR
22
+ * - `process.env.NODE_ENV === 'production'` (runtime Node environment)
23
+ *
24
+ * Checking both layers protects staging deployments that set `config.env = 'staging'`
25
+ * but do not set `NODE_ENV=production`, and vice versa.
26
+ *
27
+ * @since 11.25.0
28
+ */
29
+ export function isProductionLikeEnv(env?: string): boolean {
30
+ if (env === 'production' || env === 'staging') return true;
31
+ return IS_PRODUCTION_NODE_ENV;
32
+ }
33
+
34
+ /**
35
+ * Standard cookie options for authentication cookies.
36
+ *
37
+ * Applied to both Legacy Auth (`token`, `refreshToken`) and BetterAuth cookies.
38
+ * Enforces baseline security (httpOnly, sameSite=lax) and environment-aware
39
+ * secure flag (HTTPS-only in production).
40
+ *
41
+ * @since 11.25.0
42
+ */
43
+ export interface AuthCookieDefaultOptions {
44
+ httpOnly: true;
45
+ sameSite: 'lax';
46
+ secure: boolean;
47
+ }
48
+
49
+ /**
50
+ * Returns the default secure cookie options for authentication cookies.
51
+ *
52
+ * - `httpOnly: true` — prevents JavaScript access (XSS mitigation)
53
+ * - `sameSite: 'lax'` — CSRF mitigation
54
+ * - `secure: isProductionLikeEnv(env)` — HTTPS-only in production/staging
55
+ *
56
+ * Used by both Legacy Auth and BetterAuth cookie helpers for consistent security.
57
+ *
58
+ * @param env - App-level environment from `IServerOptions.env` (e.g. `'production'`, `'staging'`, `'ci'`).
59
+ * Falls back to `process.env.NODE_ENV === 'production'` if absent.
60
+ *
61
+ * @since 11.25.0
62
+ */
63
+ export function getDefaultAuthCookieOptions(env?: string): AuthCookieDefaultOptions {
64
+ return {
65
+ httpOnly: true,
66
+ sameSite: 'lax',
67
+ secure: isProductionLikeEnv(env),
68
+ };
69
+ }
70
+
71
+ /**
72
+ * Asserts that the cookies configuration is safe for production/staging environments.
73
+ *
74
+ * Throws if `exposeTokenInBody: true` is combined with a production-like environment.
75
+ * Rationale: Exposing the session token in the response body negates the XSS
76
+ * protection of the httpOnly session cookie — it enables XSS attacks to read
77
+ * the token from the response body.
78
+ *
79
+ * Test environments (ci, e2e, development, local) may use `exposeTokenInBody: true`
80
+ * for TestHelper to read tokens — these are guarded against by the env check.
81
+ *
82
+ * @throws Error if configuration is unsafe for production/staging
83
+ *
84
+ * @since 11.25.0
85
+ */
86
+ export function assertCookiesProductionSafe(
87
+ cookies: boolean | ICookiesConfig | undefined,
88
+ env: string | undefined,
89
+ ): void {
90
+ // Use the shared production-like detection so the guard triggers symmetrically with the
91
+ // `secure` cookie flag: both app-level `env` ('production'/'staging') AND runtime
92
+ // `NODE_ENV=production` are covered. A deployment that sets only `NODE_ENV=production`
93
+ // but forgets the app `env` field must still be blocked from exposeTokenInBody.
94
+ if (isProductionLikeEnv(env) && isExposeTokenInBodyEnabled(cookies)) {
95
+ throw new Error(
96
+ 'SECURITY: cookies.exposeTokenInBody must not be true in production or staging. ' +
97
+ 'Exposing the session token in the response body negates the XSS protection of ' +
98
+ 'the httpOnly session cookie. If hybrid JWT+Cookie auth is required, handle the ' +
99
+ 'token at the client layer (e.g., via getToken endpoint) instead of exposing it in ' +
100
+ 'the login response body.',
101
+ );
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Sets legacy auth cookies (`token`, `refreshToken`) on the response with secure defaults.
107
+ *
108
+ * Consolidates cookie-setting logic shared between `CoreAuthController` (REST) and
109
+ * `CoreAuthResolver` (GraphQL). Applies standard security options (httpOnly, sameSite,
110
+ * secure-in-production) and honors `exposeTokenInBody` by optionally stripping tokens
111
+ * from the response body after setting cookies.
112
+ *
113
+ * @param res - Express Response object
114
+ * @param result - Auth result containing `token` and `refreshToken` (modified in place)
115
+ * @param cookies - The `cookies` config value from IServerOptions
116
+ * @param env - App-level env from `IServerOptions.env` (used for `secure` flag derivation)
117
+ * @returns The (possibly modified) result object
118
+ *
119
+ * @since 11.25.0
120
+ */
121
+ export function setLegacyAuthCookies<T extends { refreshToken?: string; token?: string } | null | undefined>(
122
+ res: Response,
123
+ result: T,
124
+ cookies: boolean | ICookiesConfig | undefined,
125
+ env?: string,
126
+ ): T {
127
+ if (!isCookiesEnabled(cookies)) {
128
+ return result;
129
+ }
130
+
131
+ const cookieOptions = getDefaultAuthCookieOptions(env);
132
+
133
+ // If result is absent or not an object, clear any existing cookies (logout path)
134
+ if (!result || typeof result !== 'object') {
135
+ res.cookie('token', '', cookieOptions);
136
+ res.cookie('refreshToken', '', cookieOptions);
137
+ return result;
138
+ }
139
+
140
+ res.cookie('token', result.token || '', cookieOptions);
141
+ res.cookie('refreshToken', result.refreshToken || '', cookieOptions);
142
+
143
+ // Remove tokens from response body unless exposeTokenInBody is enabled
144
+ if (!isExposeTokenInBodyEnabled(cookies)) {
145
+ if (result.token) {
146
+ delete result.token;
147
+ }
148
+ if (result.refreshToken) {
149
+ delete result.refreshToken;
150
+ }
151
+ }
152
+
153
+ return result;
154
+ }
155
+
156
+ /**
157
+ * Checks if cookies are enabled based on the cookies config value.
158
+ *
159
+ * Follows the Boolean Shorthand Pattern:
160
+ * - `undefined` → true (enabled by default)
161
+ * - `true` → true
162
+ * - `false` → false
163
+ * - `{}` → true (presence implies enabled)
164
+ * - `{ enabled: true }` → true
165
+ * - `{ enabled: false }` → false
166
+ * - `{ exposeTokenInBody: true }` → true (enabled, token exposed)
167
+ *
168
+ * @since 11.25.0
169
+ */
170
+ export function isCookiesEnabled(cookies: boolean | ICookiesConfig | undefined): boolean {
171
+ if (cookies === false) return false;
172
+ if (typeof cookies === 'object' && cookies !== null) return cookies.enabled !== false;
173
+ return true; // undefined or true → enabled (default)
174
+ }
175
+
176
+ /**
177
+ * Checks if exposeTokenInBody is enabled based on the cookies config value.
178
+ *
179
+ * When true, authentication endpoints keep the token in the response body
180
+ * even when cookies are active. By default false — the httpOnly cookie
181
+ * provides XSS protection, exposing the token in the body would negate that.
182
+ *
183
+ * @since 11.25.0
184
+ */
185
+ export function isExposeTokenInBodyEnabled(cookies: boolean | ICookiesConfig | undefined): boolean {
186
+ if (typeof cookies === 'object' && cookies !== null) return cookies.exposeTokenInBody === true;
187
+ return false; // Default: false
188
+ }
189
+
190
+ /**
191
+ * Determines whether a BetterAuth session token must be converted to a JWT
192
+ * before being returned to the client.
193
+ *
194
+ * Conversion is required when the client actually reads the token from the
195
+ * response body — otherwise the opaque session token travels via cookie only
196
+ * and never needs to be a JWT.
197
+ *
198
+ * Truth table:
199
+ *
200
+ * | cookies | jwtEnabled | convert? |
201
+ * |---------------------------------|------------|----------|
202
+ * | `false` (JWT-only mode) | true | yes |
203
+ * | `true` / `{}` (cookie-only) | true | no |
204
+ * | `{ exposeTokenInBody: true }` | true | yes |
205
+ * | any | false | no |
206
+ *
207
+ * @param cookies - The `cookies` config value from IServerOptions
208
+ * @param jwtEnabled - Whether the BetterAuth JWT plugin is enabled
209
+ * @returns true if the caller should convert the session token to a JWT
210
+ *
211
+ * @since 11.25.0
212
+ */
213
+ export function shouldConvertSessionTokenToJwt(
214
+ cookies: boolean | ICookiesConfig | undefined,
215
+ jwtEnabled: boolean,
216
+ ): boolean {
217
+ if (!jwtEnabled) return false;
218
+ const cookiesEnabled = isCookiesEnabled(cookies);
219
+ const exposeTokenInBody = isExposeTokenInBodyEnabled(cookies);
220
+ // Cookies-only mode: token delivered via cookie, no body JWT needed
221
+ if (cookiesEnabled && !exposeTokenInBody) return false;
222
+ return true;
223
+ }
224
+
225
+ /**
226
+ * Checks if CORS is disabled based on the cors config value.
227
+ *
228
+ * CORS is disabled when:
229
+ * - `cors === false`
230
+ * - `cors.enabled === false`
231
+ *
232
+ * @since 11.25.0
233
+ */
234
+ export function isCorsDisabled(cors: boolean | ICorsConfig | undefined): boolean {
235
+ if (cors === false) return true;
236
+ if (typeof cors === 'object' && cors !== null) return cors.enabled === false;
237
+ return false;
238
+ }
239
+
240
+ /**
241
+ * Builds a CORS configuration object from server options.
242
+ *
243
+ * Resolution priority:
244
+ * 1. CORS disabled → empty object (no CORS)
245
+ * 2. Cookies disabled → empty object (no credentials needed, handled by simple enableCors())
246
+ * 3. `cors.allowAll` → `{ credentials: true, origin: true }` (mirror request origin)
247
+ * 4. `cors.allowedOrigins` + `appUrl`/`baseUrl` → deduplicated origin list
248
+ * 5. Only `appUrl`/`baseUrl` → those origins
249
+ * 6. Nothing configured → `{}` (no credentialed CORS — caller decides fallback)
250
+ *
251
+ * Used by both:
252
+ * - `CoreModule.buildCorsConfig()` for GraphQL (Apollo) CORS
253
+ * - `main.ts` reference implementation for REST (Express) CORS
254
+ *
255
+ * Security note: when no origins are resolvable AND cookies are enabled, the function
256
+ * returns `{}` rather than `{ credentials: true, origin: true }`. Returning open CORS
257
+ * with credentials would allow any website to make credentialed requests. Callers
258
+ * should either configure `appUrl`/`baseUrl`/`allowedOrigins`, enable `cors.allowAll`
259
+ * explicitly (for development), or accept no credentialed CORS.
260
+ *
261
+ * @param options - Server options containing `cors`, `cookies`, `appUrl`, `baseUrl`
262
+ * @returns CORS config object for Apollo/Express, or empty object if disabled/unconfigured
263
+ *
264
+ * @since 11.25.0
265
+ */
266
+ export function buildCorsConfig(options: Partial<IServerOptions>): Record<string, unknown> {
267
+ if (isCorsDisabled(options?.cors)) {
268
+ return {};
269
+ }
270
+
271
+ if (!isCookiesEnabled(options?.cookies)) {
272
+ return {};
273
+ }
274
+
275
+ const corsObj = typeof options?.cors === 'object' ? options.cors : {};
276
+
277
+ // allowAll → mirror request origin (explicit opt-in for dev/test)
278
+ if (corsObj.allowAll) {
279
+ return { credentials: true, origin: true };
280
+ }
281
+
282
+ // Build origin list from appUrl, baseUrl, and allowedOrigins
283
+ const origins: string[] = [];
284
+ if (options?.appUrl) origins.push(options.appUrl);
285
+ if (options?.baseUrl) origins.push(options.baseUrl);
286
+ if (corsObj.allowedOrigins?.length) {
287
+ origins.push(...corsObj.allowedOrigins);
288
+ }
289
+
290
+ const uniqueOrigins = [...new Set(origins)];
291
+
292
+ if (uniqueOrigins.length > 0) {
293
+ return { credentials: true, origin: uniqueOrigins };
294
+ }
295
+
296
+ // No origins resolvable → return empty (secure default — no open CORS with credentials)
297
+ return {};
298
+ }
@@ -944,6 +944,107 @@ export interface IMultiTenancy {
944
944
  cacheTtlMs?: number;
945
945
  }
946
946
 
947
+ /**
948
+ * Cookie configuration for authentication handling.
949
+ *
950
+ * Follows the Boolean Shorthand Pattern:
951
+ * - `undefined` / `true`: Cookies enabled with default settings
952
+ * - `false`: Cookies disabled (JWT-only mode)
953
+ * - `{ exposeTokenInBody: true }`: Cookies enabled AND token returned in response body
954
+ *
955
+ * When cookies are enabled, the server:
956
+ * - Loads `cookie-parser` middleware
957
+ * - Sets `credentials: true` in CORS configuration
958
+ * - Sets signed httpOnly session cookies on authentication responses
959
+ *
960
+ * JWT authentication via `Authorization: Bearer` header works independently
961
+ * of the cookie configuration — it is always available regardless of this setting.
962
+ *
963
+ * @default true (cookies enabled, exposeTokenInBody disabled)
964
+ * @since 11.25.0
965
+ */
966
+ export interface ICookiesConfig {
967
+ /**
968
+ * Whether cookies are enabled.
969
+ * @default true
970
+ */
971
+ enabled?: boolean;
972
+
973
+ /**
974
+ * Whether to include the session token in the response body when cookies are enabled.
975
+ *
976
+ * By default, when cookies are active, the token is removed from the response body
977
+ * because the httpOnly cookie protects against XSS — exposing the token in the body
978
+ * would negate this security benefit.
979
+ *
980
+ * Enable this only when clients need both JWT (via response body) AND cookies
981
+ * in parallel (e.g., hybrid mobile/web apps).
982
+ *
983
+ * @default false
984
+ */
985
+ exposeTokenInBody?: boolean;
986
+ }
987
+
988
+ /**
989
+ * CORS (Cross-Origin Resource Sharing) configuration.
990
+ *
991
+ * Controls which origins can access the API. This configuration propagates to
992
+ * all three CORS layers: GraphQL (Apollo), REST (Express), and BetterAuth (trustedOrigins).
993
+ *
994
+ * Follows the Boolean Shorthand Pattern:
995
+ * - `undefined` / `true` / `{}`: CORS enabled with auto-derived origins from `appUrl`/`baseUrl`
996
+ * - `false`: CORS disabled on all layers (including BetterAuth)
997
+ * - `{ allowedOrigins: [...] }`: Additional origins beyond `appUrl`/`baseUrl`
998
+ * - `{ allowAll: true }`: Allow all origins (mirrors request origin — NOT recommended for production)
999
+ *
1000
+ * When cookies are enabled (default), CORS automatically includes `credentials: true`.
1001
+ * Browsers reject `Access-Control-Allow-Origin: *` with credentials, so origins must
1002
+ * be explicitly listed or `allowAll` must be set (which mirrors the request origin).
1003
+ *
1004
+ * @default undefined (enabled, origins auto-derived from appUrl/baseUrl)
1005
+ * @since 11.25.0
1006
+ */
1007
+ export interface ICorsConfig {
1008
+ /**
1009
+ * Allow all origins by mirroring the request Origin header back.
1010
+ *
1011
+ * This effectively allows any website to make credentialed requests to the API.
1012
+ * Convenient for development but NOT recommended for production as it enables
1013
+ * CSRF-like attacks from any domain.
1014
+ *
1015
+ * When true, overrides `allowedOrigins`. Also propagates to BetterAuth
1016
+ * (trustedOrigins is set to undefined, allowing all origins).
1017
+ *
1018
+ * @default false
1019
+ */
1020
+ allowAll?: boolean;
1021
+
1022
+ /**
1023
+ * Additional allowed origins beyond `appUrl` and `baseUrl`.
1024
+ *
1025
+ * These origins are merged with the auto-derived origins from `appUrl` and `baseUrl`
1026
+ * (duplicates are removed). The combined list is used for both Express/Apollo CORS
1027
+ * and BetterAuth's `trustedOrigins`.
1028
+ *
1029
+ * Only effective when `allowAll` is not set to `true`.
1030
+ *
1031
+ * @example ['https://admin.example.com', 'https://partner.example.com']
1032
+ */
1033
+ allowedOrigins?: string[];
1034
+
1035
+ /**
1036
+ * Whether CORS is enabled.
1037
+ *
1038
+ * When set to `false`, disables CORS on all layers:
1039
+ * - GraphQL: No CORS headers
1040
+ * - REST: `enableCors()` not called
1041
+ * - BetterAuth: `trustedOrigins` set to empty array (no origins allowed)
1042
+ *
1043
+ * @default true
1044
+ */
1045
+ enabled?: boolean;
1046
+ }
1047
+
947
1048
  /**
948
1049
  * Options for the server
949
1050
  */
@@ -1123,10 +1224,48 @@ export interface IServerOptions {
1123
1224
  compression?: boolean | compression.CompressionOptions;
1124
1225
 
1125
1226
  /**
1126
- * Whether to use cookies for authentication handling
1227
+ * Cookie configuration for authentication handling.
1228
+ *
1229
+ * Controls whether session cookies are set on authentication responses and
1230
+ * whether `cookie-parser` middleware is loaded.
1231
+ *
1232
+ * Accepts (Boolean Shorthand Pattern):
1233
+ * - `undefined` / `true`: Cookies enabled (token removed from response body)
1234
+ * - `false`: Cookies disabled (JWT-only mode, token stays in response body)
1235
+ * - `{ exposeTokenInBody: true }`: Cookies enabled AND token in response body
1236
+ *
1237
+ * JWT authentication via `Authorization: Bearer` header works independently
1238
+ * of this setting — always available regardless of cookie configuration.
1239
+ *
1127
1240
  * See: https://docs.nestjs.com/techniques/cookies
1241
+ *
1242
+ * @default true
1243
+ * @since 11.25.0 Changed default from false to true, added ICookiesConfig
1244
+ * @see ICookiesConfig
1245
+ */
1246
+ cookies?: boolean | ICookiesConfig;
1247
+
1248
+ /**
1249
+ * CORS (Cross-Origin Resource Sharing) configuration.
1250
+ *
1251
+ * Controls which origins can access the API. Propagates to all layers:
1252
+ * GraphQL (Apollo), REST (Express), and BetterAuth (trustedOrigins).
1253
+ *
1254
+ * Origins are auto-derived from `appUrl` and `baseUrl` when available.
1255
+ * Use `cors.allowedOrigins` to add additional origins, or `cors.allowAll`
1256
+ * to allow all origins (development only).
1257
+ *
1258
+ * Accepts (Boolean Shorthand Pattern):
1259
+ * - `undefined` / `true` / `{}`: CORS enabled with auto-derived origins
1260
+ * - `false`: CORS disabled on all layers (including BetterAuth)
1261
+ * - `{ allowedOrigins: [...] }`: Additional origins beyond appUrl/baseUrl
1262
+ * - `{ allowAll: true }`: Allow all origins (not recommended for production)
1263
+ *
1264
+ * @default undefined (enabled with auto-derived origins)
1265
+ * @since 11.25.0
1266
+ * @see ICorsConfig
1128
1267
  */
1129
- cookies?: boolean;
1268
+ cors?: boolean | ICorsConfig;
1130
1269
 
1131
1270
  /**
1132
1271
  * Cron jobs configuration object with the name of the cron job function as key
@@ -14,6 +14,8 @@ import { ApiCommonErrorResponses } from '../../common/decorators/common-error.de
14
14
  import { CurrentUser } from '../../common/decorators/current-user.decorator';
15
15
  import { Roles } from '../../common/decorators/roles.decorator';
16
16
  import { RoleEnum } from '../../common/enums/role.enum';
17
+ import { setLegacyAuthCookies } from '../../common/helpers/cookies.helper';
18
+ import type { ICookiesConfig } from '../../common/interfaces/server-options.interface';
17
19
  import { ConfigService } from '../../common/services/config.service';
18
20
  import { AuthGuardStrategy } from './auth-guard-strategy.enum';
19
21
  import { CoreAuthModel } from './core-auth.model';
@@ -187,30 +189,16 @@ export class CoreAuthController {
187
189
  // ===================================================================================================================
188
190
 
189
191
  /**
190
- * Process cookies
192
+ * Process cookies — sets legacy auth cookies (token, refreshToken) with secure defaults
193
+ * and optionally strips tokens from the response body per `cookies.exposeTokenInBody`.
194
+ *
195
+ * Delegates to the shared `setLegacyAuthCookies()` helper to ensure consistent
196
+ * security options (httpOnly, sameSite=lax, secure in production) between the REST
197
+ * controller and GraphQL resolver.
191
198
  */
192
199
  protected processCookies(res: ResponseType, result: any) {
193
- // Check if cookie handling is activated (enabled by default, unless explicitly set to false)
194
- if (this.configService.getFastButReadOnly('cookies') !== false) {
195
- // Set cookies
196
- if (!result || typeof result !== 'object') {
197
- res.cookie('token', '', { httpOnly: true });
198
- res.cookie('refreshToken', '', { httpOnly: true });
199
- return result;
200
- }
201
- res.cookie('token', result?.token || '', { httpOnly: true });
202
- res.cookie('refreshToken', result?.refreshToken || '', { httpOnly: true });
203
-
204
- // Remove tokens from result
205
- if (result.token) {
206
- delete result.token;
207
- }
208
- if (result.refreshToken) {
209
- delete result.refreshToken;
210
- }
211
- }
212
-
213
- // Return prepared result
214
- return result;
200
+ const cookiesConfig = this.configService.getFastButReadOnly<boolean | ICookiesConfig>('cookies');
201
+ const env = this.configService.getFastButReadOnly<string>('env');
202
+ return setLegacyAuthCookies(res, result, cookiesConfig, env);
215
203
  }
216
204
  }
@@ -6,6 +6,8 @@ import { CurrentUser } from '../../common/decorators/current-user.decorator';
6
6
  import { GraphQLServiceOptions } from '../../common/decorators/graphql-service-options.decorator';
7
7
  import { Roles } from '../../common/decorators/roles.decorator';
8
8
  import { RoleEnum } from '../../common/enums/role.enum';
9
+ import { setLegacyAuthCookies } from '../../common/helpers/cookies.helper';
10
+ import type { ICookiesConfig } from '../../common/interfaces/server-options.interface';
9
11
  import { ServiceOptions } from '../../common/interfaces/service-options.interface';
10
12
  import { ConfigService } from '../../common/services/config.service';
11
13
  import { AuthGuardStrategy } from './auth-guard-strategy.enum';
@@ -169,30 +171,16 @@ export class CoreAuthResolver {
169
171
  // ===================================================================================================================
170
172
 
171
173
  /**
172
- * Process cookies
174
+ * Process cookies — sets legacy auth cookies (token, refreshToken) with secure defaults
175
+ * and optionally strips tokens from the response body per `cookies.exposeTokenInBody`.
176
+ *
177
+ * Delegates to the shared `setLegacyAuthCookies()` helper to ensure consistent
178
+ * security options (httpOnly, sameSite=lax, secure in production) between the REST
179
+ * controller and GraphQL resolver.
173
180
  */
174
181
  protected processCookies(ctx: { res: ResponseType }, result: any) {
175
- // Check if cookie handling is activated (enabled by default, unless explicitly set to false)
176
- if (this.configService.getFastButReadOnly('cookies') !== false) {
177
- // Set cookies
178
- if (!result || typeof result !== 'object') {
179
- ctx.res.cookie('token', '', { httpOnly: true });
180
- ctx.res.cookie('refreshToken', '', { httpOnly: true });
181
- return result;
182
- }
183
- ctx.res.cookie('token', result?.token || '', { httpOnly: true });
184
- ctx.res.cookie('refreshToken', result?.refreshToken || '', { httpOnly: true });
185
-
186
- // Remove tokens from result
187
- if (result.token) {
188
- delete result.token;
189
- }
190
- if (result.refreshToken) {
191
- delete result.refreshToken;
192
- }
193
- }
194
-
195
- // Return prepared result
196
- return result;
182
+ const cookiesConfig = this.configService.getFastButReadOnly<boolean | ICookiesConfig>('cookies');
183
+ const env = this.configService.getFastButReadOnly<string>('env');
184
+ return setLegacyAuthCookies(ctx.res, result, cookiesConfig, env);
197
185
  }
198
186
  }
@@ -163,6 +163,24 @@ export class ServerModule {}
163
163
  **Modify:** `src/config.env.ts`
164
164
  **Reference:** `node_modules/@lenne.tech/nest-server/src/config.env.ts`
165
165
 
166
+ > **Since v11.25.0 — Cookie & CORS Defaults**
167
+ >
168
+ > Cookies are now **enabled by default** (`cookies: true`). Authentication responses
169
+ > set signed httpOnly session cookies and **remove the token from the response body**.
170
+ >
171
+ > For test environments where `TestHelper` reads the token from the response body
172
+ > (signIn → token → subsequent requests with `Authorization: Bearer`), set:
173
+ >
174
+ > ```typescript
175
+ > ci: { cookies: { exposeTokenInBody: true }, cors: { allowAll: true } },
176
+ > e2e: { cookies: { exposeTokenInBody: true }, cors: { allowAll: true } },
177
+ > ```
178
+ >
179
+ > **Never set `exposeTokenInBody: true` in production** — the framework throws at
180
+ > startup if this is detected in `production` or `staging` environments (XSS-risk guard).
181
+ >
182
+ > To keep the old behavior (cookies off, tokens in body everywhere), set `cookies: false`.
183
+
166
184
  #### Zero-Config (Default):
167
185
 
168
186
  BetterAuth is **enabled by default** with JWT + 2FA. No configuration required!
@@ -260,6 +260,13 @@ Read the security section below for production deployments.
260
260
  | `basePath` | Endpoint routing | 404 on API calls |
261
261
  | `passkey.origin` | WebAuthn security | Passkey auth fails |
262
262
 
263
+ **Global server-level settings that affect BetterAuth behavior (since v11.25.0):**
264
+
265
+ | Setting (top-level `IServerOptions`) | Technical Purpose | Impact of Wrong Value |
266
+ | ----------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
267
+ | `cookies` (`boolean \| ICookiesConfig`, default: `true`) | Controls cookie-parser middleware and session cookie setting. `cookies.exposeTokenInBody` additionally returns the token in the response body (test-only; **forbidden in production**) | Tokens missing from response body surprises test clients; `exposeTokenInBody` in prod = XSS-risk, framework throws at startup |
268
+ | `cors` (`boolean \| ICorsConfig`, default: enabled with auto-derived origins) | Unified CORS config — propagates to GraphQL (Apollo), REST (Express), and BetterAuth `trustedOrigins` from a single source | `cors.enabled: false` disables all three layers; `cors.allowAll` allows any origin (dev only) |
269
+
263
270
  **For Development:** The defaults (`http://localhost:3000`, `/iam`) are correct.
264
271
 
265
272
  ### Passkey Auto-Detection (Recommended)