@better-auth/infra 0.4.8 → 0.4.10

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 CHANGED
@@ -5,6 +5,22 @@ All notable changes to `@better-auth/infra` are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0/).
7
7
 
8
+ ## [0.4.10] - 2026-09-21
9
+
10
+ ### Added
11
+
12
+ - **`GET /dash/directory-sync/connection-count`** — Project-wide directory sync connection count (`{ count, mode }`). Managed mode counts `active` / `decommissioning` connections; legacy mode counts `scimProvider` rows. Returns 404 when directory sync is unavailable.
13
+
14
+ ### Fixed
15
+
16
+ - **Stored vs resolved auth base** — Platform-stored `baseURL` + `basePath` always rejoin (a path on stored `baseURL` no longer overrides a saved `basePath`). Request-scoped mount resolution still follows Better Auth path-in-`baseURL` via `context.baseURL` (dash config, Sentinel).
17
+
18
+ ## [0.4.9] - 2026-09-09
19
+
20
+ ### Fixed
21
+
22
+ - **Auth base URL / path** — `GET /dash/config` reports origin + effective mount path when `baseURL` embeds a path or uses dynamic `allowedHosts`. Invitation callbacks, SSO/SCIM endpoint URLs, and Sentinel route matching use the same resolved mount.
23
+
8
24
  ## [0.4.8] - 2026-09-09
9
25
 
10
26
  ### Added
package/dist/client.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as DashGetAllAuditLogsInput, i as DashClientOptions, n as DashAuditLog, o as DashGetAuditLogsInput, r as DashAuditLogsResponse, s as dashClient, t as IdentifyClientOptions } from "./identify-client-options-DgKA4Eq8.mjs";
1
+ import { a as DashGetAllAuditLogsInput, i as DashClientOptions, n as DashAuditLog, o as DashGetAuditLogsInput, r as DashAuditLogsResponse, s as dashClient, t as IdentifyClientOptions } from "./identify-client-options-BfGftEgh.mjs";
2
2
  import { BetterFetchPlugin } from "@better-fetch/fetch";
3
3
  //#region src/sentinel/client.d.ts
