@lenne.tech/nest-server 11.15.1 → 11.15.3

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 (27) hide show
  1. package/dist/core/common/interfaces/server-options.interface.d.ts +5 -0
  2. package/dist/core/modules/better-auth/better-auth.config.d.ts +17 -1
  3. package/dist/core/modules/better-auth/better-auth.config.js +72 -2
  4. package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
  5. package/dist/core/modules/better-auth/core-better-auth-api.middleware.js +1 -0
  6. package/dist/core/modules/better-auth/core-better-auth-api.middleware.js.map +1 -1
  7. package/dist/core/modules/better-auth/core-better-auth-cookie.helper.d.ts +3 -0
  8. package/dist/core/modules/better-auth/core-better-auth-cookie.helper.js +6 -1
  9. package/dist/core/modules/better-auth/core-better-auth-cookie.helper.js.map +1 -1
  10. package/dist/core/modules/better-auth/core-better-auth.controller.js +1 -0
  11. package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -1
  12. package/dist/core/modules/better-auth/core-better-auth.module.d.ts +2 -0
  13. package/dist/core/modules/better-auth/core-better-auth.module.js +29 -9
  14. package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
  15. package/dist/core/modules/better-auth/core-better-auth.service.d.ts +4 -1
  16. package/dist/core/modules/better-auth/core-better-auth.service.js +11 -3
  17. package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
  18. package/dist/tsconfig.build.tsbuildinfo +1 -1
  19. package/package.json +1 -1
  20. package/src/core/common/interfaces/server-options.interface.ts +121 -0
  21. package/src/core/modules/better-auth/README.md +79 -17
  22. package/src/core/modules/better-auth/better-auth.config.ts +148 -4
  23. package/src/core/modules/better-auth/core-better-auth-api.middleware.ts +1 -0
  24. package/src/core/modules/better-auth/core-better-auth-cookie.helper.ts +18 -2
  25. package/src/core/modules/better-auth/core-better-auth.controller.ts +2 -1
  26. package/src/core/modules/better-auth/core-better-auth.module.ts +40 -9
  27. package/src/core/modules/better-auth/core-better-auth.service.ts +21 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/nest-server",
3
- "version": "11.15.1",
3
+ "version": "11.15.3",
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",
@@ -1681,6 +1681,38 @@ export interface ITusExpirationConfig {
1681
1681
  expiresIn?: string;
1682
1682
  }
1683
1683
 
1684
+ /**
1685
+ * Configuration for cross-subdomain cookie sharing.
1686
+ *
1687
+ * When enabled, authentication cookies are set with a `domain` attribute
1688
+ * so they are shared across all subdomains (e.g., `api.example.com` ↔ `ws.example.com`).
1689
+ *
1690
+ * Also configures Better-Auth's native `advanced.crossSubDomainCookies` setting
1691
+ * and sets the domain on nest-server's own cookie helper.
1692
+ *
1693
+ * **Note:** This configures only the cookie `domain` attribute (browser sends cookies).
1694
+ * For the server to accept cross-subdomain requests, `trustedOrigins` must also include
1695
+ * the origins of all subdomains that make requests.
1696
+ *
1697
+ * @see IBetterAuthBase.crossSubDomainCookies
1698
+ * @since 11.15.1
1699
+ */
1700
+ export interface IBetterAuthCrossSubDomainCookiesConfig {
1701
+ /**
1702
+ * Cookie domain for cross-subdomain sharing.
1703
+ * When omitted, auto-derived from `baseUrl` hostname.
1704
+ *
1705
+ * @example 'example.com' → cookies shared across *.example.com
1706
+ */
1707
+ domain?: string;
1708
+
1709
+ /**
1710
+ * Whether cross-subdomain cookies are enabled.
1711
+ * @default true (when object is provided)
1712
+ */
1713
+ enabled?: boolean;
1714
+ }
1715
+
1684
1716
  /**
1685
1717
  * Base interface for better-auth configuration (shared properties)
1686
1718
  * This contains all properties except passkey and trustedOrigins,
@@ -1767,6 +1799,62 @@ interface IBetterAuthBase {
1767
1799
  */
