@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 +41 -0
- package/dist/client.mjs +2 -2
- package/dist/{constants-CvriWQVc.mjs → constants-CLYqEwMV.mjs} +7 -1
- package/dist/email.mjs +5 -2
- package/dist/{fetch-DiAhoiKA.mjs → fetch-Bl0S3xUi.mjs} +3 -3
- package/dist/index.d.mts +12 -2
- package/dist/index.mjs +281 -123
- package/dist/native.mjs +2 -2
- package/package.json +7 -5
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-
|
|
2
|
-
import { n as createKV } from "./fetch-
|
|
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-
|
|
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: {
|
|
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-
|
|
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":
|
|
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":
|
|
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.
|
|
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-
|
|
2
|
-
import { n as createKV, t as createAPI } from "./fetch-
|
|
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
|
|
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
|
|
602
|
+
const trackSocialSignInFailure = (ctx, providerId, trigger, location) => {
|
|
578
603
|
const track = async () => {
|
|
579
|
-
const user = await
|
|
604
|
+
const user = await resolveOAuthUser(providerId, ctx);
|
|
580
605
|
trackEvent({
|
|
581
|
-
eventKey: user?.
|
|
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?.
|
|
586
|
-
userName: user?.
|
|
587
|
-
userEmail: user?.
|
|
610
|
+
userId: user?.id ?? "unknown",
|
|
611
|
+
userName: user?.name ?? "unknown",
|
|
612
|
+
userEmail: user?.email ?? "unknown",
|
|
588
613
|
loginMethod: getLoginMethod(ctx),
|
|
589
|
-
triggeredBy: user
|
|
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
|
-
|
|
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
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
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 ||
|
|
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
|
|
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
|
|
2786
|
+
//#region ../utils/dist/crypto/index.mjs
|
|
2778
2787
|
/**
|
|
2779
|
-
*
|
|
2780
|
-
*
|
|
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
|
-
|
|
2857
|
-
|
|
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
|
|
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:
|
|
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
|
-
})
|
|
6765
|
+
});
|
|
6664
6766
|
const { generateRandomString, symmetricEncrypt } = await import("better-auth/crypto");
|
|
6665
|
-
const secret = generateRandomString(32
|
|
6666
|
-
const
|
|
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:
|
|
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
|
|
6692
|
-
const issuer = twoFactorPlugin.options?.issuer || ctx.context.appName || "BetterAuth";
|
|
6785
|
+
const totpConfig = resolveTotpConfig(twoFactorPlugin);
|
|
6693
6786
|
return {
|
|
6694
6787
|
success: true,
|
|
6695
|
-
totpURI:
|
|
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:
|
|
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
|
|
6726
|
-
return { totpURI:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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: {
|
|
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
|
-
|
|
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-
|
|
2
|
-
import { n as createKV } from "./fetch-
|
|
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.
|
|
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.
|
|
65
|
-
"@better-auth/sso": "1.6.
|
|
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.
|
|
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",
|