4
4
  interface SentinelClientOptions {
@@ -34,9 +34,9 @@ interface SentinelClientOptions {
34
34
  */
35
35
  onChallengeFailed?: (error: Error) => void;
36
36
  }
37
- declare const sentinelClient: (options?: SentinelClientOptions) => {
37
+ export declare const sentinelClient: (options?: SentinelClientOptions) => {
38
38
  id: "sentinel";
39
39
  fetchPlugins: BetterFetchPlugin[];
40
40
  };
41
41
  //#endregion
42
- export { type DashAuditLog, type DashAuditLogsResponse, type DashClientOptions, type DashGetAllAuditLogsInput, type DashGetAuditLogsInput, type SentinelClientOptions, dashClient, sentinelClient };
42
+ export { type DashAuditLog, type DashAuditLogsResponse, type DashClientOptions, type DashGetAllAuditLogsInput, type DashGetAuditLogsInput, type SentinelClientOptions, dashClient };
package/dist/client.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { n as hash, o as createKV } from "./crypto-DRS7yhmR.mjs";
2
- import { a as resolveSentinelClientIdentifyUrl, c as identify, i as solvePoWChallenge, l as dashClient, n as decodePoWChallenge, o as resolveIdentifyClientRetry, r as encodePoWSolution, s as generateRequestId, t as createPowRetryTimeout } from "./pow-retry-Di0eUKbA.mjs";
1
+ import { n as hash, o as createKV } from "./crypto-Doziv2tH.mjs";
2
+ import { a as resolveSentinelClientIdentifyUrl, c as identify, i as solvePoWChallenge, l as dashClient, n as decodePoWChallenge, o as resolveIdentifyClientRetry, r as encodePoWSolution, s as generateRequestId, t as createPowRetryTimeout } from "./pow-retry-DIkxDwZf.mjs";
3
3
  import { env } from "@better-auth/core/env";
4
4
  //#region src/sentinel/fingerprint.ts
5
5
  function murmurhash3(str, seed = 0) {
@@ -1,7 +1,7 @@
1
1
  import { env } from "@better-auth/core/env";
2
2
  //#endregion
3
3
  //#region src/version.ts
4
- const PLUGIN_VERSION = "0.4.8";
4
+ const PLUGIN_VERSION = "0.4.10";
5
5
  //#endregion
6
6
  //#region src/constants.ts
7
7
  /**
@@ -1,4 +1,4 @@
1
- import { i as INFRA_USER_AGENT } from "./constants-C8_UJzzu.mjs";
1
+ import { i as INFRA_USER_AGENT } from "./constants-DLZKaIkd.mjs";
2
2
  import { createFetch } from "@better-fetch/fetch";
3
3
  import { sha256 } from "@noble/hashes/sha2.js";
4
4
  //#region src/fetch.ts
package/dist/email.d.mts CHANGED
@@ -8,7 +8,7 @@
8
8
  /**
9
9
  * Email template definitions with their required variables
10
10
  */
11
- declare const EMAIL_TEMPLATES: {
11
+ export declare const EMAIL_TEMPLATES: {
12
12
  readonly "verify-email": {
13
13
  readonly variables: {
14
14
  verificationCode?: string;
@@ -136,14 +136,14 @@ declare const EMAIL_TEMPLATES: {
136
136
  };
137
137
  };
138
138
  };
139
- type EmailTemplateId = keyof typeof EMAIL_TEMPLATES;
140
- type EmailTemplateVariables<T extends EmailTemplateId> = (typeof EMAIL_TEMPLATES)[T]["variables"];
141
- interface SendEmailResult {
139
+ export type EmailTemplateId = keyof typeof EMAIL_TEMPLATES;
140
+ export type EmailTemplateVariables<T extends EmailTemplateId> = (typeof EMAIL_TEMPLATES)[T]["variables"];
141
+ export interface SendEmailResult {
142
142
  success: boolean;
143
143
  messageId?: string;
144
144
  error?: string;
145
145
  }
146
- interface EmailConfig {
146
+ export interface EmailConfig {
147
147
  apiKey?: string;
148
148
  apiUrl?: string;
149
149
  /**
@@ -171,7 +171,7 @@ interface EmailConfig {
171
171
  /**
172
172
  * Type-safe send email options
173
173
  */
174
- type SendEmailOptions<T extends EmailTemplateId = EmailTemplateId> = {
174
+ export type SendEmailOptions<T extends EmailTemplateId = EmailTemplateId> = {
175
175
  /**
176
176
  * The template ID to use
177
177
  */
@@ -192,7 +192,7 @@ type SendEmailOptions<T extends EmailTemplateId = EmailTemplateId> = {
192
192
  /**
193
193
  * Options for sending bulk emails
194
194
  */
195
- type SendBulkEmailsOptions<T extends EmailTemplateId = EmailTemplateId> = {
195
+ export type SendBulkEmailsOptions<T extends EmailTemplateId = EmailTemplateId> = {
196
196
  /**
197
197
  * The template ID to use for all emails
198
198
  */
@@ -216,7 +216,7 @@ type SendBulkEmailsOptions<T extends EmailTemplateId = EmailTemplateId> = {
216
216
  /**
217
217
  * Result of a bulk email send operation
218
218
  */
219
- interface SendBulkEmailsResult {
219
+ export interface SendBulkEmailsResult {
220
220
  success: boolean;
221
221
  failures?: Record<string, {
222
222
  error?: string;
@@ -226,7 +226,7 @@ interface SendBulkEmailsResult {
226
226
  /**
227
227
  * Create an email sender instance
228
228
  */
229
- declare function createEmailSender(config?: EmailConfig): {
229
+ export declare function createEmailSender(config?: EmailConfig): {
230
230
  send: <T extends EmailTemplateId>(options: SendEmailOptions<T>) => Promise<SendEmailResult>;
231
231
  sendBulk: <T extends EmailTemplateId>(options: SendBulkEmailsOptions<T>) => Promise<SendBulkEmailsResult>;
232
232
  getTemplates: () => Promise<{
@@ -253,7 +253,7 @@ declare function createEmailSender(config?: EmailConfig): {
253
253
  * });
254
254
  * ```
255
255
  */
256
- declare function sendEmail<T extends EmailTemplateId>(options: SendEmailOptions<T>, config?: EmailConfig): Promise<SendEmailResult>;
256
+ export declare function sendEmail<T extends EmailTemplateId>(options: SendEmailOptions<T>, config?: EmailConfig): Promise<SendEmailResult>;
257
257
  /**
258
258
  * Send bulk emails using the Better Auth dashboard's email templates.
259
259
  *
@@ -270,6 +270,5 @@ declare function sendEmail<T extends EmailTemplateId>(options: SendEmailOptions<
270
270
  * });
271
271
  * ```
272
272
  */
273
- declare function sendBulkEmails<T extends EmailTemplateId>(options: SendBulkEmailsOptions<T>, config?: EmailConfig): Promise<SendBulkEmailsResult>;
274
- //#endregion
275
- export { EMAIL_TEMPLATES, EmailConfig, EmailTemplateId, EmailTemplateVariables, SendBulkEmailsOptions, SendBulkEmailsResult, SendEmailOptions, SendEmailResult, createEmailSender, sendBulkEmails, sendEmail };
273
+ export declare function sendBulkEmails<T extends EmailTemplateId>(options: SendBulkEmailsOptions<T>, config?: EmailConfig): Promise<SendBulkEmailsResult>;
274
+ //#endregion
package/dist/email.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { i as INFRA_USER_AGENT, n as INFRA_API_URL } from "./constants-C8_UJzzu.mjs";
1
+ import { i as INFRA_USER_AGENT, n as INFRA_API_URL } from "./constants-DLZKaIkd.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";
@@ -1,4 +1,4 @@
1
- import { g as KvRetryOptions } from "./types-BkUWsMpe.mjs";
1
+ import { g as KvRetryOptions } from "./types-sFD6YavS.mjs";
2
2
  import { BetterAuthClientOptions, ClientStore } from "better-auth";
3
3
  import { BetterFetch } from "@better-fetch/fetch";
4
4
  //#region src/dash-client.d.ts
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { A as SecurityOptions, C as createAuthMiddleware, D as ImpossibleTravelResult, E as CredentialStuffingResult, M as StaleUserResult, N as ThresholdConfig, O as SecurityEvent, S as asInfraContext, T as CompromisedPasswordResult, _ as KvRetryOptionsResolved, a as DashOptionsInternal, b as SentinelOptions, c as EndpointOptions, d as InfraEndpointContextValue, f as InfraPluginConnectionOptions, g as KvRetryOptions, h as KvOptionsResolved, i as DashOptions, j as SecurityVerdict, k as SecurityEventType, l as InfraAuthContext, m as KvOptions, n as ApiOptions, o as DashOptionsResolved, p as InfraPluginConnectionOptionsInternal, r as ApiOptionsResolved, s as Endpoint, t as APIError, u as InfraEndpointContext, v as LocationData, w as sentinel, x as SentinelOptionsInternal, y as LocationDataContext } from "./types-BkUWsMpe.mjs";
1
+ import { A as SecurityOptions, C as createAuthMiddleware, D as ImpossibleTravelResult, E as CredentialStuffingResult, M as StaleUserResult, N as ThresholdConfig, O as SecurityEvent, S as asInfraContext, T as CompromisedPasswordResult, _ as KvRetryOptionsResolved, a as DashOptionsInternal, b as SentinelOptions, c as EndpointOptions, d as InfraEndpointContextValue, f as InfraPluginConnectionOptions, g as KvRetryOptions, h as KvOptionsResolved, i as DashOptions, j as SecurityVerdict, k as SecurityEventType, l as InfraAuthContext, m as KvOptions, n as ApiOptions, o as DashOptionsResolved, p as InfraPluginConnectionOptionsInternal, r as ApiOptionsResolved, s as Endpoint, t as APIError, u as InfraEndpointContext, v as LocationData, w as sentinel, x as SentinelOptionsInternal, y as LocationDataContext } from "./types-sFD6YavS.mjs";
2
2
  import { EMAIL_TEMPLATES, EmailConfig, EmailTemplateId, EmailTemplateVariables, SendBulkEmailsOptions, SendBulkEmailsResult, SendEmailOptions, SendEmailResult, createEmailSender, sendBulkEmails, sendEmail } from "./email.mjs";
3
3
  import { Account, AuthContext, GenericEndpointContext, Session, User } from "better-auth";
4
4
  import z from "zod";
@@ -30,26 +30,26 @@ interface PoWSolution {
30
30
  counter: number;
31
31
  }
32
32
  /** Default difficulty in bits (18 = ~500ms solve time) */
33
- declare const DEFAULT_DIFFICULTY = 18;
33
+ export declare const DEFAULT_DIFFICULTY = 18;
34
34
  /** Challenge TTL in seconds */
35
- declare const CHALLENGE_TTL = 60;
35
+ export declare const CHALLENGE_TTL = 60;
36
36
  /**
37
37
  * Solve a PoW challenge (browser-compatible)
38
38
  * This function is designed to run in a browser environment
39
39
  */
40
- declare function solvePoWChallenge(challenge: PoWChallenge): Promise<PoWSolution>;
40
+ export declare function solvePoWChallenge(challenge: PoWChallenge): Promise<PoWSolution>;
41
41
  /**
42
42
  * Decode a base64-encoded challenge string (browser-compatible)
43
43
  */
44
- declare function decodePoWChallenge(encoded: string): PoWChallenge | null;
44
+ export declare function decodePoWChallenge(encoded: string): PoWChallenge | null;
45
45
  /**
46
46
  * Encode a solution string (browser-compatible)
47
47
  */
48
- declare function encodePoWSolution(solution: PoWSolution): string;
48
+ export declare function encodePoWSolution(solution: PoWSolution): string;
49
49
  /**
50
50
  * Verify a PoW solution locally (for testing purposes)
51
51
  */
52
- declare function verifyPoWSolution(nonce: string, counter: number, difficulty: number): Promise<boolean>;
52
+ export declare function verifyPoWSolution(nonce: string, counter: number, difficulty: number): Promise<boolean>;
53
53
  //#endregion
54
54
  //#region src/sms.d.ts
55
55
  /**
@@ -61,7 +61,7 @@ declare function verifyPoWSolution(nonce: string, counter: number, difficulty: n
61
61
  /**
62
62
  * SMS template definitions with their required variables
63
63
  */
64
- declare const SMS_TEMPLATES: {
64
+ export declare const SMS_TEMPLATES: {
65
65
  readonly "phone-verification": {
66
66
  readonly variables: {
67
67
  code: string;
@@ -140,7 +140,7 @@ interface SendSMSOptions {
140
140
  /**
141
141
  * Create an SMS sender instance
142
142
  */
143
- declare function createSMSSender(config?: SMSConfig): {
143
+ export declare function createSMSSender(config?: SMSConfig): {
144
144
  send: (options: SendSMSOptions) => Promise<SendSMSResult>;
145
145
  };
146
146
  /**
@@ -171,7 +171,7 @@ declare function createSMSSender(config?: SMSConfig): {
171
171
  * });
172
172
  * ```
173
173
  */
174
- declare function sendSMS(options: SendSMSOptions, config?: SMSConfig): Promise<SendSMSResult>;
174
+ export declare function sendSMS(options: SendSMSOptions, config?: SMSConfig): Promise<SendSMSResult>;
175
175
  //#endregion
176
176
  //#region src/routes/auth/types.d.ts
177
177
  type DBField = {
@@ -205,7 +205,9 @@ interface DashConfigResponse {
205
205
  deleteUserEnabled: boolean;
206
206
  modelName?: string;
207
207
  };
208
- baseURL: unknown;
208
+ /** Origin only (never the dynamic allowedHosts object). */
209
+ baseURL: string;
210
+ /** Mount path; reflects path embedded in baseURL when present. */
209
211
  basePath: string;
210
212
  emailVerification: {
211
213
  sendVerificationEmailEnabled: boolean;
@@ -368,6 +370,10 @@ interface DashDirectoryRegenerateTokenResponse {
368
370
  scimToken: string;
369
371
  scimEndpoint: string;
370
372
  }
373
+ interface DashDirectorySyncConnectionCountResponse {
374
+ count: number;
375
+ mode: "managed" | "legacy";
376
+ }
371
377
  //#endregion
372
378
  //#region src/events/constants.d.ts
373
379
  declare const EVENT_TYPES: {
@@ -414,7 +420,7 @@ declare const ORGANIZATION_EVENT_TYPES: {
414
420
  readonly ORGANIZATION_TEAM_MEMBER_REMOVED: "organization_team_member_removed";
415
421
  };
416
422
  /** All audit event type string constants (user + organization). */
417
- declare const USER_EVENT_TYPES: {
423
+ export declare const USER_EVENT_TYPES: {
418
424
  readonly ORGANIZATION_CREATED: "organization_created";
419
425
  readonly ORGANIZATION_UPDATED: "organization_updated";
420
426
  readonly ORGANIZATION_MEMBER_ADDED: "organization_member_added";
@@ -789,10 +795,10 @@ interface DashCheckUserExistsResponse {
789
795
  * @param email - Raw email to normalize
790
796
  * @param context - Auth context
791
797
  */
792
- declare function normalizeEmail(email: string, context: AuthContext): string;
798
+ export declare function normalizeEmail(email: string, context: AuthContext): string;
793
799
  //#endregion
794
800
  //#region src/index.d.ts
795
- declare const dash: <O extends DashOptions>(options?: O) => {
801
+ export declare const dash: <O extends DashOptions>(options?: O) => {
796
802
  id: "dash";
797
803
  options: DashOptionsResolved;
798
804
  version: string;
@@ -836,7 +842,7 @@ declare const dash: <O extends DashOptions>(options?: O) => {
836
842
  };
837
843
  session: {
838
844
  create: {
839
- before(session: {
845
+ before(_session: {
840
846
  id: string;
841
847
  createdAt: Date;
842
848
  updatedAt: Date;
@@ -845,7 +851,7 @@ declare const dash: <O extends DashOptions>(options?: O) => {
845
851
  token: string;
846
852
  ipAddress?: string | null | undefined;
847
853
  userAgent?: string | null | undefined;
848
- } & Record<string, unknown>, _ctx: GenericEndpointContext | null): Promise<{
854
+ } & Record<string, unknown>, untypedCtx: GenericEndpointContext | null): Promise<{
849
855
  data: {
850
856
  loginMethod: string | null;
851
857
  };
@@ -859,7 +865,7 @@ declare const dash: <O extends DashOptions>(options?: O) => {
859
865
  token: string;
860
866
  ipAddress?: string | null | undefined;
861
867
  userAgent?: string | null | undefined;
862
- } & Record<string, unknown>, _ctx: GenericEndpointContext | null): Promise<void>;
868
+ } & Record<string, unknown>, untypedCtx: GenericEndpointContext | null): Promise<void>;
863
869
  };
864
870
  delete: {
865
871
  after(session: {
@@ -2195,6 +2201,12 @@ declare const dash: <O extends DashOptions>(options?: O) => {
2195
2201
  }>>[];
2196
2202
  body: import("zod").ZodObject<{}, import("zod/v4/core").$strip>;
2197
2203
  }, DirectorySyncConnection>;
2204
+ countDashDirectorySyncConnections: import("better-call").StrictEndpoint<"/dash/directory-sync/connection-count", {
2205
+ method: "GET";
2206
+ use: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<{
2207
+ payload: Record<string, unknown>;
2208
+ }>>[];
2209
+ }, DashDirectorySyncConnectionCountResponse>;
2198
2210
  dashExecuteAdapter: import("better-call").StrictEndpoint<"/dash/execute-adapter", {
2199
2211
  method: "POST";
2200
2212
  use: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<{
@@ -2339,4 +2351,4 @@ declare const dash: <O extends DashOptions>(options?: O) => {
2339
2351
  } : {});
2340
2352
  };
2341
2353
  //#endregion
2342
- export { type APIError, type ApiOptions, type ApiOptionsResolved, 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 DashDirectoryCredential, type DashDirectoryDeleteResponse, type DashDirectoryEvent, type DashDirectoryEventsResponse, type DashDirectoryItem, type DashDirectoryRegenerateTokenResponse, type DashDirectoryRotateCredentialResponse, type DashDirectorySyncSSOPairing, 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 DashSCIMManagedConnectionEventType, type DashSCIMManagedCredentialStatus, type DashSCIMScope, 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, type DirectorySyncConnectionStatus, type DirectorySyncMode, EMAIL_TEMPLATES, type EmailConfig, type EmailTemplateId, type EmailTemplateVariables, type Endpoint, type EndpointOptions, type EventLocation, type EventTypesResponse, type ImpossibleTravelResult, type InfraAuthContext, type InfraEndpointContext, type InfraEndpointContextValue, type InfraPluginConnectionOptions, type InfraPluginConnectionOptionsInternal, type KvOptions, type KvOptionsResolved, type KvRetryOptions, type KvRetryOptionsResolved, type LegacyDashDirectoryCreateResponse, type LegacyDashDirectoryItem, type LocationData, type LocationDataContext, type ORGANIZATION_USER_PREVIEW_SELECT, type OrganizationUserPreview, 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, type asInfraContext, type createAuthMiddleware, createEmailSender, createSMSSender, dash, decodePoWChallenge, encodePoWSolution, normalizeEmail, sendBulkEmails, sendEmail, sendSMS, sentinel, solvePoWChallenge, verifyPoWSolution };
2354
+ export { type APIError, type ApiOptions, type ApiOptionsResolved, type CompromisedPasswordResult, type CredentialStuffingResult, type DBField, 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 DashDirectoryCredential, type DashDirectoryDeleteResponse, type DashDirectoryEvent, type DashDirectoryEventsResponse, type DashDirectoryItem, type DashDirectoryRegenerateTokenResponse, type DashDirectoryRotateCredentialResponse, type DashDirectorySyncConnectionCountResponse, type DashDirectorySyncSSOPairing, 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 DashSCIMManagedConnectionEventType, type DashSCIMManagedCredentialStatus, type DashSCIMScope, 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, type DirectorySyncConnectionStatus, type DirectorySyncMode, EMAIL_TEMPLATES, type EmailConfig, type EmailTemplateId, type EmailTemplateVariables, type Endpoint, type EndpointOptions, type EventLocation, type EventTypesResponse, type ImpossibleTravelResult, type InfraAuthContext, type InfraEndpointContext, type InfraEndpointContextValue, type InfraPluginConnectionOptions, type InfraPluginConnectionOptionsInternal, type KvOptions, type KvOptionsResolved, type KvRetryOptions, type KvRetryOptionsResolved, type LegacyDashDirectoryCreateResponse, type LegacyDashDirectoryItem, type LocationData, type LocationDataContext, type ORGANIZATION_USER_PREVIEW_SELECT, type OrganizationUserPreview, type PoWChallenge, type PoWSolution, type SCIMPlugin, type SMSConfig, type SMSTemplateId, type SMSTemplateVariables, 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, type UserEvent, type UserEventType, type UserEventsResponse, type asInfraContext, type createAuthMiddleware, createEmailSender, sendBulkEmails, sendEmail, sentinel };
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { i as INFRA_USER_AGENT, n as INFRA_API_URL, o as PLUGIN_VERSION, r as INFRA_KV_URL } from "./constants-C8_UJzzu.mjs";
2
- import { a as createAPI, n as hash$1, o as createKV, r as hmacSha256Hex } from "./crypto-DRS7yhmR.mjs";
1
+ import { i as INFRA_USER_AGENT, n as INFRA_API_URL, o as PLUGIN_VERSION, r as INFRA_KV_URL } from "./constants-DLZKaIkd.mjs";
2
+ import { a as createAPI, n as hash$1, o as createKV, r as hmacSha256Hex } from "./crypto-Doziv2tH.mjs";
3
3
  import { EMAIL_TEMPLATES, createEmailSender, sendBulkEmails, sendEmail } from "./email.mjs";
4
4
  import { getCurrentAdapter, getCurrentAuthContext, getCurrentDBAdapterAsyncLocalStorage, runWithTransaction } from "@better-auth/core/context";
5
5
  import { APIError, generateId, getAuthTables, logger } from "better-auth";
@@ -13,14 +13,24 @@ import z, { z as z$1 } from "zod";
13
13
  import { DOMParser } from "@xmldom/xmldom";
14
14
  import { base64Url } from "@better-auth/utils/base64";
15
15
  import { createLocalJWKSet, jwtVerify } from "jose";
16
- import { generateRandomString, symmetricEncrypt } from "better-auth/crypto";
16
+ import { generateRandomString, symmetricDecrypt, symmetricEncrypt } from "better-auth/crypto";
17
17
  import { createOTP } from "@better-auth/utils/otp";
18
18
  //#region src/options.ts
19
+ /** @internal */
20
+ function resolveApiKeys(raw) {
21
+ const apiKeys = (raw?.trim() || env.BETTER_AUTH_API_KEY?.trim() || "").split(",").map((k) => k.trim()).filter((k) => k.length > 0);
22
+ return {
23
+ apiKey: apiKeys[0] ?? "",
24
+ apiKeys
25
+ };
26
+ }
19
27
  function resolveConnectionOptions(options) {
28
+ const { apiKey, apiKeys } = resolveApiKeys(options?.apiKey);
20
29
  return {
21
30
  apiUrl: options?.apiUrl || INFRA_API_URL,
22
31
  kvUrl: options?.kvUrl || INFRA_KV_URL,
23
- apiKey: options?.apiKey || env.BETTER_AUTH_API_KEY || "",
32
+ apiKey,
33
+ apiKeys,
24
34
  apiOptions: {
25
35
  timeout: options?.apiOptions?.timeout ?? options?.apiTimeout ?? 3e3,
26
36
  headers: options?.apiOptions?.headers
@@ -96,6 +106,92 @@ function resolveClientIpFromHeaders(headers, ipAddressHeaders) {
96
106
  }
97
107
  }
98
108
  //#endregion
109
+ //#region ../utils/dist/url.mjs
110
+ /**
111
+ * URL helpers: normalization, SSRF-safe parsing/validation, and path/query segments.
112
+ */
113
+ const HTTP_PROTOCOLS = ["http:", "https:"];
114
+ function isAllowedProtocol(protocol, allowedProtocols) {
115
+ return (allowedProtocols?.length ? allowedProtocols : HTTP_PROTOCOLS).includes(protocol);
116
+ }
117
+ /** Normalizes a base URL and path by trimming slashes and joining them. */
118
+ function normalizeUrl(base, path) {
119
+ return `${base.replace(/\/+$/, "")}${path ? `/${path.replace(/^\/+|\/+$/g, "")}` : ""}`;
120
+ }
121
+ function normalizePath(path) {
122
+ const trimmed = path.trim();
123
+ if (!trimmed) throw new Error("Path must be a non-empty string");
124
+ return `/${trimmed.replace(/^\/+|\/+$/g, "")}`;
125
+ }
126
+ /** Normalizes a base URL and path into separate parts and a joined URL. */
127
+ function resolveBaseUrlPath(baseURL, basePath) {
128
+ const cleanBase = baseURL.trim().replace(/\/+$/, "");
129
+ const resolvedBasePath = normalizePath(basePath);
130
+ return {
131
+ baseURL: cleanBase,
132
+ basePath: resolvedBasePath,
133
+ url: normalizeUrl(cleanBase, resolvedBasePath)
134
+ };
135
+ }
136
+ /** Falls back when basePath is null, undefined, or blank/whitespace. */
137
+ function resolveProjectBasePath(basePath) {
138
+ return basePath?.trim() || "/api/auth";
139
+ }
140
+ /**
141
+ * Split a Better Auth **already-resolved** mount URL (`ctx.context.baseURL`).
142
+ * Path embedded in that URL is the effective mount (BA path-in-baseURL /
143
+ * withPath result). `fallbackBasePath` is used only when the mount URL has no
144
+ * path (e.g. dynamic allowedHosts before a request sets the full URL).
145
+ */
146
+ function resolveAuthMountBase(mountURL, fallbackBasePath) {
147
+ try {
148
+ const url = new URL(mountURL);
149
+ const pathname = url.pathname.replace(/\/+$/, "") || "";
150
+ if (pathname && pathname !== "/") return resolveBaseUrlPath(url.origin, pathname);
151
+ return resolveBaseUrlPath(url.origin, resolveProjectBasePath(fallbackBasePath));
152
+ } catch {
153
+ return resolveBaseUrlPath(mountURL, resolveProjectBasePath(fallbackBasePath));
154
+ }
155
+ }
156
+ function toAllowedOriginSet(allowedOrigins) {
157
+ const set = /* @__PURE__ */ new Set();
158
+ for (const raw of allowedOrigins) try {
159
+ const parsed = new URL(raw.trim());
160
+ if (isAllowedProtocol(parsed.protocol, HTTP_PROTOCOLS)) set.add(parsed.origin);
161
+ } catch {}
162
+ return set;
163
+ }
164
+ function isAllowedOrigin(origin, allowedOrigins) {
165
+ if (!allowedOrigins.length) return false;
166
+ return toAllowedOriginSet(allowedOrigins).has(origin);
167
+ }
168
+ function safeResolveUrl(raw, baseURL) {
169
+ const trim = raw.trim();
170
+ if (!trim) return null;
171
+ if (trim.startsWith("//") || trim.includes("\\")) return null;
172
+ if (/[\u0000-\u001F\u007F]/.test(trim)) return null;
173
+ try {
174
+ if (trim.startsWith("/")) return new URL(trim, baseURL);
175
+ return new URL(trim);
176
+ } catch {
177
+ return null;
178
+ }
179
+ }
180
+ function getSafeUniqueOrigins(urls) {
181
+ return [...toAllowedOriginSet(urls)];
182
+ }
183
+ /**
184
+ * Returns an absolute URL only if its protocol and origin are allowed (after
185
+ * resolving relative paths against `baseURL`). Otherwise returns `null`.
186
+ */
187
+ function safeUrlParse(input) {
188
+ if (input.raw == null || input.raw.trim() === "") return null;
189
+ const target = safeResolveUrl(input.raw, input.baseURL);
190
+ if (!target || !isAllowedProtocol(target.protocol, input.allowedProtocols)) return null;
191
+ if (!isAllowedOrigin(target.origin, input.allowedOrigins ?? [])) return null;
192
+ return target.href;
193
+ }
194
+ //#endregion
99
195
  //#region src/events/constants.ts
100
196
  const EVENT_TYPES = {
101
197
  USER_CREATED: "user_created",
@@ -1256,7 +1352,7 @@ function createIdentificationMiddleware($kv, options) {
1256
1352
  if (!skipIdentification) {
1257
1353
  const extracted = extractIdentificationHeaders(ctx.request);
1258
1354
  headerVisitorId = extracted.visitorId;
1259
- requestId = extracted.requestId ?? ctx.getCookie("__infra-rid") ?? null;
1355
+ requestId = extracted.requestId ?? ctx.getCookie(IDENTIFICATION_COOKIE_NAME) ?? null;
1260
1356
  }
1261
1357
  ctx.context.requestId = requestId;
1262
1358
  if (skipIdentification) ctx.context.identification = null;
@@ -2547,7 +2643,7 @@ const sentinel = (options) => {
2547
2643
  id: "sentinel",
2548
2644
  init(ctx) {
2549
2645
  activityTrackingEnabled = (ctx.getPlugin("dash")?.options)?.activityTracking?.enabled === true;
2550
- const basePath = new URL(ctx.baseURL).pathname;
2646
+ const basePath = ctx.baseURL ? resolveAuthMountBase(ctx.baseURL, ctx.options.basePath).basePath : resolveProjectBasePath(ctx.options.basePath);
2551
2647
  const ipHeaders = ctx.options.advanced?.ipAddress?.ipAddressHeaders || null;
2552
2648
  const disableIpTracking = ctx.options.advanced?.ipAddress?.disableIpTracking === true;
2553
2649
  /**
@@ -4197,14 +4293,14 @@ const jwtMiddleware = (options, schema, getJWT) => {
4197
4293
  throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
4198
4294
  });
4199
4295
  const apiKeyHash = payload.apiKeyHash;
4200
- if (typeof apiKeyHash !== "string" || !options.apiKey) {
4296
+ if (typeof apiKeyHash !== "string" || options.apiKeys.length === 0) {
4201
4297
  ctx.context.logger.warn("[Dash] API key hash is missing or invalid", {
4202
4298
  apiKeyHash,
4203
- apiKey: options.apiKey ? "present" : "missing"
4299
+ apiKey: options.apiKeys.length > 0 ? "present" : "missing"
4204
4300
  });
4205
4301
  throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
4206
4302
  }
4207
- if (!timingSafeEqualHash(apiKeyHash, await hash(options.apiKey))) {
4303
+ if (!await apiKeyHashMatchesAny(apiKeyHash, options.apiKeys)) {
4208
4304
  ctx.context.logger.warn("[Dash] API key hash mismatch");
4209
4305
  throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
4210
4306
  }
@@ -4252,20 +4348,85 @@ const jwtValidateMiddleware = (options) => {
4252
4348
  throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
4253
4349
  });
4254
4350
  const apiKeyHash = payload.apiKeyHash;
4255
- if (typeof apiKeyHash !== "string" || !options.apiKey) {
4351
+ if (typeof apiKeyHash !== "string" || options.apiKeys.length === 0) {
4256
4352
  ctx.context.logger.warn("[Dash] API key hash is missing or invalid", {
4257
4353
  apiKeyHash: typeof apiKeyHash,
4258
- apiKey: options.apiKey ? "present" : "missing"
4354
+ apiKey: options.apiKeys.length > 0 ? "present" : "missing"
4259
4355
  });
4260
4356
  throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
4261
4357
  }
4262
- if (!timingSafeEqualHash(apiKeyHash, await hash(options.apiKey))) {
4358
+ if (!await apiKeyHashMatchesAny(apiKeyHash, options.apiKeys)) {
4263
4359
  ctx.context.logger.warn("[Dash] API key hash mismatch");
4264
4360
  throw ctx.error("UNAUTHORIZED", { message: "Invalid API key" });
4265
4361
  }
4266
4362
  return { payload };
4267
4363
  });
4268
4364
  };
4365
+ async function apiKeyHashMatchesAny(apiKeyHash, apiKeys) {
4366
+ let matched = false;
4367
+ for (const key of apiKeys) if (timingSafeEqualHash(apiKeyHash, await hash(key))) matched = true;
4368
+ return matched;
4369
+ }
4370
+ //#endregion
4371
+ //#region src/lib/safe-url.ts
4372
+ function isSafeHttpUrl(value) {
4373
+ try {
4374
+ const url = new URL(value);
4375
+ return (url.protocol === "http:" || url.protocol === "https:") && url.username === "" && url.password === "";
4376
+ } catch {
4377
+ return false;
4378
+ }
4379
+ }
4380
+ /** http(s) absolute URL without embedded credentials. */
4381
+ const safeUrlSchema = z.string().trim().min(1).pipe(z.url().refine(isSafeHttpUrl, { message: "URL must be a valid http(s) URL without credentials" })).transform((value) => new URL(value).href);
4382
+ const optionalSafeUrlSchema = z.union([safeUrlSchema, z.literal("")]).optional();
4383
+ /**
4384
+ * Request-scoped full auth URL (origin + mount). Prefer over `options.baseURL`,
4385
+ * which is origin-only or a dynamic config object.
4386
+ * BA resolves this before endpoint handlers; missing value is a programming error.
4387
+ */
4388
+ function getAuthBaseUrl(ctx) {
4389
+ const resolved = ctx.context.baseURL?.trim();
4390
+ if (!resolved) throw new APIError$1("INTERNAL_SERVER_ERROR", { message: "Auth base URL is not resolved for this request." });
4391
+ return resolved;
4392
+ }
4393
+ /** App origin for post-auth redirects (not the auth mount path). */
4394
+ function getAuthOrigin(ctx) {
4395
+ return new URL(getAuthBaseUrl(ctx)).origin;
4396
+ }
4397
+ function getAllowedAuthOrigins(ctx, extraOrigins = []) {
4398
+ return getSafeUniqueOrigins([
4399
+ getAuthBaseUrl(ctx),
4400
+ ...ctx.context.trustedOrigins ?? [],
4401
+ ...extraOrigins
4402
+ ]);
4403
+ }
4404
+ /**
4405
+ * Resolve a redirect against the auth server's base URL and trusted origins.
4406
+ * Falls back when the target origin is not allowed.
4407
+ */
4408
+ function resolveTrustedAuthRedirectUrl(ctx, raw, fallback) {
4409
+ return safeUrlParse({
4410
+ raw,
4411
+ baseURL: getAuthBaseUrl(ctx),
4412
+ allowedOrigins: getAllowedAuthOrigins(ctx)
4413
+ }) ?? fallback;
4414
+ }
4415
+ /** Returns a trusted redirect URL or null when the origin is not allowed. */
4416
+ function parseTrustedAuthRedirectUrl(ctx, raw) {
4417
+ return safeUrlParse({
4418
+ raw,
4419
+ baseURL: getAuthBaseUrl(ctx),
4420
+ allowedOrigins: getAllowedAuthOrigins(ctx)
4421
+ });
4422
+ }
4423
+ const TRUSTED_CALLBACK_URL_ERROR = "Callback URL must use http(s) and match the auth base URL or a trusted origin.";
4424
+ /** Rejects email/redirect callbacks whose origin is not on the auth allowlist. */
4425
+ function requireTrustedAuthCallbackUrl(ctx, raw) {
4426
+ const trusted = parseTrustedAuthRedirectUrl(ctx, raw);
4427
+ if (!trusted) throw new APIError$1("BAD_REQUEST", { message: TRUSTED_CALLBACK_URL_ERROR });
4428
+ return trusted;
4429
+ }
4269
4430
  //#endregion
4270
4431
  //#region ../utils/dist/semver.mjs
4271
4432
  /**
@@ -4337,6 +4498,7 @@ const getConfig = (options) => {
4337
4498
  }, async (ctx) => {
4338
4499
  const advancedOptions = ctx.context.options.advanced;
4339
4500
  const organizationPlugin = ctx.context.getPlugin("organization");
4501
+ const authBase = resolveAuthMountBase(getAuthBaseUrl(ctx), ctx.context.options.basePath);
4340
4502
  return redactDashSettings({
4341
4503
  version: ctx.context.version || null,
4342
4504
  socialProviders: Object.keys(ctx.context.options.socialProviders || {}),
@@ -4406,8 +4568,8 @@ const getConfig = (options) => {
4406
4568
  deleteUserEnabled: !!ctx.context.options.user?.deleteUser?.enabled,
4407
4569
  modelName: ctx.context.options.user?.modelName
4408
4570
  },
4409
- baseURL: ctx.context.options.baseURL,
4410
- basePath: ctx.context.options.basePath || "/api/auth",
4571
+ baseURL: authBase.baseURL,
4572
+ basePath: authBase.basePath,
4411
4573
  emailVerification: { sendVerificationEmailEnabled: !!ctx.context.options.emailVerification?.sendVerificationEmail },
4412
4574
  insights: {
4413
4575
  hasDatabase: !!ctx.context.options.database,
@@ -4601,7 +4763,7 @@ const directorySyncClaimsSchema = z.object({
4601
4763
  setupOperationId: z.string().trim().min(16).max(255).optional()
4602
4764
  });
4603
4765
  function getScimEndpoint$1(baseUrl) {
4604
- return `${baseUrl}/scim/v2`;
4766
+ return normalizeUrl(baseUrl, "scim/v2");
4605
4767
  }
4606
4768
  function assertDirectorySyncClaims(ctx, organizationId) {
4607
4769
  const claims = ctx.context.payload;
@@ -5503,7 +5665,7 @@ async function scimProviderExists(ctx, organizationId, providerId) {
5503
5665
  }) != null;
5504
5666
  }
5505
5667
  function getScimEndpoint(baseUrl) {
5506
- return `${baseUrl}/scim/v2`;
5668
+ return normalizeUrl(baseUrl, "scim/v2");
5507
5669
  }
5508
5670
  function resolveMode(ctx) {
5509
5671
  return resolveDirectorySyncMode(getSCIMPlugin(ctx), isManagedDirectorySyncEnabled(ctx));
@@ -5518,6 +5680,36 @@ function isDuplicateDirectorySyncError(e) {
5518
5680
  if (e instanceof Error) return /unique constraint|duplicate key|already exists|P2002/i.test(e.message);
5519
5681
  return false;
5520
5682
  }
5683
+ const countDirectorySyncConnections = (options) => {
5684
+ return createAuthEndpoint("/dash/directory-sync/connection-count", {
5685
+ method: "GET",
5686
+ use: [jwtMiddleware(options)]
5687
+ }, async (ctx) => {
5688
+ const mode = resolveMode(ctx);
5689
+ if (mode === "managed") return {
5690
+ count: await ctx.context.adapter.count({
5691
+ model: "directorySyncConnection",
5692
+ where: [{
5693
+ field: "status",
5694
+ value: ["active", "decommissioning"],
5695
+ operator: "in"
5696
+ }]
5697
+ }),
5698
+ mode: "managed"
5699
+ };
5700
+ if (mode === "legacy") {
5701
+ if (!isOrganizationEnabled(ctx)) return {
5702
+ count: 0,
5703
+ mode: "legacy"
5704
+ };
5705
+ return {
5706
+ count: await ctx.context.adapter.count({ model: "scimProvider" }),
5707
+ mode: "legacy"
5708
+ };
5709
+ }
5710
+ throw ctx.error("NOT_FOUND", { message: "Directory sync connection count unavailable" });
5711
+ });
5712
+ };
5521
5713
  const listOrganizationDirectories = (options) => {
5522
5714
  return createAuthEndpoint("/dash/organization/:id/directories", {
5523
5715
  method: "GET",
@@ -6062,105 +6254,6 @@ function allowsPasswordlessInviteComplete(authMode) {
6062
6254
  return authMode === "create_with_session";
6063
6255
  }
6064
6256
  //#endregion
6065
- //#region ../utils/dist/url.mjs
6066
- /**
6067
- * URL helpers: normalization, SSRF-safe parsing/validation, and path/query segments.
6068
- */
6069
- const HTTP_PROTOCOLS = ["http:", "https:"];
6070
- function isAllowedProtocol(protocol, allowedProtocols) {
6071
- return (allowedProtocols?.length ? allowedProtocols : HTTP_PROTOCOLS).includes(protocol);
6072
- }
6073
- function toAllowedOriginSet(allowedOrigins) {
6074
- const set = /* @__PURE__ */ new Set();
6075
- for (const raw of allowedOrigins) try {
6076
- const parsed = new URL(raw.trim());
6077
- if (isAllowedProtocol(parsed.protocol, HTTP_PROTOCOLS)) set.add(parsed.origin);
6078
- } catch {}
6079
- return set;
6080
- }
6081
- function isAllowedOrigin(origin, allowedOrigins) {
6082
- if (!allowedOrigins.length) return false;
6083
- return toAllowedOriginSet(allowedOrigins).has(origin);
6084
- }
6085
- function safeResolveUrl(raw, baseURL) {
6086
- const trim = raw.trim();
6087
- if (!trim) return null;
6088
- if (trim.startsWith("//") || trim.includes("\\")) return null;
6089
- if (/[\u0000-\u001F\u007F]/.test(trim)) return null;
6090
- try {
6091
- if (trim.startsWith("/")) return new URL(trim, baseURL);
6092
- return new URL(trim);
6093
- } catch {
6094
- return null;
6095
- }
6096
- }
6097
- function getSafeUniqueOrigins(urls) {
6098
- return [...toAllowedOriginSet(urls)];
6099
- }
6100
- /**
6101
- * Returns an absolute URL only if its protocol and origin are allowed (after
6102
- * resolving relative paths against `baseURL`). Otherwise returns `null`.
6103
- */
6104
- function safeUrlParse(input) {
6105
- if (input.raw == null || input.raw.trim() === "") return null;
6106
- const target = safeResolveUrl(input.raw, input.baseURL);
6107
- if (!target || !isAllowedProtocol(target.protocol, input.allowedProtocols)) return null;
6108
- if (!isAllowedOrigin(target.origin, input.allowedOrigins ?? [])) return null;
6109
- return target.href;
6110
- }
6111
- //#endregion
6112
- //#region src/lib/safe-url.ts
6113
- function isSafeHttpUrl(value) {
6114
- try {
6115
- const url = new URL(value);
6116
- return (url.protocol === "http:" || url.protocol === "https:") && url.username === "" && url.password === "";
6117
- } catch {
6118
- return false;
6119
- }
6120
- }
6121
- /** http(s) absolute URL without embedded credentials. */
6122
- const safeUrlSchema = z.string().trim().min(1).pipe(z.url().refine(isSafeHttpUrl, { message: "URL must be a valid http(s) URL without credentials" })).transform((value) => new URL(value).href);
6123
- const optionalSafeUrlSchema = z.union([safeUrlSchema, z.literal("")]).optional();
6124
- function getAuthBaseUrl(ctx) {
6125
- const baseURL = ctx.context.options.baseURL;
6126
- return typeof baseURL === "string" && baseURL.trim() ? baseURL : "/";
6127
- }
6128
- function getAllowedAuthOrigins(ctx, extraOrigins = []) {
6129
- const baseURL = getAuthBaseUrl(ctx);
6130
- const trusted = ctx.context.options.trustedOrigins;
6131
- return getSafeUniqueOrigins([
6132
- baseURL,
6133
- ...Array.isArray(trusted) ? trusted.filter((origin) => typeof origin === "string") : [],
6134
- ...extraOrigins
6135
- ]);
6136
- }
6137
- /**
6138
- * Resolve a redirect against the auth server's base URL and trusted origins.
6139
- * Falls back when the target origin is not allowed.
6140
- */
6141
- function resolveTrustedAuthRedirectUrl(ctx, raw, fallback) {
6142
- return safeUrlParse({
6143
- raw,
6144
- baseURL: getAuthBaseUrl(ctx),
6145
- allowedOrigins: getAllowedAuthOrigins(ctx)
6146
- }) ?? fallback;
6147
- }
6148
- /** Returns a trusted redirect URL or null when the origin is not allowed. */
6149
- function parseTrustedAuthRedirectUrl(ctx, raw) {
6150
- return safeUrlParse({
6151
- raw,
6152
- baseURL: getAuthBaseUrl(ctx),
6153
- allowedOrigins: getAllowedAuthOrigins(ctx)
6154
- });
6155
- }
6156
- const TRUSTED_CALLBACK_URL_ERROR = "Callback URL must use http(s) and match the auth base URL or a trusted origin.";
6157
- /** Rejects email/redirect callbacks whose origin is not on the auth allowlist. */
6158
- function requireTrustedAuthCallbackUrl(ctx, raw) {
6159
- const trusted = parseTrustedAuthRedirectUrl(ctx, raw);
6160
- if (!trusted) throw new APIError$1("BAD_REQUEST", { message: TRUSTED_CALLBACK_URL_ERROR });
6161
- return trusted;
6162
- }
6163
- //#endregion
6164
6257
  //#region src/routes/invitations/index.ts
6165
6258
  async function verifyPendingInvitation(token, $api, ctx) {
6166
6259
  const { data: invitation, error } = await $api("/api/internal/invitations/verify", {
@@ -6206,7 +6299,7 @@ async function executePlatformInvitationCompletion(ctx, $api, invitation, args)
6206
6299
  user: existingUser
6207
6300
  });
6208
6301
  }
6209
- return { redirectUrl: resolveTrustedAuthRedirectUrl(ctx, invitation.redirectUrl, getAuthBaseUrl(ctx)) };
6302
+ return { redirectUrl: resolveTrustedAuthRedirectUrl(ctx, invitation.redirectUrl, getAuthOrigin(ctx)) };
6210
6303
  }
6211
6304
  if (!password && !allowsPasswordlessInviteComplete(invitation.authMode)) throw new APIError("BAD_REQUEST", { message: "Password is required to complete this invitation." });
6212
6305
  const userPayload = {
@@ -6237,7 +6330,7 @@ async function executePlatformInvitationCompletion(ctx, $api, invitation, args)
6237
6330
  user
6238
6331
  });
6239
6332
  }
6240
- return { redirectUrl: resolveTrustedAuthRedirectUrl(ctx, invitation.redirectUrl, getAuthBaseUrl(ctx)) };
6333
+ return { redirectUrl: resolveTrustedAuthRedirectUrl(ctx, invitation.redirectUrl, getAuthOrigin(ctx)) };
6241
6334
  }
6242
6335
  /**
6243
6336
  * Accept invitation endpoint
@@ -6270,14 +6363,14 @@ const acceptInvitation = (options) => {
6270
6363
  user: existingUser
6271
6364
  });
6272
6365
  }
6273
- const redirectUrl = resolveTrustedAuthRedirectUrl(ctx, invitation.redirectUrl, getAuthBaseUrl(ctx));
6366
+ const redirectUrl = resolveTrustedAuthRedirectUrl(ctx, invitation.redirectUrl, getAuthOrigin(ctx));
6274
6367
  return ctx.redirect(redirectUrl);
6275
6368
  }
6276
6369
  if (usesPlatformInviteAcceptFlow(invitation.authMode)) {
6277
6370
  const platformUrl = options.apiUrl || INFRA_API_URL;
6278
6371
  const acceptPageUrl = new URL("/invite/accept", platformUrl);
6279
6372
  acceptPageUrl.searchParams.set("token", token);
6280
- const callbackUrl = `${ctx.context.options.baseURL}${ctx.context.options.basePath || "/api/auth"}/dash/complete-invitation`;
6373
+ const callbackUrl = normalizeUrl(ctx.context.baseURL, "dash/complete-invitation");
6281
6374
  acceptPageUrl.searchParams.set("callback", callbackUrl);
6282
6375
  return ctx.redirect(acceptPageUrl.toString());
6283
6376
  }
@@ -6305,7 +6398,7 @@ const acceptInvitation = (options) => {
6305
6398
  user
6306
6399
  });
6307
6400
  }
6308
- const redirectUrl = resolveTrustedAuthRedirectUrl(ctx, invitation.redirectUrl, getAuthBaseUrl(ctx));
6401
+ const redirectUrl = resolveTrustedAuthRedirectUrl(ctx, invitation.redirectUrl, getAuthOrigin(ctx));
6309
6402
  return ctx.redirect(redirectUrl);
6310
6403
  });
6311
6404
  };
@@ -6385,7 +6478,7 @@ const completeInvitationSocial = (options) => {
6385
6478
  if (sessionCookieToken) await ctx.context.internalAdapter.deleteSession(sessionCookieToken);
6386
6479
  deleteSessionCookie(ctx);
6387
6480
  }
6388
- return ctx.redirect(resolveTrustedAuthRedirectUrl(ctx, invitation.redirectUrl, getAuthBaseUrl(ctx)));
6481
+ return ctx.redirect(resolveTrustedAuthRedirectUrl(ctx, invitation.redirectUrl, getAuthOrigin(ctx)));
6389
6482
  });
6390
6483
  };
6391
6484
  /**
@@ -8439,7 +8532,7 @@ async function resolveSAMLConfig(samlConfig, providerId, baseURL, ctx) {
8439
8532
  const ba17OrLater = isBetterAuth17OrLater(ctx.context.version);
8440
8533
  if (saml17OrNewer && !resolvedEntryPoint) throw ctx.error("BAD_REQUEST", { message: "SAML entry point URL is required; provide entryPoint or IdP metadata with SingleSignOnService Location" });
8441
8534
  if (ba17OrLater && !idpMetadataXml && !samlConfig.entityId?.trim()) throw ctx.error("BAD_REQUEST", { message: "IdP entity ID is required when IdP metadata XML is not provided" });
8442
- const spIssuer = `${baseURL}/sso/saml2/sp/metadata?providerId=${providerId}`;
8535
+ const spIssuer = `${normalizeUrl(baseURL, "sso/saml2/sp/metadata")}?providerId=${providerId}`;
8443
8536
  const idpMetadata = idpMetadataXml ? { metadata: idpMetadataXml } : {
8444
8537
  ...ba17OrLater ? { entityID: samlConfig.entityId } : {},
8445
8538
  ...samlConfig.entryPoint ? { singleSignOnService: [{
@@ -8457,7 +8550,7 @@ async function resolveSAMLConfig(samlConfig, providerId, baseURL, ctx) {
8457
8550
  entryPoint: resolvedEntryPoint,
8458
8551
  ...samlConfig.cert ? { cert: samlConfig.cert } : {}
8459
8552
  } : {
8460
- callbackUrl: `${baseURL}/sso/saml2/sp/acs/${providerId}`,
8553
+ callbackUrl: normalizeUrl(baseURL, `sso/saml2/sp/acs/${providerId}`),
8461
8554
  spMetadata: {},
8462
8555
  entryPoint: resolvedEntryPoint ?? "",
8463
8556
  cert: samlConfig.cert ?? ""
@@ -8949,7 +9042,6 @@ const enableTwoFactor = (options) => createAuthEndpoint("/dash/enable-two-factor
8949
9042
  value: userId
8950
9043
  }]
8951
9044
  });
8952
- const { generateRandomString, symmetricEncrypt } = await import("better-auth/crypto");
8953
9045
  const secret = generateRandomString(32);
8954
9046
  const { backupCodes, encryptedBackupCodes } = await generateBackupCodes$1(ctx.context.secret, resolveBackupCodeOptions(twoFactorPlugin));
8955
9047
  const encryptedSecret = await symmetricEncrypt({
@@ -9032,7 +9124,6 @@ const viewTwoFactorTotpUri = (options) => createAuthEndpoint("/dash/view-two-fac
9032
9124
  if (!twoFactorRecord) throw new APIError("NOT_FOUND", { message: "Two-factor authentication not set up for this user" });
9033
9125
  let secret = twoFactorRecord.secret;
9034
9126
  try {
9035
- const { symmetricDecrypt } = await import("better-auth/crypto");
9036
9127
  secret = await symmetricDecrypt({
9037
9128
  key: ctx.context.secret,
9038
9129
  data: twoFactorRecord.secret
@@ -10883,13 +10974,13 @@ const dash = (options) => {
10883
10974
  },
10884
10975
  session: {
10885
10976
  create: {
10886
- async before(session, _ctx) {
10887
- const ctx = _ctx;
10977
+ async before(_session, untypedCtx) {
10978
+ const ctx = untypedCtx;
10888
10979
  if (!ctx) return;
10889
10980
  return { data: { loginMethod: getLoginMethod(ctx) } };
10890
10981
  },
10891
- async after(session, _ctx) {
10892
- const ctx = _ctx;
10982
+ async after(session, untypedCtx) {
10983
+ const ctx = untypedCtx;
10893
10984
  if (!ctx || !session.userId) return;
10894
10985
  const location = ctx.context.location;
10895
10986
  const loginMethod = getLoginMethod(ctx) ?? void 0;
@@ -11152,6 +11243,7 @@ const dash = (options) => {
11152
11243
  listDashManagedDirectoryEvents: listDirectoryEvents(settings),
11153
11244
  decommissionDashManagedOrganizationDirectory: decommissionOrganizationDirectory(settings),
11154
11245
  unpairDashManagedOrganizationDirectory: unpairOrganizationDirectory(settings),
11246
+ countDashDirectorySyncConnections: countDirectorySyncConnections(settings),
11155
11247
  dashExecuteAdapter: executeAdapter(settings)
11156
11248
  },
11157
11249
  schema: {
package/dist/native.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as DashGetAllAuditLogsInput, i as DashClientOptions, n as DashAuditLog, o as DashGetAuditLogsInput, r as DashAuditLogsResponse, s as dashClient, t as IdentifyClientOptions } from "./identify-client-options-DgKA4Eq8.mjs";
1
+ import { a as DashGetAllAuditLogsInput, i as DashClientOptions, n as DashAuditLog, o as DashGetAuditLogsInput, r as DashAuditLogsResponse, s as dashClient, t as IdentifyClientOptions } from "./identify-client-options-BfGftEgh.mjs";
2
2
  import { BetterAuthClientPlugin } from "better-auth";
3
3
  //#region src/sentinel/native/client.d.ts
4
4
  interface SentinelNativeClientOptions {
@@ -22,6 +22,6 @@ interface SentinelNativeClientOptions {
22
22
  setItem: (key: string, value: string) => Promise<void>;
23
23
  };
24
24
  }
25
- declare const sentinelNativeClient: (options?: SentinelNativeClientOptions) => BetterAuthClientPlugin;
25
+ export declare const sentinelNativeClient: (options?: SentinelNativeClientOptions) => BetterAuthClientPlugin;
26
26
  //#endregion
27
- export { type DashAuditLog, type DashAuditLogsResponse, type DashClientOptions, type DashGetAllAuditLogsInput, type DashGetAuditLogsInput, type SentinelNativeClientOptions, dashClient, sentinelNativeClient };
27
+ export { type DashAuditLog, type DashAuditLogsResponse, type DashClientOptions, type DashGetAllAuditLogsInput, type DashGetAuditLogsInput, type SentinelNativeClientOptions, dashClient };
package/dist/native.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { o as createKV, t as bytesToHex } from "./crypto-DRS7yhmR.mjs";
2
- import { a as resolveSentinelClientIdentifyUrl, c as identify, i as solvePoWChallenge, l as dashClient, n as decodePoWChallenge, o as resolveIdentifyClientRetry, r as encodePoWSolution, t as createPowRetryTimeout } from "./pow-retry-Di0eUKbA.mjs";
1
+ import { o as createKV, t as bytesToHex } from "./crypto-Doziv2tH.mjs";
2
+ import { a as resolveSentinelClientIdentifyUrl, c as identify, i as solvePoWChallenge, l as dashClient, n as decodePoWChallenge, o as resolveIdentifyClientRetry, r as encodePoWSolution, t as createPowRetryTimeout } from "./pow-retry-DIkxDwZf.mjs";
3
3
  import { env } from "@better-auth/core/env";
4
4
  import { Dimensions, InteractionManager, PixelRatio, Platform } from "react-native";
5
5
  //#region src/sentinel/native/components.ts
@@ -1,4 +1,4 @@
1
- import { i as randomBytes, n as hash, t as bytesToHex } from "./crypto-DRS7yhmR.mjs";
1
+ import { i as randomBytes, n as hash, t as bytesToHex } from "./crypto-Doziv2tH.mjs";
2
2
  //#region src/dash-client.ts
3
3
  function resolveDashUserId(input, options) {
4
4
  return input.userId || options?.resolveUserId?.({
@@ -481,6 +481,8 @@ interface InfraPluginConnectionOptionsInternal extends Omit<InfraPluginConnectio
481
481
  apiUrl: string;
482
482
  kvUrl: string;
483
483
  apiKey: string;
484
+ /** @internal */
485
+ apiKeys: string[];
484
486
  apiOptions: ApiOptionsResolved;
485
487
  kvOptions: KvOptionsResolved;
486
488
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-auth/infra",
3
- "version": "0.4.8",
3
+ "version": "0.4.10",
4
4
  "description": "Dashboard and analytics plugin for Better Auth",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",
@@ -66,32 +66,32 @@
66
66
  },
67
67
  "homepage": "https://better-auth.com",
68
68
  "devDependencies": {
69
- "@better-auth/kysely-adapter": "1.7.3",
70
- "@better-auth/scim": "1.7.3",
71
- "@better-auth/sso": "1.7.3",
69
+ "@better-auth/kysely-adapter": "1.7.5",
70
+ "@better-auth/scim": "1.7.5",
71
+ "@better-auth/sso": "1.7.5",
72
72
  "@infra/mocks": "0.0.0",
73
73
  "@infra/utils": "0.0.0",
74
74
  "@types/bun": "latest",
75
75
  "@types/node": "^24.12.0",
76
76
  "@vitest/browser-playwright": "5.0.0",
77
77
  "@vitest/coverage-istanbul": "^5.0.0",
78
- "better-auth": "1.7.3",
79
- "expo-crypto": "^57.0.1",
78
+ "better-auth": "1.7.5",
79
+ "expo-crypto": "^57.0.3",
80
80
  "msw": "^2.14.6",
81
81
  "playwright": "^1.63.0",
82
- "tsdown": "^0.22.14",
82
+ "tsdown": "^0.23.0",
83
83
  "typescript": "^6.0.3",
84
84
  "vitest": "^5.0.0",
85
85
  "zod": "4.5.4"
86
86
  },
87
87
  "dependencies": {
88
88
  "@better-auth/utils": "^0.4.2",
89
- "@better-fetch/fetch": "1.3.1",
89
+ "@better-fetch/fetch": "1.3.2",
90
90
  "@noble/hashes": "^2.2.0",
91
91
  "@xmldom/xmldom": "^0.9.10",
92
92
  "better-call": "1.4.0",
93
- "jose": "^6.2.10",
94
- "libphonenumber-js": "^1.13.11"
93
+ "jose": "^6.2.12",
94
+ "libphonenumber-js": "^1.13.13"
95
95
  },
96
96
  "peerDependencies": {
97
97
  "better-auth": ">=1.4.0",