1768
1800
  controller?: Type<any>;
1769
1801
 
1802
+ /**
1803
+ * Enable cross-subdomain cookie sharing.
1804
+ *
1805
+ * When your API and frontend (or other services) run on different subdomains
1806
+ * (e.g., `api.example.com` and `app.example.com`), cookies are not shared by default.
1807
+ * This option configures a cookie domain so authentication cookies work across subdomains.
1808
+ *
1809
+ * Accepts (Boolean Shorthand Pattern):
1810
+ * - `undefined`: Disabled (default, backward compatible)
1811
+ * - `true` or `{}`: Enable, domain auto-derived from `appUrl` or `baseUrl`
1812
+ * - `{ domain: 'example.com' }`: Enable with explicit domain
1813
+ * - `false` or `{ enabled: false }`: Disabled
1814
+ *
1815
+ * Domain auto-derivation priority:
1816
+ * 1. `appUrl` hostname (e.g., `https://dev.example.com` → `dev.example.com`)
1817
+ * 2. `baseUrl` hostname with `api.` prefix stripped (e.g., `https://api.dev.example.com` → `dev.example.com`)
1818
+ * 3. `baseUrl` hostname as-is (if no `api.` prefix)
1819
+ *
1820
+ * When enabled, sets:
1821
+ * - Better-Auth's native `advanced.crossSubDomainCookies`
1822
+ * - Domain attribute on nest-server's own cookie helper
1823
+ *
1824
+ * **Important:** This only configures cookie sharing (browser sends cookies to subdomains).
1825
+ * For cross-subdomain requests to be accepted by the server, `trustedOrigins` must also
1826
+ * include the origins of all subdomains that make requests. `trustedOrigins` is auto-derived
1827
+ * from `baseUrl` and `appUrl`, but additional subdomains must be added explicitly.
1828
+ *
1829
+ * @see trustedOrigins
1830
+ * @default undefined (disabled)
1831
+ * @since 11.15.1
1832
+ *
1833
+ * @example
1834
+ * ```typescript
1835
+ * // Recommended: appUrl + baseUrl for auto-derivation
1836
+ * betterAuth: {
1837
+ * appUrl: 'https://dev.example.com',
1838
+ * baseUrl: 'https://api.dev.example.com',
1839
+ * crossSubDomainCookies: true,
1840
+ * // → domain = 'dev.example.com' (from appUrl)
1841
+ * }
1842
+ *
1843
+ * // Without appUrl: strips api. prefix from baseUrl
1844
+ * betterAuth: {
1845
+ * baseUrl: 'https://api.dev.example.com',
1846
+ * crossSubDomainCookies: true,
1847
+ * // → domain = 'dev.example.com'
1848
+ * }
1849
+ *
1850
+ * // Explicit domain
1851
+ * betterAuth: {
1852
+ * crossSubDomainCookies: { domain: 'example.com' },
1853
+ * }
1854
+ * ```
1855
+ */
1856
+ crossSubDomainCookies?: boolean | IBetterAuthCrossSubDomainCookiesConfig;
1857
+
1770
1858
  /**
1771
1859
  * Email/password authentication configuration.
1772
1860
  * Enabled by default.
@@ -1881,9 +1969,16 @@ interface IBetterAuthBase {
1881
1969
  * advanced: {
1882
1970
  * cookiePrefix: 'my-app',
1883
1971
  * useSecureCookies: true,
1972
+ * crossSubDomainCookies: {
1973
+ * domain: 'example.com', // Cookies shared across *.example.com
1974
+ * },
1884
1975
  * },
1885
1976
  * }
1886
1977
  * ```
1978
+ *
1979
+ * **Note on `advanced` options:** The `advanced` object is deep-merged with internal defaults
1980
+ * (e.g., `cookiePrefix` derived from `basePath`). You do not need to re-specify `cookiePrefix`
1981
+ * when adding other `advanced` options like `crossSubDomainCookies`.
1887
1982
  */
