@better-auth/infra 0.4.7 → 0.4.9

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/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { i as INFRA_USER_AGENT, n as INFRA_API_URL, o as PLUGIN_VERSION, r as INFRA_KV_URL } from "./constants-BypXAAvQ.mjs";
2
- import { a as createAPI, n as hash$1, o as createKV, r as hmacSha256Hex } from "./crypto-C9Iqqv8l.mjs";
1
+ import { i as INFRA_USER_AGENT, n as INFRA_API_URL, o as PLUGIN_VERSION, r as INFRA_KV_URL } from "./constants-CdZodAXp.mjs";
2
+ import { a as createAPI, n as hash$1, o as createKV, r as hmacSha256Hex } from "./crypto-DDadesq-.mjs";
3
3
  import { EMAIL_TEMPLATES, createEmailSender, sendBulkEmails, sendEmail } from "./email.mjs";
4
4
  import { getCurrentAdapter, getCurrentAuthContext, getCurrentDBAdapterAsyncLocalStorage, runWithTransaction } from "@better-auth/core/context";
5
5
  import { APIError, generateId, getAuthTables, logger } from "better-auth";
@@ -21,14 +21,18 @@ function resolveConnectionOptions(options) {
21
21
  apiUrl: options?.apiUrl || INFRA_API_URL,
22
22
  kvUrl: options?.kvUrl || INFRA_KV_URL,
23
23
  apiKey: options?.apiKey || env.BETTER_AUTH_API_KEY || "",
24
- apiOptions: { timeout: options?.apiOptions?.timeout ?? options?.apiTimeout ?? 3e3 },
24
+ apiOptions: {
25
+ timeout: options?.apiOptions?.timeout ?? options?.apiTimeout ?? 3e3,
26
+ headers: options?.apiOptions?.headers
27
+ },
25
28
  kvOptions: {
26
29
  timeout: options?.kvOptions?.timeout ?? options?.kvTimeout ?? 1e3,
27
30
  retry: {
28
31
  attempts: options?.kvOptions?.retry?.attempts ?? 2,
29
32
  baseDelay: options?.kvOptions?.retry?.baseDelay ?? 400,
30
33
  maxDelay: options?.kvOptions?.retry?.maxDelay ?? 600
31
- }
34
+ },
35
+ headers: options?.kvOptions?.headers
32
36
  }
33
37
  };
34
38
  }
@@ -56,6 +60,21 @@ function resolveSentinelOptions(options) {
56
60
  };
57
61
  }
58
62
  //#endregion
63
+ //#region src/types.ts
64
+ /** Type-only cast — does not wrap or mutate ctx. */
65
+ function asInfraContext(ctx) {
66
+ return ctx;
67
+ }
68
+ /**
69
+ * better-auth createAuthMiddleware with InfraAuthContext on `ctx.context`.
70
+ * Pass-through at runtime — type-only (does not wrap the handler).
71
+ *
72
+ * Prefer this for dash/sentinel/identification hooks. Keep better-auth's
73
+ * createAuthMiddleware where middleware return values must extend AuthContext
74
+ * (e.g. JWT `payload` via `use: [...]`).
75
+ */
76
+ const createAuthMiddleware$1 = createAuthMiddleware;
77
+ //#endregion
59
78
  //#region ../utils/dist/ip.mjs
60
79
  /** Platform-set client IP headers (not accepted from clients on the open internet). */
61
80
  const DEFAULT_TRUSTED_IP_HEADERS = [
@@ -77,6 +96,91 @@ function resolveClientIpFromHeaders(headers, ipAddressHeaders) {
77
96
  }
78
97
  }
79
98
  //#endregion
