UNPKG

firebase-auth-cloudflare-workers

Version:

Zero-dependencies firebase auth library for Cloudflare Workers.

157 lines (156 loc) 6.14 kB
import { useEmulator } from './emulator'; import { AppErrorCodes, FirebaseAppError, FirebaseAuthError } from './errors'; import { isNonNullObject } from './validator'; import { version } from './version'; /** * Default retry configuration for HTTP requests. Retries up to 4 times on connection reset and timeout errors * as well as HTTP 503 errors. Exposed as a function to ensure that every HttpClient gets its own RetryConfig * instance. */ export function defaultRetryConfig() { return { maxRetries: 4, statusCodes: [503], ioErrorCodes: ['ECONNRESET', 'ETIMEDOUT'], backOffFactor: 0.5, maxDelayInMillis: 60 * 1000, }; } export function buildApiUrl(projectId, apiSettings, env) { const defaultAuthURL = 'https://identitytoolkit.googleapis.com'; const baseUrl = env?.FIREBASE_AUTH_EMULATOR_HOST ? `http://${env.FIREBASE_AUTH_EMULATOR_HOST}/identitytoolkit.googleapis.com` : defaultAuthURL; const endpoint = apiSettings.getEndpoint(); return `${baseUrl}/${apiSettings.getVersion()}/projects/${projectId}${endpoint}`; } export class BaseClient { projectId; credential; retryConfig; constructor(projectId, credential, retryConfig = defaultRetryConfig()) { this.projectId = projectId; this.credential = credential; this.retryConfig = retryConfig; } async getToken(env) { if (useEmulator(env)) { return 'owner'; } const result = await this.credential.getAccessToken(); return result.access_token; } async fetch(apiSettings, requestData, env) { const fullUrl = buildApiUrl(this.projectId, apiSettings, env); if (requestData) { const requestValidator = apiSettings.getRequestValidator(); requestValidator(requestData); } const token = await this.getToken(env); const method = apiSettings.getHttpMethod(); const signal = AbortSignal.timeout(25000); // 25s return await this.fetchWithRetry(fullUrl, { method, headers: { Authorization: `Bearer ${token}`, 'User-Agent': `Code-Hex/firebase-auth-cloudflare-workers/${version}`, 'X-Client-Version': `Code-Hex/firebase-auth-cloudflare-workers/${version}`, 'Content-Type': 'application/json;charset=utf-8', }, body: requestData ? JSON.stringify(requestData) : undefined, signal, }); } async fetchWithRetry(url, init, retryAttempts = 0) { try { const res = await fetch(url, init); const text = await res.text(); if (!res.ok) { throw new HttpError(res.status, text); } try { return JSON.parse(text); } catch (err) { throw new HttpError(res.status, text, { cause: new FirebaseAppError(AppErrorCodes.UNABLE_TO_PARSE_RESPONSE, `Error while parsing response data: "${String(err)}". Raw server ` + `response: "${text}". Status code: "${res.status}". Outgoing ` + `request: "${init.method} ${url}."`), }); } } catch (err) { const canRetry = this.isRetryEligible(retryAttempts, err); const delayMillis = this.backOffDelayMillis(retryAttempts); if (canRetry && delayMillis <= this.retryConfig.maxDelayInMillis) { await this.waitForRetry(delayMillis); return await this.fetchWithRetry(url, init, retryAttempts + 1); } if (err instanceof HttpError) { if (err.cause) { throw err.cause; } try { const json = JSON.parse(err.message); const errorCode = this.getErrorCode(json); if (errorCode) { throw FirebaseAuthError.fromServerError(errorCode, json); } } catch (err) { if (err instanceof FirebaseAuthError) { throw err; } } throw new FirebaseAppError(AppErrorCodes.INTERNAL_ERROR, `Error while sending request or reading response: "${err}". Raw server ` + `response: Status code: "${err.status}". Outgoing ` + `request: "${init.method} ${url}."`); } throw new FirebaseAppError(AppErrorCodes.NETWORK_ERROR, `Error while making request: ${String(err)}`); } } /** * @param response - The response to check for errors. * @returns The error code if present; null otherwise. */ getErrorCode(response) { return (isNonNullObject(response) && response.error && response.error.message) || null; } waitForRetry(delayMillis) { if (delayMillis > 0) { return new Promise(resolve => { setTimeout(resolve, delayMillis); }); } return Promise.resolve(); } isRetryEligible(retryAttempts, err) { if (retryAttempts >= this.retryConfig.maxRetries) { return false; } if (err instanceof HttpError) { const statusCodes = this.retryConfig.statusCodes || []; return statusCodes.includes(err.status); } if (err instanceof Error && err.name === 'AbortError') { return false; } return true; } backOffDelayMillis(retryAttempts) { if (retryAttempts === 0) { return 0; } const backOffFactor = this.retryConfig.backOffFactor || 0; const delayInSeconds = 2 ** retryAttempts * backOffFactor; return Math.min(delayInSeconds * 1000, this.retryConfig.maxDelayInMillis); } } class HttpError extends Error { status; constructor(status, message, opts) { super(message, opts); this.status = status; this.name = 'HttpError'; } }