1888
1983
  options?: Record<string, unknown>;
1889
1984
 
@@ -2125,11 +2220,24 @@ interface IBetterAuthWithoutPasskey extends IBetterAuthBase {
2125
2220
  * Optional when Passkey is disabled.
2126
2221
  * If not set, all origins are allowed (CORS `*`).
2127
2222
  *
2223
+ * Auto-derived from `baseUrl` and `appUrl` when these are configured.
2224
+ * Explicitly listed origins are merged with the auto-derived ones.
2225
+ *
2226
+ * **Important for cross-subdomain setups:** When using `crossSubDomainCookies`,
2227
+ * cookies are shared across subdomains, but the server also needs to accept requests
2228
+ * from those subdomains. `baseUrl` and `appUrl` are auto-added, but additional
2229
+ * subdomains (e.g., `https://ws.example.com`) must be listed here explicitly.
2230
+ *
2231
+ * @see crossSubDomainCookies
2232
+ *
2128
2233
  * @example
2129
2234
  * ```typescript
2130
2235
  * // Restrict origins even without Passkey
2131
2236
  * trustedOrigins: ['https://app.example.com'],
2132
2237
  *
2238
+ * // Cross-subdomain: add extra subdomains beyond baseUrl/appUrl
2239
+ * trustedOrigins: ['https://ws.example.com', 'https://admin.example.com'],
2240
+ *
2133
2241
  * // Or leave undefined for open CORS
2134
2242
  * ```
2135
2243
  */
@@ -2172,6 +2280,16 @@ interface IBetterAuthWithPasskey extends IBetterAuthBase {
2172
2280
  * Passkey uses `credentials: 'include'` which requires explicit CORS origins.
2173
2281
  * Browsers don't allow wildcard `*` with credentials.
2174
2282
  *
2283
+ * Auto-derived from `baseUrl` and `appUrl` when these are configured.
2284
+ * Explicitly listed origins are merged with the auto-derived ones.
2285
+ *
2286
+ * **Important for cross-subdomain setups:** When using `crossSubDomainCookies`,
2287
+ * cookies are shared across subdomains, but the server also needs to accept requests
2288
+ * from those subdomains. `baseUrl` and `appUrl` are auto-added, but additional
2289
+ * subdomains (e.g., `https://ws.example.com`) must be listed here explicitly.
2290
+ *
2291
+ * @see crossSubDomainCookies
2292
+ *
2175
2293
  * @example
2176
2294
  * ```typescript
2177
2295
  * // Development
@@ -2179,6 +2297,9 @@ interface IBetterAuthWithPasskey extends IBetterAuthBase {
2179
2297
  *
2180
2298
  * // Production
2181
2299
  * trustedOrigins: process.env.TRUSTED_ORIGINS?.split(',') || [],
2300
+ *
2301
+ * // Cross-subdomain: add extra subdomains beyond baseUrl/appUrl
2302
+ * trustedOrigins: ['https://ws.example.com', 'https://admin.example.com'],
2182
2303
  * ```
2183
2304
  */
2184
2305
  trustedOrigins: string[];
@@ -727,6 +727,67 @@ const config = {
727
727
 
728
728
  See [Better-Auth Options Reference](https://www.better-auth.com/docs/reference/options) for all available options.
729
729
 
730
+ #### Cross-Subdomain Cookies (v11.15.1+)
731
+
732
+ When your API and other services run on different subdomains (e.g., `api.example.com` and `ws.example.com`), authentication cookies need to be shared across subdomains.
733
+
734
+ **Recommended: Boolean Shorthand (v11.15.1+)**
735
+
736
+ ```typescript
737
+ const config = {
738
+ baseUrl: 'https://api.dev.example.com',
739
+ appUrl: 'https://dev.example.com',
740
+ betterAuth: {
741
+ crossSubDomainCookies: true, // Domain auto-derived → 'dev.example.com'
742
+ },
743
+ };
744
+ ```
745
+
746
+ **Domain resolution order:**
747
+
748
+ 1. `appUrl` hostname (if set) — e.g., `https://dev.example.com` → `dev.example.com`
749
+ 2. `baseUrl` hostname with `api.` prefix removed — e.g., `https://api.dev.example.com` → `dev.example.com`
750
+ 3. `baseUrl` hostname as-is (if no `api.` prefix) — e.g., `https://example.com` → `example.com`
751
+
752
+ **Options:**
753
+
754
+ | Value | Behavior |
755
+ | ------------------------------- | --------------------------------------------------------- |
756
+ | `undefined` | Disabled (default, backward compatible) |
757
+ | `true` or `{}` | Enabled, domain auto-derived (see resolution order above) |
758
+ | `{ domain: 'example.com' }` | Enabled with explicit domain |
759
+ | `false` or `{ enabled: false }` | Disabled |
760
+
761
+ **Explicit domain:**
762
+
763
+ ```typescript
764
+ betterAuth: {
765
+ crossSubDomainCookies: { domain: 'example.com' }, // Cookies shared across *.example.com
766
+ }
767
+ ```
768
+
769
+ This sets the `domain` attribute on all authentication cookies (both Better-Auth's native cookies and nest-server's cookie helper), allowing them to be sent to any subdomain of the configured domain.
770
+
771
+ > **Important: `crossSubDomainCookies` + `trustedOrigins`**
772
+ >
773
+ > `crossSubDomainCookies` only configures cookie sharing (the `domain` attribute). Better-Auth also validates the **origin** of incoming requests via `trustedOrigins`. Both are required for cross-subdomain setups to work:
774
+ >
775
+ > - `crossSubDomainCookies` → Browser **sends** cookies to subdomains
776
+ > - `trustedOrigins` → Server **accepts** requests from those subdomains
777
+ >
778
+ > `trustedOrigins` is auto-derived from `baseUrl` and `appUrl` (e.g., `https://api.example.com` and `https://example.com`). If you have **additional** subdomains (e.g., `https://ws.example.com`), add them explicitly:
779
+ >
780
+ > ```typescript
781
+ > betterAuth: {
782
+ > crossSubDomainCookies: true,
783
+ > trustedOrigins: ['https://ws.example.com'], // Merged with auto-derived origins
784
+ > }
785
+ > ```
786
+
787
+ > **Note:** For localhost environments, cross-subdomain cookies are automatically disabled even when `true` is set, since subdomains are not meaningful on localhost.
788
+
789
+ **Legacy approach (still supported):** You can also configure cross-subdomain cookies via `options.advanced.crossSubDomainCookies` passthrough. However, the Boolean Shorthand above is recommended as it also sets the domain on nest-server's own cookie helper (middleware + controller).
790
+
730
791
  ## Plugins and Extensions
731
792
 
732
793
  Better-Auth provides a rich plugin ecosystem. This module uses a **hybrid approach**:
@@ -1290,23 +1351,24 @@ export class MyService {
1290
1351
 
1291
1352
  ### Available Methods
1292
1353
 
1293
- | Method | Description |
1294
- | ------------------------------------ | ------------------------------------- |
1295
- | `isEnabled()` | Check if Better-Auth is enabled |
1296
- | `getInstance()` | Get the Better-Auth instance |
1297
- | `getApi()` | Get the Better-Auth API |
1298
- | `getConfig()` | Get the current configuration |
1299
- | `isJwtEnabled()` | Check if JWT plugin is enabled |
1300
- | `isTwoFactorEnabled()` | Check if 2FA is enabled |
1301
- | `isPasskeyEnabled()` | Check if Passkey is enabled |
1302
- | `isSignUpEnabled()` | Check if sign-up is enabled |
1303
- | `getEnabledSocialProviders()` | Get list of enabled social providers |
1304
- | `getBasePath()` | Get the base path for endpoints |
1305
- | `getBaseUrl()` | Get the base URL |
1306
- | `getSession(req)` | Get current session from request |
1307
- | `revokeSession(token)` | Revoke a session (logout) |
1308
- | `isSessionExpiringSoon(session, t?)` | Check if session is expiring soon |
1309
- | `getSessionTimeRemaining(session)` | Get remaining session time in seconds |
1354
+ | Method | Description |
1355
+ | ------------------------------------ | ------------------------------------------ |
1356
+ | `isEnabled()` | Check if Better-Auth is enabled |
1357
+ | `getInstance()` | Get the Better-Auth instance |
1358
+ | `getApi()` | Get the Better-Auth API |
1359
+ | `getConfig()` | Get the current configuration |
1360
+ | `isJwtEnabled()` | Check if JWT plugin is enabled |
1361
+ | `isTwoFactorEnabled()` | Check if 2FA is enabled |
1362
+ | `isPasskeyEnabled()` | Check if Passkey is enabled |
1363
+ | `isSignUpEnabled()` | Check if sign-up is enabled |
1364
+ | `getEnabledSocialProviders()` | Get list of enabled social providers |
1365
+ | `getBasePath()` | Get the base path for endpoints |
1366
+ | `getBaseUrl()` | Get the base URL |
1367
+ | `getCookieDomain()` | Get resolved cross-subdomain cookie domain |
1368
+ | `getSession(req)` | Get current session from request |
1369
+ | `revokeSession(token)` | Revoke a session (logout) |
1370
+ | `isSessionExpiringSoon(session, t?)` | Check if session is expiring soon |
1371
+ | `getSessionTimeRemaining(session)` | Get remaining session time in seconds |
1310
1372
 
1311
1373
  ## Security Integration
1312
1374
 
@@ -247,7 +247,14 @@ interface ValidationResult {
247
247
  * @returns Configured better-auth instance or null if not enabled
248
248
  * @throws Error if configuration validation fails
249
249
  */
250
- export function createBetterAuthInstance(options: CreateBetterAuthOptions): BetterAuthInstance | null {
250
+ export interface CreateBetterAuthResult {
251
+ /** Resolved cookie domain for cross-subdomain cookies, or undefined if disabled */
252
+ cookieDomain: string | undefined;
253
+ /** The better-auth instance */
254
+ instance: BetterAuthInstance;
255
+ }
256
+
257
+ export function createBetterAuthInstance(options: CreateBetterAuthOptions): CreateBetterAuthResult | null {
251
258
  const logger = new Logger('BetterAuthConfig');
252
259
  const { config, db, fallbackSecrets, onEmailVerified, sendVerificationEmail, serverEnv } = options;
253
260
 
@@ -294,6 +301,12 @@ export function createBetterAuthInstance(options: CreateBetterAuthOptions): Bett
294
301
  const trustedOrigins = buildTrustedOrigins(config, { passkeyNormalization, resolvedUrls });
295
302
  const additionalFields = buildUserFields(config);
296
303
 
304
+ // Resolve cross-subdomain cookies (Boolean Shorthand)
305
+ const crossSubDomain = resolveCrossSubDomainCookies(config, resolvedUrls);
306
+ if (crossSubDomain.enabled) {
307
+ logger.log(`Cross-subdomain cookies enabled (domain: ${crossSubDomain.domain})`);
308
+ }
309
+
297
310
  // Build email verification configuration
298
311
  const emailVerificationConfig = buildEmailVerificationConfig(config, { onEmailVerified, sendVerificationEmail });
299
312
 
@@ -307,6 +320,9 @@ export function createBetterAuthInstance(options: CreateBetterAuthOptions): Bett
307
320
  const betterAuthConfig: Record<string, unknown> = {
308
321
  advanced: {
309
322
  cookiePrefix,
323
+ ...(crossSubDomain.enabled && {
324
+ crossSubDomainCookies: { domain: crossSubDomain.domain, enabled: true },
325
+ }),
310
326
  },
311
327
  basePath,
312
328
  baseURL: resolvedUrls.baseUrl || config.baseUrl || 'http://localhost:3000',
@@ -348,12 +364,28 @@ export function createBetterAuthInstance(options: CreateBetterAuthOptions): Bett
348
364
 
349
365
  // Merge with custom options passthrough
350
366
  // This allows projects to configure any Better-Auth option not explicitly defined
351
- const finalConfig = config.options ? { ...betterAuthConfig, ...config.options } : betterAuthConfig;
367
+ // Deep-merge 'advanced' to preserve cookiePrefix when options.advanced is provided
368
+ let finalConfig: Record<string, unknown>;
369
+ if (config.options) {
370
+ const { advanced: optionsAdvanced, ...restOptions } = config.options as Record<string, unknown>;
371
+ finalConfig = { ...betterAuthConfig, ...restOptions };
372
+ if (optionsAdvanced && typeof optionsAdvanced === 'object') {
373
+ finalConfig.advanced = {
374
+ ...(betterAuthConfig.advanced as Record<string, unknown>),
375
+ ...(optionsAdvanced as Record<string, unknown>),
376
+ };
377
+ }
378
+ } else {
379
+ finalConfig = betterAuthConfig;
380
+ }
352
381
 
353
- // Create and return the better-auth instance
382
+ // Create and return the better-auth instance with resolved metadata
354
383
  // Type assertion needed for maximum flexibility - allows projects to use any Better-Auth option
355
384
 
356
- return betterAuth(finalConfig as any);
385
+ return {
386
+ cookieDomain: crossSubDomain.enabled ? crossSubDomain.domain : undefined,
387
+ instance: betterAuth(finalConfig as any),
388
+ };
357
389
  }
358
390
 
359
391
  /**
@@ -1001,6 +1033,118 @@ function normalizePasskeyConfig(
1001
1033
  };
1002
1034
  }
1003
1035
 
1036
+ /**
1037
+ * Result of cross-subdomain cookies resolution
1038
+ */
1039
+ export interface ResolvedCrossSubDomainCookies {
1040
+ /** The resolved cookie domain (e.g., 'example.com') */
1041
+ domain: string | undefined;
1042
+ /** Whether cross-subdomain cookies are enabled */
1043
+ enabled: boolean;
1044
+ }
1045
+
1046
+ /**
1047
+ * Resolves the cross-subdomain cookies configuration.
1048
+ *
1049
+ * Follows the Boolean Shorthand Pattern:
1050
+ * - `undefined` / `false` → disabled
1051
+ * - `true` / `{}` → enabled, domain auto-derived from appUrl or baseUrl
1052
+ * - `{ domain: 'x.com' }` → enabled with explicit domain
1053
+ * - `{ enabled: false }` → disabled (allows pre-configuration)
1054
+ *
1055
+ * Domain auto-derivation priority:
1056
+ * 1. Explicit `crossSubDomainCookies.domain`
1057
+ * 2. `appUrl` hostname (e.g., `https://dev.example.com` → `dev.example.com`)
1058
+ * 3. `baseUrl` hostname with `api.` prefix stripped (e.g., `https://api.dev.example.com` → `dev.example.com`)
1059
+ * 4. `baseUrl` hostname as-is (if no `api.` prefix)
1060
+ *
1061
+ * The parent domain is preferred because cross-subdomain cookies need to cover
1062
+ * both the API subdomain and the App domain (e.g., `api.dev.example.com` ↔ `dev.example.com`).
1063
+ *
1064
+ * @param config - Better-auth configuration
1065
+ * @param resolvedUrls - Resolved URLs for domain auto-detection
1066
+ * @returns Resolved cross-subdomain cookie settings
1067
+ */
1068
+ export function resolveCrossSubDomainCookies(
1069
+ config: IBetterAuth,
1070
+ resolvedUrls: ResolvedUrls,
1071
+ ): ResolvedCrossSubDomainCookies {
1072
+ const csdc = config.crossSubDomainCookies;
1073
+
1074
+ // undefined or false → disabled
1075
+ if (csdc === undefined || csdc === null || csdc === false) {
1076
+ return { domain: undefined, enabled: false };
1077
+ }
1078
+
1079
+ // Object with enabled: false → disabled (pre-configuration)
1080
+ if (typeof csdc === 'object' && csdc.enabled === false) {
1081
+ return { domain: undefined, enabled: false };
1082
+ }
1083
+
1084
+ // true or {} or { domain: ... } → enabled
1085
+ const explicitDomain = typeof csdc === 'object' ? csdc.domain : undefined;
1086
+
1087
+ if (explicitDomain) {
1088
+ return { domain: explicitDomain, enabled: true };
1089
+ }
1090
+
1091
+ // Auto-derive domain: prefer appUrl (= parent domain), then strip api. from baseUrl
1092
+ const domain = deriveCookieDomainFromUrls(resolvedUrls.appUrl, resolvedUrls.baseUrl || config.baseUrl);
1093
+ if (domain) {
1094
+ return { domain, enabled: true };
1095
+ }
1096
+
1097
+ // No usable URL available → cannot derive domain, disable
1098
+ return { domain: undefined, enabled: false };
1099
+ }
1100
+
1101
+ /**
1102
+ * Derives the cookie domain from available URLs.
1103
+ *
1104
+ * For cross-subdomain cookies, the domain must be the PARENT domain that covers
1105
+ * both the API and App subdomains. For example:
1106
+ * - API: `api.dev.example.com`, App: `dev.example.com` → domain: `dev.example.com`
1107
+ *
1108
+ * Resolution:
1109
+ * 1. Use appUrl hostname (if available and not localhost)
1110
+ * 2. Strip `api.` prefix from baseUrl hostname
1111
+ * 3. Use baseUrl hostname as-is
1112
+ *
1113
+ * Returns undefined for localhost (cross-subdomain not meaningful there).
1114
+ */
1115
+ function deriveCookieDomainFromUrls(appUrl?: string, baseUrl?: string): string | undefined {
1116
+ // Priority 1: appUrl hostname (this IS the parent domain in typical setups)
1117
+ if (appUrl) {
1118
+ try {
1119
+ const hostname = new URL(appUrl).hostname;
1120
+ if (hostname !== 'localhost' && hostname !== '127.0.0.1') {
1121
+ return hostname;
1122
+ }
1123
+ } catch {
1124
+ // Fall through to baseUrl
1125
+ }
1126
+ }
1127
+
1128
+ // Priority 2: baseUrl hostname with api. prefix stripped
1129
+ if (baseUrl) {
1130
+ try {
1131
+ const hostname = new URL(baseUrl).hostname;
1132
+ if (hostname === 'localhost' || hostname === '127.0.0.1') {
1133
+ return undefined;
1134
+ }
1135
+ // Strip api. prefix to get parent domain
1136
+ if (hostname.startsWith('api.')) {
1137
+ return hostname.substring(4);
1138
+ }
1139
+ return hostname;
1140
+ } catch {
1141
+ return undefined;
1142
+ }
1143
+ }
1144
+
1145
+ return undefined;
1146
+ }
1147
+
1004
1148
  function resolveUrls(options: CreateBetterAuthOptions): ResolvedUrls {
1005
1149
  const { config, serverAppUrl, serverBaseUrl, serverEnv } = options;
1006
1150
  const warnings: string[] = [];
@@ -74,6 +74,7 @@ export class CoreBetterAuthApiMiddleware implements NestMiddleware {
74
74
  this.cookieHelper = createCookieHelper(
75
75
  this.betterAuthService.getBasePath(),
76
76
  {
77
+ domain: this.betterAuthService.getCookieDomain(),
77
78
  legacyCookieEnabled: false, // Middleware doesn't need legacy cookie
78
79
  secret: config?.secret, // Required for cookie signing
79
80
  },
@@ -24,6 +24,7 @@ export const AUTH_COOKIE_NAMES = {
24
24
  * Cookie options for authentication cookies.
25
25
  */
26
26
  export interface AuthCookieOptions {
27
+ domain?: string;
27
28
  httpOnly: boolean;
28
29
  maxAge?: number;
29
30
  sameSite: 'lax' | 'none' | 'strict';
@@ -36,6 +37,14 @@ export interface AuthCookieOptions {
36
37
  export interface BetterAuthCookieHelperConfig {
37
38
  /** Base path for Better-Auth (e.g., '/iam' or 'iam') */
38
39
  basePath: string;
40
+ /**
41
+ * Cookie domain for cross-subdomain cookie sharing.
42
+ * When set, cookies are sent to all subdomains of this domain.
43
+ * Read from betterAuth.options.advanced.crossSubDomainCookies.domain.
44
+ *
45
+ * @example 'example.com' → cookies shared across api.example.com, ws.example.com, etc.
46
+ */
47
+ domain?: string;
39
48
  /**
40
49
  * Enable legacy 'token' cookie for backwards compatibility with < 11.7.0.
41
50
  * Only needed when Legacy Auth (Passport) is also active.
@@ -116,11 +125,17 @@ export class BetterAuthCookieHelper {
116
125
  * @returns Cookie options with httpOnly, sameSite, and secure settings
117
126
  */
118
127
  getDefaultCookieOptions(): AuthCookieOptions {
119
- return {
128
+ const options: AuthCookieOptions = {
120
129
  httpOnly: true,
121
130
  sameSite: 'lax',
122
131
  secure: process.env.NODE_ENV === 'production',
123
132
  };
133
+
134
+ if (this.config.domain) {
135
+ options.domain = this.config.domain;
136
+ }
137
+
138
+ return options;
124
139
  }
125
140
 
126
141
  /**
@@ -311,11 +326,12 @@ export class BetterAuthCookieHelper {
311
326
  */
312
327
  export function createCookieHelper(
313
328
  basePath: string,
314
- options?: { legacyCookieEnabled?: boolean; secret?: string },
329
+ options?: { domain?: string; legacyCookieEnabled?: boolean; secret?: string },
315
330
  logger?: Logger,
316
331
  ): BetterAuthCookieHelper {
317
332
  return new BetterAuthCookieHelper({
318
333
  basePath,
334
+ domain: options?.domain,
319
335
  legacyCookieEnabled: options?.legacyCookieEnabled ?? false,
320
336
  logger,
321
337
  secret: options?.secret,
@@ -219,10 +219,11 @@ export class CoreBetterAuthController {
219
219
  // CRITICAL: Cookies must be signed for Passkey/2FA to work
220
220
  const betterAuthConfig = this.betterAuthService.getConfig();
221
221
 
222
- // Initialize cookie helper with Legacy Auth detection and secret
222
+ // Initialize cookie helper with Legacy Auth detection, secret, and optional cross-subdomain domain
223
223
  this.cookieHelper = createCookieHelper(
224
224
  this.betterAuthService.getBasePath(),
225
225
  {
226
+ domain: this.betterAuthService.getCookieDomain(),
226
227
  legacyCookieEnabled: legacyAuthEnabled,
227
228
  secret: betterAuthConfig?.secret,
228
229
  },