| import { API_VERSION_HEADER_NAME, BASE64URL_REGEX } from './constants' |
| import { AuthInvalidJwtError } from './errors' |
| import { base64UrlToUint8Array, stringFromBase64URL } from './base64url' |
| import { JwtHeader, JwtPayload, SupportedStorage, User } from './types' |
| import { Uint8Array_ } from './webauthn.dom' |
|
|
| export function expiresAt(expiresIn: number) { |
| const timeNow = Math.round(Date.now() / 1000) |
| return timeNow + expiresIn |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function generateCallbackId(): symbol { |
| return Symbol('auth-callback') |
| } |
|
|
| export const isBrowser = () => typeof window !== 'undefined' && typeof document !== 'undefined' |
|
|
| const localStorageWriteTests = { |
| tested: false, |
| writable: false, |
| } |
|
|
| |
| |
| |
| export const supportsLocalStorage = () => { |
| if (!isBrowser()) { |
| return false |
| } |
|
|
| try { |
| if (typeof globalThis.localStorage !== 'object') { |
| return false |
| } |
| } catch (e) { |
| |
| return false |
| } |
|
|
| if (localStorageWriteTests.tested) { |
| return localStorageWriteTests.writable |
| } |
|
|
| const randomKey = `lswt-${Math.random()}${Math.random()}` |
|
|
| try { |
| globalThis.localStorage.setItem(randomKey, randomKey) |
| globalThis.localStorage.removeItem(randomKey) |
|
|
| localStorageWriteTests.tested = true |
| localStorageWriteTests.writable = true |
| } catch (e) { |
| |
| |
|
|
| localStorageWriteTests.tested = true |
| localStorageWriteTests.writable = false |
| } |
|
|
| return localStorageWriteTests.writable |
| } |
|
|
| |
| |
| |
| export function parseParametersFromURL(href: string) { |
| const result: { [parameter: string]: string } = {} |
|
|
| const url = new URL(href) |
|
|
| if (url.hash && url.hash[0] === '#') { |
| try { |
| const hashSearchParams = new URLSearchParams(url.hash.substring(1)) |
| hashSearchParams.forEach((value, key) => { |
| result[key] = value |
| }) |
| } catch (_e) { |
| |
| } |
| } |
|
|
| |
| url.searchParams.forEach((value, key) => { |
| result[key] = value |
| }) |
|
|
| return result |
| } |
|
|
| type Fetch = typeof fetch |
|
|
| export const resolveFetch = (customFetch?: Fetch): Fetch => { |
| if (customFetch) { |
| return (...args) => customFetch(...args) |
| } |
| return (...args) => fetch(...args) |
| } |
|
|
| export const looksLikeFetchResponse = (maybeResponse: unknown): maybeResponse is Response => { |
| return ( |
| typeof maybeResponse === 'object' && |
| maybeResponse !== null && |
| 'status' in maybeResponse && |
| 'ok' in maybeResponse && |
| 'json' in maybeResponse && |
| typeof (maybeResponse as any).json === 'function' |
| ) |
| } |
|
|
| |
| export const setItemAsync = async ( |
| storage: SupportedStorage, |
| key: string, |
| data: any |
| ): Promise<void> => { |
| await storage.setItem(key, JSON.stringify(data)) |
| } |
|
|
| export const getItemAsync = async (storage: SupportedStorage, key: string): Promise<unknown> => { |
| const value = await storage.getItem(key) |
|
|
| if (!value) { |
| return null |
| } |
|
|
| try { |
| return JSON.parse(value) |
| } catch { |
| |
| |
| |
| |
| |
| return null |
| } |
| } |
|
|
| export const removeItemAsync = async (storage: SupportedStorage, key: string): Promise<void> => { |
| await storage.removeItem(key) |
| } |
|
|
| |
| |
| |
| |
| |
| export class Deferred<T = any> { |
| public static promiseConstructor: PromiseConstructor = Promise |
|
|
| public readonly promise!: PromiseLike<T> |
|
|
| public readonly resolve!: (value?: T | PromiseLike<T>) => void |
|
|
| public readonly reject!: (reason?: any) => any |
|
|
| public constructor() { |
| |
| ;(this as any).promise = new Deferred.promiseConstructor((res, rej) => { |
| |
| ;(this as any).resolve = res |
| |
| ;(this as any).reject = rej |
| }) |
| } |
| } |
|
|
| export function decodeJWT(token: string): { |
| header: JwtHeader |
| payload: JwtPayload |
| signature: Uint8Array_ |
| raw: { |
| header: string |
| payload: string |
| } |
| } { |
| const parts = token.split('.') |
|
|
| if (parts.length !== 3) { |
| throw new AuthInvalidJwtError('Invalid JWT structure') |
| } |
|
|
| |
| for (let i = 0; i < parts.length; i++) { |
| if (!BASE64URL_REGEX.test(parts[i] as string)) { |
| throw new AuthInvalidJwtError('JWT not in base64url format') |
| } |
| } |
| const data = { |
| |
| header: JSON.parse(stringFromBase64URL(parts[0])), |
| payload: JSON.parse(stringFromBase64URL(parts[1])), |
| signature: base64UrlToUint8Array(parts[2]), |
| raw: { |
| header: parts[0], |
| payload: parts[1], |
| }, |
| } |
| return data |
| } |
|
|
| |
| |
| |
| export async function sleep(time: number): Promise<null> { |
| return await new Promise((accept) => { |
| setTimeout(() => accept(null), time) |
| }) |
| } |
|
|
| |
| |
| |
| |
| |
| export function retryable<T>( |
| fn: (attempt: number) => Promise<T>, |
| isRetryable: (attempt: number, error: any | null, result?: T) => boolean |
| ): Promise<T> { |
| const promise = new Promise<T>((accept, reject) => { |
| |
| ;(async () => { |
| for (let attempt = 0; attempt < Infinity; attempt++) { |
| try { |
| const result = await fn(attempt) |
|
|
| if (!isRetryable(attempt, null, result)) { |
| accept(result) |
| return |
| } |
| } catch (e) { |
| if (!isRetryable(attempt, e)) { |
| reject(e) |
| return |
| } |
| } |
| } |
| })() |
| }) |
|
|
| return promise |
| } |
|
|
| function dec2hex(dec: number) { |
| return ('0' + dec.toString(16)).substr(-2) |
| } |
|
|
| |
| export function generatePKCEVerifier() { |
| const verifierLength = 56 |
| const array = new Uint32Array(verifierLength) |
| if (typeof crypto === 'undefined') { |
| const charSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~' |
| const charSetLen = charSet.length |
| let verifier = '' |
| for (let i = 0; i < verifierLength; i++) { |
| verifier += charSet.charAt(Math.floor(Math.random() * charSetLen)) |
| } |
| return verifier |
| } |
| crypto.getRandomValues(array) |
| return Array.from(array, dec2hex).join('') |
| } |
|
|
| async function sha256(randomString: string) { |
| const encoder = new TextEncoder() |
| const encodedData = encoder.encode(randomString) |
| const hash = await crypto.subtle.digest('SHA-256', encodedData) |
| const bytes = new Uint8Array(hash) |
|
|
| return Array.from(bytes) |
| .map((c) => String.fromCharCode(c)) |
| .join('') |
| } |
|
|
| export async function generatePKCEChallenge(verifier: string) { |
| const hasCryptoSupport = |
| typeof crypto !== 'undefined' && |
| typeof crypto.subtle !== 'undefined' && |
| typeof TextEncoder !== 'undefined' |
|
|
| if (!hasCryptoSupport) { |
| console.warn( |
| 'WebCrypto API is not supported. Code challenge method will default to use plain instead of sha256.' |
| ) |
| return verifier |
| } |
| const hashed = await sha256(verifier) |
| return btoa(hashed).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') |
| } |
|
|
| export async function getCodeChallengeAndMethod( |
| storage: SupportedStorage, |
| storageKey: string, |
| isPasswordRecovery = false |
| ) { |
| const codeVerifier = generatePKCEVerifier() |
| let storedCodeVerifier = codeVerifier |
| if (isPasswordRecovery) { |
| storedCodeVerifier += '/recovery' |
| } |
| await setItemAsync(storage, `${storageKey}-code-verifier`, storedCodeVerifier) |
| const codeChallenge = await generatePKCEChallenge(codeVerifier) |
| const codeChallengeMethod = codeVerifier === codeChallenge ? 'plain' : 's256' |
| return [codeChallenge, codeChallengeMethod] |
| } |
|
|
| |
| const API_VERSION_REGEX = /^2[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|1[0-9]|2[0-9]|3[0-1])$/i |
|
|
| export function parseResponseAPIVersion(response: Response) { |
| const apiVersion = response.headers.get(API_VERSION_HEADER_NAME) |
|
|
| if (!apiVersion) { |
| return null |
| } |
|
|
| if (!apiVersion.match(API_VERSION_REGEX)) { |
| return null |
| } |
|
|
| try { |
| const date = new Date(`${apiVersion}T00:00:00.0Z`) |
| return date |
| } catch (_e) { |
| return null |
| } |
| } |
|
|
| export function validateExp(exp: number) { |
| if (!exp) { |
| throw new Error('Missing exp claim') |
| } |
| const timeNow = Math.floor(Date.now() / 1000) |
| if (exp <= timeNow) { |
| throw new Error('JWT has expired') |
| } |
| } |
|
|
| export function getAlgorithm( |
| alg: 'HS256' | 'RS256' | 'ES256' | (string & {}) |
| ): RsaHashedImportParams | EcKeyImportParams { |
| switch (alg) { |
| case 'RS256': |
| return { |
| name: 'RSASSA-PKCS1-v1_5', |
| hash: { name: 'SHA-256' }, |
| } |
| case 'ES256': |
| return { |
| name: 'ECDSA', |
| namedCurve: 'P-256', |
| hash: { name: 'SHA-256' }, |
| } |
| default: |
| throw new Error('Invalid alg claim') |
| } |
| } |
|
|
| const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/ |
|
|
| export function validateUUID(str: string) { |
| if (!UUID_REGEX.test(str)) { |
| throw new Error('@supabase/auth-js: Expected parameter to be UUID but is not') |
| } |
| } |
|
|
| export function assertPasskeyExperimentalEnabled(experimental: { passkey?: boolean }): void { |
| if (!experimental.passkey) { |
| throw new Error( |
| '@supabase/auth-js: the passkey API is experimental and disabled by default. Enable it by passing `auth: { experimental: { passkey: true } }` to createClient (or to the GoTrueClient constructor).' |
| ) |
| } |
| } |
|
|
| export function userNotAvailableProxy(): User { |
| const proxyTarget = {} as User |
|
|
| return new Proxy(proxyTarget, { |
| get: (target: any, prop: string) => { |
| if (prop === '__isUserNotAvailableProxy') { |
| return true |
| } |
| |
| |
| if (typeof prop === 'symbol') { |
| const sProp = (prop as symbol).toString() |
| if ( |
| sProp === 'Symbol(Symbol.toPrimitive)' || |
| sProp === 'Symbol(Symbol.toStringTag)' || |
| sProp === 'Symbol(util.inspect.custom)' |
| ) { |
| |
| return undefined |
| } |
| } |
| throw new Error( |
| `@supabase/auth-js: client was created with userStorage option and there was no user stored in the user storage. Accessing the "${prop}" property of the session object is not supported. Please use getUser() instead.` |
| ) |
| }, |
| set: (_target: any, prop: string) => { |
| throw new Error( |
| `@supabase/auth-js: client was created with userStorage option and there was no user stored in the user storage. Setting the "${prop}" property of the session object is not supported. Please use getUser() to fetch a user object you can manipulate.` |
| ) |
| }, |
| deleteProperty: (_target: any, prop: string) => { |
| throw new Error( |
| `@supabase/auth-js: client was created with userStorage option and there was no user stored in the user storage. Deleting the "${prop}" property of the session object is not supported. Please use getUser() to fetch a user object you can manipulate.` |
| ) |
| }, |
| }) |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export function insecureUserWarningProxy(user: User, suppressWarningRef: { value: boolean }): User { |
| return new Proxy(user, { |
| get: (target: any, prop: string | symbol, receiver: any) => { |
| |
| if (prop === '__isInsecureUserWarningProxy') { |
| return true |
| } |
|
|
| |
| |
| if (typeof prop === 'symbol') { |
| const sProp = prop.toString() |
| if ( |
| sProp === 'Symbol(Symbol.toPrimitive)' || |
| sProp === 'Symbol(Symbol.toStringTag)' || |
| sProp === 'Symbol(util.inspect.custom)' || |
| sProp === 'Symbol(nodejs.util.inspect.custom)' |
| ) { |
| |
| return Reflect.get(target, prop, receiver) |
| } |
| } |
|
|
| |
| if (!suppressWarningRef.value && typeof prop === 'string') { |
| console.warn( |
| 'Using the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.' |
| ) |
| suppressWarningRef.value = true |
| } |
|
|
| return Reflect.get(target, prop, receiver) |
| }, |
| }) |
| } |
|
|
| |
| |
| |
| |
| export function deepClone<T>(obj: T): T { |
| return JSON.parse(JSON.stringify(obj)) |
| } |
|
|