UNPKG

firebase

Version:

Firebase JavaScript library for web and Node.js

1,376 lines (1,355 loc) • 105 kB
import { registerVersion, _registerComponent, _getProvider, getApp } from 'https://www.gstatic.com/firebasejs/9.7.0/firebase-app.js'; /** * @license * Copyright 2017 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * The JS SDK supports 5 log levels and also allows a user the ability to * silence the logs altogether. * * The order is a follows: * DEBUG < VERBOSE < INFO < WARN < ERROR * * All of the log types above the current log level will be captured (i.e. if * you set the log level to `INFO`, errors will still be logged, but `DEBUG` and * `VERBOSE` logs will not) */ var LogLevel; (function (LogLevel) { LogLevel[LogLevel["DEBUG"] = 0] = "DEBUG"; LogLevel[LogLevel["VERBOSE"] = 1] = "VERBOSE"; LogLevel[LogLevel["INFO"] = 2] = "INFO"; LogLevel[LogLevel["WARN"] = 3] = "WARN"; LogLevel[LogLevel["ERROR"] = 4] = "ERROR"; LogLevel[LogLevel["SILENT"] = 5] = "SILENT"; })(LogLevel || (LogLevel = {})); const levelStringToEnum = { 'debug': LogLevel.DEBUG, 'verbose': LogLevel.VERBOSE, 'info': LogLevel.INFO, 'warn': LogLevel.WARN, 'error': LogLevel.ERROR, 'silent': LogLevel.SILENT }; /** * The default log level */ const defaultLogLevel = LogLevel.INFO; /** * By default, `console.debug` is not displayed in the developer console (in * chrome). To avoid forcing users to have to opt-in to these logs twice * (i.e. once for firebase, and once in the console), we are sending `DEBUG` * logs to the `console.log` function. */ const ConsoleMethod = { [LogLevel.DEBUG]: 'log', [LogLevel.VERBOSE]: 'log', [LogLevel.INFO]: 'info', [LogLevel.WARN]: 'warn', [LogLevel.ERROR]: 'error' }; /** * The default log handler will forward DEBUG, VERBOSE, INFO, WARN, and ERROR * messages on to their corresponding console counterparts (if the log method * is supported by the current log level) */ const defaultLogHandler = (instance, logType, ...args) => { if (logType < instance.logLevel) { return; } const now = new Date().toISOString(); const method = ConsoleMethod[logType]; if (method) { console[method](`[${now}] ${instance.name}:`, ...args); } else { throw new Error(`Attempted to log a message with an invalid logType (value: ${logType})`); } }; class Logger { /** * Gives you an instance of a Logger to capture messages according to * Firebase's logging scheme. * * @param name The name that the logs will be associated with */ constructor(name) { this.name = name; /** * The log level of the given Logger instance. */ this._logLevel = defaultLogLevel; /** * The main (internal) log handler for the Logger instance. * Can be set to a new function in internal package code but not by user. */ this._logHandler = defaultLogHandler; /** * The optional, additional, user-defined log handler for the Logger instance. */ this._userLogHandler = null; } get logLevel() { return this._logLevel; } set logLevel(val) { if (!(val in LogLevel)) { throw new TypeError(`Invalid value "${val}" assigned to \`logLevel\``); } this._logLevel = val; } // Workaround for setter/getter having to be the same type. setLogLevel(val) { this._logLevel = typeof val === 'string' ? levelStringToEnum[val] : val; } get logHandler() { return this._logHandler; } set logHandler(val) { if (typeof val !== 'function') { throw new TypeError('Value assigned to `logHandler` must be a function'); } this._logHandler = val; } get userLogHandler() { return this._userLogHandler; } set userLogHandler(val) { this._userLogHandler = val; } /** * The functions below are all based on the `console` interface */ debug(...args) { this._userLogHandler && this._userLogHandler(this, LogLevel.DEBUG, ...args); this._logHandler(this, LogLevel.DEBUG, ...args); } log(...args) { this._userLogHandler && this._userLogHandler(this, LogLevel.VERBOSE, ...args); this._logHandler(this, LogLevel.VERBOSE, ...args); } info(...args) { this._userLogHandler && this._userLogHandler(this, LogLevel.INFO, ...args); this._logHandler(this, LogLevel.INFO, ...args); } warn(...args) { this._userLogHandler && this._userLogHandler(this, LogLevel.WARN, ...args); this._logHandler(this, LogLevel.WARN, ...args); } error(...args) { this._userLogHandler && this._userLogHandler(this, LogLevel.ERROR, ...args); this._logHandler(this, LogLevel.ERROR, ...args); } } /** * @license * Copyright 2017 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ function isBrowserExtension() { const runtime = typeof chrome === 'object' ? chrome.runtime : typeof browser === 'object' ? browser.runtime : undefined; return typeof runtime === 'object' && runtime.id !== undefined; } /** * This method checks if indexedDB is supported by current browser/service worker context * @return true if indexedDB is supported by current browser/service worker context */ function isIndexedDBAvailable() { return typeof indexedDB === 'object'; } /** * This method validates browser/sw context for indexedDB by opening a dummy indexedDB database and reject * if errors occur during the database open operation. * * @throws exception if current browser/sw context can't run idb.open (ex: Safari iframe, Firefox * private browsing) */ function validateIndexedDBOpenable() { return new Promise((resolve, reject) => { try { let preExist = true; const DB_CHECK_NAME = 'validate-browser-context-for-indexeddb-analytics-module'; const request = self.indexedDB.open(DB_CHECK_NAME); request.onsuccess = () => { request.result.close(); // delete database only when it doesn't pre-exist if (!preExist) { self.indexedDB.deleteDatabase(DB_CHECK_NAME); } resolve(true); }; request.onupgradeneeded = () => { preExist = false; }; request.onerror = () => { var _a; reject(((_a = request.error) === null || _a === void 0 ? void 0 : _a.message) || ''); }; } catch (error) { reject(error); } }); } /** * * This method checks whether cookie is enabled within current browser * @return true if cookie is enabled within current browser */ function areCookiesEnabled() { if (typeof navigator === 'undefined' || !navigator.cookieEnabled) { return false; } return true; } /** * @license * Copyright 2017 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * @fileoverview Standardized Firebase Error. * * Usage: * * // Typescript string literals for type-safe codes * type Err = * 'unknown' | * 'object-not-found' * ; * * // Closure enum for type-safe error codes * // at-enum {string} * var Err = { * UNKNOWN: 'unknown', * OBJECT_NOT_FOUND: 'object-not-found', * } * * let errors: Map<Err, string> = { * 'generic-error': "Unknown error", * 'file-not-found': "Could not find file: {$file}", * }; * * // Type-safe function - must pass a valid error code as param. * let error = new ErrorFactory<Err>('service', 'Service', errors); * * ... * throw error.create(Err.GENERIC); * ... * throw error.create(Err.FILE_NOT_FOUND, {'file': fileName}); * ... * // Service: Could not file file: foo.txt (service/file-not-found). * * catch (e) { * assert(e.message === "Could not find file: foo.txt."); * if (e.code === 'service/file-not-found') { * console.log("Could not read file: " + e['file']); * } * } */ const ERROR_NAME = 'FirebaseError'; // Based on code from: // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#Custom_Error_Types class FirebaseError extends Error { constructor( /** The error code for this error. */ code, message, /** Custom data for this error. */ customData) { super(message); this.code = code; this.customData = customData; /** The custom name for all FirebaseErrors. */ this.name = ERROR_NAME; // Fix For ES5 // https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work Object.setPrototypeOf(this, FirebaseError.prototype); // Maintains proper stack trace for where our error was thrown. // Only available on V8. if (Error.captureStackTrace) { Error.captureStackTrace(this, ErrorFactory.prototype.create); } } } class ErrorFactory { constructor(service, serviceName, errors) { this.service = service; this.serviceName = serviceName; this.errors = errors; } create(code, ...data) { const customData = data[0] || {}; const fullCode = `${this.service}/${code}`; const template = this.errors[code]; const message = template ? replaceTemplate(template, customData) : 'Error'; // Service Name: Error message (service/code). const fullMessage = `${this.serviceName}: ${message} (${fullCode}).`; const error = new FirebaseError(fullCode, fullMessage, customData); return error; } } function replaceTemplate(template, data) { return template.replace(PATTERN, (_, key) => { const value = data[key]; return value != null ? String(value) : `<${key}?>`; }); } const PATTERN = /\{\$([^}]+)}/g; /** * Deep equal two objects. Support Arrays and Objects. */ function deepEqual(a, b) { if (a === b) { return true; } const aKeys = Object.keys(a); const bKeys = Object.keys(b); for (const k of aKeys) { if (!bKeys.includes(k)) { return false; } const aProp = a[k]; const bProp = b[k]; if (isObject(aProp) && isObject(bProp)) { if (!deepEqual(aProp, bProp)) { return false; } } else if (aProp !== bProp) { return false; } } for (const k of bKeys) { if (!aKeys.includes(k)) { return false; } } return true; } function isObject(thing) { return thing !== null && typeof thing === 'object'; } /** * @license * Copyright 2019 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * The amount of milliseconds to exponentially increase. */ const DEFAULT_INTERVAL_MILLIS = 1000; /** * The factor to backoff by. * Should be a number greater than 1. */ const DEFAULT_BACKOFF_FACTOR = 2; /** * The maximum milliseconds to increase to. * * <p>Visible for testing */ const MAX_VALUE_MILLIS = 4 * 60 * 60 * 1000; // Four hours, like iOS and Android. /** * The percentage of backoff time to randomize by. * See * http://go/safe-client-behavior#step-1-determine-the-appropriate-retry-interval-to-handle-spike-traffic * for context. * * <p>Visible for testing */ const RANDOM_FACTOR = 0.5; /** * Based on the backoff method from * https://github.com/google/closure-library/blob/master/closure/goog/math/exponentialbackoff.js. * Extracted here so we don't need to pass metadata and a stateful ExponentialBackoff object around. */ function calculateBackoffMillis(backoffCount, intervalMillis = DEFAULT_INTERVAL_MILLIS, backoffFactor = DEFAULT_BACKOFF_FACTOR) { // Calculates an exponentially increasing value. // Deviation: calculates value from count and a constant interval, so we only need to save value // and count to restore state. const currBaseValue = intervalMillis * Math.pow(backoffFactor, backoffCount); // A random "fuzz" to avoid waves of retries. // Deviation: randomFactor is required. const randomWait = Math.round( // A fraction of the backoff value to add/subtract. // Deviation: changes multiplication order to improve readability. RANDOM_FACTOR * currBaseValue * // A random float (rounded to int by Math.round above) in the range [-1, 1]. Determines // if we add or subtract. (Math.random() - 0.5) * 2); // Limits backoff to max to avoid effectively permanent backoff. return Math.min(MAX_VALUE_MILLIS, currBaseValue + randomWait); } /** * @license * Copyright 2021 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ function getModularInstance(service) { if (service && service._delegate) { return service._delegate; } else { return service; } } /** * @license * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * @internal */ function promisifyRequest(request, errorMessage) { return new Promise((resolve, reject) => { request.onsuccess = event => { resolve(event.target.result); }; request.onerror = event => { var _a; reject(`${errorMessage}: ${(_a = event.target.error) === null || _a === void 0 ? void 0 : _a.message}`); }; }); } /** * @internal */ class DBWrapper { constructor(_db) { this._db = _db; this.objectStoreNames = this._db.objectStoreNames; } transaction(storeNames, mode = 'readonly') { return new TransactionWrapper(this._db.transaction.call(this._db, storeNames, mode)); } createObjectStore(storeName, options) { return new ObjectStoreWrapper(this._db.createObjectStore(storeName, options)); } close() { this._db.close(); } } /** * @internal */ class TransactionWrapper { constructor(_transaction) { this._transaction = _transaction; this.complete = new Promise((resolve, reject) => { this._transaction.oncomplete = function () { resolve(); }; this._transaction.onerror = () => { reject(this._transaction.error); }; this._transaction.onabort = () => { reject(this._transaction.error); }; }); } objectStore(storeName) { return new ObjectStoreWrapper(this._transaction.objectStore(storeName)); } } /** * @internal */ class ObjectStoreWrapper { constructor(_store) { this._store = _store; } index(name) { return new IndexWrapper(this._store.index(name)); } createIndex(name, keypath, options) { return new IndexWrapper(this._store.createIndex(name, keypath, options)); } get(key) { const request = this._store.get(key); return promisifyRequest(request, 'Error reading from IndexedDB'); } put(value, key) { const request = this._store.put(value, key); return promisifyRequest(request, 'Error writing to IndexedDB'); } delete(key) { const request = this._store.delete(key); return promisifyRequest(request, 'Error deleting from IndexedDB'); } clear() { const request = this._store.clear(); return promisifyRequest(request, 'Error clearing IndexedDB object store'); } } /** * @internal */ class IndexWrapper { constructor(_index) { this._index = _index; } get(key) { const request = this._index.get(key); return promisifyRequest(request, 'Error reading from IndexedDB'); } } /** * @internal */ function openDB(dbName, dbVersion, upgradeCallback) { return new Promise((resolve, reject) => { try { const request = indexedDB.open(dbName, dbVersion); request.onsuccess = event => { resolve(new DBWrapper(event.target.result)); }; request.onerror = event => { var _a; reject(`Error opening indexedDB: ${(_a = event.target.error) === null || _a === void 0 ? void 0 : _a.message}`); }; request.onupgradeneeded = event => { upgradeCallback(new DBWrapper(request.result), event.oldVersion, event.newVersion, new TransactionWrapper(request.transaction)); }; } catch (e) { reject(`Error opening indexedDB: ${e.message}`); } }); } /** * Component for service name T, e.g. `auth`, `auth-internal` */ class Component { /** * * @param name The public service name, e.g. app, auth, firestore, database * @param instanceFactory Service factory responsible for creating the public interface * @param type whether the service provided by the component is public or private */ constructor(name, instanceFactory, type) { this.name = name; this.instanceFactory = instanceFactory; this.type = type; this.multipleInstances = false; /** * Properties to be added to the service namespace */ this.serviceProps = {}; this.instantiationMode = "LAZY" /* LAZY */; this.onInstanceCreated = null; } setInstantiationMode(mode) { this.instantiationMode = mode; return this; } setMultipleInstances(multipleInstances) { this.multipleInstances = multipleInstances; return this; } setServiceProps(props) { this.serviceProps = props; return this; } setInstanceCreatedCallback(callback) { this.onInstanceCreated = callback; return this; } } const name$1 = "@firebase/installations"; const version$1 = "0.5.8"; /** * @license * Copyright 2019 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const PENDING_TIMEOUT_MS = 10000; const PACKAGE_VERSION = `w:${version$1}`; const INTERNAL_AUTH_VERSION = 'FIS_v2'; const INSTALLATIONS_API_URL = 'https://firebaseinstallations.googleapis.com/v1'; const TOKEN_EXPIRATION_BUFFER = 60 * 60 * 1000; // One hour const SERVICE = 'installations'; const SERVICE_NAME = 'Installations'; /** * @license * Copyright 2019 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const ERROR_DESCRIPTION_MAP = { ["missing-app-config-values" /* MISSING_APP_CONFIG_VALUES */]: 'Missing App configuration value: "{$valueName}"', ["not-registered" /* NOT_REGISTERED */]: 'Firebase Installation is not registered.', ["installation-not-found" /* INSTALLATION_NOT_FOUND */]: 'Firebase Installation not found.', ["request-failed" /* REQUEST_FAILED */]: '{$requestName} request failed with error "{$serverCode} {$serverStatus}: {$serverMessage}"', ["app-offline" /* APP_OFFLINE */]: 'Could not process request. Application offline.', ["delete-pending-registration" /* DELETE_PENDING_REGISTRATION */]: "Can't delete installation while there is a pending registration request." }; const ERROR_FACTORY$1 = new ErrorFactory(SERVICE, SERVICE_NAME, ERROR_DESCRIPTION_MAP); /** Returns true if error is a FirebaseError that is based on an error from the server. */ function isServerError(error) { return (error instanceof FirebaseError && error.code.includes("request-failed" /* REQUEST_FAILED */)); } /** * @license * Copyright 2019 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ function getInstallationsEndpoint({ projectId }) { return `${INSTALLATIONS_API_URL}/projects/${projectId}/installations`; } function extractAuthTokenInfoFromResponse(response) { return { token: response.token, requestStatus: 2 /* COMPLETED */, expiresIn: getExpiresInFromResponseExpiresIn(response.expiresIn), creationTime: Date.now() }; } async function getErrorFromResponse(requestName, response) { const responseJson = await response.json(); const errorData = responseJson.error; return ERROR_FACTORY$1.create("request-failed" /* REQUEST_FAILED */, { requestName, serverCode: errorData.code, serverMessage: errorData.message, serverStatus: errorData.status }); } function getHeaders$1({ apiKey }) { return new Headers({ 'Content-Type': 'application/json', Accept: 'application/json', 'x-goog-api-key': apiKey }); } function getHeadersWithAuth(appConfig, { refreshToken }) { const headers = getHeaders$1(appConfig); headers.append('Authorization', getAuthorizationHeader(refreshToken)); return headers; } /** * Calls the passed in fetch wrapper and returns the response. * If the returned response has a status of 5xx, re-runs the function once and * returns the response. */ async function retryIfServerError(fn) { const result = await fn(); if (result.status >= 500 && result.status < 600) { // Internal Server Error. Retry request. return fn(); } return result; } function getExpiresInFromResponseExpiresIn(responseExpiresIn) { // This works because the server will never respond with fractions of a second. return Number(responseExpiresIn.replace('s', '000')); } function getAuthorizationHeader(refreshToken) { return `${INTERNAL_AUTH_VERSION} ${refreshToken}`; } /** * @license * Copyright 2019 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ async function createInstallationRequest({ appConfig, heartbeatServiceProvider }, { fid }) { const endpoint = getInstallationsEndpoint(appConfig); const headers = getHeaders$1(appConfig); // If heartbeat service exists, add the heartbeat string to the header. const heartbeatService = heartbeatServiceProvider.getImmediate({ optional: true }); if (heartbeatService) { const heartbeatsHeader = await heartbeatService.getHeartbeatsHeader(); if (heartbeatsHeader) { headers.append('x-firebase-client', heartbeatsHeader); } } const body = { fid, authVersion: INTERNAL_AUTH_VERSION, appId: appConfig.appId, sdkVersion: PACKAGE_VERSION }; const request = { method: 'POST', headers, body: JSON.stringify(body) }; const response = await retryIfServerError(() => fetch(endpoint, request)); if (response.ok) { const responseValue = await response.json(); const registeredInstallationEntry = { fid: responseValue.fid || fid, registrationStatus: 2 /* COMPLETED */, refreshToken: responseValue.refreshToken, authToken: extractAuthTokenInfoFromResponse(responseValue.authToken) }; return registeredInstallationEntry; } else { throw await getErrorFromResponse('Create Installation', response); } } /** * @license * Copyright 2019 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** Returns a promise that resolves after given time passes. */ function sleep(ms) { return new Promise(resolve => { setTimeout(resolve, ms); }); } /** * @license * Copyright 2019 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ function bufferToBase64UrlSafe(array) { const b64 = btoa(String.fromCharCode(...array)); return b64.replace(/\+/g, '-').replace(/\//g, '_'); } /** * @license * Copyright 2019 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const VALID_FID_PATTERN = /^[cdef][\w-]{21}$/; const INVALID_FID = ''; /** * Generates a new FID using random values from Web Crypto API. * Returns an empty string if FID generation fails for any reason. */ function generateFid() { try { // A valid FID has exactly 22 base64 characters, which is 132 bits, or 16.5 // bytes. our implementation generates a 17 byte array instead. const fidByteArray = new Uint8Array(17); const crypto = self.crypto || self.msCrypto; crypto.getRandomValues(fidByteArray); // Replace the first 4 random bits with the constant FID header of 0b0111. fidByteArray[0] = 0b01110000 + (fidByteArray[0] % 0b00010000); const fid = encode(fidByteArray); return VALID_FID_PATTERN.test(fid) ? fid : INVALID_FID; } catch (_a) { // FID generation errored return INVALID_FID; } } /** Converts a FID Uint8Array to a base64 string representation. */ function encode(fidByteArray) { const b64String = bufferToBase64UrlSafe(fidByteArray); // Remove the 23rd character that was added because of the extra 4 bits at the // end of our 17 byte array, and the '=' padding. return b64String.substr(0, 22); } /** * @license * Copyright 2019 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** Returns a string key that can be used to identify the app. */ function getKey(appConfig) { return `${appConfig.appName}!${appConfig.appId}`; } /** * @license * Copyright 2019 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const fidChangeCallbacks = new Map(); /** * Calls the onIdChange callbacks with the new FID value, and broadcasts the * change to other tabs. */ function fidChanged(appConfig, fid) { const key = getKey(appConfig); callFidChangeCallbacks(key, fid); broadcastFidChange(key, fid); } function callFidChangeCallbacks(key, fid) { const callbacks = fidChangeCallbacks.get(key); if (!callbacks) { return; } for (const callback of callbacks) { callback(fid); } } function broadcastFidChange(key, fid) { const channel = getBroadcastChannel(); if (channel) { channel.postMessage({ key, fid }); } closeBroadcastChannel(); } let broadcastChannel = null; /** Opens and returns a BroadcastChannel if it is supported by the browser. */ function getBroadcastChannel() { if (!broadcastChannel && 'BroadcastChannel' in self) { broadcastChannel = new BroadcastChannel('[Firebase] FID Change'); broadcastChannel.onmessage = e => { callFidChangeCallbacks(e.data.key, e.data.fid); }; } return broadcastChannel; } function closeBroadcastChannel() { if (fidChangeCallbacks.size === 0 && broadcastChannel) { broadcastChannel.close(); broadcastChannel = null; } } /** * @license * Copyright 2019 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const DATABASE_NAME = 'firebase-installations-database'; const DATABASE_VERSION = 1; const OBJECT_STORE_NAME = 'firebase-installations-store'; let dbPromise = null; function getDbPromise() { if (!dbPromise) { dbPromise = openDB(DATABASE_NAME, DATABASE_VERSION, (db, oldVersion) => { // We don't use 'break' in this switch statement, the fall-through // behavior is what we want, because if there are multiple versions between // the old version and the current version, we want ALL the migrations // that correspond to those versions to run, not only the last one. // eslint-disable-next-line default-case switch (oldVersion) { case 0: db.createObjectStore(OBJECT_STORE_NAME); } }); } return dbPromise; } /** Assigns or overwrites the record for the given key with the given value. */ async function set(appConfig, value) { const key = getKey(appConfig); const db = await getDbPromise(); const tx = db.transaction(OBJECT_STORE_NAME, 'readwrite'); const objectStore = tx.objectStore(OBJECT_STORE_NAME); const oldValue = (await objectStore.get(key)); await objectStore.put(value, key); await tx.complete; if (!oldValue || oldValue.fid !== value.fid) { fidChanged(appConfig, value.fid); } return value; } /** Removes record(s) from the objectStore that match the given key. */ async function remove(appConfig) { const key = getKey(appConfig); const db = await getDbPromise(); const tx = db.transaction(OBJECT_STORE_NAME, 'readwrite'); await tx.objectStore(OBJECT_STORE_NAME).delete(key); await tx.complete; } /** * Atomically updates a record with the result of updateFn, which gets * called with the current value. If newValue is undefined, the record is * deleted instead. * @return Updated value */ async function update(appConfig, updateFn) { const key = getKey(appConfig); const db = await getDbPromise(); const tx = db.transaction(OBJECT_STORE_NAME, 'readwrite'); const store = tx.objectStore(OBJECT_STORE_NAME); const oldValue = (await store.get(key)); const newValue = updateFn(oldValue); if (newValue === undefined) { await store.delete(key); } else { await store.put(newValue, key); } await tx.complete; if (newValue && (!oldValue || oldValue.fid !== newValue.fid)) { fidChanged(appConfig, newValue.fid); } return newValue; } /** * @license * Copyright 2019 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * Updates and returns the InstallationEntry from the database. * Also triggers a registration request if it is necessary and possible. */ async function getInstallationEntry(installations) { let registrationPromise; const installationEntry = await update(installations.appConfig, oldEntry => { const installationEntry = updateOrCreateInstallationEntry(oldEntry); const entryWithPromise = triggerRegistrationIfNecessary(installations, installationEntry); registrationPromise = entryWithPromise.registrationPromise; return entryWithPromise.installationEntry; }); if (installationEntry.fid === INVALID_FID) { // FID generation failed. Waiting for the FID from the server. return { installationEntry: await registrationPromise }; } return { installationEntry, registrationPromise }; } /** * Creates a new Installation Entry if one does not exist. * Also clears timed out pending requests. */ function updateOrCreateInstallationEntry(oldEntry) { const entry = oldEntry || { fid: generateFid(), registrationStatus: 0 /* NOT_STARTED */ }; return clearTimedOutRequest(entry); } /** * If the Firebase Installation is not registered yet, this will trigger the * registration and return an InProgressInstallationEntry. * * If registrationPromise does not exist, the installationEntry is guaranteed * to be registered. */ function triggerRegistrationIfNecessary(installations, installationEntry) { if (installationEntry.registrationStatus === 0 /* NOT_STARTED */) { if (!navigator.onLine) { // Registration required but app is offline. const registrationPromiseWithError = Promise.reject(ERROR_FACTORY$1.create("app-offline" /* APP_OFFLINE */)); return { installationEntry, registrationPromise: registrationPromiseWithError }; } // Try registering. Change status to IN_PROGRESS. const inProgressEntry = { fid: installationEntry.fid, registrationStatus: 1 /* IN_PROGRESS */, registrationTime: Date.now() }; const registrationPromise = registerInstallation(installations, inProgressEntry); return { installationEntry: inProgressEntry, registrationPromise }; } else if (installationEntry.registrationStatus === 1 /* IN_PROGRESS */) { return { installationEntry, registrationPromise: waitUntilFidRegistration(installations) }; } else { return { installationEntry }; } } /** This will be executed only once for each new Firebase Installation. */ async function registerInstallation(installations, installationEntry) { try { const registeredInstallationEntry = await createInstallationRequest(installations, installationEntry); return set(installations.appConfig, registeredInstallationEntry); } catch (e) { if (isServerError(e) && e.customData.serverCode === 409) { // Server returned a "FID can not be used" error. // Generate a new ID next time. await remove(installations.appConfig); } else { // Registration failed. Set FID as not registered. await set(installations.appConfig, { fid: installationEntry.fid, registrationStatus: 0 /* NOT_STARTED */ }); } throw e; } } /** Call if FID registration is pending in another request. */ async function waitUntilFidRegistration(installations) { // Unfortunately, there is no way of reliably observing when a value in // IndexedDB changes (yet, see https://github.com/WICG/indexed-db-observers), // so we need to poll. let entry = await updateInstallationRequest(installations.appConfig); while (entry.registrationStatus === 1 /* IN_PROGRESS */) { // createInstallation request still in progress. await sleep(100); entry = await updateInstallationRequest(installations.appConfig); } if (entry.registrationStatus === 0 /* NOT_STARTED */) { // The request timed out or failed in a different call. Try again. const { installationEntry, registrationPromise } = await getInstallationEntry(installations); if (registrationPromise) { return registrationPromise; } else { // if there is no registrationPromise, entry is registered. return installationEntry; } } return entry; } /** * Called only if there is a CreateInstallation request in progress. * * Updates the InstallationEntry in the DB based on the status of the * CreateInstallation request. * * Returns the updated InstallationEntry. */ function updateInstallationRequest(appConfig) { return update(appConfig, oldEntry => { if (!oldEntry) { throw ERROR_FACTORY$1.create("installation-not-found" /* INSTALLATION_NOT_FOUND */); } return clearTimedOutRequest(oldEntry); }); } function clearTimedOutRequest(entry) { if (hasInstallationRequestTimedOut(entry)) { return { fid: entry.fid, registrationStatus: 0 /* NOT_STARTED */ }; } return entry; } function hasInstallationRequestTimedOut(installationEntry) { return (installationEntry.registrationStatus === 1 /* IN_PROGRESS */ && installationEntry.registrationTime + PENDING_TIMEOUT_MS < Date.now()); } /** * @license * Copyright 2019 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ async function generateAuthTokenRequest({ appConfig, heartbeatServiceProvider }, installationEntry) { const endpoint = getGenerateAuthTokenEndpoint(appConfig, installationEntry); const headers = getHeadersWithAuth(appConfig, installationEntry); // If heartbeat service exists, add the heartbeat string to the header. const heartbeatService = heartbeatServiceProvider.getImmediate({ optional: true }); if (heartbeatService) { const heartbeatsHeader = await heartbeatService.getHeartbeatsHeader(); if (heartbeatsHeader) { headers.append('x-firebase-client', heartbeatsHeader); } } const body = { installation: { sdkVersion: PACKAGE_VERSION, appId: appConfig.appId } }; const request = { method: 'POST', headers, body: JSON.stringify(body) }; const response = await retryIfServerError(() => fetch(endpoint, request)); if (response.ok) { const responseValue = await response.json(); const completedAuthToken = extractAuthTokenInfoFromResponse(responseValue); return completedAuthToken; } else { throw await getErrorFromResponse('Generate Auth Token', response); } } function getGenerateAuthTokenEndpoint(appConfig, { fid }) { return `${getInstallationsEndpoint(appConfig)}/${fid}/authTokens:generate`; } /** * @license * Copyright 2019 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * Returns a valid authentication token for the installation. Generates a new * token if one doesn't exist, is expired or about to expire. * * Should only be called if the Firebase Installation is registered. */ async function refreshAuthToken(installations, forceRefresh = false) { let tokenPromise; const entry = await update(installations.appConfig, oldEntry => { if (!isEntryRegistered(oldEntry)) { throw ERROR_FACTORY$1.create("not-registered" /* NOT_REGISTERED */); } const oldAuthToken = oldEntry.authToken; if (!forceRefresh && isAuthTokenValid(oldAuthToken)) { // There is a valid token in the DB. return oldEntry; } else if (oldAuthToken.requestStatus === 1 /* IN_PROGRESS */) { // There already is a token request in progress. tokenPromise = waitUntilAuthTokenRequest(installations, forceRefresh); return oldEntry; } else { // No token or token expired. if (!navigator.onLine) { throw ERROR_FACTORY$1.create("app-offline" /* APP_OFFLINE */); } const inProgressEntry = makeAuthTokenRequestInProgressEntry(oldEntry); tokenPromise = fetchAuthTokenFromServer(installations, inProgressEntry); return inProgressEntry; } }); const authToken = tokenPromise ? await tokenPromise : entry.authToken; return authToken; } /** * Call only if FID is registered and Auth Token request is in progress. * * Waits until the current pending request finishes. If the request times out, * tries once in this thread as well. */ async function waitUntilAuthTokenRequest(installations, forceRefresh) { // Unfortunately, there is no way of reliably observing when a value in // IndexedDB changes (yet, see https://github.com/WICG/indexed-db-observers), // so we need to poll. let entry = await updateAuthTokenRequest(installations.appConfig); while (entry.authToken.requestStatus === 1 /* IN_PROGRESS */) { // generateAuthToken still in progress. await sleep(100); entry = await updateAuthTokenRequest(installations.appConfig); } const authToken = entry.authToken; if (authToken.requestStatus === 0 /* NOT_STARTED */) { // The request timed out or failed in a different call. Try again. return refreshAuthToken(installations, forceRefresh); } else { return authToken; } } /** * Called only if there is a GenerateAuthToken request in progress. * * Updates the InstallationEntry in