99
+ //#region ../utils/dist/url.mjs
100
+ /**
101
+ * URL helpers: normalization, SSRF-safe parsing/validation, and path/query segments.
102
+ */
103
+ const HTTP_PROTOCOLS = ["http:", "https:"];
104
+ function isAllowedProtocol(protocol, allowedProtocols) {
105
+ return (allowedProtocols?.length ? allowedProtocols : HTTP_PROTOCOLS).includes(protocol);
106
+ }
107
+ /** Normalizes a base URL and path by trimming slashes and joining them. */
108
+ function normalizeUrl(base, path) {
109
+ return `${base.replace(/\/+$/, "")}${path ? `/${path.replace(/^\/+|\/+$/g, "")}` : ""}`;
110
+ }
111
+ function normalizePath(path) {
112
+ const trimmed = path.trim();
113
+ if (!trimmed) throw new Error("Path must be a non-empty string");
114
+ return `/${trimmed.replace(/^\/+|\/+$/g, "")}`;
115
+ }
116
+ /** Normalizes a base URL and path into separate parts and a joined URL. */
117
+ function resolveBaseUrlPath(baseURL, basePath) {
118
+ const cleanBase = baseURL.trim().replace(/\/+$/, "");
119
+ const resolvedBasePath = normalizePath(basePath);
120
+ return {
121
+ baseURL: cleanBase,
122
+ basePath: resolvedBasePath,
123
+ url: normalizeUrl(cleanBase, resolvedBasePath)
124
+ };
125
+ }
126
+ /** Falls back when basePath is null, undefined, or blank/whitespace. */
127
+ function resolveProjectBasePath(basePath) {
128
+ return basePath?.trim() || "/api/auth";
129
+ }
130
+ /**
131
+ * Normalized project auth base URL + path (blank basePath → default).
132
+ * If `baseURL` already has a non-root pathname, that path wins over `basePath`
133
+ * (Better Auth path-in-baseURL form).
134
+ */
135
+ function resolveProjectAuthBase(baseURL, basePath) {
136
+ try {
137
+ const url = new URL(baseURL);
138
+ const pathname = url.pathname.replace(/\/+$/, "") || "";
139
+ if (pathname && pathname !== "/") return resolveBaseUrlPath(url.origin, pathname);
140
+ return resolveBaseUrlPath(url.origin, resolveProjectBasePath(basePath));
141
+ } catch {
142
+ return resolveBaseUrlPath(baseURL, resolveProjectBasePath(basePath));
143
+ }
144
+ }
145
+ function toAllowedOriginSet(allowedOrigins) {
146
+ const set = /* @__PURE__ */ new Set();
147
+ for (const raw of allowedOrigins) try {
148
+ const parsed = new URL(raw.trim());
149
+ if (isAllowedProtocol(parsed.protocol, HTTP_PROTOCOLS)) set.add(parsed.origin);
150
+ } catch {}
151
+ return set;
152
+ }
153
+ function isAllowedOrigin(origin, allowedOrigins) {
154
+ if (!allowedOrigins.length) return false;
155
+ return toAllowedOriginSet(allowedOrigins).has(origin);
156
+ }
157
+ function safeResolveUrl(raw, baseURL) {
158
+ const trim = raw.trim();
159
+ if (!trim) return null;
160
+ if (trim.startsWith("//") || trim.includes("\\")) return null;
161
+ if (/[\u0000-\u001F\u007F]/.test(trim)) return null;
162
+ try {
163
+ if (trim.startsWith("/")) return new URL(trim, baseURL);
164
+ return new URL(trim);
165
+ } catch {
166
+ return null;
167
+ }
168
+ }
169
+ function getSafeUniqueOrigins(urls) {
170
+ return [...toAllowedOriginSet(urls)];
171
+ }
172
+ /**
173
+ * Returns an absolute URL only if its protocol and origin are allowed (after
174
+ * resolving relative paths against `baseURL`). Otherwise returns `null`.
175
+ */
176
+ function safeUrlParse(input) {
177
+ if (input.raw == null || input.raw.trim() === "") return null;
178
+ const target = safeResolveUrl(input.raw, input.baseURL);
179
+ if (!target || !isAllowedProtocol(target.protocol, input.allowedProtocols)) return null;
180
+ if (!isAllowedOrigin(target.origin, input.allowedOrigins ?? [])) return null;
181
+ return target.href;
182
+ }
183
+ //#endregion
80
184
  //#region src/events/constants.ts
