| import { API_VERSIONS, API_VERSION_HEADER_NAME } from './constants' |
| import { expiresAt, looksLikeFetchResponse, parseResponseAPIVersion } from './helpers' |
| import { |
| AuthResponse, |
| AuthResponsePassword, |
| Session, |
| SSOResponse, |
| GenerateLinkProperties, |
| GenerateLinkResponse, |
| User, |
| UserResponse, |
| WeakPassword, |
| } from './types' |
| import { |
| AuthApiError, |
| AuthRetryableFetchError, |
| AuthWeakPasswordError, |
| AuthUnknownError, |
| AuthSessionMissingError, |
| } from './errors' |
|
|
| export type Fetch = typeof fetch |
|
|
| |
| interface GoTrueSessionData { |
| access_token?: string |
| refresh_token?: string |
| expires_in?: number |
| expires_at?: number |
| user?: User |
| [key: string]: any |
| } |
|
|
| |
| interface GoTrueSessionPasswordData extends GoTrueSessionData { |
| weak_password?: WeakPassword |
| } |
|
|
| |
| interface GoTrueUserData { |
| user?: User |
| [key: string]: any |
| } |
|
|
| |
| type GoTrueGenerateLinkData = GenerateLinkProperties & Record<string, any> |
|
|
| export interface FetchOptions { |
| headers?: { |
| [key: string]: string |
| } |
| noResolveJson?: boolean |
| } |
|
|
| export interface FetchParameters { |
| signal?: AbortSignal |
| } |
|
|
| export type RequestMethodType = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' |
|
|
| const _getErrorMessage = (err: unknown): string => { |
| if (typeof err === 'object' && err !== null) { |
| const e = err as Record<string, unknown> |
| if (typeof e.msg === 'string') return e.msg |
| if (typeof e.message === 'string') return e.message |
| if (typeof e.error_description === 'string') return e.error_description |
| if (typeof e.error === 'string') return e.error |
| } |
| return JSON.stringify(err) |
| } |
|
|
| |
| |
| |
| const NETWORK_ERROR_CODES = [ |
| 500, 501, 502, 503, 504, 520, 521, 522, 523, 524, 525, 526, 527, 528, 529, 530, |
| ] |
|
|
| export async function handleError(error: unknown) { |
| if (!looksLikeFetchResponse(error)) { |
| throw new AuthRetryableFetchError(_getErrorMessage(error), 0) |
| } |
|
|
| if (NETWORK_ERROR_CODES.includes(error.status)) { |
| |
| throw new AuthRetryableFetchError(_getErrorMessage(error), error.status) |
| } |
|
|
| let data: any |
| try { |
| data = await error.json() |
| } catch (e) { |
| throw new AuthUnknownError(_getErrorMessage(e), e) |
| } |
|
|
| let errorCode: string | undefined = undefined |
|
|
| const responseAPIVersion = parseResponseAPIVersion(error) |
| if ( |
| responseAPIVersion && |
| responseAPIVersion.getTime() >= API_VERSIONS['2024-01-01'].timestamp && |
| typeof data === 'object' && |
| data && |
| typeof data.code === 'string' |
| ) { |
| errorCode = data.code |
| } else if (typeof data === 'object' && data && typeof data.error_code === 'string') { |
| errorCode = data.error_code |
| } |
|
|
| if (!errorCode) { |
| |
| if ( |
| typeof data === 'object' && |
| data && |
| typeof data.weak_password === 'object' && |
| data.weak_password && |
| Array.isArray(data.weak_password.reasons) && |
| data.weak_password.reasons.length && |
| data.weak_password.reasons.reduce((a: boolean, i: any) => a && typeof i === 'string', true) |
| ) { |
| throw new AuthWeakPasswordError( |
| _getErrorMessage(data), |
| error.status, |
| data.weak_password.reasons |
| ) |
| } |
| } else if (errorCode === 'weak_password') { |
| throw new AuthWeakPasswordError( |
| _getErrorMessage(data), |
| error.status, |
| data.weak_password?.reasons || [] |
| ) |
| } else if (errorCode === 'session_not_found') { |
| |
| |
| |
| throw new AuthSessionMissingError() |
| } |
|
|
| throw new AuthApiError(_getErrorMessage(data), error.status || 500, errorCode) |
| } |
|
|
| const _getRequestParams = ( |
| method: RequestMethodType, |
| options?: FetchOptions, |
| parameters?: FetchParameters, |
| body?: object |
| ) => { |
| const params: { [k: string]: any } = { method, headers: options?.headers || {} } |
|
|
| if (method === 'GET') { |
| return params |
| } |
|
|
| params.headers = { 'Content-Type': 'application/json;charset=UTF-8', ...options?.headers } |
| params.body = JSON.stringify(body) |
| return { ...params, ...parameters } |
| } |
|
|
| interface GotrueRequestOptions extends FetchOptions { |
| jwt?: string |
| redirectTo?: string |
| body?: object |
| query?: { [key: string]: string } |
| |
| |
| |
| xform?: (data: any) => any |
| } |
|
|
| export async function _request( |
| fetcher: Fetch, |
| method: RequestMethodType, |
| url: string, |
| options?: GotrueRequestOptions |
| ) { |
| const headers = { |
| ...options?.headers, |
| } |
|
|
| if (!headers[API_VERSION_HEADER_NAME]) { |
| headers[API_VERSION_HEADER_NAME] = API_VERSIONS['2024-01-01'].name |
| } |
|
|
| if (options?.jwt) { |
| headers['Authorization'] = `Bearer ${options.jwt}` |
| } |
|
|
| const qs = options?.query ?? {} |
| if (options?.redirectTo) { |
| qs['redirect_to'] = options.redirectTo |
| } |
|
|
| const queryString = Object.keys(qs).length ? '?' + new URLSearchParams(qs).toString() : '' |
| const data = await _handleRequest( |
| fetcher, |
| method, |
| url + queryString, |
| { |
| headers, |
| noResolveJson: options?.noResolveJson, |
| }, |
| {}, |
| options?.body |
| ) |
| return options?.xform ? options?.xform(data) : { data: { ...data }, error: null } |
| } |
|
|
| async function _handleRequest( |
| fetcher: Fetch, |
| method: RequestMethodType, |
| url: string, |
| options?: FetchOptions, |
| parameters?: FetchParameters, |
| body?: object |
| ): Promise<any> { |
| const requestParams = _getRequestParams(method, options, parameters, body) |
|
|
| let result: Response |
|
|
| try { |
| result = await fetcher(url, { |
| ...requestParams, |
| }) |
| } catch (e) { |
| |
| |
| |
| |
| throw new AuthRetryableFetchError(_getErrorMessage(e), 0) |
| } |
|
|
| if (!result.ok) { |
| await handleError(result) |
| } |
|
|
| if (options?.noResolveJson) { |
| return result |
| } |
|
|
| try { |
| return await result.json() |
| } catch (e) { |
| await handleError(e) |
| } |
| } |
|
|
| export function _sessionResponse(data: GoTrueSessionData): AuthResponse { |
| let session = null |
| if (hasSession(data)) { |
| session = { ...data } as Session |
|
|
| if (!data.expires_at) { |
| session.expires_at = expiresAt(data.expires_in!) |
| } |
| } |
|
|
| |
| |
| const user: User | null = data.user ?? (typeof data?.id === 'string' ? (data as User) : null) |
| return { data: { session, user }, error: null } |
| } |
|
|
| export function _sessionResponsePassword(data: GoTrueSessionPasswordData): AuthResponsePassword { |
| const response = _sessionResponse(data) as AuthResponsePassword |
|
|
| if ( |
| !response.error && |
| data.weak_password && |
| typeof data.weak_password === 'object' && |
| Array.isArray(data.weak_password.reasons) && |
| data.weak_password.reasons.length && |
| data.weak_password.message && |
| typeof data.weak_password.message === 'string' && |
| data.weak_password.reasons.reduce((a: boolean, i: unknown) => a && typeof i === 'string', true) |
| ) { |
| response.data.weak_password = data.weak_password |
| } |
|
|
| return response |
| } |
|
|
| export function _userResponse(data: GoTrueUserData): UserResponse { |
| const user: User = data.user ?? (data as User) |
| return { data: { user }, error: null } |
| } |
|
|
| export function _ssoResponse(data: Record<string, any>): SSOResponse { |
| return { data, error: null } as SSOResponse |
| } |
|
|
| export function _generateLinkResponse(data: GoTrueGenerateLinkData): GenerateLinkResponse { |
| const { action_link, email_otp, hashed_token, redirect_to, verification_type, ...rest } = data |
|
|
| const properties: GenerateLinkProperties = { |
| action_link, |
| email_otp, |
| hashed_token, |
| redirect_to, |
| verification_type, |
| } |
|
|
| const user = { ...rest } as User |
| return { |
| data: { |
| properties, |
| user, |
| }, |
| error: null, |
| } |
| } |
|
|
| export function _noResolveJsonResponse(data: Response): Response { |
| return data |
| } |
|
|
| |
| |
| |
| |
| |
| function hasSession(data: GoTrueSessionData): boolean { |
| return !!data.access_token && !!data.refresh_token && !!data.expires_in |
| } |
|
|