import { API_VERSION_HEADER_NAME, BASE64URL_REGEX } from './constants'; import { AuthInvalidJwtError } from './errors'; import { base64UrlToUint8Array, stringFromBase64URL } from './base64url'; export function expiresAt(expiresIn) { const timeNow = Math.round(Date.now() / 1000); return timeNow + expiresIn; } export function uuid() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { const r = (Math.random() * 16) | 0, v = c == 'x' ? r : (r & 0x3) | 0x8; return v.toString(16); }); } export const isBrowser = () => typeof window !== 'undefined' && typeof document !== 'undefined'; const localStorageWriteTests = { tested: false, writable: false, }; /** * Checks whether localStorage is supported on this browser. */ export const supportsLocalStorage = () => { if (!isBrowser()) { return false; } try { if (typeof globalThis.localStorage !== 'object') { return false; } } catch (e) { // DOM exception when accessing `localStorage` 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) { // localStorage can't be written to // https://www.chromium.org/for-testers/bug-reporting-guidelines/uncaught-securityerror-failed-to-read-the-localstorage-property-from-window-access-is-denied-for-this-document localStorageWriteTests.tested = true; localStorageWriteTests.writable = false; } return localStorageWriteTests.writable; }; /** * Extracts parameters encoded in the URL both in the query and fragment. */ export function parseParametersFromURL(href) { const result = {}; 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) { // hash is not a query string } } // search parameters take precedence over hash parameters url.searchParams.forEach((value, key) => { result[key] = value; }); return result; } export const resolveFetch = (customFetch) => { let _fetch; if (customFetch) { _fetch = customFetch; } else if (typeof fetch === 'undefined') { _fetch = (...args) => import('@supabase/node-fetch').then(({ default: fetch }) => fetch(...args)); } else { _fetch = fetch; } return (...args) => _fetch(...args); }; export const looksLikeFetchResponse = (maybeResponse) => { return (typeof maybeResponse === 'object' && maybeResponse !== null && 'status' in maybeResponse && 'ok' in maybeResponse && 'json' in maybeResponse && typeof maybeResponse.json === 'function'); }; // Storage helpers export const setItemAsync = async (storage, key, data) => { await storage.setItem(key, JSON.stringify(data)); }; export const getItemAsync = async (storage, key) => { const value = await storage.getItem(key); if (!value) { return null; } try { return JSON.parse(value); } catch (_a) { return value; } }; export const removeItemAsync = async (storage, key) => { await storage.removeItem(key); }; /** * A deferred represents some asynchronous work that is not yet finished, which * may or may not culminate in a value. * Taken from: https://github.com/mike-north/types/blob/master/src/async.ts */ export class Deferred { constructor() { // eslint-disable-next-line @typescript-eslint/no-extra-semi ; this.promise = new Deferred.promiseConstructor((res, rej) => { // eslint-disable-next-line @typescript-eslint/no-extra-semi ; this.resolve = res; this.reject = rej; }); } } Deferred.promiseConstructor = Promise; export function decodeJWT(token) { const parts = token.split('.'); if (parts.length !== 3) { throw new AuthInvalidJwtError('Invalid JWT structure'); } // Regex checks for base64url format for (let i = 0; i < parts.length; i++) { if (!BASE64URL_REGEX.test(parts[i])) { throw new AuthInvalidJwtError('JWT not in base64url format'); } } const data = { // using base64url lib 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; } /** * Creates a promise that resolves to null after some time. */ export async function sleep(time) { return await new Promise((accept) => { setTimeout(() => accept(null), time); }); } /** * Converts the provided async function into a retryable function. Each result * or thrown error is sent to the isRetryable function which should return true * if the function should run again. */ export function retryable(fn, isRetryable) { const promise = new Promise((accept, reject) => { // eslint-disable-next-line @typescript-eslint/no-extra-semi ; (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) { return ('0' + dec.toString(16)).substr(-2); } // Functions below taken from: https://stackoverflow.com/questions/63309409/creating-a-code-verifier-and-challenge-for-pkce-auth-on-spotify-api-in-reactjs 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) { 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) { 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, storageKey, isPasswordRecovery = false) { const codeVerifier = generatePKCEVerifier(); let storedCodeVerifier = codeVerifier; if (isPasswordRecovery) { storedCodeVerifier += '/PASSWORD_RECOVERY'; } await setItemAsync(storage, `${storageKey}-code-verifier`, storedCodeVerifier); const codeChallenge = await generatePKCEChallenge(codeVerifier); const codeChallengeMethod = codeVerifier === codeChallenge ? 'plain' : 's256'; return [codeChallenge, codeChallengeMethod]; } /** Parses the API version which is 2YYY-MM-DD. */ 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) { 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) { 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) { 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) { if (!UUID_REGEX.test(str)) { throw new Error('@supabase/auth-js: Expected parameter to be UUID but is not'); } } export function userNotAvailableProxy() { const proxyTarget = {}; return new Proxy(proxyTarget, { get: (target, prop) => { if (prop === '__isUserNotAvailableProxy') { return true; } // Preventative check for common problematic symbols during cloning/inspection // These symbols might be accessed by structuredClone or other internal mechanisms. if (typeof prop === 'symbol') { const sProp = prop.toString(); if (sProp === 'Symbol(Symbol.toPrimitive)' || sProp === 'Symbol(Symbol.toStringTag)' || sProp === 'Symbol(util.inspect.custom)') { // Node.js util.inspect 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, prop) => { 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, prop) => { 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.`); }, }); } /** * Deep clones a JSON-serializable object using JSON.parse(JSON.stringify(obj)). * Note: Only works for JSON-safe data. */ export function deepClone(obj) { return JSON.parse(JSON.stringify(obj)); } //# sourceMappingURL=helpers.js.map