UNPKG

@better-auth-ui/core

Version:

Authentication components and data utilities for [Better Auth](https://better-auth.com), available for React and Solid.

166 lines (134 loc) 4.2 kB
import type { BetterFetchError, BetterFetchOption, BetterFetchPlugin } from "better-auth/client" const DEFAULT_AUTH_QUERY_RETRY_COUNT = 3 const MAX_AUTH_QUERY_RETRY_DELAY = 30_000 const RETRYABLE_AUTH_QUERY_STATUS_CODES = new Set([ 408, 429, 500, 502, 503, 504 ]) export type AuthQueryError = BetterFetchError & { retryAfterMs?: number } export const REAUTHENTICATION_REQUIRED_ERROR_CODES = new Set([ "SESSION_NOT_FRESH", "SESSION_EXPIRED" ]) /** Return whether Better Auth requires the user to authenticate again. */ export function isReauthenticationRequiredError(error: unknown) { if (typeof error !== "object" || error === null) return false const code = (error as { code?: unknown }).code if ( typeof code === "string" && REAUTHENTICATION_REQUIRED_ERROR_CODES.has(code) ) { return true } const errorBody = (error as { error?: unknown }).error if (typeof errorBody !== "object" || errorBody === null) return false const nestedCode = (errorBody as { code?: unknown }).code return ( typeof nestedCode === "string" && REAUTHENTICATION_REQUIRED_ERROR_CODES.has(nestedCode) ) } class AuthQueryResponseError extends Error implements AuthQueryError { constructor( readonly status: number, readonly statusText: string, readonly error: unknown, readonly retryAfterMs: number ) { super(statusText || String(status), { cause: error }) } } function getNumericProperty( value: unknown, property: "retryAfterMs" | "status" ) { if (typeof value !== "object" || value === null) { return undefined } const propertyValue = (value as Record<PropertyKey, unknown>)[property] return typeof propertyValue === "number" ? propertyValue : undefined } function parseRetryAfter(value: string) { const seconds = Number(value) if (Number.isFinite(seconds) && seconds >= 0) { return seconds * 1000 } const date = Date.parse(value) if (Number.isNaN(date)) { return undefined } return Math.max(0, date - Date.now()) } function getRetryAfter(response: Response) { const retryAfter = response.headers.get("Retry-After") const parsedRetryAfter = retryAfter === null ? undefined : parseRetryAfter(retryAfter) if (parsedRetryAfter !== undefined) { return parsedRetryAfter } const legacyRetryAfter = response.headers.get("X-Retry-After") return legacyRetryAfter === null ? undefined : parseRetryAfter(legacyRetryAfter) } const authQueryRetryPlugin = { id: "better-auth-ui-query-retry", name: "Better Auth UI query retry", hooks: { onError: async (context) => { const retryAfterMs = getRetryAfter(context.response) if (retryAfterMs !== undefined) { throw new AuthQueryResponseError( context.response.status, context.response.statusText, context.error, retryAfterMs ) } } } } satisfies BetterFetchPlugin export function createAuthQueryFetchOptions( fetchOptions: BetterFetchOption | undefined, signal: AbortSignal ): BetterFetchOption & { throw: true } { return { ...fetchOptions, signal, throw: true, plugins: [...(fetchOptions?.plugins ?? []), authQueryRetryPlugin] } } export function isRetryableAuthQueryError(error: unknown) { const status = getNumericProperty(error, "status") return status === undefined || RETRYABLE_AUTH_QUERY_STATUS_CODES.has(status) } export function shouldRetryAuthQuery(failureCount: number, error: unknown) { return ( failureCount < DEFAULT_AUTH_QUERY_RETRY_COUNT && isRetryableAuthQueryError(error) ) } export function authQueryRetryDelay(failureCount: number, error: unknown) { const retryAfterMs = getNumericProperty(error, "retryAfterMs") if ( retryAfterMs !== undefined && Number.isFinite(retryAfterMs) && retryAfterMs >= 0 ) { return retryAfterMs } return Math.min(1000 * 2 ** failureCount, MAX_AUTH_QUERY_RETRY_DELAY) } export function createAuthQueryRetryOptions(isServer: () => boolean) { return { retry: (failureCount: number, error: unknown) => !isServer() && shouldRetryAuthQuery(failureCount, error), retryDelay: authQueryRetryDelay } as const }