@better-auth/infra 0.2.12 → 0.2.14

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,15 +1,17 @@
1
- import { n as INFRA_API_URL, r as INFRA_KV_URL } from "./constants-CvriWQVc.mjs";
2
- import { n as createKV, t as createAPI } from "./fetch-DiAhoiKA.mjs";
1
+ import { i as INFRA_USER_AGENT, n as INFRA_API_URL, o as PLUGIN_VERSION, r as INFRA_KV_URL } from "./constants-CLYqEwMV.mjs";
2
+ import { n as createKV, t as createAPI } from "./fetch-Bl0S3xUi.mjs";
3
3
  import { EMAIL_TEMPLATES, createEmailSender, sendBulkEmails, sendEmail } from "./email.mjs";
4
- import { APIError, generateId, getAuthTables, logger, parseState } from "better-auth";
4
+ import { getCurrentAuthContext } from "@better-auth/core/context";
5
+ import { APIError, generateId, getAuthTables, logger } from "better-auth";
5
6
  import { env } from "@better-auth/core/env";
6
7
  import { APIError as APIError$1, createAuthEndpoint, createAuthMiddleware, requestPasswordReset, sendVerificationEmailFn, sessionMiddleware } from "better-auth/api";
7
- import { getCurrentAuthContext } from "@better-auth/core/context";
8
8
  import { createFetch } from "@better-fetch/fetch";
9
9
  import { isValidPhoneNumber, parsePhoneNumberFromString } from "libphonenumber-js";
10
10
  import { createLocalJWKSet, jwtVerify } from "jose";
11
11
  import z$1, { z } from "zod";
12
12
  import { setSessionCookie } from "better-auth/cookies";
13
+ import { generateRandomString, symmetricEncrypt } from "better-auth/crypto";
14
+ import { createOTP } from "@better-auth/utils/otp";
13
15
  //#region src/options.ts
