@better-auth/infra 0.2.13 → 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/CHANGELOG.md ADDED
@@ -0,0 +1,41 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@better-auth/infra` are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.2.14] - 2026-06-09
9
+
10
+ ### Added
11
+
12
+ - **`dashCompleteTwoFactorSetup`** — New dashboard endpoint (`POST /dash/complete-two-factor-setup`) to mark 2FA as enabled after the user verifies their TOTP code, separating "setup started" from "setup complete"
13
+ - **`twoFactorStatus` on user details** — `getUserDetails` now returns `twoFactorStatus: "disabled" | "pending" | "enabled"`, so the dashboard can distinguish users who started but haven't finished 2FA setup.
14
+
15
+ ### Changed
16
+
17
+ - **Two-factor routes align with Better Auth** — Dashboard 2FA endpoints now respect the two-factor plugin's configuration: custom `twoFactorTable`, TOTP issuer/digits/period, and backup code options.
18
+ - **`enableTwoFactor` behavior** — Orphaned setup records (user started `/two-factor/enable` but never verified) are cleared and replaced instead of returning "already enabled." Only users with `twoFactorEnabled: true` are treated as fully enabled.
19
+ - **Event location coverage** — Tracked events now consistently include location, including: email verification, magic link verification, invitation acceptance, and social invitation completion.
20
+ - **Outbound HTTP clients** — All outbound HTTP clients now send the same `User-Agent` header (`@better-auth/infra v{version}`) for simpler observability.
21
+
22
+ ### Fixed
23
+
24
+ - **OAuth social sign-in failures** — Failed social login attempts are now correctly attributed to the user.
25
+
26
+ ### Security
27
+
28
+ - **Timing-safe API key comparison** — JWT middleware now uses constant-time hash comparison when validating API keys.
29
+
30
+ ## [0.2.13] - 2026-06-05
31
+
32
+ ### Changed
33
+
34
+ - Event tracking now consistently records location data on tracked events.
35
+
36
+ ### Fixed
37
+
38
+ - Fixed impossible travel security challenges.
39
+
40
+ [0.2.14]: https://github.com/better-auth/better-auth-infra/compare/@better-auth/infra@0.2.13...@better-auth/infra@0.2.14
41
+ [0.2.13]: https://github.com/better-auth/better-auth-infra/compare/@better-auth/infra@0.2.12...@better-auth/infra@0.2.13
package/dist/client.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import "./constants-CvriWQVc.mjs";
2
- import { n as createKV } from "./fetch-DiAhoiKA.mjs";
1
+ import "./constants-CLYqEwMV.mjs";
2
+ import { n as createKV } from "./fetch-Bl0S3xUi.mjs";
3
3
  import { a as resolveSentinelClientIdentifyUrl, i as solvePoWChallenge, l as hash, n as decodePoWChallenge, o as generateRequestId, r as encodePoWSolution, s as identify, t as createPowRetryTimeout, u as dashClient } from "./pow-retry-D2RRftJr.mjs";
4
4
  import { env } from "@better-auth/core/env";
5
5
  //#region src/sentinel/fingerprint.ts
@@ -1,4 +1,8 @@
1
1
  import { env } from "@better-auth/core/env";
2
+ //#endregion
3
+ //#region src/version.ts
4
+ const PLUGIN_VERSION = "0.2.14";
5
+ //#endregion
2
6
  //#region src/constants.ts
3
7
  /**
4
8
  * Infrastructure API URL
@@ -16,5 +20,7 @@ const KV_TIMEOUT_MS = 1e3;
16
20
  * Timeout for API calls HTTP calls (ms)
17
21
  */
18
22
  const INFRA_API_TIMEOUT_MS = 3e3;
23
+ /** User-Agent for outbound @better-auth/infra HTTP clients. */
24
+ const INFRA_USER_AGENT = `@better-auth/infra v${PLUGIN_VERSION}`;
19
25
  //#endregion
20
- export { KV_TIMEOUT_MS as i, INFRA_API_URL as n, INFRA_KV_URL as r, INFRA_API_TIMEOUT_MS as t };
26
+ export { KV_TIMEOUT_MS as a, INFRA_USER_AGENT as i, INFRA_API_URL as n, PLUGIN_VERSION as o, INFRA_KV_URL as r, INFRA_API_TIMEOUT_MS as t };
package/dist/email.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { n as INFRA_API_URL } from "./constants-CvriWQVc.mjs";
1
+ import { i as INFRA_USER_AGENT, n as INFRA_API_URL } from "./constants-CLYqEwMV.mjs";
2
2
  import { logger } from "better-auth";
3
3
  import { env } from "@better-auth/core/env";
4
4
  import { createFetch } from "@better-fetch/fetch";
@@ -37,7 +37,10 @@ function createEmailSender(config) {
37
37
  if (!apiKey) logger.warn("[Dash] No API key provided for email sending. Set BETTER_AUTH_API_KEY environment variable or pass apiKey in config.");
38
38
  const $api = createFetch({
39
39
  baseURL: apiUrl,
40
- headers: { Authorization: `Bearer ${apiKey}` },
40
+ headers: {
41
+ "user-agent": INFRA_USER_AGENT,
42
+ Authorization: `Bearer ${apiKey}`
43
+ },
41
44
  timeout: config?.apiTimeout ?? 3e3
42
45
  });
43
46
  /**
@@ -1,11 +1,11 @@
1
- import "./constants-CvriWQVc.mjs";
1
+ import { i as INFRA_USER_AGENT } from "./constants-CLYqEwMV.mjs";
2
2
  import { createFetch } from "@better-fetch/fetch";
3
3
  //#region src/fetch.ts
4
4
  function createAPI(options, fetchOptions) {
5
5
  return createFetch({
6
6
  baseURL: options.apiUrl,
7
7
  headers: {
8
- "user-agent": "better-auth",
8
+ "user-agent": INFRA_USER_AGENT,
9
9
  "x-api-key": options.apiKey
10
10
  },
11
11
  timeout: options.apiTimeout,
@@ -13,7 +13,7 @@ function createAPI(options, fetchOptions) {
13
13
  });
14
14
  }
15
15
  function createKV(options, fetchOptions) {
16
- const headers = { "user-agent": "better-auth" };
16
+ const headers = { "user-agent": INFRA_USER_AGENT };
17
17
  if (options.apiKey) headers["x-api-key"] = options.apiKey;
18
18
  return createFetch({
19
19
  baseURL: options.kvUrl,
package/dist/index.d.mts CHANGED
@@ -775,7 +775,7 @@ interface DashIdRow {
775
775
  id: string;
776
776
  }
777
777
  //#endregion
778
- //#region ../../node_modules/.bun/@better-auth+scim@1.6.14+790dbc3a881c5018/node_modules/@better-auth/scim/dist/index.d.mts
778
+ //#region ../../node_modules/.bun/@better-auth+scim@1.6.15+095c64a03bdccbe8/node_modules/@better-auth/scim/dist/index.d.mts
779
779
  //#region src/types.d.ts
780
780
  interface SCIMProvider {
781
781
  id: string;
@@ -4532,6 +4532,7 @@ interface DashTwoFactorTotpViewResponse {
4532
4532
  interface DashTwoFactorBackupCodesResponse {
4533
4533
  backupCodes: string[];
4534
4534
  }
4535
+ type DashTwoFactorStatus = "disabled" | "pending" | "enabled";
4535
4536
  //#endregion
4536
4537
  //#region src/routes/users/types.d.ts
4537
4538
  type DashUser = User & {
@@ -4554,6 +4555,7 @@ type DashUserDetailsResponse = DashUser & {
4554
4555
  city?: string | null;
4555
4556
  country?: string | null;
4556
4557
  countryCode?: string | null;
4558
+ twoFactorStatus?: DashTwoFactorStatus;
4557
4559
  };
4558
4560
  type DashUserOrganization = Pick<Organization, "id" | "name" | "slug" | "logo" | "createdAt"> & {
4559
4561
  role: string;
@@ -5591,6 +5593,14 @@ declare const dash: <O extends DashOptions>(options?: O) => {
5591
5593
  };
5592
5594
  }>)[];
5593
5595
  }, DashTwoFactorEnableResponse>;
5596
+ dashCompleteTwoFactorSetup: import("better-call").StrictEndpoint<"/dash/complete-two-factor-setup", {
5597
+ method: "POST";
5598
+ use: ((inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<{
5599
+ payload: {
5600
+ userId: string;
5601
+ };
5602
+ }>)[];
5603
+ }, DashSuccessResponse>;
5594
5604
  dashViewTwoFactorTotpUri: import("better-call").StrictEndpoint<"/dash/view-two-factor-totp-uri", {
5595
5605
  method: "POST";
5596
5606
  metadata: {
@@ -6001,4 +6011,4 @@ declare const dash: <O extends DashOptions>(options?: O) => {
6001
6011
  } : {};
6002
6012
  };
6003
6013
  //#endregion
6004
- export { type APIError, CHALLENGE_TTL, type CompromisedPasswordResult, type CredentialStuffingResult, type DBField, DEFAULT_DIFFICULTY, type DashAddTeamMemberResponse, type DashBanManyResponse, type DashCheckUserByEmailResponse, type DashCheckUserExistsResponse, type DashCompleteInvitationResponse, type DashConfigResponse, type DashCreateOrganizationBody, type DashCreateOrganizationResponse, type DashCreateTeamResponse, type DashCreateUserResponse, type DashDeleteManyUsersResponse, type DashDirectoryCreateResponse, type DashDirectoryDeleteResponse, type DashDirectoryItem, type DashDirectoryRegenerateTokenResponse, type DashExecuteAdapterCountResponse, type DashExecuteAdapterFindManyResponse, type DashExecuteAdapterFindOneResponse, type DashExecuteAdapterMutationResponse, type DashExecuteAdapterResponse, type DashExportOrganizationsResponse, type DashIdRow, type DashInviteMemberResponse, type DashMaybeSuccessResponse, type DashOptions, type DashOptionsInternal, type DashOptionsResolved, type DashOrganizationAddMemberResponse, type DashOrganizationDeleteManyResponse, type DashOrganizationDetailResponse, type DashOrganizationInvitationItem, type DashOrganizationInvitationListResponse, type DashOrganizationInvitationStatusItem, type DashOrganizationListResponse, type DashOrganizationMember, type DashOrganizationMemberListItem, type DashOrganizationMemberListResponse, type DashOrganizationMemberUser, type DashOrganizationOptionsResponse, type DashOrganizationTeamItem, type DashOrganizationTeamListResponse, type DashOrganizationUpdateMemberRoleResponse, type DashOrganizationUpdateResponse, type DashSendManyVerificationEmailsResponse, type DashSessionRevokeManyResponse, type DashSsoCreateProviderResponse, type DashSsoDeleteResponse, type DashSsoMarkDomainVerifiedResponse, type DashSsoProviderItem, type DashSsoProviderSummary, type DashSsoUpdateProviderResponse, type DashSsoVerificationTokenResponse, type DashSsoVerifyDomainResponse, type DashSuccessResponse, type DashTeam, type DashTeamMember, type DashTeamMemberListResponse, type DashTwoFactorBackupCodesResponse, type DashTwoFactorEnableResponse, type DashTwoFactorTotpViewResponse, type DashUpdateTeamResponse, type DashUpdateUserResponse, type DashUserDetailsResponse, type DashUserGraphDataResponse, type DashUserListResponse, type DashUserOrganizationsResponse, type DashUserRetentionDataResponse, type DashUserStatsActivePeriod, type DashUserStatsResponse, type DashUserStatsSignUpPeriod, type DashValidateResponse, type DirectorySyncConnection, EMAIL_TEMPLATES, type EmailConfig, type EmailTemplateId, type EmailTemplateVariables, type Endpoint, type EndpointOptions, type EventLocation, type EventTypesResponse, type ImpossibleTravelResult, type InfraEndpointContext, type InfraPluginConnectionOptions, type InfraPluginConnectionOptionsInternal, type LocationData, type LocationDataContext, type PoWChallenge, type PoWSolution, type SCIMPlugin, type SMSConfig, type SMSTemplateId, type SMSTemplateVariables, SMS_TEMPLATES, type SecurityEvent, type SecurityEventType, type SecurityOptions, type SecurityVerdict, type SendBulkEmailsOptions, type SendBulkEmailsResult, type SendEmailOptions, type SendEmailResult, type SendSMSOptions, type SendSMSResult, type SentinelOptions, type SentinelOptionsInternal, type StaleUserResult, type ThresholdConfig, USER_EVENT_TYPES, type UserEvent, type UserEventType, type UserEventsResponse, createEmailSender, createSMSSender, dash, decodePoWChallenge, encodePoWSolution, normalizeEmail, sendBulkEmails, sendEmail, sendSMS, sentinel, solvePoWChallenge, verifyPoWSolution };
6014
+ export { type APIError, CHALLENGE_TTL, type CompromisedPasswordResult, type CredentialStuffingResult, type DBField, DEFAULT_DIFFICULTY, type DashAddTeamMemberResponse, type DashBanManyResponse, type DashCheckUserByEmailResponse, type DashCheckUserExistsResponse, type DashCompleteInvitationResponse, type DashConfigResponse, type DashCreateOrganizationBody, type DashCreateOrganizationResponse, type DashCreateTeamResponse, type DashCreateUserResponse, type DashDeleteManyUsersResponse, type DashDirectoryCreateResponse, type DashDirectoryDeleteResponse, type DashDirectoryItem, type DashDirectoryRegenerateTokenResponse, type DashExecuteAdapterCountResponse, type DashExecuteAdapterFindManyResponse, type DashExecuteAdapterFindOneResponse, type DashExecuteAdapterMutationResponse, type DashExecuteAdapterResponse, type DashExportOrganizationsResponse, type DashIdRow, type DashInviteMemberResponse, type DashMaybeSuccessResponse, type DashOptions, type DashOptionsInternal, type DashOptionsResolved, type DashOrganizationAddMemberResponse, type DashOrganizationDeleteManyResponse, type DashOrganizationDetailResponse, type DashOrganizationInvitationItem, type DashOrganizationInvitationListResponse, type DashOrganizationInvitationStatusItem, type DashOrganizationListResponse, type DashOrganizationMember, type DashOrganizationMemberListItem, type DashOrganizationMemberListResponse, type DashOrganizationMemberUser, type DashOrganizationOptionsResponse, type DashOrganizationTeamItem, type DashOrganizationTeamListResponse, type DashOrganizationUpdateMemberRoleResponse, type DashOrganizationUpdateResponse, type DashSendManyVerificationEmailsResponse, type DashSessionRevokeManyResponse, type DashSsoCreateProviderResponse, type DashSsoDeleteResponse, type DashSsoMarkDomainVerifiedResponse, type DashSsoProviderItem, type DashSsoProviderSummary, type DashSsoUpdateProviderResponse, type DashSsoVerificationTokenResponse, type DashSsoVerifyDomainResponse, type DashSuccessResponse, type DashTeam, type DashTeamMember, type DashTeamMemberListResponse, type DashTwoFactorBackupCodesResponse, type DashTwoFactorEnableResponse, type DashTwoFactorStatus, type DashTwoFactorTotpViewResponse, type DashUpdateTeamResponse, type DashUpdateUserResponse, type DashUserDetailsResponse, type DashUserGraphDataResponse, type DashUserListResponse, type DashUserOrganizationsResponse, type DashUserRetentionDataResponse, type DashUserStatsActivePeriod, type DashUserStatsResponse, type DashUserStatsSignUpPeriod, type DashValidateResponse, type DirectorySyncConnection, EMAIL_TEMPLATES, type EmailConfig, type EmailTemplateId, type EmailTemplateVariables, type Endpoint, type EndpointOptions, type EventLocation, type EventTypesResponse, type ImpossibleTravelResult, type InfraEndpointContext, type InfraPluginConnectionOptions, type InfraPluginConnectionOptionsInternal, type LocationData, type LocationDataContext, type PoWChallenge, type PoWSolution, type SCIMPlugin, type SMSConfig, type SMSTemplateId, type SMSTemplateVariables, SMS_TEMPLATES, type SecurityEvent, type SecurityEventType, type SecurityOptions, type SecurityVerdict, type SendBulkEmailsOptions, type SendBulkEmailsResult, type SendEmailOptions, type SendEmailResult, type SendSMSOptions, type SendSMSResult, type SentinelOptions, type SentinelOptionsInternal, type StaleUserResult, type ThresholdConfig, USER_EVENT_TYPES, type UserEvent, type UserEventType, type UserEventsResponse, createEmailSender, createSMSSender, dash, decodePoWChallenge, encodePoWSolution, normalizeEmail, sendBulkEmails, sendEmail, sendSMS, sentinel, solvePoWChallenge, verifyPoWSolution };
package/dist/index.mjs CHANGED
@@ -1,8 +1,8 @@
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
4
  import { getCurrentAuthContext } from "@better-auth/core/context";
5
- import { APIError, generateId, getAuthTables, logger, parseState } from "better-auth";
5
+ import { APIError, generateId, getAuthTables, logger } from "better-auth";
6
6
  import { env } from "@better-auth/core/env";
7
7
  import { APIError as APIError$1, createAuthEndpoint, createAuthMiddleware, requestPasswordReset, sendVerificationEmailFn, sessionMiddleware } from "better-auth/api";
8
8
  import { createFetch } from "@better-fetch/fetch";
@@ -10,6 +10,8 @@ import { isValidPhoneNumber, parsePhoneNumberFromString } from "libphonenumber-j
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,6 +109,8 @@ 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",
111
116
  DASH_IMPERSONATE_USER: "/dash/impersonate-user",
@@ -181,34 +186,6 @@ async function getUserById(userId, ctx) {
181
186
  }
182
187
  return user;
183
188
  }
184
- const getUserByIdToken = async (providerId, idToken, ctx) => {
185
- const provider = ctx.context.socialProviders.find((p) => p.id === providerId);
186
- let user = null;
187
- if (provider) try {
188
- user = await provider.getUserInfo(idToken);
189
- } catch (error) {
190
- logger.debug("[Dash] Failed to fetch user info:", error);
191
- }
192
- return user;
193
- };
194
- const getUserByAuthorizationCode = async (providerId, ctx) => {
195
- let userInfo = null;
196
- const provider = ctx.context.socialProviders.find((p) => p.id === providerId);
197
- if (provider) try {
198
- const codeVerifier = (await parseState(ctx)).codeVerifier;
199
- const { code, device_id } = ctx.query ?? {};
200
- const tokens = await provider.validateAuthorizationCode({
201
- code,
202
- codeVerifier,
203
- deviceId: device_id,
204
- redirectURI: `${ctx.context.baseURL}/callback/${provider.id}`
205
- });
206
- userInfo = await provider.getUserInfo({ ...tokens }).then((res) => res?.user);
207
- } catch (error) {
208
- logger.debug("[Dash] Failed to fetch user info:", error);
209
- }
210
- return userInfo;
211
- };
212
189
  //#endregion
213
190
  //#region src/events/core/events-account.ts
214
191
  const initAccountEvents = (tracker) => {
@@ -310,6 +287,7 @@ const routeToRegex = (route) => {
310
287
  return new RegExp(`${pattern}(?:$|[/?])`);
311
288
  };
312
289
  const matchesAnyRoute = (path, routes) => {
290
+ if (!path) return false;
313
291
  const cleanPath = stripQuery(path);
314
292
  return routes.some((route) => routeToRegex(route).test(cleanPath));
315
293
  };
@@ -345,6 +323,53 @@ const getLoginMethod = (ctx) => {
345
323
  return null;
346
324
  };
347
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
348
373
  //#region src/events/core/events-session.ts
349
374
  const initSessionEvents = (tracker) => {
350
375
  const { trackEvent } = tracker;
@@ -574,19 +599,19 @@ const initSessionEvents = (tracker) => {
574
599
  };
575
600
  ctx.context.runInBackground(track());
576
601
  };
577
- const trackSocialSignInAttempt = (ctx, trigger, location) => {
602
+ const trackSocialSignInFailure = (ctx, providerId, trigger, location) => {
578
603
  const track = async () => {
579
- const user = await getUserByIdToken(ctx.body.provider, ctx.body.idToken, ctx);
604
+ const user = await resolveOAuthUser(providerId, ctx);
580
605
  trackEvent({
581
- eventKey: user?.user.id.toString() ?? "unknown",
606
+ eventKey: user?.id ?? "unknown",
582
607
  eventType: EVENT_TYPES.USER_SIGN_IN_FAILED,
583
608
  eventDisplayName: "User sign-in attempt failed",
584
609
  eventData: {
585
- userId: user?.user.id.toString() ?? "unknown",
586
- userName: user?.user.name ?? "unknown",
587
- userEmail: user?.user.email ?? "unknown",
610
+ userId: user?.id ?? "unknown",
611
+ userName: user?.name ?? "unknown",
612
+ userEmail: user?.email ?? "unknown",
588
613
  loginMethod: getLoginMethod(ctx),
589
- triggeredBy: user?.user.id ?? trigger.triggeredBy,
614
+ triggeredBy: user && user.id !== "unknown" ? user.id : trigger.triggeredBy,
590
615
  triggerContext: trigger.triggerContext
591
616
  },
592
617
  ipAddress: location?.ipAddress,
@@ -597,28 +622,11 @@ const initSessionEvents = (tracker) => {
597
622
  };
598
623
  ctx.context.runInBackground(track());
599
624
  };
625
+ const trackSocialSignInAttempt = (ctx, trigger, location) => {
626
+ trackSocialSignInFailure(ctx, ctx.body.provider, trigger, location);
627
+ };
600
628
  const trackSocialSignInRedirectionAttempt = (ctx, trigger, location) => {
601
- const track = async () => {
602
- const user = await getUserByAuthorizationCode(tryDecode(ctx.params?.id), ctx);
603
- trackEvent({
604
- eventKey: user?.id.toString() ?? "unknown",
605
- eventType: EVENT_TYPES.USER_SIGN_IN_FAILED,
606
- eventDisplayName: "User sign-in attempt failed",
607
- eventData: {
608
- userId: user?.id.toString() ?? "unknown",
609
- userName: user?.name ?? "unknown",
610
- userEmail: user?.id ?? "unknown",
611
- loginMethod: getLoginMethod(ctx),
612
- triggeredBy: trigger.triggeredBy,
613
- triggerContext: trigger.triggerContext
614
- },
615
- ipAddress: location?.ipAddress,
616
- city: location?.city,
617
- country: location?.country,
618
- countryCode: location?.countryCode
619
- });
620
- };
621
- ctx.context.runInBackground(track());
629
+ trackSocialSignInFailure(ctx, tryDecode(ctx.params?.id), trigger, location);
622
630
  };
623
631
  return {
624
632
  trackUserSignedIn,
@@ -1004,23 +1012,22 @@ function createIdentificationMiddleware($kv) {
1004
1012
  ctx.context.visitorId = visitorId;
1005
1013
  const ipConfig = ctx.context.options?.advanced?.ipAddress;
1006
1014
  let location;
1015
+ const requestIp = getClientIpFromRequest(ctx.request, ipConfig?.ipAddressHeaders || null);
1016
+ const requestCountryCode = getCountryCodeFromRequest(ctx.request);
1007
1017
  if (ipConfig?.disableIpTracking === true) location = void 0;
1008
1018
  else if (requestId && identification) {
1009
1019
  const loc = getLocation(identification);
1010
1020
  location = {
1011
- ipAddress: identification.ip || void 0,
1021
+ ipAddress: identification.ip || requestIp || void 0,
1012
1022
  city: loc?.city || void 0,
1013
1023
  country: loc?.country?.name || void 0,
1014
- countryCode: loc?.country?.code || void 0
1024
+ countryCode: loc?.country?.code || requestCountryCode || void 0
1015
1025
  };
1016
- } else {
1017
- const ipAddress = getClientIpFromRequest(ctx.request, ipConfig?.ipAddressHeaders || null);
1018
- if (ipAddress) location = {
1019
- ipAddress,
1020
- countryCode: getCountryCodeFromRequest(ctx.request)
1021
- };
1022
- else location = void 0;
1023
- }
1026
+ } else if (requestIp) location = {
1027
+ ipAddress: requestIp,
1028
+ countryCode: requestCountryCode
1029
+ };
1030
+ else location = void 0;
1024
1031
  ctx.context.location = location;
1025
1032
  ctx.context.ip = resolveSecurityIp(identification, location);
1026
1033
  ctx.context.untrustedVisitorId = resolveUntrustedVisitorId(visitorId, ctx.context.ip, headerVisitorId);
@@ -1092,9 +1099,11 @@ function emitEvaluation(ctx, trackEvent, options = {}) {
1092
1099
  ev.emitted = true;
1093
1100
  const identification = ctx.context.identification ?? null;
1094
1101
  const visitorId = ctx.context.visitorId ?? null;
1102
+ const location = ctx.context.location;
1095
1103
  const path = ctx.path ?? "";
1104
+ const ipAddress = location?.ipAddress || identification?.ip || ctx.context.ip || void 0;
1096
1105
  trackEvent({
1097
- eventKey: visitorId || identification?.ip || "unknown",
1106
+ eventKey: visitorId || ipAddress || "unknown",
1098
1107
  eventType: "security_check",
1099
1108
  eventDisplayName: ev.outcome === "blocked" ? "Security: blocked" : ev.outcome === "challenged" ? "Security: challenged" : "Security: passed",
1100
1109
  eventData: {
@@ -1107,10 +1116,10 @@ function emitEvaluation(ctx, trackEvent, options = {}) {
1107
1116
  userAgent: options.userAgent,
1108
1117
  details: ev.details
1109
1118
  },
1110
- ipAddress: identification?.ip || void 0,
1111
- city: identification?.location?.city || void 0,
1112
- country: identification?.location?.country?.name || void 0,
1113
- 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
1114
1123
  });
1115
1124
  }
1116
1125
  //#endregion
@@ -2774,11 +2783,10 @@ const initTeamEvents = (tracker) => {
2774
2783
  };
2775
2784
  };
2776
2785
  //#endregion
2777
- //#region src/jwt.ts
2786
+ //#region ../utils/dist/crypto/index.mjs
2778
2787
  /**
2779
- * Hash the given value
2780
- * Note: Must match @infra/utils/crypto hash()
2781
- * @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.
2782
2790
  */
2783
2791
  async function hash(value) {
2784
2792
  const data = new TextEncoder().encode(value);
@@ -2786,6 +2794,32 @@ async function hash(value) {
2786
2794
  const hashArray = new Uint8Array(hashBuffer);
2787
2795
  return Array.from(hashArray).map((b) => b.toString(16).padStart(2, "0")).join("");
2788
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
2789
2823
  /**
2790
2824
  * Skip JTI check for JWTs issued within this many seconds.
2791
2825
  * This is safe because replay attacks require time for interception.
@@ -2853,9 +2887,8 @@ const jwtMiddleware = (options, schema, getJWT) => {
2853
2887
  });
2854
2888
  throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
2855
2889
  }
2856
- const expectedHash = await hash(options.apiKey);
2857
- if (apiKeyHash !== expectedHash) {
2858
- 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");
2859
2892
  throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
2860
2893
  }
2861
2894
  if (!isRecentlyIssued(payload)) {
@@ -2908,7 +2941,7 @@ const jwtValidateMiddleware = (options) => {
2908
2941
  });
2909
2942
  throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
2910
2943
  }
2911
- if (apiKeyHash !== await hash(options.apiKey)) {
2944
+ if (!timingSafeEqualHash(apiKeyHash, await hash(options.apiKey))) {
2912
2945
  ctx.context.logger.warn("[Dash] API key hash mismatch");
2913
2946
  throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
2914
2947
  }
@@ -2916,9 +2949,6 @@ const jwtValidateMiddleware = (options) => {
2916
2949
  });
2917
2950
  };
2918
2951
  //#endregion
2919
- //#region src/version.ts
2920
- const PLUGIN_VERSION = "0.2.13";
2921
- //#endregion
2922
2952
  //#region src/routes/auth/config.ts
2923
2953
  const PLUGIN_OPTIONS_EXCLUDE_KEYS = { stripe: new Set(["stripeClient"]) };
2924
2954
  const EXACT_SENSITIVE_STRING_KEYS_LOWER = new Set([...new Set([
@@ -6646,6 +6676,64 @@ const markSsoProviderDomainVerified = (options) => {
6646
6676
  });
6647
6677
  };
6648
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
6649
6737
  //#region src/routes/two-factor/index.ts
6650
6738
  const enableTwoFactor = (options) => createAuthEndpoint("/dash/enable-two-factor", {
6651
6739
  method: "POST",
@@ -6654,31 +6742,37 @@ const enableTwoFactor = (options) => createAuthEndpoint("/dash/enable-two-factor
6654
6742
  const { userId } = ctx.context.payload;
6655
6743
  const twoFactorPlugin = ctx.context.getPlugin("two-factor");
6656
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" });
6657
6753
  if (await ctx.context.adapter.findOne({
6658
- model: "twoFactor",
6754
+ model: twoFactorTable,
6755
+ where: [{
6756
+ field: "userId",
6757
+ value: userId
6758
+ }]
6759
+ })) await ctx.context.adapter.delete({
6760
+ model: twoFactorTable,
6659
6761
  where: [{
6660
6762
  field: "userId",
6661
6763
  value: userId
6662
6764
  }]
6663
- })) throw new APIError("BAD_REQUEST", { message: "Two-factor authentication is already enabled for this user" });
6765
+ });
6664
6766
  const { generateRandomString, symmetricEncrypt } = await import("better-auth/crypto");
6665
- const secret = generateRandomString(32, "A-Z");
6666
- const backupCodeAmount = 10;
6667
- const backupCodeLength = 10;
6668
- const backupCodes = [];
6669
- 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));
6670
6769
  const encryptedSecret = await symmetricEncrypt({
6671
6770
  key: ctx.context.secret,
6672
6771
  data: secret
6673
6772
  });
6674
- const encryptedBackupCodes = await symmetricEncrypt({
6675
- key: ctx.context.secret,
6676
- data: JSON.stringify(backupCodes)
6677
- });
6678
6773
  await ctx.context.adapter.create({
6679
- model: "twoFactor",
6774
+ model: twoFactorTable,
6680
6775
  data: {
6681
- id: generateId(32),
6682
6776
  userId,
6683
6777
  secret: encryptedSecret,
6684
6778
  backupCodes: encryptedBackupCodes
@@ -6688,15 +6782,47 @@ const enableTwoFactor = (options) => createAuthEndpoint("/dash/enable-two-factor
6688
6782
  twoFactorEnabled: true,
6689
6783
  updatedAt: /* @__PURE__ */ new Date()
6690
6784
  });
6691
- const user = await ctx.context.internalAdapter.findUserById(userId);
6692
- const issuer = twoFactorPlugin.options?.issuer || ctx.context.appName || "BetterAuth";
6785
+ const totpConfig = resolveTotpConfig(twoFactorPlugin);
6693
6786
  return {
6694
6787
  success: true,
6695
- 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
+ }),
6696
6795
  secret,
6697
6796
  backupCodes
6698
6797
  };
6699
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
+ });
6700
6826
  const viewTwoFactorTotpUri = (options) => createAuthEndpoint("/dash/view-two-factor-totp-uri", {
6701
6827
  method: "POST",
6702
6828
  metadata: { scope: "http" },
@@ -6705,8 +6831,13 @@ const viewTwoFactorTotpUri = (options) => createAuthEndpoint("/dash/view-two-fac
6705
6831
  const { userId } = ctx.context.payload;
6706
6832
  const twoFactorPlugin = ctx.context.getPlugin("two-factor");
6707
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";
6708
6839
  const twoFactorRecord = await ctx.context.adapter.findOne({
6709
- model: "twoFactor",
6840
+ model: twoFactorTable,
6710
6841
  where: [{
6711
6842
  field: "userId",
6712
6843
  value: userId
@@ -6722,8 +6853,14 @@ const viewTwoFactorTotpUri = (options) => createAuthEndpoint("/dash/view-two-fac
6722
6853
  });
6723
6854
  } catch {}
6724
6855
  const user = await ctx.context.internalAdapter.findUserById(userId);
6725
- const issuer = twoFactorPlugin.options?.issuer || ctx.context.appName || "BetterAuth";
6726
- 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
+ }) };
6727
6864
  });
6728
6865
  const viewBackupCodes = (options) => createAuthEndpoint("/dash/view-backup-codes", {
6729
6866
  method: "POST",
@@ -6736,9 +6873,11 @@ const disableTwoFactor = (options) => createAuthEndpoint("/dash/disable-two-fact
6736
6873
  use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))]
6737
6874
  }, async (ctx) => {
6738
6875
  const { userId } = ctx.context.payload;
6739
- 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";
6740
6879
  await ctx.context.adapter.delete({
6741
- model: "twoFactor",
6880
+ model: twoFactorTable,
6742
6881
  where: [{
6743
6882
  field: "userId",
6744
6883
  value: userId
@@ -6755,25 +6894,19 @@ const generateBackupCodes = (options) => createAuthEndpoint("/dash/generate-back
6755
6894
  use: [jwtMiddleware(options, z$1.object({ userId: z$1.string() }))]
6756
6895
  }, async (ctx) => {
6757
6896
  const { userId } = ctx.context.payload;
6897
+ const twoFactorPlugin = ctx.context.getPlugin("two-factor");
6898
+ const twoFactorTable = twoFactorPlugin?.options?.twoFactorTable ?? "twoFactor";
6758
6899
  const twoFactorRecord = await ctx.context.adapter.findOne({
6759
- model: "twoFactor",
6900
+ model: twoFactorTable,
6760
6901
  where: [{
6761
6902
  field: "userId",
6762
6903
  value: userId
6763
6904
  }]
6764
6905
  });
6765
6906
  if (!twoFactorRecord) throw new APIError("NOT_FOUND", { message: "Two-factor authentication not set up for this user" });
6766
- const backupCodeAmount = 10;
6767
- const backupCodeLength = 10;
6768
- const newBackupCodes = [];
6769
- for (let i = 0; i < backupCodeAmount; i++) newBackupCodes.push(generateId(backupCodeLength).toUpperCase());
6770
- const { symmetricEncrypt } = await import("better-auth/crypto");
6771
- const encryptedCodes = await symmetricEncrypt({
6772
- key: ctx.context.secret,
6773
- data: JSON.stringify(newBackupCodes)
6774
- });
6907
+ const { backupCodes: newBackupCodes, encryptedBackupCodes: encryptedCodes } = await generateBackupCodes$1(ctx.context.secret, resolveBackupCodeOptions(twoFactorPlugin));
6775
6908
  await ctx.context.adapter.update({
6776
- model: "twoFactor",
6909
+ model: twoFactorTable,
6777
6910
  where: [{
6778
6911
  field: "id",
6779
6912
  value: twoFactorRecord.id
@@ -6783,6 +6916,20 @@ const generateBackupCodes = (options) => createAuthEndpoint("/dash/generate-back
6783
6916
  return { backupCodes: newBackupCodes };
6784
6917
  });
6785
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
6786
6933
  //#region src/routes/users/index.ts
6787
6934
  function parseWhereClause(val) {
6788
6935
  if (!val) return [];
@@ -7136,6 +7283,7 @@ const getUserDetails = (options) => {
7136
7283
  } }
7137
7284
  });
7138
7285
  if (!user) throw ctx.error("NOT_FOUND", { message: "User not found" });
7286
+ const twoFactorStatus = await resolveTwoFactorStatus(ctx.context, userId, user);
7139
7287
  if (minimal) return {
7140
7288
  ...user,
7141
7289
  lastActiveAt: user.lastActiveAt ?? null,
@@ -7143,7 +7291,8 @@ const getUserDetails = (options) => {
7143
7291
  banReason: hasAdminPlugin ? user.banReason ?? null : null,
7144
7292
  banExpires: hasAdminPlugin ? user.banExpires ?? null : null,
7145
7293
  account: [],
7146
- session: []
7294
+ session: [],
7295
+ twoFactorStatus
7147
7296
  };
7148
7297
  const activityTrackingEnabled = !!options.activityTracking?.enabled;
7149
7298
  const sessions = secondaryStorageOnly ? await ctx.context.internalAdapter.listSessions(userId) : user.session || [];
@@ -7181,7 +7330,8 @@ const getUserDetails = (options) => {
7181
7330
  lastActiveAt,
7182
7331
  banned: hasAdminPlugin ? user.banned ?? false : false,
7183
7332
  banReason: hasAdminPlugin ? user.banReason ?? null : null,
7184
- banExpires: hasAdminPlugin ? user.banExpires ?? null : null
7333
+ banExpires: hasAdminPlugin ? user.banExpires ?? null : null,
7334
+ twoFactorStatus
7185
7335
  };
7186
7336
  });
7187
7337
  };
@@ -7978,7 +8128,10 @@ function createSMSSender(config) {
7978
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.");
7979
8129
  const $api = createFetch({
7980
8130
  baseURL: apiUrl,
7981
- headers: { Authorization: `Bearer ${apiKey}` },
8131
+ headers: {
8132
+ "user-agent": INFRA_USER_AGENT,
8133
+ Authorization: `Bearer ${apiKey}`
8134
+ },
7982
8135
  timeout: config?.apiTimeout ?? 3e3
7983
8136
  });
7984
8137
  /**
@@ -8084,6 +8237,7 @@ const dash = (options) => {
8084
8237
  options: opts,
8085
8238
  version: PLUGIN_VERSION,
8086
8239
  init(ctx) {
8240
+ instrumentSocialProviders(ctx.socialProviders);
8087
8241
  const organizationPlugin = ctx.getPlugin("organization");
8088
8242
  if (organizationPlugin) {
8089
8243
  const instrumentOrganizationHooks = (organizationPluginOptions) => {
@@ -8349,11 +8503,14 @@ const dash = (options) => {
8349
8503
  before: [{
8350
8504
  matcher: (ctx) => {
8351
8505
  if (ctx.request?.method !== "GET") return true;
8352
- const path = new URL(ctx.request.url).pathname;
8353
- return matchesAnyRoute(path, [
8506
+ return matchesAnyRoute(ctx.path, [
8354
8507
  routes.SIGN_IN_SOCIAL_CALLBACK,
8355
8508
  routes.SIGN_IN_OAUTH_CALLBACK,
8356
- routes.DASH_IMPERSONATE_USER
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
8357
8514
  ]);
8358
8515
  },
8359
8516
  handler: createIdentificationMiddleware($kv)
@@ -8470,6 +8627,7 @@ const dash = (options) => {
8470
8627
  dashSendManyVerificationEmails: sendManyVerificationEmails(settings),
8471
8628
  dashSendResetPasswordEmail: sendResetPasswordEmail(settings),
8472
8629
  dashEnableTwoFactor: enableTwoFactor(settings),
8630
+ dashCompleteTwoFactorSetup: completeTwoFactorSetup(settings),
8473
8631
  dashViewTwoFactorTotpUri: viewTwoFactorTotpUri(settings),
8474
8632
  dashViewBackupCodes: viewBackupCodes(settings),
8475
8633
  dashDisableTwoFactor: disableTwoFactor(settings),
package/dist/native.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import "./constants-CvriWQVc.mjs";
2
- import { n as createKV } from "./fetch-DiAhoiKA.mjs";
1
+ import "./constants-CLYqEwMV.mjs";
2
+ import { n as createKV } from "./fetch-Bl0S3xUi.mjs";
3
3
  import { a as resolveSentinelClientIdentifyUrl, c as bytesToHex, i as solvePoWChallenge, n as decodePoWChallenge, r as encodePoWSolution, s as identify, t as createPowRetryTimeout, u as dashClient } from "./pow-retry-D2RRftJr.mjs";
4
4
  import { env } from "@better-auth/core/env";
5
5
  import { Dimensions, InteractionManager, PixelRatio, Platform } from "react-native";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-auth/infra",
3
- "version": "0.2.13",
3
+ "version": "0.2.14",
4
4
  "description": "Dashboard and analytics plugin for Better Auth",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",
@@ -39,7 +39,8 @@
39
39
  },
40
40
  "files": [
41
41
  "dist",
42
- "README.md"
42
+ "README.md",
43
+ "CHANGELOG.md"
43
44
  ],
44
45
  "keywords": [
45
46
  "better-auth",
@@ -61,13 +62,13 @@
61
62
  },
62
63
  "homepage": "https://better-auth.com",
63
64
  "devDependencies": {
64
- "@better-auth/scim": "1.6.14",
65
- "@better-auth/sso": "1.6.14",
65
+ "@better-auth/scim": "1.6.15",
66
+ "@better-auth/sso": "1.6.15",
66
67
  "@infra/mocks": "0.0.0",
67
68
  "@infra/utils": "0.0.0",
68
69
  "@types/bun": "latest",
69
70
  "@types/node": "^24.12.0",
70
- "better-auth": "1.6.14",
71
+ "better-auth": "1.6.15",
71
72
  "expo-crypto": "^14.0.2",
72
73
  "happy-dom": "^20.9.0",
73
74
  "msw": "^2.14.6",
@@ -76,6 +77,7 @@
76
77
  "zod": "^4.3.6"
77
78
  },
78
79
  "dependencies": {
80
+ "@better-auth/utils": "^0.4.1",
79
81
  "@better-fetch/fetch": "^1.1.21",
80
82
  "better-call": "^1.3.2",
81
83
  "jose": "^6.1.0",