81
185
  const EVENT_TYPES = {
82
186
  USER_CREATED: "user_created",
@@ -1102,7 +1206,7 @@ function shouldRunIdentification(ctx, getRoutes = IDENTIFICATION_GET_ROUTES) {
1102
1206
  * needs it; together they would append duplicate Set-Cookie headers).
1103
1207
  */
1104
1208
  function createIdentificationCookieAfterMiddleware() {
1105
- return createAuthMiddleware(async (ctx) => {
1209
+ return createAuthMiddleware$1(async (ctx) => {
1106
1210
  if (ctx.context.infraRidCookieSet) return;
1107
1211
  ctx.context.infraRidCookieSet = true;
1108
1212
  const headerRequestId = ctx.request?.headers.get("X-Request-Id");
@@ -1230,7 +1334,7 @@ function resolveUntrustedVisitorId(visitorId, ip, headerVisitorId) {
1230
1334
  */
1231
1335
  function createIdentificationMiddleware($kv, options) {
1232
1336
  const retry = options?.retry ?? DEFAULT_KV_RETRY;
1233
- return createAuthMiddleware(async (ctx) => {
1337
+ return createAuthMiddleware$1(async (ctx) => {
1234
1338
  const skipIdentification = options?.skipIdentification?.(ctx) ?? false;
1235
1339
  let headerVisitorId = null;
1236
1340
  let requestId = null;
@@ -1368,7 +1472,8 @@ function createSecurityClient(conn, $api, options, onSecurityEvent) {
1368
1472
  const resolvedApiUrl = conn.apiUrl || INFRA_API_URL;
1369
1473
  const emailSender = createEmailSender({
1370
1474
  apiUrl: resolvedApiUrl,
1371
- apiKey: conn.apiKey
1475
+ apiKey: conn.apiKey,
1476
+ apiOptions: { headers: conn.apiOptions.headers }
1372
1477
  });
1373
1478
  function logEvent(event) {
1374
1479
  const fullEvent = {
@@ -2475,7 +2580,8 @@ const sentinel = (options) => {
2475
2580
  const $kv = createKV({
2476
2581
  kvUrl: opts.kvUrl,
2477
2582
  apiKey: opts.apiKey,
2478
- timeout: opts.kvOptions.timeout
2583
+ timeout: opts.kvOptions.timeout,
2584
+ headers: opts.kvOptions.headers
2479
2585
  });
2480
2586
  const { tracker } = initTrackEvents($api);
2481
2587
  const { trackEvent } = tracker;
@@ -2526,7 +2632,7 @@ const sentinel = (options) => {
2526
2632
  id: "sentinel",
2527
2633
  init(ctx) {
2528
2634
  activityTrackingEnabled = (ctx.getPlugin("dash")?.options)?.activityTracking?.enabled === true;
2529
- const basePath = new URL(ctx.baseURL).pathname;
2635
+ const basePath = ctx.baseURL ? resolveProjectAuthBase(ctx.baseURL, ctx.options.basePath).basePath : resolveProjectBasePath(ctx.options.basePath);
2530
2636
  const ipHeaders = ctx.options.advanced?.ipAddress?.ipAddressHeaders || null;
2531
2637
  const disableIpTracking = ctx.options.advanced?.ipAddress?.disableIpTracking === true;
2532
2638
  /**
@@ -2582,8 +2688,9 @@ const sentinel = (options) => {
2582
2688
  databaseHooks: {
2583
2689
  user: {
2584
2690
  create: {
2585
- async before(user, ctx) {
2586
- if (!ctx) return;
2691
+ async before(user, _ctx) {
2692
+ if (!_ctx) return;
2693
+ const ctx = asInfraContext(_ctx);
2587
2694
  if (isDashRoute(ctx.path)) {
2588
2695
  if (user.email && typeof user.email === "string" && isEmailNormalizationEnabled(opts.security)) return { data: {
2589
2696
  ...user,
@@ -2617,8 +2724,9 @@ const sentinel = (options) => {
2617
2724
  email: normalizeEmail(user.email, ctx.context)
2618
2725
  } };
2619
2726
  },
2620
- async after(user, ctx) {
2621
- if (!ctx || isDashRoute(ctx.path)) return;
2727
+ async after(user, _ctx) {
2728
+ if (!_ctx || isDashRoute(_ctx.path)) return;
2729
+ const ctx = asInfraContext(_ctx);
2622
2730
  const visitorId = ctx.context.visitorId;
2623
2731
  const reservationId = ctx.context.freeTrialReservationId;
2624
2732
  if (visitorId && reservationId && opts.security?.freeTrialAbuse?.enabled) await ctx.context.runInBackgroundOrAwait(securityService.confirmFreeTrialSignup(visitorId, reservationId, user.id, ctx.context.requestId ?? null));
@@ -2633,8 +2741,9 @@ const sentinel = (options) => {
2633
2741
  } }
2634
2742
  },
2635
2743
  session: { create: {
2636
- async before(session, ctx) {
2637
- if (!ctx || isDashRoute(ctx.path)) return;
2744
+ async before(session, _ctx) {
2745
+ if (!_ctx || isDashRoute(_ctx.path)) return;
2746
+ const ctx = asInfraContext(_ctx);
2638
2747
  const visitorId = ctx.context.visitorId;
2639
2748
  const identification = ctx.context.identification;
2640
2749
  if (session.userId && identification?.location && visitorId) {
@@ -2666,8 +2775,9 @@ const sentinel = (options) => {
2666
2775
  }
2667
2776
  }
2668
2777
  },
2669
- async after(session, ctx) {
2670
- if (!ctx || !session.userId || isDashRoute(ctx.path)) return;
2778
+ async after(session, _ctx) {
2779
+ if (!_ctx || !session.userId || isDashRoute(_ctx.path)) return;
2780
+ const ctx = asInfraContext(_ctx);
2671
2781
  const visitorId = ctx.context.visitorId;
2672
2782
  const identification = ctx.context.identification;
2673
2783
  let user = null;
@@ -2699,7 +2809,7 @@ const sentinel = (options) => {
2699
2809
  ...phoneValidationHooks.before,
2700
2810
  {
2701
2811
  matcher: (ctx) => ctx.request?.method !== "GET" && !isDashRoute(ctx.path),
2702
- handler: createAuthMiddleware(async (ctx) => {
2812
+ handler: createAuthMiddleware$1(async (ctx) => {
2703
2813
  const visitorId = ctx.context.visitorId;
2704
2814
  const identification = ctx.context.identification;
2705
2815
  const ip = ctx.context.ip;
@@ -2759,7 +2869,7 @@ const sentinel = (options) => {
2759
2869
  },
2760
2870
  {
2761
2871
  matcher: (ctx) => !!opts.security?.staleUsers?.enabled && !isDashRoute(ctx.path),
2762
- handler: createAuthMiddleware(async (ctx) => {
2872
+ handler: createAuthMiddleware$1(async (ctx) => {
2763
2873
  if (ctx.context.returned instanceof Error) return;
2764
2874
  const created = ctx.context.newSession;
2765
2875
  const userId = created?.user?.id ?? created?.session?.userId;
@@ -2806,7 +2916,7 @@ const sentinel = (options) => {
2806
2916
  },
2807
2917
  {
2808
2918
  matcher: (ctx) => ctx.request?.method !== "GET" && !isDashRoute(ctx.path),
2809
- handler: createAuthMiddleware(async (ctx) => {
2919
+ handler: createAuthMiddleware$1(async (ctx) => {
2810
2920
  const untrustedVisitorId = ctx.context.untrustedVisitorId;
2811
2921
  const ip = ctx.context.ip;
2812
2922
  const body = ctx.body;
@@ -4242,6 +4352,66 @@ const jwtValidateMiddleware = (options) => {
4242
4352
  });
4243
4353
  };
4244
4354
  //#endregion
4355
+ //#region src/lib/safe-url.ts
4356
+ function isSafeHttpUrl(value) {
4357
+ try {
4358
+ const url = new URL(value);
4359
+ return (url.protocol === "http:" || url.protocol === "https:") && url.username === "" && url.password === "";
4360
+ } catch {
4361
+ return false;
4362
+ }
4363
+ }
4364
+ /** http(s) absolute URL without embedded credentials. */
4365
+ const safeUrlSchema = z.string().trim().min(1).pipe(z.url().refine(isSafeHttpUrl, { message: "URL must be a valid http(s) URL without credentials" })).transform((value) => new URL(value).href);
4366
+ const optionalSafeUrlSchema = z.union([safeUrlSchema, z.literal("")]).optional();
4367
+ /**
4368
+ * Request-scoped full auth URL (origin + mount). Prefer over `options.baseURL`,
4369
+ * which is origin-only or a dynamic config object.
4370
+ * BA resolves this before endpoint handlers; missing value is a programming error.
4371
+ */
4372
+ function getAuthBaseUrl(ctx) {
4373
+ const resolved = ctx.context.baseURL?.trim();
4374
+ if (!resolved) throw new APIError$1("INTERNAL_SERVER_ERROR", { message: "Auth base URL is not resolved for this request." });
4375
+ return resolved;
4376
+ }
4377
+ /** App origin for post-auth redirects (not the auth mount path). */
4378
+ function getAuthOrigin(ctx) {
4379
+ return new URL(getAuthBaseUrl(ctx)).origin;
4380
+ }
4381
+ function getAllowedAuthOrigins(ctx, extraOrigins = []) {
4382
+ return getSafeUniqueOrigins([
4383
+ getAuthBaseUrl(ctx),
4384
+ ...ctx.context.trustedOrigins ?? [],
4385
+ ...extraOrigins
4386
+ ]);
4387
+ }
4388
+ /**
4389
+ * Resolve a redirect against the auth server's base URL and trusted origins.
4390
+ * Falls back when the target origin is not allowed.
4391
+ */
4392
+ function resolveTrustedAuthRedirectUrl(ctx, raw, fallback) {
4393
+ return safeUrlParse({
4394
+ raw,
4395
+ baseURL: getAuthBaseUrl(ctx),
4396
+ allowedOrigins: getAllowedAuthOrigins(ctx)
4397
+ }) ?? fallback;
4398
+ }
4399
+ /** Returns a trusted redirect URL or null when the origin is not allowed. */
4400
+ function parseTrustedAuthRedirectUrl(ctx, raw) {
4401
+ return safeUrlParse({
4402
+ raw,
4403
+ baseURL: getAuthBaseUrl(ctx),
4404
+ allowedOrigins: getAllowedAuthOrigins(ctx)
4405
+ });
4406
+ }
4407
+ const TRUSTED_CALLBACK_URL_ERROR = "Callback URL must use http(s) and match the auth base URL or a trusted origin.";
4408
+ /** Rejects email/redirect callbacks whose origin is not on the auth allowlist. */
4409
+ function requireTrustedAuthCallbackUrl(ctx, raw) {
4410
+ const trusted = parseTrustedAuthRedirectUrl(ctx, raw);
4411
+ if (!trusted) throw new APIError$1("BAD_REQUEST", { message: TRUSTED_CALLBACK_URL_ERROR });
4412
+ return trusted;
4413
+ }
4414
+ //#endregion
4245
4415
  //#region ../utils/dist/semver.mjs
4246
4416
  /**
4247
4417
  * Parses a semver string into components.
@@ -4312,6 +4482,7 @@ const getConfig = (options) => {
4312
4482
  }, async (ctx) => {
4313
4483
  const advancedOptions = ctx.context.options.advanced;
4314
4484
  const organizationPlugin = ctx.context.getPlugin("organization");
4485
+ const authBase = resolveProjectAuthBase(getAuthBaseUrl(ctx), ctx.context.options.basePath);
4315
4486
  return redactDashSettings({
4316
4487
  version: ctx.context.version || null,
4317
4488
  socialProviders: Object.keys(ctx.context.options.socialProviders || {}),
@@ -4381,8 +4552,8 @@ const getConfig = (options) => {
4381
4552
  deleteUserEnabled: !!ctx.context.options.user?.deleteUser?.enabled,
4382
4553
  modelName: ctx.context.options.user?.modelName
4383
4554
  },
4384
- baseURL: ctx.context.options.baseURL,
4385
- basePath: ctx.context.options.basePath || "/api/auth",
4555
+ baseURL: authBase.baseURL,
4556
+ basePath: authBase.basePath,
4386
4557
  emailVerification: { sendVerificationEmailEnabled: !!ctx.context.options.emailVerification?.sendVerificationEmail },
4387
4558
  insights: {
4388
4559
  hasDatabase: !!ctx.context.options.database,
@@ -4576,7 +4747,7 @@ const directorySyncClaimsSchema = z.object({
4576
4747
  setupOperationId: z.string().trim().min(16).max(255).optional()
4577
4748
  });
4578
4749
  function getScimEndpoint$1(baseUrl) {
4579
- return `${baseUrl}/scim/v2`;
4750
+ return normalizeUrl(baseUrl, "scim/v2");
4580
4751
  }
4581
4752
  function assertDirectorySyncClaims(ctx, organizationId) {
4582
4753
  const claims = ctx.context.payload;
@@ -5478,7 +5649,7 @@ async function scimProviderExists(ctx, organizationId, providerId) {
5478
5649
  }) != null;
5479
5650
  }
5480
5651
  function getScimEndpoint(baseUrl) {
5481
- return `${baseUrl}/scim/v2`;
5652
+ return normalizeUrl(baseUrl, "scim/v2");
5482
5653
  }
5483
5654
  function resolveMode(ctx) {
5484
5655
  return resolveDirectorySyncMode(getSCIMPlugin(ctx), isManagedDirectorySyncEnabled(ctx));
@@ -6037,105 +6208,6 @@ function allowsPasswordlessInviteComplete(authMode) {
6037
6208
  return authMode === "create_with_session";
6038
6209
  }
6039
6210
  //#endregion
6040
- //#region ../utils/dist/url.mjs
6041
- /**
6042
- * URL helpers: normalization, SSRF-safe parsing/validation, and path/query segments.
6043
- */
6044
- const HTTP_PROTOCOLS = ["http:", "https:"];
6045
- function isAllowedProtocol(protocol, allowedProtocols) {
6046
- return (allowedProtocols?.length ? allowedProtocols : HTTP_PROTOCOLS).includes(protocol);
6047
- }
6048
- function toAllowedOriginSet(allowedOrigins) {
6049
- const set = /* @__PURE__ */ new Set();
6050
- for (const raw of allowedOrigins) try {
6051
- const parsed = new URL(raw.trim());
6052
- if (isAllowedProtocol(parsed.protocol, HTTP_PROTOCOLS)) set.add(parsed.origin);
6053
- } catch {}
6054
- return set;
6055
- }
6056
- function isAllowedOrigin(origin, allowedOrigins) {
6057
- if (!allowedOrigins.length) return false;
6058
- return toAllowedOriginSet(allowedOrigins).has(origin);
6059
- }
6060
- function safeResolveUrl(raw, baseURL) {
6061
- const trim = raw.trim();
6062
- if (!trim) return null;
6063
- if (trim.startsWith("//") || trim.includes("\\")) return null;
6064
- if (/[\u0000-\u001F\u007F]/.test(trim)) return null;
6065
- try {
6066
- if (trim.startsWith("/")) return new URL(trim, baseURL);
6067
- return new URL(trim);
6068
- } catch {
6069
- return null;
6070
- }
6071
- }
6072
- function getSafeUniqueOrigins(urls) {
6073
- return [...toAllowedOriginSet(urls)];
6074
- }
6075
- /**
6076
- * Returns an absolute URL only if its protocol and origin are allowed (after
6077
- * resolving relative paths against `baseURL`). Otherwise returns `null`.
6078
- */
6079
- function safeUrlParse(input) {
6080
- if (input.raw == null || input.raw.trim() === "") return null;
6081
- const target = safeResolveUrl(input.raw, input.baseURL);
6082
- if (!target || !isAllowedProtocol(target.protocol, input.allowedProtocols)) return null;
6083
- if (!isAllowedOrigin(target.origin, input.allowedOrigins ?? [])) return null;
6084
- return target.href;
6085
- }
6086
- //#endregion
6087
- //#region src/lib/safe-url.ts
6088
- function isSafeHttpUrl(value) {
6089
- try {
6090
- const url = new URL(value);
6091
- return (url.protocol === "http:" || url.protocol === "https:") && url.username === "" && url.password === "";
6092
- } catch {
6093
- return false;
6094
- }
6095
- }
6096
- /** http(s) absolute URL without embedded credentials. */
6097
- const safeUrlSchema = z.string().trim().min(1).pipe(z.url().refine(isSafeHttpUrl, { message: "URL must be a valid http(s) URL without credentials" })).transform((value) => new URL(value).href);
6098
- const optionalSafeUrlSchema = z.union([safeUrlSchema, z.literal("")]).optional();
6099
- function getAuthBaseUrl(ctx) {
6100
- const baseURL = ctx.context.options.baseURL;
6101
- return typeof baseURL === "string" && baseURL.trim() ? baseURL : "/";
6102
- }
6103
- function getAllowedAuthOrigins(ctx, extraOrigins = []) {
6104
- const baseURL = getAuthBaseUrl(ctx);
6105
- const trusted = ctx.context.options.trustedOrigins;
6106
- return getSafeUniqueOrigins([
6107
- baseURL,
6108
- ...Array.isArray(trusted) ? trusted.filter((origin) => typeof origin === "string") : [],
6109
- ...extraOrigins
6110
- ]);
6111
- }
6112
- /**
6113
- * Resolve a redirect against the auth server's base URL and trusted origins.
6114
- * Falls back when the target origin is not allowed.
6115
- */
6116
- function resolveTrustedAuthRedirectUrl(ctx, raw, fallback) {
6117
- return safeUrlParse({
6118
- raw,
6119
- baseURL: getAuthBaseUrl(ctx),
6120
- allowedOrigins: getAllowedAuthOrigins(ctx)
6121
- }) ?? fallback;
6122
- }
6123
- /** Returns a trusted redirect URL or null when the origin is not allowed. */
6124
- function parseTrustedAuthRedirectUrl(ctx, raw) {
6125
- return safeUrlParse({
6126
- raw,
6127
- baseURL: getAuthBaseUrl(ctx),
6128
- allowedOrigins: getAllowedAuthOrigins(ctx)
6129
- });
6130
- }
6131
- const TRUSTED_CALLBACK_URL_ERROR = "Callback URL must use http(s) and match the auth base URL or a trusted origin.";
6132
- /** Rejects email/redirect callbacks whose origin is not on the auth allowlist. */
6133
- function requireTrustedAuthCallbackUrl(ctx, raw) {
6134
- const trusted = parseTrustedAuthRedirectUrl(ctx, raw);
6135
- if (!trusted) throw new APIError$1("BAD_REQUEST", { message: TRUSTED_CALLBACK_URL_ERROR });
6136
- return trusted;
6137
- }
6138
- //#endregion
6139
6211
  //#region src/routes/invitations/index.ts
6140
6212
  async function verifyPendingInvitation(token, $api, ctx) {
6141
6213
  const { data: invitation, error } = await $api("/api/internal/invitations/verify", {
@@ -6181,7 +6253,7 @@ async function executePlatformInvitationCompletion(ctx, $api, invitation, args)
6181
6253
  user: existingUser
6182
6254
  });
6183
6255
  }
6184
- return { redirectUrl: resolveTrustedAuthRedirectUrl(ctx, invitation.redirectUrl, getAuthBaseUrl(ctx)) };
6256
+ return { redirectUrl: resolveTrustedAuthRedirectUrl(ctx, invitation.redirectUrl, getAuthOrigin(ctx)) };
6185
6257
  }
6186
6258
  if (!password && !allowsPasswordlessInviteComplete(invitation.authMode)) throw new APIError("BAD_REQUEST", { message: "Password is required to complete this invitation." });
6187
6259
  const userPayload = {
@@ -6212,7 +6284,7 @@ async function executePlatformInvitationCompletion(ctx, $api, invitation, args)
6212
6284
  user
6213
6285
  });
6214
6286
  }
6215
- return { redirectUrl: resolveTrustedAuthRedirectUrl(ctx, invitation.redirectUrl, getAuthBaseUrl(ctx)) };
6287
+ return { redirectUrl: resolveTrustedAuthRedirectUrl(ctx, invitation.redirectUrl, getAuthOrigin(ctx)) };
6216
6288
  }
6217
6289
  /**
6218
6290
  * Accept invitation endpoint
@@ -6245,14 +6317,14 @@ const acceptInvitation = (options) => {
6245
6317
  user: existingUser
6246
6318
  });
6247
6319
  }
6248
- const redirectUrl = resolveTrustedAuthRedirectUrl(ctx, invitation.redirectUrl, getAuthBaseUrl(ctx));
6320
+ const redirectUrl = resolveTrustedAuthRedirectUrl(ctx, invitation.redirectUrl, getAuthOrigin(ctx));
6249
6321
  return ctx.redirect(redirectUrl);
6250
6322
  }
6251
6323
  if (usesPlatformInviteAcceptFlow(invitation.authMode)) {
6252
6324
  const platformUrl = options.apiUrl || INFRA_API_URL;
6253
6325
  const acceptPageUrl = new URL("/invite/accept", platformUrl);
6254
6326
  acceptPageUrl.searchParams.set("token", token);
6255
- const callbackUrl = `${ctx.context.options.baseURL}${ctx.context.options.basePath || "/api/auth"}/dash/complete-invitation`;
6327
+ const callbackUrl = normalizeUrl(ctx.context.baseURL, "dash/complete-invitation");
6256
6328
  acceptPageUrl.searchParams.set("callback", callbackUrl);
6257
6329
  return ctx.redirect(acceptPageUrl.toString());
6258
6330
  }
@@ -6280,7 +6352,7 @@ const acceptInvitation = (options) => {
6280
6352
  user
6281
6353
  });
6282
6354
  }
6283
- const redirectUrl = resolveTrustedAuthRedirectUrl(ctx, invitation.redirectUrl, getAuthBaseUrl(ctx));
6355
+ const redirectUrl = resolveTrustedAuthRedirectUrl(ctx, invitation.redirectUrl, getAuthOrigin(ctx));
6284
6356
  return ctx.redirect(redirectUrl);
6285
6357
  });
6286
6358
  };
@@ -6360,7 +6432,7 @@ const completeInvitationSocial = (options) => {
6360
6432
  if (sessionCookieToken) await ctx.context.internalAdapter.deleteSession(sessionCookieToken);
6361
6433
  deleteSessionCookie(ctx);
6362
6434
  }
6363
- return ctx.redirect(resolveTrustedAuthRedirectUrl(ctx, invitation.redirectUrl, getAuthBaseUrl(ctx)));
6435
+ return ctx.redirect(resolveTrustedAuthRedirectUrl(ctx, invitation.redirectUrl, getAuthOrigin(ctx)));
6364
6436
  });
6365
6437
  };
6366
6438
  /**
@@ -8414,7 +8486,7 @@ async function resolveSAMLConfig(samlConfig, providerId, baseURL, ctx) {
8414
8486
  const ba17OrLater = isBetterAuth17OrLater(ctx.context.version);
8415
8487
  if (saml17OrNewer && !resolvedEntryPoint) throw ctx.error("BAD_REQUEST", { message: "SAML entry point URL is required; provide entryPoint or IdP metadata with SingleSignOnService Location" });
8416
8488
  if (ba17OrLater && !idpMetadataXml && !samlConfig.entityId?.trim()) throw ctx.error("BAD_REQUEST", { message: "IdP entity ID is required when IdP metadata XML is not provided" });
8417
- const spIssuer = `${baseURL}/sso/saml2/sp/metadata?providerId=${providerId}`;
8489
+ const spIssuer = `${normalizeUrl(baseURL, "sso/saml2/sp/metadata")}?providerId=${providerId}`;
8418
8490
  const idpMetadata = idpMetadataXml ? { metadata: idpMetadataXml } : {
8419
8491
  ...ba17OrLater ? { entityID: samlConfig.entityId } : {},
8420
8492
  ...samlConfig.entryPoint ? { singleSignOnService: [{
@@ -8432,7 +8504,7 @@ async function resolveSAMLConfig(samlConfig, providerId, baseURL, ctx) {
8432
8504
  entryPoint: resolvedEntryPoint,
8433
8505
  ...samlConfig.cert ? { cert: samlConfig.cert } : {}
8434
8506
  } : {
8435
- callbackUrl: `${baseURL}/sso/saml2/sp/acs/${providerId}`,
8507
+ callbackUrl: normalizeUrl(baseURL, `sso/saml2/sp/acs/${providerId}`),
8436
8508
  spMetadata: {},
8437
8509
  entryPoint: resolvedEntryPoint ?? "",
8438
8510
  cert: samlConfig.cert ?? ""
@@ -10525,11 +10597,12 @@ function createSMSSender(config) {
10525
10597
  if (!apiKey) logger.warn("[Dash] No API key provided for SMS sending. Set BETTER_AUTH_API_KEY environment variable or pass apiKey in config.");
10526
10598
  const $api = createFetch({
10527
10599
  baseURL: apiUrl,
10600
+ timeout: config?.apiOptions?.timeout ?? config?.apiTimeout ?? 3e3,
10528
10601
  headers: {
10602
+ ...config?.apiOptions?.headers,
10529
10603
  "user-agent": INFRA_USER_AGENT,
10530
10604
  Authorization: `Bearer ${apiKey}`
10531
- },
10532
- timeout: config?.apiOptions?.timeout ?? config?.apiTimeout ?? 3e3
10605
+ }
10533
10606
  });
10534
10607
  /**
10535
10608
  * Send an SMS with OTP code
@@ -10622,7 +10695,7 @@ function setPendingSessionTracking(ctx, pending) {
10622
10695
  }
10623
10696
  async function getRequestLocation() {
10624
10697
  try {
10625
- return (await getCurrentAuthContext()).context.location;
10698
+ return asInfraContext(await getCurrentAuthContext()).context.location;
10626
10699
  } catch {
10627
10700
  return;
10628
10701
  }
@@ -10638,7 +10711,8 @@ const dash = (options) => {
10638
10711
  const $kv = createKV({
10639
10712
  kvUrl: opts.kvUrl,
10640
10713
  apiKey: opts.apiKey,
10641
- timeout: opts.kvOptions.timeout
10714
+ timeout: opts.kvOptions.timeout,
10715
+ headers: opts.kvOptions.headers
10642
10716
  });
10643
10717
  const activityUpdateInterval = opts.activityTracking?.updateInterval ?? 3e5;
10644
10718
  const scheduleLastActiveUpdate = async (ctx, userId) => {
@@ -10970,7 +11044,8 @@ const dash = (options) => {
10970
11044
  const location = ctx.context.location;
10971
11045
  if (matchesAnyRoute(path, [routes.REQUEST_PASSWORD_RESET])) trackPasswordResetRequest(verification, trigger, ctx, location);
10972
11046
  } },
10973
- delete: { async after(verification, ctx) {
11047
+ delete: { async after(verification, _ctx) {
11048
+ const ctx = _ctx;
10974
11049
  if (!ctx) return;
10975
11050
  const path = ctx.path;
10976
11051
  const maybeUserId = ctx.context.session?.user.id ?? "unknown";
@@ -10994,7 +11069,7 @@ const dash = (options) => {
10994
11069
  after: [
10995
11070
  {
10996
11071
  matcher: (ctx) => !!getPendingSessionTracking(ctx),
10997
- handler: createAuthMiddleware(async (ctx) => {
11072
+ handler: createAuthMiddleware$1(async (ctx) => {
10998
11073
  const pending = getPendingSessionTracking(ctx);
10999
11074
  if (!pending) return;
11000
11075
  trackSessionLifecycle(pending.enrichedSession, pending.userId, ctx, pending.location, resolveUserFromContext(pending.userId, ctx), pending.trackSignIn, pending.impersonatedBy);
@@ -11009,8 +11084,7 @@ const dash = (options) => {
11009
11084
  routes.DASH_IMPERSONATE_USER
11010
11085
  ]);
11011
11086
  },
11012
- handler: createAuthMiddleware(async (_ctx) => {
11013
- const ctx = _ctx;
11087
+ handler: createAuthMiddleware$1(async (ctx) => {
11014
11088
  const maybeUserId = ctx.context.session?.user.id ?? "unknown";
11015
11089
  const trigger = getTriggerInfo(ctx, maybeUserId);
11016
11090
  if (matchesAnyRoute(ctx.path, [routes.SEND_VERIFICATION_EMAIL, routes.DASH_SEND_VERIFICATION_EMAIL]) && ctx.context.session && !(ctx.context.returned instanceof Error)) trackEmailVerificationSent(ctx.context.session.session, ctx.context.session.user, trigger, ctx.context.location);
@@ -11025,7 +11099,7 @@ const dash = (options) => {
11025
11099
  handler: createIdentificationCookieAfterMiddleware()
11026
11100
  },
11027
11101
  {
11028
- handler: createAuthMiddleware(async (ctx) => {
11102
+ handler: createAuthMiddleware$1(async (ctx) => {
11029
11103
  if (!opts.activityTracking?.enabled) return;
11030
11104
  if (activityUpdateInterval === 0) return;
11031
11105
  const session = ctx.context.session || ctx.context.newSession;
package/dist/native.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as DashGetAllAuditLogsInput, i as DashClientOptions, n as DashAuditLog, o as DashGetAuditLogsInput, r as DashAuditLogsResponse, s as dashClient, t as IdentifyClientOptions } from "./identify-client-options-Be9mAljS.mjs";
1
+ import { a as DashGetAllAuditLogsInput, i as DashClientOptions, n as DashAuditLog, o as DashGetAuditLogsInput, r as DashAuditLogsResponse, s as dashClient, t as IdentifyClientOptions } from "./identify-client-options-DgKA4Eq8.mjs";
2
2
  import { BetterAuthClientPlugin } from "better-auth";
3
3
  //#region src/sentinel/native/client.d.ts
4
4
  interface SentinelNativeClientOptions {
package/dist/native.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { o as createKV, t as bytesToHex } from "./crypto-C9Iqqv8l.mjs";
2
- import { a as resolveSentinelClientIdentifyUrl, c as identify, i as solvePoWChallenge, l as dashClient, n as decodePoWChallenge, o as resolveIdentifyClientRetry, r as encodePoWSolution, t as createPowRetryTimeout } from "./pow-retry-Bk5otjQT.mjs";
1
+ import { o as createKV, t as bytesToHex } from "./crypto-DDadesq-.mjs";
2
+ import { a as resolveSentinelClientIdentifyUrl, c as identify, i as solvePoWChallenge, l as dashClient, n as decodePoWChallenge, o as resolveIdentifyClientRetry, r as encodePoWSolution, t as createPowRetryTimeout } from "./pow-retry-DRUkPcBn.mjs";
3
3
  import { env } from "@better-auth/core/env";
4
4
  import { Dimensions, InteractionManager, PixelRatio, Platform } from "react-native";
5
5
  //#region src/sentinel/native/components.ts
@@ -1,4 +1,4 @@
1
- import { i as randomBytes, n as hash, t as bytesToHex } from "./crypto-C9Iqqv8l.mjs";
1
+ import { i as randomBytes, n as hash, t as bytesToHex } from "./crypto-DDadesq-.mjs";
2
2
  //#region src/dash-client.ts
3
3
  function resolveDashUserId(input, options) {
4
4
  return input.userId || options?.resolveUserId?.({