14
16
  function resolveConnectionOptions(options) {
15
17
  return {
@@ -66,6 +68,7 @@ const EVENT_TYPES = {
66
68
  USER_IMPERSONATED: "user_impersonated",
67
69
  USER_IMPERSONATED_STOPPED: "user_impersonated_stopped"
68
70
  };
71
+ const UNKNOWN_USER = "unknown";
69
72
  const routes = {
70
73
  SEND_VERIFICATION_EMAIL: "/send-verification-email",
71
74
  SIGN_IN: "/sign-in",
@@ -106,8 +109,11 @@ const routes = {
106
109
  API_KEY_CREATE: "/api-key/create",
107
110
  LINK_SOCIAL: "/link-social",
108
111
  DASH_ROUTE: "/dash",
112
+ DASH_ACCEPT_INVITATION: "/dash/accept-invitation",
113
+ DASH_COMPLETE_INVITATION_SOCIAL: "/dash/complete-invitation-social",
109
114
  DASH_UPDATE_USER: "/dash/update-user",
110
115
  DASH_REVOKE_SESSIONS_ALL: "/dash/sessions/revoke-all",
116
+ DASH_IMPERSONATE_USER: "/dash/impersonate-user",
111
117
  DASH_BAN_USER: "/dash/ban-user",
112
118
  DASH_UNBAN_USER: "/dash/unban-user",
113
119
  ADMIN_ROUTE: "/admin",
@@ -180,34 +186,6 @@ async function getUserById(userId, ctx) {
180
186
  }
181
187
  return user;
182
188
  }
183
- const getUserByIdToken = async (providerId, idToken, ctx) => {
184
- const provider = ctx.context.socialProviders.find((p) => p.id === providerId);
185
- let user = null;
186
- if (provider) try {
187
- user = await provider.getUserInfo(idToken);
188
- } catch (error) {
189
- logger.debug("[Dash] Failed to fetch user info:", error);
190
- }
191
- return user;
192
- };
193
- const getUserByAuthorizationCode = async (providerId, ctx) => {
194
- let userInfo = null;
195
- const provider = ctx.context.socialProviders.find((p) => p.id === providerId);
196
- if (provider) try {
197
- const codeVerifier = (await parseState(ctx)).codeVerifier;
198
- const { code, device_id } = ctx.query ?? {};
199
- const tokens = await provider.validateAuthorizationCode({
200
- code,
201
- codeVerifier,
202
- deviceId: device_id,
203
- redirectURI: `${ctx.context.baseURL}/callback/${provider.id}`
204
- });
205
- userInfo = await provider.getUserInfo({ ...tokens }).then((res) => res?.user);
206
- } catch (error) {
207
- logger.debug("[Dash] Failed to fetch user info:", error);
208
- }
209
- return userInfo;
210
- };
211
189
  //#endregion
212
190
  //#region src/events/core/events-account.ts
213
191
  const initAccountEvents = (tracker) => {
@@ -309,6 +287,7 @@ const routeToRegex = (route) => {
309
287
  return new RegExp(`${pattern}(?:$|[/?])`);
310
288
  };
311
289
  const matchesAnyRoute = (path, routes) => {
290
+ if (!path) return false;
312
291
  const cleanPath = stripQuery(path);
313
292
  return routes.some((route) => routeToRegex(route).test(cleanPath));
314
293
  };
@@ -344,6 +323,53 @@ const getLoginMethod = (ctx) => {
344
323
  return null;
345
324
  };
346
325
  //#endregion
326
+ //#region src/events/core/oauth-callback-user.ts
327
+ const OAUTH_CALLBACK_USER = Symbol.for("dash.oauthCallbackUser");
328
+ /**
329
+ * Stash OAuth profile on the request context when better-auth calls getUserInfo
330
+ * during the callback, before the authorization code is consumed.
331
+ */
332
+ function instrumentSocialProviders(providers) {
333
+ for (const provider of providers) {
334
+ const originalGetUserInfo = provider.getUserInfo.bind(provider);
335
+ provider.getUserInfo = async (token) => {
336
+ const result = await originalGetUserInfo(token);
337
+ const user = result?.user;
338
+ if (user) try {
339
+ const endpointCtx = await getCurrentAuthContext();
340
+ endpointCtx.context[OAUTH_CALLBACK_USER] = user;
341
+ } catch {}
342
+ return result;
343
+ };
344
+ }
345
+ }
346
+ async function resolveOAuthUser(providerId, ctx) {
347
+ const oauthUser = ctx.context[OAUTH_CALLBACK_USER];
348
+ if (!oauthUser) return null;
349
+ const email = oauthUser.email?.toLowerCase();
350
+ const accountId = oauthUser.id !== void 0 && oauthUser.id !== null ? String(oauthUser.id) : void 0;
351
+ if (email && accountId) try {
352
+ const result = await ctx.context.internalAdapter.findOAuthUser(email, accountId, providerId);
353
+ if (result?.user) return {
354
+ id: result.user.id,
355
+ email: result.user.email,
356
+ name: result.user.name
357
+ };
358
+ } catch (error) {
359
+ logger.debug("[Dash] Failed to find OAuth user:", error);
360
+ }
361
+ if (email) {
362
+ const user = await getUserByEmail(email, ctx);
363
+ if (user) return user;
364
+ }
365
+ if (!email && !oauthUser.name) return null;
366
+ return {
367
+ id: UNKNOWN_USER,
368
+ email: oauthUser.email ?? null,
369
+ name: oauthUser.name ?? "unknown"
370
+ };
371
+ }
372
+ //#endregion
347
373
  //#region src/events/core/events-session.ts
348
374
  const initSessionEvents = (tracker) => {
349
375
  const { trackEvent } = tracker;
@@ -425,7 +451,7 @@ const initSessionEvents = (tracker) => {
425
451
  };
426
452
  ctx.context.runInBackground(track());
427
453
  };
428
- const trackSessionRevokedAll = (session, trigger, ctx, _location) => {
454
+ const trackSessionRevokedAll = (session, trigger, ctx, location) => {
429
455
  const track = async () => {
430
456
  const user = await getUserById(session.userId, ctx);
431
457
  trackEvent({
@@ -438,7 +464,11 @@ const initSessionEvents = (tracker) => {
438
464
  userEmail: user?.email ?? "unknown",
439
465
  triggeredBy: trigger.triggeredBy,
440
466
  triggerContext: trigger.triggerContext
441
- }
467
+ },
468
+ ipAddress: location?.ipAddress,
469
+ city: location?.city,
470
+ country: location?.country,
471
+ countryCode: location?.countryCode
442
472
  });
443
473
  };
444
474
  ctx.context.runInBackground(track());
@@ -527,7 +557,7 @@ const initSessionEvents = (tracker) => {
527
557
  };
528
558
  ctx.context.runInBackground(track());
529
559
  };
530
- const trackEmailVerificationSent = (session, user, trigger, _ctx) => {
560
+ const trackEmailVerificationSent = (session, user, trigger, location) => {
531
561
  trackEvent({
532
562
  eventKey: session.userId,
533
563
  eventType: EVENT_TYPES.EMAIL_VERIFICATION_SENT,
@@ -539,10 +569,14 @@ const initSessionEvents = (tracker) => {
539
569
  sessionId: session.id,
540
570
  triggeredBy: trigger.triggeredBy,
541
571
  triggerContext: trigger.triggerContext
542
- }
572
+ },
573
+ ipAddress: location?.ipAddress,
574
+ city: location?.city,
575
+ country: location?.country,
576
+ countryCode: location?.countryCode
543
577
  });
544
578
  };
545
- const trackEmailSignInAttempt = (ctx, trigger) => {
579
+ const trackEmailSignInAttempt = (ctx, trigger, location) => {
546
580
  const track = async () => {
547
581
  const user = await getUserByEmail(ctx.body.email, ctx);
548
582
  trackEvent({
@@ -556,49 +590,44 @@ const initSessionEvents = (tracker) => {
556
590
  loginMethod: getLoginMethod(ctx),
557
591
  triggeredBy: user?.id ?? trigger.triggeredBy,
558
592
  triggerContext: trigger.triggerContext
559
- }
560
- });
561
- };
562
- ctx.context.runInBackground(track());
563
- };
564
- const trackSocialSignInAttempt = (ctx, trigger) => {
565
- const track = async () => {
566
- const user = await getUserByIdToken(ctx.body.provider, ctx.body.idToken, ctx);
567
- trackEvent({
568
- eventKey: user?.user.id.toString() ?? "unknown",
569
- eventType: EVENT_TYPES.USER_SIGN_IN_FAILED,
570
- eventDisplayName: "User sign-in attempt failed",
571
- eventData: {
572
- userId: user?.user.id.toString() ?? "unknown",
573
- userName: user?.user.name ?? "unknown",
574
- userEmail: user?.user.email ?? "unknown",
575
- loginMethod: getLoginMethod(ctx),
576
- triggeredBy: user?.user.id ?? trigger.triggeredBy,
577
- triggerContext: trigger.triggerContext
578
- }
593
+ },
594
+ ipAddress: location?.ipAddress,
595
+ city: location?.city,
596
+ country: location?.country,
597
+ countryCode: location?.countryCode
579
598
  });
580
599
  };
581
600
  ctx.context.runInBackground(track());
582
601
  };
583
- const trackSocialSignInRedirectionAttempt = (ctx, trigger) => {
602
+ const trackSocialSignInFailure = (ctx, providerId, trigger, location) => {
584
603
  const track = async () => {
585
- const user = await getUserByAuthorizationCode(tryDecode(ctx.params?.id), ctx);
604
+ const user = await resolveOAuthUser(providerId, ctx);
586
605
  trackEvent({
587
- eventKey: user?.id.toString() ?? "unknown",
606
+ eventKey: user?.id ?? "unknown",
588
607
  eventType: EVENT_TYPES.USER_SIGN_IN_FAILED,
589
608
  eventDisplayName: "User sign-in attempt failed",
590
609
  eventData: {
591
- userId: user?.id.toString() ?? "unknown",
610
+ userId: user?.id ?? "unknown",
592
611
  userName: user?.name ?? "unknown",
593
- userEmail: user?.id ?? "unknown",
612
+ userEmail: user?.email ?? "unknown",
594
613
  loginMethod: getLoginMethod(ctx),
595
- triggeredBy: trigger.triggeredBy,
614
+ triggeredBy: user && user.id !== "unknown" ? user.id : trigger.triggeredBy,
596
615
  triggerContext: trigger.triggerContext
597
- }
616
+ },
617
+ ipAddress: location?.ipAddress,
618
+ city: location?.city,
619
+ country: location?.country,
620
+ countryCode: location?.countryCode
598
621
  });
599
622
  };
600
623
  ctx.context.runInBackground(track());
601
624
  };
625
+ const trackSocialSignInAttempt = (ctx, trigger, location) => {
626
+ trackSocialSignInFailure(ctx, ctx.body.provider, trigger, location);
627
+ };
628
+ const trackSocialSignInRedirectionAttempt = (ctx, trigger, location) => {
629
+ trackSocialSignInFailure(ctx, tryDecode(ctx.params?.id), trigger, location);
630
+ };
602
631
  return {
603
632
  trackUserSignedIn,
604
633
  trackUserSignedOut,
@@ -983,23 +1012,22 @@ function createIdentificationMiddleware($kv) {
983
1012
  ctx.context.visitorId = visitorId;
984
1013
  const ipConfig = ctx.context.options?.advanced?.ipAddress;
985
1014
  let location;
1015
+ const requestIp = getClientIpFromRequest(ctx.request, ipConfig?.ipAddressHeaders || null);
1016
+ const requestCountryCode = getCountryCodeFromRequest(ctx.request);
986
1017
  if (ipConfig?.disableIpTracking === true) location = void 0;
987
1018
  else if (requestId && identification) {
988
1019
  const loc = getLocation(identification);
989
1020
  location = {
990
- ipAddress: identification.ip || void 0,
1021
+ ipAddress: identification.ip || requestIp || void 0,
991
1022
  city: loc?.city || void 0,
992
1023
  country: loc?.country?.name || void 0,
993
- countryCode: loc?.country?.code || void 0
994
- };
995
- } else {
996
- const ipAddress = getClientIpFromRequest(ctx.request, ipConfig?.ipAddressHeaders || null);
997
- if (ipAddress) location = {
998
- ipAddress,
999
- countryCode: getCountryCodeFromRequest(ctx.request)
1024
+ countryCode: loc?.country?.code || requestCountryCode || void 0
1000
1025
  };
1001
- else location = void 0;
1002
- }
1026
+ } else if (requestIp) location = {
1027
+ ipAddress: requestIp,
1028
+ countryCode: requestCountryCode
1029
+ };
1030
+ else location = void 0;
1003
1031
  ctx.context.location = location;
1004
1032
  ctx.context.ip = resolveSecurityIp(identification, location);
1005
1033
  ctx.context.untrustedVisitorId = resolveUntrustedVisitorId(visitorId, ctx.context.ip, headerVisitorId);
@@ -1071,9 +1099,11 @@ function emitEvaluation(ctx, trackEvent, options = {}) {
1071
1099
  ev.emitted = true;
1072
1100
  const identification = ctx.context.identification ?? null;
1073
1101
  const visitorId = ctx.context.visitorId ?? null;
1102
+ const location = ctx.context.location;
1074
1103
  const path = ctx.path ?? "";
1104
+ const ipAddress = location?.ipAddress || identification?.ip || ctx.context.ip || void 0;
1075
1105
  trackEvent({
1076
- eventKey: visitorId || identification?.ip || "unknown",
1106
+ eventKey: visitorId || ipAddress || "unknown",
1077
1107
  eventType: "security_check",
1078
1108
  eventDisplayName: ev.outcome === "blocked" ? "Security: blocked" : ev.outcome === "challenged" ? "Security: challenged" : "Security: passed",
1079
1109
  eventData: {
@@ -1086,10 +1116,10 @@ function emitEvaluation(ctx, trackEvent, options = {}) {
1086
1116
  userAgent: options.userAgent,
1087
1117
  details: ev.details
1088
1118
  },
1089
- ipAddress: identification?.ip || void 0,
1090
- city: identification?.location?.city || void 0,
1091
- country: identification?.location?.country?.name || void 0,
1092
- countryCode: identification?.location?.country?.code || void 0
1119
+ ipAddress,
1120
+ city: location?.city || identification?.location?.city || void 0,
1121
+ country: location?.country || identification?.location?.country?.name || void 0,
1122
+ countryCode: location?.countryCode || identification?.location?.country?.code || void 0
1093
1123
  });
1094
1124
  }
1095
1125
  //#endregion
@@ -1241,7 +1271,7 @@ function createSecurityClient(conn, $api, options, onSecurityEvent) {
1241
1271
  return "";
1242
1272
  }
1243
1273
  },
1244
- async checkImpossibleTravel(userId, currentLocation, visitorId, ip = null) {
1274
+ async checkImpossibleTravel(userId, currentLocation, visitorId, ip = null, powSolution) {
1245
1275
  if (!options.impossibleTravel?.enabled || !currentLocation) return null;
1246
1276
  try {
1247
1277
  const data = await $api("/security/impossible-travel", {
@@ -1251,7 +1281,8 @@ function createSecurityClient(conn, $api, options, onSecurityEvent) {
1251
1281
  visitorId,
1252
1282
  location: currentLocation,
1253
1283
  ip,
1254
- config: options
1284
+ config: options,
1285
+ ...powSolution ? { powSolution } : {}
1255
1286
  }
1256
1287
  });
1257
1288
  if (data.isImpossible) {
@@ -2190,7 +2221,9 @@ const sentinel = (options) => {
2190
2221
  const identification = ctx.context.identification;
2191
2222
  if (session.userId && identification?.location && visitorId) {
2192
2223
  recordCheck(ctx, "impossible_travel");
2193
- const travelCheck = await securityService.checkImpossibleTravel(session.userId, identification.location, visitorId, identification.ip);
2224
+ const powSolution = ctx.headers?.get?.("X-PoW-Solution") ?? null;
2225
+ const travelCheck = await securityService.checkImpossibleTravel(session.userId, identification.location, visitorId, identification.ip, powSolution);
2226
+ if (travelCheck?.powVerified) recordCheck(ctx, "pow_challenge");
2194
2227
  if (travelCheck?.isImpossible) {
2195
2228
  if (travelCheck.action === "block") {
2196
2229
  setOutcome(ctx, "blocked", "impossible_travel", {
@@ -2399,7 +2432,7 @@ const sentinel = (options) => {
2399
2432
  //#region src/events/organization/events-invitation.ts
2400
2433
  const initInvitationEvents = (tracker) => {
2401
2434
  const { trackEvent } = tracker;
2402
- const trackOrganizationMemberInvited = (organization, invitation, inviter, trigger) => {
2435
+ const trackOrganizationMemberInvited = (organization, invitation, inviter, trigger, location) => {
2403
2436
  trackEvent({
2404
2437
  eventKey: organization.id,
2405
2438
  eventType: ORGANIZATION_EVENT_TYPES.ORGANIZATION_MEMBER_INVITED,
@@ -2417,10 +2450,14 @@ const initInvitationEvents = (tracker) => {
2417
2450
  inviterEmail: inviter.email ?? "unknown",
2418
2451
  triggeredBy: trigger.triggeredBy,
2419
2452
  triggerContext: trigger.triggerContext
2420
- }
2453
+ },
2454
+ ipAddress: location?.ipAddress,
2455
+ city: location?.city,
2456
+ country: location?.country,
2457
+ countryCode: location?.countryCode
2421
2458
  });
2422
2459
  };
2423
- const trackOrganizationMemberInviteAccepted = (organization, invitation, member, acceptedBy, trigger) => {
2460
+ const trackOrganizationMemberInviteAccepted = (organization, invitation, member, acceptedBy, trigger, location) => {
2424
2461
  trackEvent({
2425
2462
  eventKey: organization.id,
2426
2463
  eventType: ORGANIZATION_EVENT_TYPES.ORGANIZATION_MEMBER_INVITE_ACCEPTED,
@@ -2440,10 +2477,14 @@ const initInvitationEvents = (tracker) => {
2440
2477
  memberRole: member.role,
2441
2478
  triggeredBy: trigger.triggeredBy,
2442
2479
  triggerContext: trigger.triggerContext
2443
- }
2480
+ },
2481
+ ipAddress: location?.ipAddress,
2482
+ city: location?.city,
2483
+ country: location?.country,
2484
+ countryCode: location?.countryCode
2444
2485
  });
2445
2486
  };
2446
- const trackOrganizationMemberInviteRejected = (organization, invitation, rejectedBy, trigger) => {
2487
+ const trackOrganizationMemberInviteRejected = (organization, invitation, rejectedBy, trigger, location) => {
2447
2488
  trackEvent({
2448
2489
  eventKey: organization.id,
2449
2490
  eventType: ORGANIZATION_EVENT_TYPES.ORGANIZATION_MEMBER_INVITE_REJECTED,
@@ -2461,10 +2502,14 @@ const initInvitationEvents = (tracker) => {
2461
2502
  rejectedByName: rejectedBy.name,
2462
2503
  triggeredBy: trigger.triggeredBy,
2463
2504
  triggerContext: trigger.triggerContext
2464
- }
2505
+ },
2506
+ ipAddress: location?.ipAddress,
2507
+ city: location?.city,
2508
+ country: location?.country,
2509
+ countryCode: location?.countryCode
2465
2510
  });
2466
2511
  };
2467
- const trackOrganizationMemberInviteCanceled = (organization, invitation, cancelledBy, trigger) => {
2512
+ const trackOrganizationMemberInviteCanceled = (organization, invitation, cancelledBy, trigger, location) => {
2468
2513
  trackEvent({
2469
2514
  eventKey: organization.id,
2470
2515
  eventType: ORGANIZATION_EVENT_TYPES.ORGANIZATION_MEMBER_INVITE_CANCELED,
@@ -2482,7 +2527,11 @@ const initInvitationEvents = (tracker) => {
2482
2527
  cancelledByEmail: cancelledBy.email ?? "unknown",
2483
2528
  triggeredBy: trigger.triggeredBy,
2484
2529
  triggerContext: trigger.triggerContext
2485
- }
2530
+ },
2531
+ ipAddress: location?.ipAddress,
2532
+ city: location?.city,
2533
+ country: location?.country,
2534
+ countryCode: location?.countryCode
2486
2535
  });
2487
2536
  };
2488
2537
  return {
@@ -2496,7 +2545,7 @@ const initInvitationEvents = (tracker) => {
2496
2545
  //#region src/events/organization/events-member.ts
2497
2546
  const initMemberEvents = (tracker) => {
2498
2547
  const { trackEvent } = tracker;
2499
- const trackOrganizationMemberAdded = (organization, member, user, trigger) => {
2548
+ const trackOrganizationMemberAdded = (organization, member, user, trigger, location) => {
2500
2549
  trackEvent({
2501
2550
  eventKey: organization.id,
2502
2551
  eventType: ORGANIZATION_EVENT_TYPES.ORGANIZATION_MEMBER_ADDED,
@@ -2512,10 +2561,14 @@ const initMemberEvents = (tracker) => {
2512
2561
  memberEmail: user.email ?? "unknown",
2513
2562
  triggeredBy: trigger.triggeredBy,
2514
2563
  triggerContext: trigger.triggerContext
2515
- }
2564
+ },
2565
+ ipAddress: location?.ipAddress,
2566
+ city: location?.city,
2567
+ country: location?.country,
2568
+ countryCode: location?.countryCode
2516
2569
  });
2517
2570
  };
2518
- const trackOrganizationMemberRemoved = (organization, member, user, trigger) => {
2571
+ const trackOrganizationMemberRemoved = (organization, member, user, trigger, location) => {
2519
2572
  trackEvent({
2520
2573
  eventKey: organization.id,
2521
2574
  eventType: ORGANIZATION_EVENT_TYPES.ORGANIZATION_MEMBER_REMOVED,
@@ -2531,10 +2584,14 @@ const initMemberEvents = (tracker) => {
2531
2584
  memberEmail: user.email ?? "unknown",
2532
2585
  triggeredBy: trigger.triggeredBy,
2533
2586
  triggerContext: trigger.triggerContext
2534
- }
2587
+ },
2588
+ ipAddress: location?.ipAddress,
2589
+ city: location?.city,
2590
+ country: location?.country,
2591
+ countryCode: location?.countryCode
2535
2592
  });
2536
2593
  };
2537
- const trackOrganizationMemberRoleUpdated = (organization, member, user, previousRole, trigger) => {
2594
+ const trackOrganizationMemberRoleUpdated = (organization, member, user, previousRole, trigger, location) => {
2538
2595
  trackEvent({
2539
2596
  eventKey: organization.id,
2540
2597
  eventType: ORGANIZATION_EVENT_TYPES.ORGANIZATION_MEMBER_ROLE_UPDATED,
@@ -2551,7 +2608,11 @@ const initMemberEvents = (tracker) => {
2551
2608
  memberEmail: user.email ?? "unknown",
2552
2609
  triggeredBy: trigger.triggeredBy,
2553
2610
  triggerContext: trigger.triggerContext
2554
- }
2611
+ },
2612
+ ipAddress: location?.ipAddress,
2613
+ city: location?.city,
2614
+ country: location?.country,
2615
+ countryCode: location?.countryCode
2555
2616
  });
2556
2617
  };
2557
2618
  return {
@@ -2564,7 +2625,7 @@ const initMemberEvents = (tracker) => {
2564
2625
  //#region src/events/organization/events-organization.ts
2565
2626
  const initOrganizationEvents = (tracker) => {
2566
2627
  const { trackEvent } = tracker;
2567
- const trackOrganizationCreated = (organization, trigger) => {
2628
+ const trackOrganizationCreated = (organization, trigger, location) => {
2568
2629
  trackEvent({
2569
2630
  eventKey: organization.id,
2570
2631
  eventType: ORGANIZATION_EVENT_TYPES.ORGANIZATION_CREATED,
@@ -2575,10 +2636,14 @@ const initOrganizationEvents = (tracker) => {
2575
2636
  organizationName: organization.name,
2576
2637
  triggeredBy: trigger.triggeredBy,
2577
2638
  triggerContext: trigger.triggerContext
2578
- }
2639
+ },
2640
+ ipAddress: location?.ipAddress,
2641
+ city: location?.city,
2642
+ country: location?.country,
2643
+ countryCode: location?.countryCode
2579
2644
  });
2580
2645
  };
2581
- const trackOrganizationUpdated = (organization, trigger) => {
2646
+ const trackOrganizationUpdated = (organization, trigger, location) => {
2582
2647
  trackEvent({
2583
2648
  eventKey: organization.id,
2584
2649
  eventType: ORGANIZATION_EVENT_TYPES.ORGANIZATION_UPDATED,
@@ -2589,7 +2654,11 @@ const initOrganizationEvents = (tracker) => {
2589
2654
  organizationName: organization.name,
2590
2655
  triggeredBy: trigger.triggeredBy,
2591
2656
  triggerContext: trigger.triggerContext
2592
- }
2657
+ },
2658
+ ipAddress: location?.ipAddress,
2659
+ city: location?.city,
2660
+ country: location?.country,
2661
+ countryCode: location?.countryCode
2593
2662
  });
2594
2663
  };
2595
2664
  return {
@@ -2601,7 +2670,7 @@ const initOrganizationEvents = (tracker) => {
2601
2670
  //#region src/events/organization/events-team.ts
2602
2671
  const initTeamEvents = (tracker) => {
2603
2672
  const { trackEvent } = tracker;
2604
- const trackOrganizationTeamCreated = (organization, team, trigger) => {
2673
+ const trackOrganizationTeamCreated = (organization, team, trigger, location) => {
2605
2674
  trackEvent({
2606
2675
  eventKey: organization.id,
2607
2676
  eventType: ORGANIZATION_EVENT_TYPES.ORGANIZATION_TEAM_CREATED,
@@ -2614,10 +2683,14 @@ const initTeamEvents = (tracker) => {
2614
2683
  teamName: team.name,
2615
2684
  triggeredBy: trigger.triggeredBy,
2616
2685
  triggerContext: trigger.triggerContext
2617
- }
2686
+ },
2687
+ ipAddress: location?.ipAddress,
2688
+ city: location?.city,
2689
+ country: location?.country,
2690
+ countryCode: location?.countryCode
2618
2691
  });
2619
2692
  };
2620
- const trackOrganizationTeamUpdated = (organization, team, trigger) => {
2693
+ const trackOrganizationTeamUpdated = (organization, team, trigger, location) => {
2621
2694
  trackEvent({
2622
2695
  eventKey: organization.id,
2623
2696
  eventType: ORGANIZATION_EVENT_TYPES.ORGANIZATION_TEAM_UPDATED,
@@ -2630,10 +2703,14 @@ const initTeamEvents = (tracker) => {
2630
2703
  teamName: team.name,
2631
2704
  triggeredBy: trigger.triggeredBy,
2632
2705
  triggerContext: trigger.triggerContext
2633
- }
2706
+ },
2707
+ ipAddress: location?.ipAddress,
2708
+ city: location?.city,
2709
+ country: location?.country,
2710
+ countryCode: location?.countryCode
2634
2711
  });
2635
2712
  };
2636
- const trackOrganizationTeamDeleted = (organization, team, trigger) => {
2713
+ const trackOrganizationTeamDeleted = (organization, team, trigger, location) => {
2637
2714
  trackEvent({
2638
2715
  eventKey: organization.id,
2639
2716
  eventType: ORGANIZATION_EVENT_TYPES.ORGANIZATION_TEAM_DELETED,
@@ -2646,10 +2723,14 @@ const initTeamEvents = (tracker) => {
2646
2723
  teamName: team.name,
2647
2724
  triggeredBy: trigger.triggeredBy,
2648
2725
  triggerContext: trigger.triggerContext
2649
- }
2726
+ },
2727
+ ipAddress: location?.ipAddress,
2728
+ city: location?.city,
2729
+ country: location?.country,
2730
+ countryCode: location?.countryCode
2650
2731
  });
2651
2732
  };
2652
- const trackOrganizationTeamMemberAdded = (organization, team, user, teamMember, trigger) => {
2733
+ const trackOrganizationTeamMemberAdded = (organization, team, user, teamMember, trigger, location) => {
2653
2734
  trackEvent({
2654
2735
  eventKey: organization.id,
2655
2736
  eventType: ORGANIZATION_EVENT_TYPES.ORGANIZATION_TEAM_MEMBER_ADDED,
@@ -2664,10 +2745,14 @@ const initTeamEvents = (tracker) => {
2664
2745
  memberName: user.name,
2665
2746
  triggeredBy: trigger.triggeredBy,
2666
2747
  triggerContext: trigger.triggerContext
2667
- }
2748
+ },
2749
+ ipAddress: location?.ipAddress,
2750
+ city: location?.city,
2751
+ country: location?.country,
2752
+ countryCode: location?.countryCode
2668
2753
  });
2669
2754
  };
2670
- const trackOrganizationTeamMemberRemoved = (organization, team, user, teamMember, trigger) => {
2755
+ const trackOrganizationTeamMemberRemoved = (organization, team, user, teamMember, trigger, location) => {
2671
2756
  trackEvent({
2672
2757
  eventKey: organization.id,
2673
2758
  eventType: ORGANIZATION_EVENT_TYPES.ORGANIZATION_TEAM_MEMBER_REMOVED,
@@ -2682,7 +2767,11 @@ const initTeamEvents = (tracker) => {
2682
2767
  memberName: user.name,
2683
2768
  triggeredBy: trigger.triggeredBy,
2684
2769
  triggerContext: trigger.triggerContext
2685
- }
2770
+ },
2771
+ ipAddress: location?.ipAddress,
2772
+ city: location?.city,
2773
+ country: location?.country,
2774
+ countryCode: location?.countryCode
2686
2775
  });
2687
2776
  };
2688
2777
  return {
@@ -2694,11 +2783,10 @@ const initTeamEvents = (tracker) => {
2694
2783
  };
2695
2784
  };
2696
2785
  //#endregion
2697
- //#region src/jwt.ts
2786
+ //#region ../utils/dist/crypto/index.mjs
2698
2787
  /**
2699
- * Hash the given value
2700
- * Note: Must match @infra/utils/crypto hash()
2701
- * @param value - The value to hash
2788
+ * One-way hash for storage and lookup. Returns hex-encoded digest.
2789
+ * Use to verify values without ever storing plaintext.
2702
2790
  */
2703
2791
  async function hash(value) {
2704
2792
  const data = new TextEncoder().encode(value);
@@ -2706,6 +2794,32 @@ async function hash(value) {
2706
2794
  const hashArray = new Uint8Array(hashBuffer);
2707
2795
  return Array.from(hashArray).map((b) => b.toString(16).padStart(2, "0")).join("");
2708
2796
  }
2797
+ function hexToBytes(hex) {
2798
+ if (hex.length % 2 !== 0) return null;
2799
+ const bytes = new Uint8Array(hex.length / 2);
2800
+ for (let i = 0; i < bytes.length; i++) {
2801
+ const byte = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
2802
+ if (Number.isNaN(byte)) return null;
2803
+ bytes[i] = byte;
2804
+ }
2805
+ return bytes;
2806
+ }
2807
+ function timingSafeEqualBytes(a, b) {
2808
+ if (a.length !== b.length) return false;
2809
+ let diff = 0;
2810
+ for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
2811
+ return diff === 0;
2812
+ }
2813
+ /** Constant-time comparison for hex-encoded digests (e.g. SHA-256 hashes). */
2814
+ function timingSafeEqualHash(a, b) {
2815
+ if (a.length !== b.length) return false;
2816
+ const aBytes = hexToBytes(a);
2817
+ const bBytes = hexToBytes(b);
2818
+ if (!aBytes || !bBytes || aBytes.length !== bBytes.length) return false;
2819
+ return timingSafeEqualBytes(aBytes, bBytes);
2820
+ }
2821
+ //#endregion
2822
+ //#region src/jwt.ts
2709
2823
  /**
2710
2824
  * Skip JTI check for JWTs issued within this many seconds.
2711
2825
  * This is safe because replay attacks require time for interception.
@@ -2773,9 +2887,8 @@ const jwtMiddleware = (options, schema, getJWT) => {
2773
2887
  });
2774
2888
  throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
2775
2889
  }
2776
- const expectedHash = await hash(options.apiKey);
2777
- if (apiKeyHash !== expectedHash) {
2778
- ctx.context.logger.warn("[Dash] API key hash is invalid", apiKeyHash, expectedHash);
2890
+ if (!timingSafeEqualHash(apiKeyHash, await hash(options.apiKey))) {
2891
+ ctx.context.logger.warn("[Dash] API key hash mismatch");
2779
2892
  throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
2780
2893
  }
2781
2894
  if (!isRecentlyIssued(payload)) {
@@ -2828,7 +2941,7 @@ const jwtValidateMiddleware = (options) => {
2828
2941
  });
2829
2942
  throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
2830
2943
  }
2831
- if (apiKeyHash !== await hash(options.apiKey)) {
2944
+ if (!timingSafeEqualHash(apiKeyHash, await hash(options.apiKey))) {
2832
2945
  ctx.context.logger.warn("[Dash] API key hash mismatch");
2833
2946
  throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
2834
2947
  }
@@ -2836,9 +2949,6 @@ const jwtValidateMiddleware = (options) => {
2836
2949
  });
2837
2950
  };
2838
2951
  //#endregion
2839
- //#region src/version.ts
2840
- const PLUGIN_VERSION = "0.2.12";
2841
- //#endregion
2842
2952
  //#region src/routes/auth/config.ts
2843
2953
  const PLUGIN_OPTIONS_EXCLUDE_KEYS = { stripe: new Set(["stripeClient"]) };
2844
2954
  const EXACT_SENSITIVE_STRING_KEYS_LOWER = new Set([...new Set([
@@ -5717,7 +5827,7 @@ const revokeAllSessions = (options) => createAuthEndpoint("/dash/sessions/revoke
5717
5827
  }, async (ctx) => {
5718
5828
  const { userId } = ctx.body;
5719
5829
  if (!await ctx.context.internalAdapter.findUserById(userId)) throw ctx.error("NOT_FOUND", { message: "User not found" });
5720
- await ctx.context.internalAdapter.deleteSessions(userId);
5830
+ await ctx.context.internalAdapter.deleteUserSessions(userId);
5721
5831
  return ctx.json({ success: true });
5722
5832
  });
5723
5833
  const revokeManySessions = (options) => createAuthEndpoint("/dash/sessions/revoke-many", {
@@ -5726,7 +5836,7 @@ const revokeManySessions = (options) => createAuthEndpoint("/dash/sessions/revok
5726
5836
  }, async (ctx) => {
5727
5837
  const { userIds } = ctx.context.payload;
5728
5838
  await withConcurrency(chunkArray(userIds, { batchSize: 50 }), async (chunk) => {
5729
- for (const userId of chunk) await ctx.context.internalAdapter.deleteSessions(userId);
5839
+ for (const userId of chunk) await ctx.context.internalAdapter.deleteUserSessions(userId);
5730
5840
  }, { concurrency: 3 });
5731
5841
  return ctx.json({
5732
5842
  success: true,
@@ -5848,39 +5958,41 @@ async function resolveHostnameAddresses(hostname) {
5848
5958
  return null;
5849
5959
  }
5850
5960
  }
5851
- function validateUrlSync(rawUrl, options = {}) {
5852
- const trimmed = rawUrl.trim();
5961
+ function validateUrlSync(input, options = {}) {
5853
5962
  let parsed;
5854
- try {
5855
- parsed = new URL(trimmed);
5856
- } catch {
5857
- return {
5858
- ok: false,
5859
- reason: "invalid_url",
5860
- url: trimmed
5861
- };
5963
+ if (input instanceof URL) parsed = input;
5964
+ else {
5965
+ const trimmed = input.trim();
5966
+ try {
5967
+ parsed = new URL(trimmed);
5968
+ } catch {
5969
+ return {
5970
+ ok: false,
5971
+ reason: "invalid_url"
5972
+ };
5973
+ }
5862
5974
  }
5863
5975
  if (!isAllowedProtocol(parsed.protocol, options.allowedProtocols)) return {
5864
5976
  ok: false,
5865
5977
  reason: "disallowed_protocol",
5866
- url: trimmed,
5978
+ url: parsed,
5867
5979
  protocol: parsed.protocol
5868
5980
  };
5869
5981
  if (parsed.username !== "" || parsed.password !== "") return {
5870
5982
  ok: false,
5871
5983
  reason: "embedded_credentials",
5872
- url: trimmed
5984
+ url: parsed
5873
5985
  };
5874
5986
  if (isPrivateHost(parsed.hostname)) return {
5875
5987
  ok: false,
5876
5988
  reason: "private_host",
5877
- url: trimmed
5989
+ url: parsed
5878
5990
  };
5879
5991
  if (options.allowedOrigins?.length) {
5880
5992
  if (!isAllowedOrigin(parsed.origin, options.allowedOrigins)) return {
5881
5993
  ok: false,
5882
5994
  reason: "disallowed_origin",
5883
- url: trimmed,
5995
+ url: parsed,
5884
5996
  hostname: parsed.hostname,
5885
5997
  origin: parsed.origin
5886
5998
  };
@@ -5891,8 +6003,8 @@ function validateUrlSync(rawUrl, options = {}) {
5891
6003
  };
5892
6004
  }
5893
6005
  /** SSRF checks with DNS resolution when available. */
5894
- async function validateUrl(rawUrl, options = {}) {
5895
- const sync = validateUrlSync(rawUrl, options);
6006
+ async function validateUrl(input, options = {}) {
6007
+ const sync = validateUrlSync(input, options);
5896
6008
  if (!sync.ok) return sync;
5897
6009
  if (options.dns === false) return sync;
5898
6010
  const hostname = sync.url.hostname;
@@ -5900,19 +6012,23 @@ async function validateUrl(rawUrl, options = {}) {
5900
6012
  if (!isIpLiteralHostname(hostname) && !addresses?.length) return {
5901
6013
  ok: false,
5902
6014
  reason: "unresolved_host",
5903
- url: sync.url.href,
6015
+ url: sync.url,
5904
6016
  hostname
5905
6017
  };
5906
6018
  if (addresses) {
5907
6019
  for (const addr of addresses) if (isPrivateHost(addr)) return {
5908
6020
  ok: false,
5909
6021
  reason: "private_dns",
5910
- url: sync.url.href,
6022
+ url: sync.url,
5911
6023
  hostname,
5912
6024
  address: addr
5913
6025
  };
5914
6026
  }
5915
- return sync;
6027
+ return {
6028
+ ok: true,
6029
+ url: sync.url,
6030
+ ...addresses ? { addresses } : {}
6031
+ };
5916
6032
  }
5917
6033
  function safeResolveUrl(raw, baseURL) {
5918
6034
  const trim = raw.trim();
@@ -5928,12 +6044,6 @@ function safeResolveUrl(raw, baseURL) {
5928
6044
  }
5929
6045
  //#endregion
5930
6046
  //#region src/validation/ssrf.ts
5931
- /**
5932
- * SSRF protection for dash plugin outbound fetches.
5933
- *
5934
- * Uses @infra/utils at build time; tsdown bundles it into published dist so
5935
- * consumers do not install the private workspace package.
5936
- */
5937
6047
  const REDIRECT_STATUSES = new Set([
5938
6048
  301,
5939
6049
  302,
@@ -5948,11 +6058,6 @@ var SsrfBlockedError = class extends Error {
5948
6058
  function isSsrfBlockedError(error) {
5949
6059
  return error instanceof SsrfBlockedError;
5950
6060
  }
5951
- /** Validates URL hostname literals and resolved DNS addresses. */
5952
- async function parseAndValidateUrl(url) {
5953
- const result = await validateUrl(url);
5954
- return result.ok ? result.url : null;
5955
- }
5956
6061
  /**
5957
6062
  * Fetch with SSRF checks on the initial URL and every redirect target.
5958
6063
  */
@@ -5960,8 +6065,9 @@ async function $outbound(url, options = {}) {
5960
6065
  const { timeout, signal: callerSignal, ...fetchInit } = options;
5961
6066
  let currentUrl = url;
5962
6067
  for (let hop = 0;; hop++) {
5963
- const validated = await parseAndValidateUrl(currentUrl);
5964
- if (!validated) throw new SsrfBlockedError("Invalid or blocked URL");
6068
+ const result = await validateUrl(currentUrl);
6069
+ if (!result.ok) throw new SsrfBlockedError("Invalid or blocked URL");
6070
+ const validated = result.url;
5965
6071
  const signals = [];
5966
6072
  if (callerSignal) signals.push(callerSignal);
5967
6073
  if (timeout !== void 0 && timeout > 0) signals.push(AbortSignal.timeout(timeout));
@@ -5976,7 +6082,7 @@ async function $outbound(url, options = {}) {
5976
6082
  const location = response.headers.get("location");
5977
6083
  const next = location ? safeResolveUrl(location, validated.href) : null;
5978
6084
  if (!next) throw new SsrfBlockedError("Invalid redirect location");
5979
- currentUrl = next.href;
6085
+ currentUrl = next;
5980
6086
  }
5981
6087
  }
5982
6088
  //#endregion
@@ -6091,23 +6197,19 @@ const oidcConfigSchema = z$1.object({
6091
6197
  });
6092
6198
  async function resolveSAMLConfig(samlConfig, providerId, baseURL, ctx) {
6093
6199
  let idpMetadataXml = samlConfig.idpMetadata?.metadata;
6094
- if (!idpMetadataXml && samlConfig.idpMetadata?.metadataUrl) {
6095
- const validatedMetadataUrl = await parseAndValidateUrl(samlConfig.idpMetadata.metadataUrl);
6096
- if (!validatedMetadataUrl) throw ctx.error("BAD_REQUEST", { message: "Invalid or blocked IdP metadata URL" });
6097
- try {
6098
- const metadataResponse = await $outbound(validatedMetadataUrl.href, {
6099
- method: "GET",
6100
- headers: { Accept: "application/xml, text/xml" },
6101
- timeout: 15e3
6102
- });
6103
- if (!metadataResponse.ok) throw ctx.error("BAD_REQUEST", { message: `Failed to fetch IdP metadata from URL: ${metadataResponse.status} ${metadataResponse.statusText}` });
6104
- idpMetadataXml = await metadataResponse.text();
6105
- } catch (e) {
6106
- if (isSsrfBlockedError(e)) throw ctx.error("BAD_REQUEST", { message: "Invalid or blocked IdP metadata URL" });
6107
- if (e instanceof APIError$1) throw e;
6108
- ctx.context.logger.error("[Dash] Failed to fetch IdP metadata from URL:", e);
6109
- throw ctx.error("BAD_REQUEST", { message: "Failed to fetch IdP metadata from URL" });
6110
- }
6200
+ if (!idpMetadataXml && samlConfig.idpMetadata?.metadataUrl) try {
6201
+ const metadataResponse = await $outbound(samlConfig.idpMetadata.metadataUrl, {
6202
+ method: "GET",
6203
+ headers: { Accept: "application/xml, text/xml" },
6204
+ timeout: 15e3
6205
+ });
6206
+ if (!metadataResponse.ok) throw ctx.error("BAD_REQUEST", { message: `Failed to fetch IdP metadata from URL: ${metadataResponse.status} ${metadataResponse.statusText}` });
6207
+ idpMetadataXml = await metadataResponse.text();
6208
+ } catch (e) {
6209
+ if (isSsrfBlockedError(e)) throw ctx.error("BAD_REQUEST", { message: "Invalid or blocked IdP metadata URL" });
6210
+ if (e instanceof APIError$1) throw e;
6211
+ ctx.context.logger.error("[Dash] Failed to fetch IdP metadata from URL:", e);
6212
+ throw ctx.error("BAD_REQUEST", { message: "Failed to fetch IdP metadata from URL" });
6111
6213
  }
6112
6214
  const metadataAlgorithmWarnings = [];
6113
6215
  if (idpMetadataXml) {
@@ -6574,6 +6676,64 @@ const markSsoProviderDomainVerified = (options) => {
6574
6676
  });
6575
6677
  };
6576
6678
  //#endregion
6679
+ //#region src/routes/two-factor/backup-codes.ts
6680
+ /** Mirrors better-auth/plugins/two-factor/backup-codes generateBackupCodesFn. */
6681
+ function generateBackupCodesPlain(options) {
6682
+ if (options?.customBackupCodesGenerate) return options.customBackupCodesGenerate();
6683
+ const amount = options?.amount ?? 10;
6684
+ const length = options?.length ?? 10;
6685
+ return Array.from({ length: amount }, () => {
6686
+ const code = generateRandomString(length, "a-z", "0-9", "A-Z");
6687
+ return `${code.slice(0, 5)}-${code.slice(5)}`;
6688
+ });
6689
+ }
6690
+ /** Mirrors better-auth/plugins/two-factor/backup-codes generateBackupCodes. */
6691
+ async function generateBackupCodes$1(secret, options) {
6692
+ const backupCodes = generateBackupCodesPlain(options);
6693
+ const json = JSON.stringify(backupCodes);
6694
+ const storeBackupCodes = options?.storeBackupCodes ?? "encrypted";
6695
+ if (storeBackupCodes === "encrypted") return {
6696
+ backupCodes,
6697
+ encryptedBackupCodes: await symmetricEncrypt({
6698
+ key: secret,
6699
+ data: json
6700
+ })
6701
+ };
6702
+ if (typeof storeBackupCodes === "object" && "encrypt" in storeBackupCodes) return {
6703
+ backupCodes,
6704
+ encryptedBackupCodes: await storeBackupCodes.encrypt(json)
6705
+ };
6706
+ return {
6707
+ backupCodes,
6708
+ encryptedBackupCodes: json
6709
+ };
6710
+ }
6711
+ function resolveBackupCodeOptions(twoFactorPlugin) {
6712
+ return {
6713
+ storeBackupCodes: "encrypted",
6714
+ ...twoFactorPlugin?.options?.backupCodeOptions
6715
+ };
6716
+ }
6717
+ //#endregion
6718
+ //#region src/routes/two-factor/two-factor-options.ts
6719
+ function resolveTotpIssuer(twoFactorPlugin, appName) {
6720
+ const options = twoFactorPlugin?.options;
6721
+ return (options?.totpOptions)?.issuer ?? options?.issuer ?? appName ?? "BetterAuth";
6722
+ }
6723
+ function resolveTotpConfig(twoFactorPlugin) {
6724
+ const totpOptions = twoFactorPlugin?.options?.totpOptions;
6725
+ return {
6726
+ digits: totpOptions?.digits ?? 6,
6727
+ period: totpOptions?.period ?? 30
6728
+ };
6729
+ }
6730
+ function buildTotpUri(params) {
6731
+ return createOTP(params.secret, {
6732
+ digits: params.digits,
6733
+ period: params.period
6734
+ }).url(params.issuer, params.account);
6735
+ }
6736
+ //#endregion
6577
6737
  //#region src/routes/two-factor/index.ts
6578
6738
  const enableTwoFactor = (options) => createAuthEndpoint("/dash/enable-two-factor", {
6579
6739
  method: "POST",
@@ -6582,31 +6742,37 @@ const enableTwoFactor = (options) => createAuthEndpoint("/dash/enable-two-factor
6582
6742
  const { userId } = ctx.context.payload;
6583
6743
  const twoFactorPlugin = ctx.context.getPlugin("two-factor");
6584
6744
  if (!twoFactorPlugin) throw new APIError("BAD_REQUEST", { message: "Two-factor authentication plugin is not enabled" });
6745
+ if (twoFactorPlugin.options?.totpOptions?.disable === true) throw new APIError("BAD_REQUEST", {
6746
+ message: "TOTP is not configured",
6747
+ code: "TOTP_NOT_CONFIGURED"
6748
+ });
6749
+ const twoFactorTable = twoFactorPlugin.options?.twoFactorTable ?? "twoFactor";
6750
+ const user = await ctx.context.internalAdapter.findUserById(userId);
6751
+ if (!user) throw new APIError("NOT_FOUND", { message: "User not found" });
6752
+ if (user.twoFactorEnabled === true) throw new APIError("BAD_REQUEST", { message: "Two-factor authentication is already enabled for this user" });
6585
6753
  if (await ctx.context.adapter.findOne({
6586
- model: "twoFactor",
6754
+ model: twoFactorTable,
6587
6755
  where: [{
6588
6756
  field: "userId",
6589
6757
  value: userId
6590
6758
  }]
6591
- })) throw new APIError("BAD_REQUEST", { message: "Two-factor authentication is already enabled for this user" });
6759
+ })) await ctx.context.adapter.delete({
6760
+ model: twoFactorTable,
6761
+ where: [{
6762
+ field: "userId",
6763
+ value: userId
6764
+ }]
6765
+ });
6592
6766
  const { generateRandomString, symmetricEncrypt } = await import("better-auth/crypto");
6593
- const secret = generateRandomString(32, "A-Z");
6594
- const backupCodeAmount = 10;
6595
- const backupCodeLength = 10;
6596
- const backupCodes = [];
6597
- for (let i = 0; i < backupCodeAmount; i++) backupCodes.push(generateId(backupCodeLength).toUpperCase());
6767
+ const secret = generateRandomString(32);
6768
+ const { backupCodes, encryptedBackupCodes } = await generateBackupCodes$1(ctx.context.secret, resolveBackupCodeOptions(twoFactorPlugin));
6598
6769
  const encryptedSecret = await symmetricEncrypt({
6599
6770
  key: ctx.context.secret,
6600
6771
  data: secret
6601
6772
  });
6602
- const encryptedBackupCodes = await symmetricEncrypt({
6603
- key: ctx.context.secret,
6604
- data: JSON.stringify(backupCodes)
6605
- });
6606
6773
  await ctx.context.adapter.create({
6607
- model: "twoFactor",
6774
+ model: twoFactorTable,
6608
6775
  data: {
6609
- id: generateId(32),
6610
6776
  userId,
6611
6777
  secret: encryptedSecret,
6612
6778
  backupCodes: encryptedBackupCodes
@@ -6616,15 +6782,47 @@ const enableTwoFactor = (options) => createAuthEndpoint("/dash/enable-two-factor
6616
6782
  twoFactorEnabled: true,
6617
6783
  updatedAt: /* @__PURE__ */ new Date()
6618
6784
  });
6619
- const user = await ctx.context.internalAdapter.findUserById(userId);
6620
- const issuer = twoFactorPlugin.options?.issuer || ctx.context.appName || "BetterAuth";
6785
+ const totpConfig = resolveTotpConfig(twoFactorPlugin);
6621
6786
  return {
6622
6787
  success: true,
6623
- totpURI: `otpauth://totp/${encodeURIComponent(issuer)}:${encodeURIComponent(user?.email || userId)}?secret=${secret}&issuer=${encodeURIComponent(issuer)}&algorithm=SHA1&digits=6&period=30`,
6788
+ totpURI: buildTotpUri({
6789
+ secret,
6790
+ issuer: resolveTotpIssuer(twoFactorPlugin, ctx.context.appName),
6791
+ account: user?.email || userId,
6792
+ digits: totpConfig.digits,
6793
+ period: totpConfig.period
6794
+ }),
6624
6795
  secret,
6625
6796
  backupCodes
6626
6797
  };
6627
6798
  });
6799
+ const completeTwoFactorSetup = (options) => createAuthEndpoint("/dash/complete-two-factor-setup", {
6800
+ method: "POST",
6801
+ use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))]
6802
+ }, async (ctx) => {
6803
+ const { userId } = ctx.context.payload;
6804
+ const twoFactorPlugin = ctx.context.getPlugin("two-factor");
6805
+ if (!twoFactorPlugin) throw new APIError("BAD_REQUEST", { message: "Two-factor authentication plugin is not enabled" });
6806
+ const user = await ctx.context.internalAdapter.findUserById(userId);
6807
+ if (!user) throw new APIError("NOT_FOUND", { message: "User not found" });
6808
+ if (user.twoFactorEnabled === true) throw new APIError("BAD_REQUEST", { message: "Two-factor authentication is already enabled for this user" });
6809
+ const twoFactorTable = twoFactorPlugin.options?.twoFactorTable ?? "twoFactor";
6810
+ if (!await ctx.context.adapter.findOne({
6811
+ model: twoFactorTable,
6812
+ where: [{
6813
+ field: "userId",
6814
+ value: userId
6815
+ }]
6816
+ })) throw new APIError("BAD_REQUEST", {
6817
+ message: "Two-factor authentication setup has not been started",
6818
+ code: "TWO_FACTOR_SETUP_NOT_PENDING"
6819
+ });
6820
+ await ctx.context.internalAdapter.updateUser(userId, {
6821
+ twoFactorEnabled: true,
6822
+ updatedAt: /* @__PURE__ */ new Date()
6823
+ });
6824
+ return { success: true };
6825
+ });
6628
6826
  const viewTwoFactorTotpUri = (options) => createAuthEndpoint("/dash/view-two-factor-totp-uri", {
6629
6827
  method: "POST",
6630
6828
  metadata: { scope: "http" },
@@ -6633,8 +6831,13 @@ const viewTwoFactorTotpUri = (options) => createAuthEndpoint("/dash/view-two-fac
6633
6831
  const { userId } = ctx.context.payload;
6634
6832
  const twoFactorPlugin = ctx.context.getPlugin("two-factor");
6635
6833
  if (!twoFactorPlugin) throw new APIError("BAD_REQUEST", { message: "Two-factor authentication plugin is not enabled" });
6834
+ if (twoFactorPlugin.options?.totpOptions?.disable === true) throw new APIError("BAD_REQUEST", {
6835
+ message: "TOTP is not configured",
6836
+ code: "TOTP_NOT_CONFIGURED"
6837
+ });
6838
+ const twoFactorTable = twoFactorPlugin.options?.twoFactorTable ?? "twoFactor";
6636
6839
  const twoFactorRecord = await ctx.context.adapter.findOne({
6637
- model: "twoFactor",
6840
+ model: twoFactorTable,
6638
6841
  where: [{
6639
6842
  field: "userId",
6640
6843
  value: userId
@@ -6650,8 +6853,14 @@ const viewTwoFactorTotpUri = (options) => createAuthEndpoint("/dash/view-two-fac
6650
6853
  });
6651
6854
  } catch {}
6652
6855
  const user = await ctx.context.internalAdapter.findUserById(userId);
6653
- const issuer = twoFactorPlugin.options?.issuer || ctx.context.appName || "BetterAuth";
6654
- return { totpURI: `otpauth://totp/${encodeURIComponent(issuer)}:${encodeURIComponent(user?.email || userId)}?secret=${secret}&issuer=${encodeURIComponent(issuer)}&algorithm=SHA1&digits=6&period=30` };
6856
+ const totpConfig = resolveTotpConfig(twoFactorPlugin);
6857
+ return { totpURI: buildTotpUri({
6858
+ secret,
6859
+ issuer: resolveTotpIssuer(twoFactorPlugin, ctx.context.appName),
6860
+ account: user?.email || userId,
6861
+ digits: totpConfig.digits,
6862
+ period: totpConfig.period
6863
+ }) };
6655
6864
  });
6656
6865
  const viewBackupCodes = (options) => createAuthEndpoint("/dash/view-backup-codes", {
6657
6866
  method: "POST",
@@ -6664,9 +6873,11 @@ const disableTwoFactor = (options) => createAuthEndpoint("/dash/disable-two-fact
6664
6873
  use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))]
6665
6874
  }, async (ctx) => {
6666
6875
  const { userId } = ctx.context.payload;
6667
- if (!ctx.context.getPlugin("two-factor")) throw new APIError("BAD_REQUEST", { message: "Two-factor authentication is not enabled" });
6876
+ const twoFactorPlugin = ctx.context.getPlugin("two-factor");
6877
+ if (!twoFactorPlugin) throw new APIError("BAD_REQUEST", { message: "Two-factor authentication is not enabled" });
6878
+ const twoFactorTable = twoFactorPlugin.options?.twoFactorTable ?? "twoFactor";
6668
6879
  await ctx.context.adapter.delete({
6669
- model: "twoFactor",
6880
+ model: twoFactorTable,
6670
6881
  where: [{
6671
6882
  field: "userId",
6672
6883
  value: userId
@@ -6683,25 +6894,19 @@ const generateBackupCodes = (options) => createAuthEndpoint("/dash/generate-back
6683
6894
  use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))]
6684
6895
  }, async (ctx) => {
6685
6896
  const { userId } = ctx.context.payload;
6897
+ const twoFactorPlugin = ctx.context.getPlugin("two-factor");
6898
+ const twoFactorTable = twoFactorPlugin?.options?.twoFactorTable ?? "twoFactor";
6686
6899
  const twoFactorRecord = await ctx.context.adapter.findOne({
6687
- model: "twoFactor",
6900
+ model: twoFactorTable,
6688
6901
  where: [{
6689
6902
  field: "userId",
6690
6903
  value: userId
6691
6904
  }]
6692
6905
  });
6693
6906
  if (!twoFactorRecord) throw new APIError("NOT_FOUND", { message: "Two-factor authentication not set up for this user" });
6694
- const backupCodeAmount = 10;
6695
- const backupCodeLength = 10;
6696
- const newBackupCodes = [];
6697
- for (let i = 0; i < backupCodeAmount; i++) newBackupCodes.push(generateId(backupCodeLength).toUpperCase());
6698
- const { symmetricEncrypt } = await import("better-auth/crypto");
6699
- const encryptedCodes = await symmetricEncrypt({
6700
- key: ctx.context.secret,
6701
- data: JSON.stringify(newBackupCodes)
6702
- });
6907
+ const { backupCodes: newBackupCodes, encryptedBackupCodes: encryptedCodes } = await generateBackupCodes$1(ctx.context.secret, resolveBackupCodeOptions(twoFactorPlugin));
6703
6908
  await ctx.context.adapter.update({
6704
- model: "twoFactor",
6909
+ model: twoFactorTable,
6705
6910
  where: [{
6706
6911
  field: "id",
6707
6912
  value: twoFactorRecord.id
@@ -6711,6 +6916,20 @@ const generateBackupCodes = (options) => createAuthEndpoint("/dash/generate-back
6711
6916
  return { backupCodes: newBackupCodes };
6712
6917
  });
6713
6918
  //#endregion
6919
+ //#region src/routes/two-factor/two-factor-status.ts
6920
+ async function resolveTwoFactorStatus(context, userId, user) {
6921
+ if (!hasPlugin(context, "two-factor")) return;
6922
+ if (user.twoFactorEnabled === true) return "enabled";
6923
+ const twoFactorTable = context.getPlugin("two-factor")?.options?.twoFactorTable ?? "twoFactor";
6924
+ return await context.adapter.findOne({
6925
+ model: twoFactorTable,
6926
+ where: [{
6927
+ field: "userId",
6928
+ value: userId
6929
+ }]
6930
+ }) ? "pending" : "disabled";
6931
+ }
6932
+ //#endregion
6714
6933
  //#region src/routes/users/index.ts
6715
6934
  function parseWhereClause(val) {
6716
6935
  if (!val) return [];
@@ -7064,6 +7283,7 @@ const getUserDetails = (options) => {
7064
7283
  } }
7065
7284
  });
7066
7285
  if (!user) throw ctx.error("NOT_FOUND", { message: "User not found" });
7286
+ const twoFactorStatus = await resolveTwoFactorStatus(ctx.context, userId, user);
7067
7287
  if (minimal) return {
7068
7288
  ...user,
7069
7289
  lastActiveAt: user.lastActiveAt ?? null,
@@ -7071,7 +7291,8 @@ const getUserDetails = (options) => {
7071
7291
  banReason: hasAdminPlugin ? user.banReason ?? null : null,
7072
7292
  banExpires: hasAdminPlugin ? user.banExpires ?? null : null,
7073
7293
  account: [],
7074
- session: []
7294
+ session: [],
7295
+ twoFactorStatus
7075
7296
  };
7076
7297
  const activityTrackingEnabled = !!options.activityTracking?.enabled;
7077
7298
  const sessions = secondaryStorageOnly ? await ctx.context.internalAdapter.listSessions(userId) : user.session || [];
@@ -7109,7 +7330,8 @@ const getUserDetails = (options) => {
7109
7330
  lastActiveAt,
7110
7331
  banned: hasAdminPlugin ? user.banned ?? false : false,
7111
7332
  banReason: hasAdminPlugin ? user.banReason ?? null : null,
7112
- banExpires: hasAdminPlugin ? user.banExpires ?? null : null
7333
+ banExpires: hasAdminPlugin ? user.banExpires ?? null : null,
7334
+ twoFactorStatus
7113
7335
  };
7114
7336
  });
7115
7337
  };
@@ -7651,7 +7873,7 @@ const banUser = (options) => createAuthEndpoint("/dash/ban-user", {
7651
7873
  banExpires: banExpires ? new Date(banExpires) : null,
7652
7874
  updatedAt: /* @__PURE__ */ new Date()
7653
7875
  });
7654
- await ctx.context.internalAdapter.deleteSessions(userId);
7876
+ await ctx.context.internalAdapter.deleteUserSessions(userId);
7655
7877
  return { success: true };
7656
7878
  });
7657
7879
  const banManyUsers = (options) => {
@@ -7906,7 +8128,10 @@ function createSMSSender(config) {
7906
8128
  if (!apiKey) logger.warn("[Dash] No API key provided for SMS sending. Set BETTER_AUTH_API_KEY environment variable or pass apiKey in config.");
7907
8129
  const $api = createFetch({
7908
8130
  baseURL: apiUrl,
7909
- headers: { Authorization: `Bearer ${apiKey}` },
8131
+ headers: {
8132
+ "user-agent": INFRA_USER_AGENT,
8133
+ Authorization: `Bearer ${apiKey}`
8134
+ },
7910
8135
  timeout: config?.apiTimeout ?? 3e3
7911
8136
  });
7912
8137
  /**
@@ -7981,6 +8206,13 @@ async function sendSMS(options, config) {
7981
8206
  }
7982
8207
  //#endregion
7983
8208
  //#region src/index.ts
8209
+ async function getRequestLocation() {
8210
+ try {
8211
+ return (await getCurrentAuthContext()).context.location;
8212
+ } catch {
8213
+ return;
8214
+ }
8215
+ }
7984
8216
  const dash = (options) => {
7985
8217
  const processedBulkOperationContexts = /* @__PURE__ */ new WeakSet();
7986
8218
  const opts = resolveDashOptions(options);
@@ -8005,6 +8237,7 @@ const dash = (options) => {
8005
8237
  options: opts,
8006
8238
  version: PLUGIN_VERSION,
8007
8239
  init(ctx) {
8240
+ instrumentSocialProviders(ctx.socialProviders);
8008
8241
  const organizationPlugin = ctx.getPlugin("organization");
8009
8242
  if (organizationPlugin) {
8010
8243
  const instrumentOrganizationHooks = (organizationPluginOptions) => {
@@ -8012,85 +8245,85 @@ const dash = (options) => {
8012
8245
  const afterCreateOrganization = organizationHooks.afterCreateOrganization;
8013
8246
  organizationHooks.afterCreateOrganization = async (...args) => {
8014
8247
  const [{ organization, user }] = args;
8015
- trackOrganizationCreated(organization, getOrganizationTriggerInfo(user));
8248
+ trackOrganizationCreated(organization, getOrganizationTriggerInfo(user), await getRequestLocation());
8016
8249
  if (afterCreateOrganization) return afterCreateOrganization(...args);
8017
8250
  };
8018
8251
  const afterUpdateOrganization = organizationHooks.afterUpdateOrganization;
8019
8252
  organizationHooks.afterUpdateOrganization = async (...args) => {
8020
8253
  const [{ organization, user }] = args;
8021
- if (organization) trackOrganizationUpdated(organization, getOrganizationTriggerInfo(user));
8254
+ if (organization) trackOrganizationUpdated(organization, getOrganizationTriggerInfo(user), await getRequestLocation());
8022
8255
  if (afterUpdateOrganization) return afterUpdateOrganization(...args);
8023
8256
  };
8024
8257
  const afterAddMember = organizationHooks.afterAddMember;
8025
8258
  organizationHooks.afterAddMember = async (...args) => {
8026
8259
  const [{ organization, member, user }] = args;
8027
- trackOrganizationMemberAdded(organization, member, user, getOrganizationTriggerInfo(user));
8260
+ trackOrganizationMemberAdded(organization, member, user, getOrganizationTriggerInfo(user), await getRequestLocation());
8028
8261
  if (afterAddMember) return afterAddMember(...args);
8029
8262
  };
8030
8263
  const afterRemoveMember = organizationHooks.afterRemoveMember;
8031
8264
  organizationHooks.afterRemoveMember = async (...args) => {
8032
8265
  const [{ organization, member, user }] = args;
8033
- trackOrganizationMemberRemoved(organization, member, user, getOrganizationTriggerInfo(user));
8266
+ trackOrganizationMemberRemoved(organization, member, user, getOrganizationTriggerInfo(user), await getRequestLocation());
8034
8267
  if (afterRemoveMember) return afterRemoveMember(...args);
8035
8268
  };
8036
8269
  const afterUpdateMemberRole = organizationHooks.afterUpdateMemberRole;
8037
8270
  organizationHooks.afterUpdateMemberRole = async (...args) => {
8038
8271
  const [{ organization, member, user, previousRole }] = args;
8039
- trackOrganizationMemberRoleUpdated(organization, member, user, previousRole, getOrganizationTriggerInfo(user));
8272
+ trackOrganizationMemberRoleUpdated(organization, member, user, previousRole, getOrganizationTriggerInfo(user), await getRequestLocation());
8040
8273
  if (afterUpdateMemberRole) return afterUpdateMemberRole(...args);
8041
8274
  };
8042
8275
  const afterCreateInvitation = organizationHooks.afterCreateInvitation;
8043
8276
  organizationHooks.afterCreateInvitation = async (...args) => {
8044
8277
  const [{ organization, invitation, inviter }] = args;
8045
- trackOrganizationMemberInvited(organization, invitation, inviter, getOrganizationTriggerInfo(inviter));
8278
+ trackOrganizationMemberInvited(organization, invitation, inviter, getOrganizationTriggerInfo(inviter), await getRequestLocation());
8046
8279
  if (afterCreateInvitation) return afterCreateInvitation(...args);
8047
8280
  };
8048
8281
  const afterAcceptInvitation = organizationHooks.afterAcceptInvitation;
8049
8282
  organizationHooks.afterAcceptInvitation = async (...args) => {
8050
8283
  const [{ organization, invitation, member, user }] = args;
8051
- trackOrganizationMemberInviteAccepted(organization, invitation, member, user, getOrganizationTriggerInfo(user));
8284
+ trackOrganizationMemberInviteAccepted(organization, invitation, member, user, getOrganizationTriggerInfo(user), await getRequestLocation());
8052
8285
  if (afterAcceptInvitation) return afterAcceptInvitation(...args);
8053
8286
  };
8054
8287
  const afterRejectInvitation = organizationHooks.afterRejectInvitation;
8055
8288
  organizationHooks.afterRejectInvitation = async (...args) => {
8056
8289
  const [{ organization, invitation, user }] = args;
8057
- trackOrganizationMemberInviteRejected(organization, invitation, user, getOrganizationTriggerInfo(user));
8290
+ trackOrganizationMemberInviteRejected(organization, invitation, user, getOrganizationTriggerInfo(user), await getRequestLocation());
8058
8291
  if (afterRejectInvitation) return afterRejectInvitation(...args);
8059
8292
  };
8060
8293
  const afterCancelInvitation = organizationHooks.afterCancelInvitation;
8061
8294
  organizationHooks.afterCancelInvitation = async (...args) => {
8062
8295
  const [{ organization, invitation, cancelledBy }] = args;
8063
- trackOrganizationMemberInviteCanceled(organization, invitation, cancelledBy, getOrganizationTriggerInfo(cancelledBy));
8296
+ trackOrganizationMemberInviteCanceled(organization, invitation, cancelledBy, getOrganizationTriggerInfo(cancelledBy), await getRequestLocation());
8064
8297
  if (afterCancelInvitation) return afterCancelInvitation(...args);
8065
8298
  };
8066
8299
  const afterCreateTeam = organizationHooks.afterCreateTeam;
8067
8300
  organizationHooks.afterCreateTeam = async (...args) => {
8068
8301
  const [{ organization, team, user }] = args;
8069
- trackOrganizationTeamCreated(organization, team, getOrganizationTriggerInfo(user));
8302
+ trackOrganizationTeamCreated(organization, team, getOrganizationTriggerInfo(user), await getRequestLocation());
8070
8303
  if (afterCreateTeam) return afterCreateTeam(...args);
8071
8304
  };
8072
8305
  const afterUpdateTeam = organizationHooks.afterUpdateTeam;
8073
8306
  organizationHooks.afterUpdateTeam = async (...args) => {
8074
8307
  const [{ organization, team, user }] = args;
8075
- if (team) trackOrganizationTeamUpdated(organization, team, getOrganizationTriggerInfo(user));
8308
+ if (team) trackOrganizationTeamUpdated(organization, team, getOrganizationTriggerInfo(user), await getRequestLocation());
8076
8309
  if (afterUpdateTeam) return afterUpdateTeam(...args);
8077
8310
  };
8078
8311
  const afterDeleteTeam = organizationHooks.afterDeleteTeam;
8079
8312
  organizationHooks.afterDeleteTeam = async (...args) => {
8080
8313
  const [{ organization, team, user }] = args;
8081
- trackOrganizationTeamDeleted(organization, team, getOrganizationTriggerInfo(user));
8314
+ trackOrganizationTeamDeleted(organization, team, getOrganizationTriggerInfo(user), await getRequestLocation());
8082
8315
  if (afterDeleteTeam) return afterDeleteTeam(...args);
8083
8316
  };
8084
8317
  const afterAddTeamMember = organizationHooks.afterAddTeamMember;
8085
8318
  organizationHooks.afterAddTeamMember = async (...args) => {
8086
8319
  const [{ organization, team, user, teamMember }] = args;
8087
- trackOrganizationTeamMemberAdded(organization, team, user, teamMember, getOrganizationTriggerInfo(user));
8320
+ trackOrganizationTeamMemberAdded(organization, team, user, teamMember, getOrganizationTriggerInfo(user), await getRequestLocation());
8088
8321
  if (afterAddTeamMember) return afterAddTeamMember(...args);
8089
8322
  };
8090
8323
  const afterRemoveTeamMember = organizationHooks.afterRemoveTeamMember;
8091
8324
  organizationHooks.afterRemoveTeamMember = async (...args) => {
8092
8325
  const [{ organization, team, user, teamMember }] = args;
8093
- trackOrganizationTeamMemberRemoved(organization, team, user, teamMember, getOrganizationTriggerInfo(user));
8326
+ trackOrganizationTeamMemberRemoved(organization, team, user, teamMember, getOrganizationTriggerInfo(user), await getRequestLocation());
8094
8327
  if (afterRemoveTeamMember) return afterRemoveTeamMember(...args);
8095
8328
  };
8096
8329
  };
@@ -8270,8 +8503,15 @@ const dash = (options) => {
8270
8503
  before: [{
8271
8504
  matcher: (ctx) => {
8272
8505
  if (ctx.request?.method !== "GET") return true;
8273
- const path = new URL(ctx.request.url).pathname;
8274
- return matchesAnyRoute(path, [routes.SIGN_IN_SOCIAL_CALLBACK, routes.SIGN_IN_OAUTH_CALLBACK]);
8506
+ return matchesAnyRoute(ctx.path, [
8507
+ routes.SIGN_IN_SOCIAL_CALLBACK,
8508
+ routes.SIGN_IN_OAUTH_CALLBACK,
8509
+ routes.DASH_IMPERSONATE_USER,
8510
+ routes.VERIFY_EMAIL,
8511
+ routes.MAGIC_LINK_VERIFY,
8512
+ routes.DASH_ACCEPT_INVITATION,
8513
+ routes.DASH_COMPLETE_INVITATION_SOCIAL
8514
+ ]);
8275
8515
  },
8276
8516
  handler: createIdentificationMiddleware($kv)
8277
8517
  }],
@@ -8279,16 +8519,20 @@ const dash = (options) => {
8279
8519
  matcher: (ctx) => {
8280
8520
  if (ctx.request?.method !== "GET") return true;
8281
8521
  const path = new URL(ctx.request.url).pathname;
8282
- return matchesAnyRoute(path, [routes.SIGN_IN_SOCIAL_CALLBACK, routes.SIGN_IN_OAUTH_CALLBACK]);
8522
+ return matchesAnyRoute(path, [
8523
+ routes.SIGN_IN_SOCIAL_CALLBACK,
8524
+ routes.SIGN_IN_OAUTH_CALLBACK,
8525
+ routes.DASH_IMPERSONATE_USER
8526
+ ]);
8283
8527
  },
8284
8528
  handler: createAuthMiddleware(async (_ctx) => {
8285
8529
  const ctx = _ctx;
8286
8530
  const trigger = getTriggerInfo(ctx, ctx.context.session?.user.id ?? "unknown");
8287
- if (matchesAnyRoute(ctx.path, [routes.SEND_VERIFICATION_EMAIL]) && ctx.context.session && !(ctx.context.returned instanceof Error)) trackEmailVerificationSent(ctx.context.session.session, ctx.context.session.user, trigger, ctx);
8531
+ if (matchesAnyRoute(ctx.path, [routes.SEND_VERIFICATION_EMAIL]) && ctx.context.session && !(ctx.context.returned instanceof Error)) trackEmailVerificationSent(ctx.context.session.session, ctx.context.session.user, trigger, ctx.context.location);
8288
8532
  const body = ctx.body;
8289
- if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_EMAIL, routes.SIGN_IN_EMAIL_OTP]) && ctx.context.returned instanceof Error && body?.email) trackEmailSignInAttempt(ctx, trigger);
8290
- if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_SOCIAL]) && ctx.context.returned instanceof Error && ctx.body.provider && ctx.body.idToken) trackSocialSignInAttempt(ctx, trigger);
8291
- if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_SOCIAL_CALLBACK]) && ctx.request?.method === "GET" && ctx.context.returned instanceof Error && !ctx.context.newSession) trackSocialSignInRedirectionAttempt(ctx, trigger);
8533
+ if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_EMAIL, routes.SIGN_IN_EMAIL_OTP]) && ctx.context.returned instanceof Error && body?.email) trackEmailSignInAttempt(ctx, trigger, ctx.context.location);
8534
+ if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_SOCIAL]) && ctx.context.returned instanceof Error && ctx.body.provider && ctx.body.idToken) trackSocialSignInAttempt(ctx, trigger, ctx.context.location);
8535
+ if (matchesAnyRoute(ctx.path, [routes.SIGN_IN_SOCIAL_CALLBACK]) && ctx.request?.method === "GET" && ctx.context.returned instanceof Error && !ctx.context.newSession) trackSocialSignInRedirectionAttempt(ctx, trigger, ctx.context.location);
8292
8536
  const headerRequestId = ctx.request?.headers.get("X-Request-Id");
8293
8537
  if (headerRequestId) ctx.setCookie(IDENTIFICATION_COOKIE_NAME, headerRequestId, {
8294
8538
  maxAge: 600,
@@ -8383,6 +8627,7 @@ const dash = (options) => {
8383
8627
  dashSendManyVerificationEmails: sendManyVerificationEmails(settings),
8384
8628
  dashSendResetPasswordEmail: sendResetPasswordEmail(settings),
8385
8629
  dashEnableTwoFactor: enableTwoFactor(settings),
8630
+ dashCompleteTwoFactorSetup: completeTwoFactorSetup(settings),
8386
8631
  dashViewTwoFactorTotpUri: viewTwoFactorTotpUri(settings),
8387
8632
  dashViewBackupCodes: viewBackupCodes(settings),
8388
8633
  dashDisableTwoFactor: disableTwoFactor(settings),