@lenne.tech/nest-server 11.15.2 → 11.15.4
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/dist/core/common/interfaces/server-options.interface.d.ts +5 -0
- package/dist/core/modules/better-auth/better-auth.config.d.ts +17 -1
- package/dist/core/modules/better-auth/better-auth.config.js +58 -1
- package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-api.middleware.js +1 -0
- package/dist/core/modules/better-auth/core-better-auth-api.middleware.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.controller.js +1 -2
- package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.module.d.ts +2 -0
- package/dist/core/modules/better-auth/core-better-auth.module.js +29 -9
- package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.service.d.ts +4 -1
- package/dist/core/modules/better-auth/core-better-auth.service.js +11 -3
- package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/core/common/interfaces/server-options.interface.ts +121 -0
- package/src/core/modules/better-auth/README.md +79 -17
- package/src/core/modules/better-auth/better-auth.config.ts +134 -3
- package/src/core/modules/better-auth/core-better-auth-api.middleware.ts +1 -0
- package/src/core/modules/better-auth/core-better-auth.controller.ts +2 -6
- package/src/core/modules/better-auth/core-better-auth.module.ts +40 -9
- 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.
|
|
3
|
+
"version": "11.15.4",
|
|
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",
|
|
@@ -189,4 +189,4 @@
|
|
|
189
189
|
"@apollo/protobufjs"
|
|
190
190
|
]
|
|
191
191
|
}
|
|
192
|
-
}
|
|
192
|
+
}
|
|
@@ -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
|
-
| `
|
|
1307
|
-
| `
|
|
1308
|
-
| `
|
|
1309
|
-
| `
|
|
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
|
|
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',
|
|
@@ -363,10 +379,13 @@ export function createBetterAuthInstance(options: CreateBetterAuthOptions): Bett
|
|
|
363
379
|
finalConfig = betterAuthConfig;
|
|
364
380
|
}
|
|
365
381
|
|
|
366
|
-
// Create and return the better-auth instance
|
|
382
|
+
// Create and return the better-auth instance with resolved metadata
|
|
367
383
|
// Type assertion needed for maximum flexibility - allows projects to use any Better-Auth option
|
|
368
384
|
|
|
369
|
-
return
|
|
385
|
+
return {
|
|
386
|
+
cookieDomain: crossSubDomain.enabled ? crossSubDomain.domain : undefined,
|
|
387
|
+
instance: betterAuth(finalConfig as any),
|
|
388
|
+
};
|
|
370
389
|
}
|
|
371
390
|
|
|
372
391
|
/**
|
|
@@ -1014,6 +1033,118 @@ function normalizePasskeyConfig(
|
|
|
1014
1033
|
};
|
|
1015
1034
|
}
|
|
1016
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
|
+
|
|
1017
1148
|
function resolveUrls(options: CreateBetterAuthOptions): ResolvedUrls {
|
|
1018
1149
|
const { config, serverAppUrl, serverBaseUrl, serverEnv } = options;
|
|
1019
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
|
},
|
|
@@ -219,15 +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
|
-
//
|
|
223
|
-
// This enables cookie sharing across subdomains (e.g., api.example.com → ws.example.com)
|
|
224
|
-
const crossSubDomainDomain = (betterAuthConfig?.options as any)?.advanced?.crossSubDomainCookies?.domain;
|
|
225
|
-
|
|
226
|
-
// Initialize cookie helper with Legacy Auth detection, secret, and optional domain
|
|
222
|
+
// Initialize cookie helper with Legacy Auth detection, secret, and optional cross-subdomain domain
|
|
227
223
|
this.cookieHelper = createCookieHelper(
|
|
228
224
|
this.betterAuthService.getBasePath(),
|
|
229
225
|
{
|
|
230
|
-
domain:
|
|
226
|
+
domain: this.betterAuthService.getCookieDomain(),
|
|
231
227
|
legacyCookieEnabled: legacyAuthEnabled,
|
|
232
228
|
secret: betterAuthConfig?.secret,
|
|
233
229
|
},
|
|
@@ -19,7 +19,7 @@ import { ConfigService } from '../../common/services/config.service';
|
|
|
19
19
|
import { RolesGuardRegistry } from '../auth/guards/roles-guard-registry';
|
|
20
20
|
import { BetterAuthRolesGuard } from './better-auth-roles.guard';
|
|
21
21
|
import { BetterAuthTokenService } from './better-auth-token.service';
|
|
22
|
-
import { BetterAuthInstance, createBetterAuthInstance } from './better-auth.config';
|
|
22
|
+
import { BetterAuthInstance, CreateBetterAuthResult, createBetterAuthInstance } from './better-auth.config';
|
|
23
23
|
import { DefaultBetterAuthResolver } from './better-auth.resolver';
|
|
24
24
|
import { CoreBetterAuthApiMiddleware } from './core-better-auth-api.middleware';
|
|
25
25
|
import { CoreBetterAuthChallengeService } from './core-better-auth-challenge.service';
|
|
@@ -31,7 +31,7 @@ import { CoreBetterAuthUserMapper } from './core-better-auth-user.mapper';
|
|
|
31
31
|
import { CoreBetterAuthController } from './core-better-auth.controller';
|
|
32
32
|
import { CoreBetterAuthMiddleware } from './core-better-auth.middleware';
|
|
33
33
|
import { CoreBetterAuthResolver } from './core-better-auth.resolver';
|
|
34
|
-
import { BETTER_AUTH_CONFIG, CoreBetterAuthService } from './core-better-auth.service';
|
|
34
|
+
import { BETTER_AUTH_CONFIG, BETTER_AUTH_COOKIE_DOMAIN, CoreBetterAuthService } from './core-better-auth.service';
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
37
|
* Token for injecting the better-auth instance
|
|
@@ -235,6 +235,7 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
235
235
|
private static serviceInstance: CoreBetterAuthService | null = null;
|
|
236
236
|
private static userMapperInstance: CoreBetterAuthUserMapper | null = null;
|
|
237
237
|
private static tokenServiceInstance: BetterAuthTokenService | null = null;
|
|
238
|
+
private static resolvedCookieDomain: string | undefined = undefined;
|
|
238
239
|
|
|
239
240
|
/**
|
|
240
241
|
* Gets the controller class to use (custom or default)
|
|
@@ -277,6 +278,14 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
277
278
|
return this.tokenServiceInstance;
|
|
278
279
|
}
|
|
279
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Gets the resolved cookie domain for cross-subdomain cookie sharing.
|
|
283
|
+
* Returns the domain resolved during Better-Auth instance creation, or undefined if disabled.
|
|
284
|
+
*/
|
|
285
|
+
static getCookieDomain(): string | undefined {
|
|
286
|
+
return this.resolvedCookieDomain;
|
|
287
|
+
}
|
|
288
|
+
|
|
280
289
|
constructor(
|
|
281
290
|
@Optional() private readonly betterAuthService?: CoreBetterAuthService,
|
|
282
291
|
@Optional() private readonly rateLimiter?: CoreBetterAuthRateLimiter,
|
|
@@ -614,13 +623,15 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
614
623
|
|
|
615
624
|
// Note: Secret validation is now handled in createBetterAuthInstance
|
|
616
625
|
// with fallback to jwt.secret, jwt.refresh.secret, or auto-generation
|
|
617
|
-
|
|
626
|
+
const result = createBetterAuthInstance({
|
|
618
627
|
config,
|
|
619
628
|
db,
|
|
620
629
|
fallbackSecrets,
|
|
621
630
|
onEmailVerified,
|
|
622
631
|
sendVerificationEmail,
|
|
623
632
|
});
|
|
633
|
+
this.authInstance = result?.instance ?? null;
|
|
634
|
+
this.resolvedCookieDomain = result?.cookieDomain;
|
|
624
635
|
|
|
625
636
|
// Store a config copy with the resolved secret so that consumers
|
|
626
637
|
// (CoreBetterAuthService, CoreBetterAuthController) can sign cookies.
|
|
@@ -645,17 +656,25 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
645
656
|
provide: BETTER_AUTH_CONFIG,
|
|
646
657
|
useFactory: () => this.currentConfig,
|
|
647
658
|
},
|
|
659
|
+
// Provide the resolved cookie domain for cross-subdomain cookie sharing
|
|
660
|
+
// IMPORTANT: Must depend on BETTER_AUTH_INSTANCE to ensure resolvedCookieDomain is set
|
|
661
|
+
{
|
|
662
|
+
inject: [BETTER_AUTH_INSTANCE],
|
|
663
|
+
provide: BETTER_AUTH_COOKIE_DOMAIN,
|
|
664
|
+
useFactory: () => this.resolvedCookieDomain,
|
|
665
|
+
},
|
|
648
666
|
// CoreBetterAuthService needs to be a factory that explicitly depends on BETTER_AUTH_INSTANCE
|
|
649
667
|
// to ensure proper initialization order
|
|
650
668
|
{
|
|
651
|
-
inject: [BETTER_AUTH_INSTANCE, BETTER_AUTH_CONFIG, getConnectionToken()],
|
|
669
|
+
inject: [BETTER_AUTH_INSTANCE, BETTER_AUTH_CONFIG, getConnectionToken(), BETTER_AUTH_COOKIE_DOMAIN],
|
|
652
670
|
provide: CoreBetterAuthService,
|
|
653
671
|
useFactory: (
|
|
654
672
|
authInstance: BetterAuthInstance | null,
|
|
655
673
|
resolvedConfig: IBetterAuth | null,
|
|
656
674
|
connection: Connection,
|
|
675
|
+
cookieDomain: string | undefined,
|
|
657
676
|
) => {
|
|
658
|
-
return new CoreBetterAuthService(authInstance, connection, resolvedConfig);
|
|
677
|
+
return new CoreBetterAuthService(authInstance, connection, resolvedConfig, undefined, cookieDomain);
|
|
659
678
|
},
|
|
660
679
|
},
|
|
661
680
|
CoreBetterAuthUserMapper,
|
|
@@ -714,6 +733,7 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
714
733
|
// Lazy GraphQL driver: Reset service references
|
|
715
734
|
this.serviceInstance = null;
|
|
716
735
|
this.userMapperInstance = null;
|
|
736
|
+
this.resolvedCookieDomain = undefined;
|
|
717
737
|
// Reset shared RolesGuard registry (shared with CoreAuthModule)
|
|
718
738
|
RolesGuardRegistry.reset();
|
|
719
739
|
}
|
|
@@ -856,6 +876,7 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
856
876
|
};
|
|
857
877
|
|
|
858
878
|
// Connection is now guaranteed to be established
|
|
879
|
+
let result: CreateBetterAuthResult | null;
|
|
859
880
|
const db = connection.db;
|
|
860
881
|
if (!db) {
|
|
861
882
|
// Fallback to global mongoose if connection.db is not yet available
|
|
@@ -864,16 +885,18 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
864
885
|
if (!globalDb) {
|
|
865
886
|
throw new Error('MongoDB database not available');
|
|
866
887
|
}
|
|
867
|
-
|
|
888
|
+
result = createBetterAuthInstance({
|
|
868
889
|
...sharedInstanceOptions,
|
|
869
890
|
db: globalDb,
|
|
870
891
|
});
|
|
871
892
|
} else {
|
|
872
|
-
|
|
893
|
+
result = createBetterAuthInstance({
|
|
873
894
|
...sharedInstanceOptions,
|
|
874
895
|
db,
|
|
875
896
|
});
|
|
876
897
|
}
|
|
898
|
+
this.authInstance = result?.instance ?? null;
|
|
899
|
+
this.resolvedCookieDomain = result?.cookieDomain;
|
|
877
900
|
|
|
878
901
|
// Store a config copy with the resolved secret (same as first forRoot variant)
|
|
879
902
|
const fallbacks = options?.fallbackSecrets;
|
|
@@ -902,17 +925,25 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
902
925
|
provide: BETTER_AUTH_CONFIG,
|
|
903
926
|
useFactory: () => this.currentConfig,
|
|
904
927
|
},
|
|
928
|
+
// Provide the resolved cookie domain for cross-subdomain cookie sharing
|
|
929
|
+
// IMPORTANT: Must depend on BETTER_AUTH_INSTANCE to ensure resolvedCookieDomain is set
|
|
930
|
+
{
|
|
931
|
+
inject: [BETTER_AUTH_INSTANCE],
|
|
932
|
+
provide: BETTER_AUTH_COOKIE_DOMAIN,
|
|
933
|
+
useFactory: () => this.resolvedCookieDomain,
|
|
934
|
+
},
|
|
905
935
|
// CoreBetterAuthService needs to be a factory that explicitly depends on BETTER_AUTH_INSTANCE
|
|
906
936
|
// to ensure proper initialization order
|
|
907
937
|
{
|
|
908
|
-
inject: [BETTER_AUTH_INSTANCE, BETTER_AUTH_CONFIG, getConnectionToken()],
|
|
938
|
+
inject: [BETTER_AUTH_INSTANCE, BETTER_AUTH_CONFIG, getConnectionToken(), BETTER_AUTH_COOKIE_DOMAIN],
|
|
909
939
|
provide: CoreBetterAuthService,
|
|
910
940
|
useFactory: (
|
|
911
941
|
authInstance: BetterAuthInstance | null,
|
|
912
942
|
resolvedConfig: IBetterAuth | null,
|
|
913
943
|
connection: Connection,
|
|
944
|
+
cookieDomain: string | undefined,
|
|
914
945
|
) => {
|
|
915
|
-
return new CoreBetterAuthService(authInstance, connection, resolvedConfig);
|
|
946
|
+
return new CoreBetterAuthService(authInstance, connection, resolvedConfig, undefined, cookieDomain);
|
|
916
947
|
},
|
|
917
948
|
},
|
|
918
949
|
CoreBetterAuthUserMapper,
|