@lenne.tech/nest-server 11.27.3 → 11.27.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/rules/configurable-features.md +1 -1
- package/FRAMEWORK-API.md +2 -1
- package/dist/config.env.js +1 -0
- package/dist/config.env.js.map +1 -1
- package/dist/core/common/helpers/cookies.helper.d.ts +18 -0
- package/dist/core/common/helpers/cookies.helper.js +81 -4
- package/dist/core/common/helpers/cookies.helper.js.map +1 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +1 -0
- package/dist/core/modules/better-auth/better-auth.config.js +14 -33
- package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/REQUEST-LIFECYCLE.md +8 -1
- package/migration-guides/11.27.3-to-11.27.4.md +236 -0
- package/migration-guides/11.27.4-to-11.27.5.md +241 -0
- package/package.json +7 -5
- package/src/config.env.ts +4 -0
- package/src/core/common/helpers/cookies.helper.ts +218 -12
- package/src/core/common/interfaces/server-options.interface.ts +22 -0
- package/src/core/modules/better-auth/better-auth.config.ts +20 -70
|
@@ -237,6 +237,190 @@ export function isCorsDisabled(cors: boolean | ICorsConfig | undefined): boolean
|
|
|
237
237
|
return false;
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
+
// =================================================================================================
|
|
241
|
+
// Server URL resolution
|
|
242
|
+
//
|
|
243
|
+
// Single source of truth for "which app/API origin is this server reachable under".
|
|
244
|
+
// Consumed by `buildCorsConfig()` (REST + GraphQL CORS) and by BetterAuth's `resolveUrls()`
|
|
245
|
+
// (trustedOrigins, Passkey rpId/origin, cross-subdomain cookies). Keeping one implementation
|
|
246
|
+
// is what makes the three CORS layers agree — previously each layer derived URLs on its own
|
|
247
|
+
// and they drifted (BetterAuth applied localhost defaults, the CORS layer did not).
|
|
248
|
+
// =================================================================================================
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Default URLs for local/test environments (`local`, `ci`, `e2e`).
|
|
252
|
+
*
|
|
253
|
+
* These environments run on localhost and have no deployed domain: the API listens on
|
|
254
|
+
* port 3000, the frontend app on port 3001.
|
|
255
|
+
*
|
|
256
|
+
* @since 11.27.5
|
|
257
|
+
*/
|
|
258
|
+
export const LOCALHOST_URL_DEFAULTS = {
|
|
259
|
+
apiUrl: 'http://localhost:3000',
|
|
260
|
+
appUrl: 'http://localhost:3001',
|
|
261
|
+
} as const;
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Environments that fall back to {@link LOCALHOST_URL_DEFAULTS} when no URLs are configured.
|
|
265
|
+
*
|
|
266
|
+
* @since 11.27.5
|
|
267
|
+
*/
|
|
268
|
+
export const LOCALHOST_URL_ENVS: readonly string[] = ['ci', 'e2e', 'local'];
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* The hostname label stripped from `baseUrl` to derive `appUrl`.
|
|
272
|
+
*/
|
|
273
|
+
const API_HOST_LABEL = 'api.';
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Normalizes a URL string to its http(s) origin, or `undefined` when it is not a usable
|
|
277
|
+
* http(s) URL.
|
|
278
|
+
*
|
|
279
|
+
* The `protocol` guard is security-relevant, not cosmetic: `URL.origin` serializes to the
|
|
280
|
+
* literal string `'null'` for opaque origins (any non-special scheme, e.g. `custom://host`).
|
|
281
|
+
* That string is exactly the `Origin` header a sandboxed iframe sends, so letting it into a
|
|
282
|
+
* `credentials: true` allowlist would grant credentialed access to any site able to frame a
|
|
283
|
+
* sandboxed document.
|
|
284
|
+
*/
|
|
285
|
+
function toHttpOrigin(value: string): string | undefined {
|
|
286
|
+
let url: URL;
|
|
287
|
+
try {
|
|
288
|
+
url = new URL(value);
|
|
289
|
+
} catch {
|
|
290
|
+
return undefined;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return url.origin;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Whether the URL points at the local machine (localhost, `*.localhost`, loopback IP).
|
|
302
|
+
*/
|
|
303
|
+
function isLocalhostUrl(value: string | undefined): boolean {
|
|
304
|
+
if (!value) {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
const { hostname } = new URL(value);
|
|
310
|
+
return (
|
|
311
|
+
hostname === 'localhost' || hostname.endsWith('.localhost') || hostname === '127.0.0.1' || hostname === '[::1]'
|
|
312
|
+
);
|
|
313
|
+
} catch {
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Whether stripping the leading `api.` label from the hostname leaves a deployable host.
|
|
320
|
+
*
|
|
321
|
+
* Guards two cases where a naive strip produces a bogus origin:
|
|
322
|
+
* - `api.dev` → `dev` — a bare TLD. `api.dev`/`api.io`/`api.co` are registrable domains, so
|
|
323
|
+
* this is reachable configuration, and the result would be an unreachable host in a
|
|
324
|
+
* credentialed allowlist.
|
|
325
|
+
* - `api.` → `` — not a host at all. Assigning an empty hostname is silently ignored by the
|
|
326
|
+
* `URL` setter for special schemes, so the strip would appear to succeed but do nothing.
|
|
327
|
+
*
|
|
328
|
+
* `localhost` is the one legitimate single-label host (`api.localhost` → `localhost`).
|
|
329
|
+
*/
|
|
330
|
+
function canStripApiLabel(hostname: string): boolean {
|
|
331
|
+
if (!hostname.startsWith(API_HOST_LABEL)) {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const remainder = hostname.slice(API_HOST_LABEL.length);
|
|
336
|
+
return remainder === 'localhost' || remainder.includes('.');
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Derives the frontend app URL from the API base URL by stripping a leading `api.`
|
|
341
|
+
* label from the hostname (e.g. `https://api.example.com` → `https://example.com`,
|
|
342
|
+
* `https://api.dev.example.com` → `https://dev.example.com`).
|
|
343
|
+
*
|
|
344
|
+
* Returns the origin unchanged when there is no strippable `api.` prefix, and returns the
|
|
345
|
+
* input unchanged when it is not an http(s) URL — callers decide what to do with a value
|
|
346
|
+
* they cannot normalize.
|
|
347
|
+
*
|
|
348
|
+
* @since 11.27.5
|
|
349
|
+
*/
|
|
350
|
+
export function deriveAppUrlFromBaseUrl(baseUrl: string): string {
|
|
351
|
+
const origin = toHttpOrigin(baseUrl);
|
|
352
|
+
if (!origin) {
|
|
353
|
+
return baseUrl;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const url = new URL(origin);
|
|
357
|
+
if (canStripApiLabel(url.hostname)) {
|
|
358
|
+
url.hostname = url.hostname.slice(API_HOST_LABEL.length);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return url.origin;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Where a resolved URL came from. Callers use this to emit accurate startup diagnostics
|
|
366
|
+
* without re-deriving the resolution logic.
|
|
367
|
+
*
|
|
368
|
+
* @since 11.27.5
|
|
369
|
+
*/
|
|
370
|
+
export interface IResolvedServerUrls {
|
|
371
|
+
appUrl: string | undefined;
|
|
372
|
+
appUrlSource: 'derived' | 'explicit' | 'localhost-default' | 'none';
|
|
373
|
+
baseUrl: string | undefined;
|
|
374
|
+
baseUrlSource: 'explicit' | 'localhost-default' | 'none';
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Resolves the effective app/API URLs for a server configuration.
|
|
379
|
+
*
|
|
380
|
+
* `baseUrl`: explicit → localhost default (local/ci/e2e only) → none.
|
|
381
|
+
* `appUrl`: explicit → localhost default (local/ci/e2e with a localhost `baseUrl`) →
|
|
382
|
+
* derived from `baseUrl` → none.
|
|
383
|
+
*
|
|
384
|
+
* `baseUrl` is returned verbatim (not origin-normalized) because BetterAuth passes it
|
|
385
|
+
* straight through as its `baseURL`; normalization for origin matching is the caller's job.
|
|
386
|
+
*
|
|
387
|
+
* @param input.deriveAppUrl - Set to `false` to disable the `api.`-strip derivation. The
|
|
388
|
+
* localhost defaults are unaffected — they are an explicit,
|
|
389
|
+
* documented behavior of the `local`/`ci`/`e2e` environments.
|
|
390
|
+
*
|
|
391
|
+
* @since 11.27.5
|
|
392
|
+
*/
|
|
393
|
+
export function resolveServerUrls(input: {
|
|
394
|
+
appUrl?: string;
|
|
395
|
+
baseUrl?: string;
|
|
396
|
+
deriveAppUrl?: boolean;
|
|
397
|
+
env?: string;
|
|
398
|
+
}): IResolvedServerUrls {
|
|
399
|
+
const usesLocalhostDefaults = LOCALHOST_URL_ENVS.includes(input.env ?? '');
|
|
400
|
+
|
|
401
|
+
let baseUrl = input.baseUrl;
|
|
402
|
+
let baseUrlSource: IResolvedServerUrls['baseUrlSource'] = baseUrl ? 'explicit' : 'none';
|
|
403
|
+
if (!baseUrl && usesLocalhostDefaults) {
|
|
404
|
+
baseUrl = LOCALHOST_URL_DEFAULTS.apiUrl;
|
|
405
|
+
baseUrlSource = 'localhost-default';
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (input.appUrl) {
|
|
409
|
+
return { appUrl: input.appUrl, appUrlSource: 'explicit', baseUrl, baseUrlSource };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// API on :3000 and app on :3001 — deriving from baseUrl would yield the API's own origin.
|
|
413
|
+
if (usesLocalhostDefaults && isLocalhostUrl(baseUrl)) {
|
|
414
|
+
return { appUrl: LOCALHOST_URL_DEFAULTS.appUrl, appUrlSource: 'localhost-default', baseUrl, baseUrlSource };
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (baseUrl && input.deriveAppUrl !== false) {
|
|
418
|
+
return { appUrl: deriveAppUrlFromBaseUrl(baseUrl), appUrlSource: 'derived', baseUrl, baseUrlSource };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return { appUrl: undefined, appUrlSource: 'none', baseUrl, baseUrlSource };
|
|
422
|
+
}
|
|
423
|
+
|
|
240
424
|
/**
|
|
241
425
|
* Builds a CORS configuration object from server options.
|
|
242
426
|
*
|
|
@@ -244,21 +428,30 @@ export function isCorsDisabled(cors: boolean | ICorsConfig | undefined): boolean
|
|
|
244
428
|
* 1. CORS disabled → empty object (no CORS)
|
|
245
429
|
* 2. Cookies disabled → empty object (no credentials needed, handled by simple enableCors())
|
|
246
430
|
* 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
|
|
431
|
+
* 4. `cors.allowedOrigins` + resolved `appUrl`/`baseUrl` → deduplicated origin list
|
|
432
|
+
* 5. Only resolved `appUrl`/`baseUrl` → those origins
|
|
249
433
|
* 6. Nothing configured → `{}` (no credentialed CORS — caller decides fallback)
|
|
250
434
|
*
|
|
435
|
+
* `appUrl`/`baseUrl` are resolved via {@link resolveServerUrls}, the same function BetterAuth
|
|
436
|
+
* uses, so all three CORS layers (GraphQL, REST, BetterAuth `trustedOrigins`) agree.
|
|
437
|
+
*
|
|
251
438
|
* Used by both:
|
|
252
439
|
* - `CoreModule.buildCorsConfig()` for GraphQL (Apollo) CORS
|
|
253
440
|
* - `main.ts` reference implementation for REST (Express) CORS
|
|
254
441
|
*
|
|
255
|
-
* Security
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
* explicitly (for development),
|
|
260
|
-
*
|
|
261
|
-
*
|
|
442
|
+
* Security notes:
|
|
443
|
+
* - When no origins are resolvable AND cookies are enabled, the function returns `{}` rather
|
|
444
|
+
* than `{ credentials: true, origin: true }`. Returning open CORS with credentials would
|
|
445
|
+
* allow any website to make credentialed requests. Callers should either configure
|
|
446
|
+
* `appUrl`/`baseUrl`/`allowedOrigins`, enable `cors.allowAll` explicitly (for development),
|
|
447
|
+
* or accept no credentialed CORS.
|
|
448
|
+
* - Configuring only `baseUrl` grants credentialed CORS to the derived app origin as well
|
|
449
|
+
* (`https://api.example.com` → also `https://example.com`). This is the documented
|
|
450
|
+
* `appUrl` auto-detection and matches BetterAuth's `trustedOrigins`. Deployments whose
|
|
451
|
+
* apex domain is not trusted (e.g. a third-party-hosted marketing site) must opt out with
|
|
452
|
+
* `cors.deriveAppUrl: false` and list the real frontend origin explicitly.
|
|
453
|
+
*
|
|
454
|
+
* @param options - Server options containing `cors`, `cookies`, `appUrl`, `baseUrl`, `env`
|
|
262
455
|
* @returns CORS config object for Apollo/Express, or empty object if disabled/unconfigured
|
|
263
456
|
*
|
|
264
457
|
* @since 11.25.0
|
|
@@ -279,10 +472,23 @@ export function buildCorsConfig(options: Partial<IServerOptions>): Record<string
|
|
|
279
472
|
return { credentials: true, origin: true };
|
|
280
473
|
}
|
|
281
474
|
|
|
282
|
-
// Build origin list from appUrl
|
|
475
|
+
// Build origin list from the shared URL resolution (appUrl auto-derived from baseUrl,
|
|
476
|
+
// localhost defaults for local/ci/e2e), then allowedOrigins.
|
|
477
|
+
const { appUrl, baseUrl } = resolveServerUrls({
|
|
478
|
+
appUrl: options?.appUrl,
|
|
479
|
+
baseUrl: options?.baseUrl,
|
|
480
|
+
deriveAppUrl: corsObj.deriveAppUrl,
|
|
481
|
+
env: options?.env,
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
// Normalize to origins before deduplicating: a browser's `Origin` header is always a bare
|
|
485
|
+
// scheme://host[:port] triple, so a configured `https://api.example.com/` (trailing slash —
|
|
486
|
+
// common in env-var-sourced URLs) could never match, and would defeat the Set below.
|
|
487
|
+
// Values we cannot normalize are passed through verbatim rather than dropped.
|
|
283
488
|
const origins: string[] = [];
|
|
284
|
-
|
|
285
|
-
|
|
489
|
+
for (const url of [appUrl, baseUrl]) {
|
|
490
|
+
if (url) origins.push(toHttpOrigin(url) ?? url);
|
|
491
|
+
}
|
|
286
492
|
if (corsObj.allowedOrigins?.length) {
|
|
287
493
|
origins.push(...corsObj.allowedOrigins);
|
|
288
494
|
}
|
|
@@ -1032,6 +1032,28 @@ export interface ICorsConfig {
|
|
|
1032
1032
|
*/
|
|
1033
1033
|
allowedOrigins?: string[];
|
|
1034
1034
|
|
|
1035
|
+
/**
|
|
1036
|
+
* Whether `appUrl` may be auto-derived from `baseUrl` when it is not set explicitly.
|
|
1037
|
+
*
|
|
1038
|
+
* By default the leading `api.` label is stripped from `baseUrl`'s hostname
|
|
1039
|
+
* (`https://api.example.com` → `https://example.com`), and the result is trusted by all
|
|
1040
|
+
* three CORS layers (GraphQL, REST, BetterAuth `trustedOrigins`). This is what makes the
|
|
1041
|
+
* common `api.<host>` / `<host>` deployment work without extra configuration.
|
|
1042
|
+
*
|
|
1043
|
+
* Set to `false` when the derived apex domain must NOT receive credentialed cross-origin
|
|
1044
|
+
* access — for example when `example.com` is a third-party-hosted marketing site whose
|
|
1045
|
+
* XSS surface you do not control. With `false`, configure the frontend origin explicitly
|
|
1046
|
+
* via `appUrl` or `allowedOrigins`.
|
|
1047
|
+
*
|
|
1048
|
+
* Has no effect on the localhost defaults applied for `env: 'local' | 'ci' | 'e2e'`, and
|
|
1049
|
+
* no effect when `appUrl` is set explicitly.
|
|
1050
|
+
*
|
|
1051
|
+
* @default true
|
|
1052
|
+
*
|
|
1053
|
+
* @since 11.27.5
|
|
1054
|
+
*/
|
|
1055
|
+
deriveAppUrl?: boolean;
|
|
1056
|
+
|
|
1035
1057
|
/**
|
|
1036
1058
|
* Whether CORS is enabled.
|
|
1037
1059
|
*
|
|
@@ -7,6 +7,7 @@ import * as crypto from 'crypto';
|
|
|
7
7
|
import * as fs from 'fs';
|
|
8
8
|
import * as path from 'path';
|
|
9
9
|
|
|
10
|
+
import { resolveServerUrls } from '../../common/helpers/cookies.helper';
|
|
10
11
|
import { IBetterAuth, ICorsConfig } from '../../common/interfaces/server-options.interface';
|
|
11
12
|
import { detectCookiePrefixDrift, resolveBetterAuthCookiePrefix } from './better-auth-cookie-prefix.helper';
|
|
12
13
|
|
|
@@ -914,21 +915,6 @@ function readProjectNameFromPackageJson(): string {
|
|
|
914
915
|
return fallback;
|
|
915
916
|
}
|
|
916
917
|
|
|
917
|
-
/**
|
|
918
|
-
* Default URLs for local/test environments (local, ci, e2e)
|
|
919
|
-
* These environments typically run on localhost and don't have a deployed domain.
|
|
920
|
-
*/
|
|
921
|
-
const LOCALHOST_DEFAULTS = {
|
|
922
|
-
API_URL: 'http://localhost:3000',
|
|
923
|
-
APP_URL: 'http://localhost:3001',
|
|
924
|
-
};
|
|
925
|
-
|
|
926
|
-
/**
|
|
927
|
-
* Environments that use localhost defaults for URLs.
|
|
928
|
-
* These are typically development/test environments without deployed domains.
|
|
929
|
-
*/
|
|
930
|
-
const LOCALHOST_ENVS = ['local', 'ci', 'e2e'];
|
|
931
|
-
|
|
932
918
|
/**
|
|
933
919
|
* Resolves the effective URLs for BetterAuth configuration.
|
|
934
920
|
*
|
|
@@ -948,37 +934,6 @@ interface ResolvedUrls {
|
|
|
948
934
|
warnings: string[];
|
|
949
935
|
}
|
|
950
936
|
|
|
951
|
-
/**
|
|
952
|
-
* Derives appUrl from baseUrl by removing 'api.' prefix from subdomain.
|
|
953
|
-
*
|
|
954
|
-
* Examples:
|
|
955
|
-
* - 'https://api.example.com' → 'https://example.com'
|
|
956
|
-
* - 'https://api.dev.example.com' → 'https://dev.example.com'
|
|
957
|
-
* - 'https://example.com' → 'https://example.com' (unchanged)
|
|
958
|
-
* - 'http://localhost:3000' → 'http://localhost:3000' (unchanged)
|
|
959
|
-
*
|
|
960
|
-
* @param baseUrl - The API base URL
|
|
961
|
-
* @returns Derived app URL or the original URL if no 'api.' prefix
|
|
962
|
-
*/
|
|
963
|
-
function deriveAppUrlFromBaseUrl(baseUrl: string): string {
|
|
964
|
-
try {
|
|
965
|
-
const url = new URL(baseUrl);
|
|
966
|
-
const hostname = url.hostname;
|
|
967
|
-
|
|
968
|
-
// Check if hostname starts with 'api.'
|
|
969
|
-
if (hostname.startsWith('api.')) {
|
|
970
|
-
// Remove 'api.' prefix
|
|
971
|
-
url.hostname = hostname.substring(4);
|
|
972
|
-
return url.origin;
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
// Return original URL if no 'api.' prefix
|
|
976
|
-
return url.origin;
|
|
977
|
-
} catch {
|
|
978
|
-
return baseUrl;
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
|
|
982
937
|
/**
|
|
983
938
|
* Extracts the root domain from a URL for use as Passkey rpId.
|
|
984
939
|
*
|
|
@@ -1263,36 +1218,31 @@ function deriveCookieDomainFromUrls(appUrl?: string, baseUrl?: string): string |
|
|
|
1263
1218
|
}
|
|
1264
1219
|
|
|
1265
1220
|
function resolveUrls(options: CreateBetterAuthOptions): ResolvedUrls {
|
|
1266
|
-
const { config, serverAppUrl, serverBaseUrl, serverEnv } = options;
|
|
1221
|
+
const { config, serverAppUrl, serverBaseUrl, serverCorsConfig, serverEnv } = options;
|
|
1267
1222
|
const warnings: string[] = [];
|
|
1268
|
-
const usesLocalhostDefaults = LOCALHOST_ENVS.includes(serverEnv || '');
|
|
1269
1223
|
|
|
1270
|
-
//
|
|
1271
|
-
//
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1224
|
+
// Steps 1+2 (baseUrl, appUrl) are delegated to the shared resolver in cookies.helper so that
|
|
1225
|
+
// BetterAuth's trustedOrigins and the REST/GraphQL CORS allowlist can never disagree about
|
|
1226
|
+
// which app origin this server is reachable under. `cors.deriveAppUrl: false` therefore
|
|
1227
|
+
// suppresses the `api.`-strip derivation on this layer too.
|
|
1228
|
+
const corsObj = typeof serverCorsConfig === 'object' && serverCorsConfig !== null ? serverCorsConfig : undefined;
|
|
1229
|
+
const resolved = resolveServerUrls({
|
|
1230
|
+
appUrl: serverAppUrl,
|
|
1231
|
+
// Priority: betterAuth.baseUrl > serverBaseUrl (> localhost default, applied by the resolver)
|
|
1232
|
+
baseUrl: config.baseUrl || serverBaseUrl,
|
|
1233
|
+
deriveAppUrl: corsObj?.deriveAppUrl,
|
|
1234
|
+
env: serverEnv,
|
|
1235
|
+
});
|
|
1236
|
+
const { appUrl, baseUrl } = resolved;
|
|
1237
|
+
|
|
1238
|
+
if (resolved.baseUrlSource === 'localhost-default') {
|
|
1275
1239
|
warnings.push(`URL: Using localhost default baseUrl="${baseUrl}" (env: '${serverEnv}')`);
|
|
1276
1240
|
}
|
|
1277
|
-
|
|
1278
|
-
// Step 2: Resolve appUrl
|
|
1279
|
-
// Priority: serverAppUrl > localhost default (when baseUrl is localhost) > derived from baseUrl
|
|
1280
|
-
let appUrl = serverAppUrl;
|
|
1281
|
-
|
|
1282
|
-
// For localhost environments with localhost baseUrl, use localhost app default
|
|
1283
|
-
// This handles the common case where API runs on :3000 and App on :3001
|
|
1284
|
-
const isBaseUrlLocalhost = baseUrl && (baseUrl.includes('localhost') || baseUrl.includes('127.0.0.1'));
|
|
1285
|
-
if (!appUrl && usesLocalhostDefaults && isBaseUrlLocalhost) {
|
|
1286
|
-
appUrl = LOCALHOST_DEFAULTS.APP_URL;
|
|
1241
|
+
if (resolved.appUrlSource === 'localhost-default') {
|
|
1287
1242
|
warnings.push(`URL: Using localhost default appUrl="${appUrl}" (env: '${serverEnv}')`);
|
|
1288
1243
|
}
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
if (!appUrl && baseUrl) {
|
|
1292
|
-
appUrl = deriveAppUrlFromBaseUrl(baseUrl);
|
|
1293
|
-
if (appUrl !== baseUrl) {
|
|
1294
|
-
warnings.push(`URL: Auto-derived appUrl="${appUrl}" from baseUrl="${baseUrl}"`);
|
|
1295
|
-
}
|
|
1244
|
+
if (resolved.appUrlSource === 'derived' && appUrl !== baseUrl) {
|
|
1245
|
+
warnings.push(`URL: Auto-derived appUrl="${appUrl}" from baseUrl="${baseUrl}"`);
|
|
1296
1246
|
}
|
|
1297
1247
|
|
|
1298
1248
|
// Step 3: Resolve rpId from appUrl (not baseUrl!)
|