@firebase/auth
Version:
The Firebase Authenticaton component of the Firebase JS SDK.
1,340 lines (1,325 loc) • 132 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var index = require('./index-c37383d8.js');
var app = require('@firebase/app');
var util = require('@firebase/util');
require('tslib');
require('@firebase/component');
require('@firebase/logger');
/**
* @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.
*/
// There are two different browser persistence types: local and session.
// Both have the same implementation but use a different underlying storage
// object.
class BrowserPersistenceClass {
constructor(storageRetriever, type) {
this.storageRetriever = storageRetriever;
this.type = type;
}
_isAvailable() {
try {
if (!this.storage) {
return Promise.resolve(false);
}
this.storage.setItem(index.STORAGE_AVAILABLE_KEY, '1');
this.storage.removeItem(index.STORAGE_AVAILABLE_KEY);
return Promise.resolve(true);
}
catch (_a) {
return Promise.resolve(false);
}
}
_set(key, value) {
this.storage.setItem(key, JSON.stringify(value));
return Promise.resolve();
}
_get(key) {
const json = this.storage.getItem(key);
return Promise.resolve(json ? JSON.parse(json) : null);
}
_remove(key) {
this.storage.removeItem(key);
return Promise.resolve();
}
get storage() {
return this.storageRetriever();
}
}
/**
* @license
* Copyright 2020 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 polling period in case events are not supported
const _POLLING_INTERVAL_MS$1 = 1000;
// The IE 10 localStorage cross tab synchronization delay in milliseconds
const IE10_LOCAL_STORAGE_SYNC_DELAY = 10;
class BrowserLocalPersistence extends BrowserPersistenceClass {
constructor() {
super(() => window.localStorage, "LOCAL" /* PersistenceType.LOCAL */);
this.boundEventHandler = (event, poll) => this.onStorageEvent(event, poll);
this.listeners = {};
this.localCache = {};
// setTimeout return value is platform specific
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.pollTimer = null;
// Whether to use polling instead of depending on window events
this.fallbackToPolling = index._isMobileBrowser();
this._shouldAllowMigration = true;
}
forAllChangedKeys(cb) {
// Check all keys with listeners on them.
for (const key of Object.keys(this.listeners)) {
// Get value from localStorage.
const newValue = this.storage.getItem(key);
const oldValue = this.localCache[key];
// If local map value does not match, trigger listener with storage event.
// Differentiate this simulated event from the real storage event.
if (newValue !== oldValue) {
cb(key, oldValue, newValue);
}
}
}
onStorageEvent(event, poll = false) {
// Key would be null in some situations, like when localStorage is cleared
if (!event.key) {
this.forAllChangedKeys((key, _oldValue, newValue) => {
this.notifyListeners(key, newValue);
});
return;
}
const key = event.key;
// Check the mechanism how this event was detected.
// The first event will dictate the mechanism to be used.
if (poll) {
// Environment detects storage changes via polling.
// Remove storage event listener to prevent possible event duplication.
this.detachListener();
}
else {
// Environment detects storage changes via storage event listener.
// Remove polling listener to prevent possible event duplication.
this.stopPolling();
}
const triggerListeners = () => {
// Keep local map up to date in case storage event is triggered before
// poll.
const storedValue = this.storage.getItem(key);
if (!poll && this.localCache[key] === storedValue) {
// Real storage event which has already been detected, do nothing.
// This seems to trigger in some IE browsers for some reason.
return;
}
this.notifyListeners(key, storedValue);
};
const storedValue = this.storage.getItem(key);
if (index._isIE10() &&
storedValue !== event.newValue &&
event.newValue !== event.oldValue) {
// IE 10 has this weird bug where a storage event would trigger with the
// correct key, oldValue and newValue but localStorage.getItem(key) does
// not yield the updated value until a few milliseconds. This ensures
// this recovers from that situation.
setTimeout(triggerListeners, IE10_LOCAL_STORAGE_SYNC_DELAY);
}
else {
triggerListeners();
}
}
notifyListeners(key, value) {
this.localCache[key] = value;
const listeners = this.listeners[key];
if (listeners) {
for (const listener of Array.from(listeners)) {
listener(value ? JSON.parse(value) : value);
}
}
}
startPolling() {
this.stopPolling();
this.pollTimer = setInterval(() => {
this.forAllChangedKeys((key, oldValue, newValue) => {
this.onStorageEvent(new StorageEvent('storage', {
key,
oldValue,
newValue
}),
/* poll */ true);
});
}, _POLLING_INTERVAL_MS$1);
}
stopPolling() {
if (this.pollTimer) {
clearInterval(this.pollTimer);
this.pollTimer = null;
}
}
attachListener() {
window.addEventListener('storage', this.boundEventHandler);
}
detachListener() {
window.removeEventListener('storage', this.boundEventHandler);
}
_addListener(key, listener) {
if (Object.keys(this.listeners).length === 0) {
// Whether browser can detect storage event when it had already been pushed to the background.
// This may happen in some mobile browsers. A localStorage change in the foreground window
// will not be detected in the background window via the storage event.
// This was detected in iOS 7.x mobile browsers
if (this.fallbackToPolling) {
this.startPolling();
}
else {
this.attachListener();
}
}
if (!this.listeners[key]) {
this.listeners[key] = new Set();
// Populate the cache to avoid spuriously triggering on first poll.
this.localCache[key] = this.storage.getItem(key);
}
this.listeners[key].add(listener);
}
_removeListener(key, listener) {
if (this.listeners[key]) {
this.listeners[key].delete(listener);
if (this.listeners[key].size === 0) {
delete this.listeners[key];
}
}
if (Object.keys(this.listeners).length === 0) {
this.detachListener();
this.stopPolling();
}
}
// Update local cache on base operations:
async _set(key, value) {
await super._set(key, value);
this.localCache[key] = JSON.stringify(value);
}
async _get(key) {
const value = await super._get(key);
this.localCache[key] = JSON.stringify(value);
return value;
}
async _remove(key) {
await super._remove(key);
delete this.localCache[key];
}
}
BrowserLocalPersistence.type = 'LOCAL';
/**
* An implementation of {@link Persistence} of type `LOCAL` using `localStorage`
* for the underlying storage.
*
* @public
*/
const browserLocalPersistence = BrowserLocalPersistence;
/**
* @license
* Copyright 2025 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 POLLING_INTERVAL_MS = 1000;
// Pull a cookie value from document.cookie
function getDocumentCookie(name) {
var _a, _b;
const escapedName = name.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
const matcher = RegExp(`${escapedName}=([^;]+)`);
return (_b = (_a = document.cookie.match(matcher)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : null;
}
// Produce a sanitized cookie name from the persistence key
function getCookieName(key) {
// __HOST- doesn't work in localhost https://issues.chromium.org/issues/40196122 but it has
// desirable security properties, so lets use a different cookie name while in dev-mode.
// Already checked isSecureContext in _isAvailable, so if it's http we're hitting local.
const isDevMode = window.location.protocol === 'http:';
return `${isDevMode ? '__dev_' : '__HOST-'}FIREBASE_${key.split(':')[3]}`;
}
class CookiePersistence {
constructor() {
this.type = "COOKIE" /* PersistenceType.COOKIE */;
this.listenerUnsubscribes = new Map();
}
// used to get the URL to the backend to proxy to
_getFinalTarget(originalUrl) {
if (typeof window === undefined) {
return originalUrl;
}
const url = new URL(`${window.location.origin}/__cookies__`);
url.searchParams.set('finalTarget', originalUrl);
return url;
}
// To be a usable persistence method in a chain browserCookiePersistence ensures that
// prerequisites have been met, namely that we're in a secureContext, navigator and document are
// available and cookies are enabled. Not all UAs support these method, so fallback accordingly.
async _isAvailable() {
var _a;
if (typeof isSecureContext === 'boolean' && !isSecureContext) {
return false;
}
if (typeof navigator === 'undefined' || typeof document === 'undefined') {
return false;
}
return (_a = navigator.cookieEnabled) !== null && _a !== void 0 ? _a : true;
}
// Set should be a noop as we expect middleware to handle this
async _set(_key, _value) {
return;
}
// Attempt to get the cookie from cookieStore, fallback to document.cookie
async _get(key) {
if (!this._isAvailable()) {
return null;
}
const name = getCookieName(key);
if (window.cookieStore) {
const cookie = await window.cookieStore.get(name);
return cookie === null || cookie === void 0 ? void 0 : cookie.value;
}
return getDocumentCookie(name);
}
// Log out by overriding the idToken with a sentinel value of ""
async _remove(key) {
if (!this._isAvailable()) {
return;
}
// To make sure we don't hit signout over and over again, only do this operation if we need to
// with the logout sentinel value of "" this can cause race conditions. Unnecessary set-cookie
// headers will reduce CDN hit rates too.
const existingValue = await this._get(key);
if (!existingValue) {
return;
}
const name = getCookieName(key);
document.cookie = `${name}=;Max-Age=34560000;Partitioned;Secure;SameSite=Strict;Path=/;Priority=High`;
await fetch(`/__cookies__`, { method: 'DELETE' }).catch(() => undefined);
}
// Listen for cookie changes, both cookieStore and fallback to polling document.cookie
_addListener(key, listener) {
if (!this._isAvailable()) {
return;
}
const name = getCookieName(key);
if (window.cookieStore) {
const cb = ((event) => {
const changedCookie = event.changed.find(change => change.name === name);
if (changedCookie) {
listener(changedCookie.value);
}
const deletedCookie = event.deleted.find(change => change.name === name);
if (deletedCookie) {
listener(null);
}
});
const unsubscribe = () => window.cookieStore.removeEventListener('change', cb);
this.listenerUnsubscribes.set(listener, unsubscribe);
return window.cookieStore.addEventListener('change', cb);
}
let lastValue = getDocumentCookie(name);
const interval = setInterval(() => {
const currentValue = getDocumentCookie(name);
if (currentValue !== lastValue) {
listener(currentValue);
lastValue = currentValue;
}
}, POLLING_INTERVAL_MS);
const unsubscribe = () => clearInterval(interval);
this.listenerUnsubscribes.set(listener, unsubscribe);
}
_removeListener(_key, listener) {
const unsubscribe = this.listenerUnsubscribes.get(listener);
if (!unsubscribe) {
return;
}
unsubscribe();
this.listenerUnsubscribes.delete(listener);
}
}
CookiePersistence.type = 'COOKIE';
/**
* An implementation of {@link Persistence} of type `COOKIE`, for use on the client side in
* applications leveraging hybrid rendering and middleware.
*
* @remarks This persistence method requires companion middleware to function, such as that provided
* by {@link https://firebaseopensource.com/projects/firebaseextended/reactfire/ | ReactFire} for
* NextJS.
* @beta
*/
const browserCookiePersistence = CookiePersistence;
/**
* @license
* Copyright 2020 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.
*/
class BrowserSessionPersistence extends BrowserPersistenceClass {
constructor() {
super(() => window.sessionStorage, "SESSION" /* PersistenceType.SESSION */);
}
_addListener(_key, _listener) {
// Listeners are not supported for session storage since it cannot be shared across windows
return;
}
_removeListener(_key, _listener) {
// Listeners are not supported for session storage since it cannot be shared across windows
return;
}
}
BrowserSessionPersistence.type = 'SESSION';
/**
* An implementation of {@link Persistence} of `SESSION` using `sessionStorage`
* for the underlying storage.
*
* @public
*/
const browserSessionPersistence = BrowserSessionPersistence;
/**
* @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.
*/
/**
* Shim for Promise.allSettled, note the slightly different format of `fulfilled` vs `status`.
*
* @param promises - Array of promises to wait on.
*/
function _allSettled(promises) {
return Promise.all(promises.map(async (promise) => {
try {
const value = await promise;
return {
fulfilled: true,
value
};
}
catch (reason) {
return {
fulfilled: false,
reason
};
}
}));
}
/**
* @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.
*/
/**
* Interface class for receiving messages.
*
*/
class Receiver {
constructor(eventTarget) {
this.eventTarget = eventTarget;
this.handlersMap = {};
this.boundEventHandler = this.handleEvent.bind(this);
}
/**
* Obtain an instance of a Receiver for a given event target, if none exists it will be created.
*
* @param eventTarget - An event target (such as window or self) through which the underlying
* messages will be received.
*/
static _getInstance(eventTarget) {
// The results are stored in an array since objects can't be keys for other
// objects. In addition, setting a unique property on an event target as a
// hash map key may not be allowed due to CORS restrictions.
const existingInstance = this.receivers.find(receiver => receiver.isListeningto(eventTarget));
if (existingInstance) {
return existingInstance;
}
const newInstance = new Receiver(eventTarget);
this.receivers.push(newInstance);
return newInstance;
}
isListeningto(eventTarget) {
return this.eventTarget === eventTarget;
}
/**
* Fans out a MessageEvent to the appropriate listeners.
*
* @remarks
* Sends an {@link Status.ACK} upon receipt and a {@link Status.DONE} once all handlers have
* finished processing.
*
* @param event - The MessageEvent.
*
*/
async handleEvent(event) {
const messageEvent = event;
const { eventId, eventType, data } = messageEvent.data;
const handlers = this.handlersMap[eventType];
if (!(handlers === null || handlers === void 0 ? void 0 : handlers.size)) {
return;
}
messageEvent.ports[0].postMessage({
status: "ack" /* _Status.ACK */,
eventId,
eventType
});
const promises = Array.from(handlers).map(async (handler) => handler(messageEvent.origin, data));
const response = await _allSettled(promises);
messageEvent.ports[0].postMessage({
status: "done" /* _Status.DONE */,
eventId,
eventType,
response
});
}
/**
* Subscribe an event handler for a particular event.
*
* @param eventType - Event name to subscribe to.
* @param eventHandler - The event handler which should receive the events.
*
*/
_subscribe(eventType, eventHandler) {
if (Object.keys(this.handlersMap).length === 0) {
this.eventTarget.addEventListener('message', this.boundEventHandler);
}
if (!this.handlersMap[eventType]) {
this.handlersMap[eventType] = new Set();
}
this.handlersMap[eventType].add(eventHandler);
}
/**
* Unsubscribe an event handler from a particular event.
*
* @param eventType - Event name to unsubscribe from.
* @param eventHandler - Optional event handler, if none provided, unsubscribe all handlers on this event.
*
*/
_unsubscribe(eventType, eventHandler) {
if (this.handlersMap[eventType] && eventHandler) {
this.handlersMap[eventType].delete(eventHandler);
}
if (!eventHandler || this.handlersMap[eventType].size === 0) {
delete this.handlersMap[eventType];
}
if (Object.keys(this.handlersMap).length === 0) {
this.eventTarget.removeEventListener('message', this.boundEventHandler);
}
}
}
Receiver.receivers = [];
/**
* @license
* Copyright 2020 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 _generateEventId(prefix = '', digits = 10) {
let random = '';
for (let i = 0; i < digits; i++) {
random += Math.floor(Math.random() * 10);
}
return prefix + random;
}
/**
* @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.
*/
/**
* Interface for sending messages and waiting for a completion response.
*
*/
class Sender {
constructor(target) {
this.target = target;
this.handlers = new Set();
}
/**
* Unsubscribe the handler and remove it from our tracking Set.
*
* @param handler - The handler to unsubscribe.
*/
removeMessageHandler(handler) {
if (handler.messageChannel) {
handler.messageChannel.port1.removeEventListener('message', handler.onMessage);
handler.messageChannel.port1.close();
}
this.handlers.delete(handler);
}
/**
* Send a message to the Receiver located at {@link target}.
*
* @remarks
* We'll first wait a bit for an ACK , if we get one we will wait significantly longer until the
* receiver has had a chance to fully process the event.
*
* @param eventType - Type of event to send.
* @param data - The payload of the event.
* @param timeout - Timeout for waiting on an ACK from the receiver.
*
* @returns An array of settled promises from all the handlers that were listening on the receiver.
*/
async _send(eventType, data, timeout = 50 /* _TimeoutDuration.ACK */) {
const messageChannel = typeof MessageChannel !== 'undefined' ? new MessageChannel() : null;
if (!messageChannel) {
throw new Error("connection_unavailable" /* _MessageError.CONNECTION_UNAVAILABLE */);
}
// Node timers and browser timers return fundamentally different types.
// We don't actually care what the value is but TS won't accept unknown and
// we can't cast properly in both environments.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let completionTimer;
let handler;
return new Promise((resolve, reject) => {
const eventId = _generateEventId('', 20);
messageChannel.port1.start();
const ackTimer = setTimeout(() => {
reject(new Error("unsupported_event" /* _MessageError.UNSUPPORTED_EVENT */));
}, timeout);
handler = {
messageChannel,
onMessage(event) {
const messageEvent = event;
if (messageEvent.data.eventId !== eventId) {
return;
}
switch (messageEvent.data.status) {
case "ack" /* _Status.ACK */:
// The receiver should ACK first.
clearTimeout(ackTimer);
completionTimer = setTimeout(() => {
reject(new Error("timeout" /* _MessageError.TIMEOUT */));
}, 3000 /* _TimeoutDuration.COMPLETION */);
break;
case "done" /* _Status.DONE */:
// Once the receiver's handlers are finished we will get the results.
clearTimeout(completionTimer);
resolve(messageEvent.data.response);
break;
default:
clearTimeout(ackTimer);
clearTimeout(completionTimer);
reject(new Error("invalid_response" /* _MessageError.INVALID_RESPONSE */));
break;
}
}
};
this.handlers.add(handler);
messageChannel.port1.addEventListener('message', handler.onMessage);
this.target.postMessage({
eventType,
eventId,
data
}, [messageChannel.port2]);
}).finally(() => {
if (handler) {
this.removeMessageHandler(handler);
}
});
}
}
/**
* @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 DB_NAME = 'firebaseLocalStorageDb';
const DB_VERSION = 1;
const DB_OBJECTSTORE_NAME = 'firebaseLocalStorage';
const DB_DATA_KEYPATH = 'fbase_key';
/**
* Promise wrapper for IDBRequest
*
* Unfortunately we can't cleanly extend Promise<T> since promises are not callable in ES6
*
*/
class DBPromise {
constructor(request) {
this.request = request;
}
toPromise() {
return new Promise((resolve, reject) => {
this.request.addEventListener('success', () => {
resolve(this.request.result);
});
this.request.addEventListener('error', () => {
reject(this.request.error);
});
});
}
}
function getObjectStore(db, isReadWrite) {
return db
.transaction([DB_OBJECTSTORE_NAME], isReadWrite ? 'readwrite' : 'readonly')
.objectStore(DB_OBJECTSTORE_NAME);
}
function _deleteDatabase() {
const request = indexedDB.deleteDatabase(DB_NAME);
return new DBPromise(request).toPromise();
}
function _openDatabase() {
const request = indexedDB.open(DB_NAME, DB_VERSION);
return new Promise((resolve, reject) => {
request.addEventListener('error', () => {
reject(request.error);
});
request.addEventListener('upgradeneeded', () => {
const db = request.result;
try {
db.createObjectStore(DB_OBJECTSTORE_NAME, { keyPath: DB_DATA_KEYPATH });
}
catch (e) {
reject(e);
}
});
request.addEventListener('success', async () => {
const db = request.result;
// Strange bug that occurs in Firefox when multiple tabs are opened at the
// same time. The only way to recover seems to be deleting the database
// and re-initializing it.
// https://github.com/firebase/firebase-js-sdk/issues/634
if (!db.objectStoreNames.contains(DB_OBJECTSTORE_NAME)) {
// Need to close the database or else you get a `blocked` event
db.close();
await _deleteDatabase();
resolve(await _openDatabase());
}
else {
resolve(db);
}
});
});
}
async function _putObject(db, key, value) {
const request = getObjectStore(db, true).put({
[DB_DATA_KEYPATH]: key,
value
});
return new DBPromise(request).toPromise();
}
async function getObject(db, key) {
const request = getObjectStore(db, false).get(key);
const data = await new DBPromise(request).toPromise();
return data === undefined ? null : data.value;
}
function _deleteObject(db, key) {
const request = getObjectStore(db, true).delete(key);
return new DBPromise(request).toPromise();
}
const _POLLING_INTERVAL_MS = 800;
const _TRANSACTION_RETRY_COUNT = 3;
class IndexedDBLocalPersistence {
constructor() {
this.type = "LOCAL" /* PersistenceType.LOCAL */;
this._shouldAllowMigration = true;
this.listeners = {};
this.localCache = {};
// setTimeout return value is platform specific
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.pollTimer = null;
this.pendingWrites = 0;
this.receiver = null;
this.sender = null;
this.serviceWorkerReceiverAvailable = false;
this.activeServiceWorker = null;
// Fire & forget the service worker registration as it may never resolve
this._workerInitializationPromise =
this.initializeServiceWorkerMessaging().then(() => { }, () => { });
}
async _openDb() {
if (this.db) {
return this.db;
}
this.db = await _openDatabase();
return this.db;
}
async _withRetries(op) {
let numAttempts = 0;
while (true) {
try {
const db = await this._openDb();
return await op(db);
}
catch (e) {
if (numAttempts++ > _TRANSACTION_RETRY_COUNT) {
throw e;
}
if (this.db) {
this.db.close();
this.db = undefined;
}
// TODO: consider adding exponential backoff
}
}
}
/**
* IndexedDB events do not propagate from the main window to the worker context. We rely on a
* postMessage interface to send these events to the worker ourselves.
*/
async initializeServiceWorkerMessaging() {
return index._isWorker() ? this.initializeReceiver() : this.initializeSender();
}
/**
* As the worker we should listen to events from the main window.
*/
async initializeReceiver() {
this.receiver = Receiver._getInstance(index._getWorkerGlobalScope());
// Refresh from persistence if we receive a KeyChanged message.
this.receiver._subscribe("keyChanged" /* _EventType.KEY_CHANGED */, async (_origin, data) => {
const keys = await this._poll();
return {
keyProcessed: keys.includes(data.key)
};
});
// Let the sender know that we are listening so they give us more timeout.
this.receiver._subscribe("ping" /* _EventType.PING */, async (_origin, _data) => {
return ["keyChanged" /* _EventType.KEY_CHANGED */];
});
}
/**
* As the main window, we should let the worker know when keys change (set and remove).
*
* @remarks
* {@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/ready | ServiceWorkerContainer.ready}
* may not resolve.
*/
async initializeSender() {
var _a, _b;
// Check to see if there's an active service worker.
this.activeServiceWorker = await index._getActiveServiceWorker();
if (!this.activeServiceWorker) {
return;
}
this.sender = new Sender(this.activeServiceWorker);
// Ping the service worker to check what events they can handle.
const results = await this.sender._send("ping" /* _EventType.PING */, {}, 800 /* _TimeoutDuration.LONG_ACK */);
if (!results) {
return;
}
if (((_a = results[0]) === null || _a === void 0 ? void 0 : _a.fulfilled) &&
((_b = results[0]) === null || _b === void 0 ? void 0 : _b.value.includes("keyChanged" /* _EventType.KEY_CHANGED */))) {
this.serviceWorkerReceiverAvailable = true;
}
}
/**
* Let the worker know about a changed key, the exact key doesn't technically matter since the
* worker will just trigger a full sync anyway.
*
* @remarks
* For now, we only support one service worker per page.
*
* @param key - Storage key which changed.
*/
async notifyServiceWorker(key) {
if (!this.sender ||
!this.activeServiceWorker ||
index._getServiceWorkerController() !== this.activeServiceWorker) {
return;
}
try {
await this.sender._send("keyChanged" /* _EventType.KEY_CHANGED */, { key },
// Use long timeout if receiver has previously responded to a ping from us.
this.serviceWorkerReceiverAvailable
? 800 /* _TimeoutDuration.LONG_ACK */
: 50 /* _TimeoutDuration.ACK */);
}
catch (_a) {
// This is a best effort approach. Ignore errors.
}
}
async _isAvailable() {
try {
if (!indexedDB) {
return false;
}
const db = await _openDatabase();
await _putObject(db, index.STORAGE_AVAILABLE_KEY, '1');
await _deleteObject(db, index.STORAGE_AVAILABLE_KEY);
return true;
}
catch (_a) { }
return false;
}
async _withPendingWrite(write) {
this.pendingWrites++;
try {
await write();
}
finally {
this.pendingWrites--;
}
}
async _set(key, value) {
return this._withPendingWrite(async () => {
await this._withRetries((db) => _putObject(db, key, value));
this.localCache[key] = value;
return this.notifyServiceWorker(key);
});
}
async _get(key) {
const obj = (await this._withRetries((db) => getObject(db, key)));
this.localCache[key] = obj;
return obj;
}
async _remove(key) {
return this._withPendingWrite(async () => {
await this._withRetries((db) => _deleteObject(db, key));
delete this.localCache[key];
return this.notifyServiceWorker(key);
});
}
async _poll() {
// TODO: check if we need to fallback if getAll is not supported
const result = await this._withRetries((db) => {
const getAllRequest = getObjectStore(db, false).getAll();
return new DBPromise(getAllRequest).toPromise();
});
if (!result) {
return [];
}
// If we have pending writes in progress abort, we'll get picked up on the next poll
if (this.pendingWrites !== 0) {
return [];
}
const keys = [];
const keysInResult = new Set();
if (result.length !== 0) {
for (const { fbase_key: key, value } of result) {
keysInResult.add(key);
if (JSON.stringify(this.localCache[key]) !== JSON.stringify(value)) {
this.notifyListeners(key, value);
keys.push(key);
}
}
}
for (const localKey of Object.keys(this.localCache)) {
if (this.localCache[localKey] && !keysInResult.has(localKey)) {
// Deleted
this.notifyListeners(localKey, null);
keys.push(localKey);
}
}
return keys;
}
notifyListeners(key, newValue) {
this.localCache[key] = newValue;
const listeners = this.listeners[key];
if (listeners) {
for (const listener of Array.from(listeners)) {
listener(newValue);
}
}
}
startPolling() {
this.stopPolling();
this.pollTimer = setInterval(async () => this._poll(), _POLLING_INTERVAL_MS);
}
stopPolling() {
if (this.pollTimer) {
clearInterval(this.pollTimer);
this.pollTimer = null;
}
}
_addListener(key, listener) {
if (Object.keys(this.listeners).length === 0) {
this.startPolling();
}
if (!this.listeners[key]) {
this.listeners[key] = new Set();
// Populate the cache to avoid spuriously triggering on first poll.
void this._get(key); // This can happen in the background async and we can return immediately.
}
this.listeners[key].add(listener);
}
_removeListener(key, listener) {
if (this.listeners[key]) {
this.listeners[key].delete(listener);
if (this.listeners[key].size === 0) {
delete this.listeners[key];
}
}
if (Object.keys(this.listeners).length === 0) {
this.stopPolling();
}
}
}
IndexedDBLocalPersistence.type = 'LOCAL';
/**
* An implementation of {@link Persistence} of type `LOCAL` using `indexedDB`
* for the underlying storage.
*
* @public
*/
const indexedDBLocalPersistence = IndexedDBLocalPersistence;
/**
* @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.
*/
/**
* Chooses a popup/redirect resolver to use. This prefers the override (which
* is directly passed in), and falls back to the property set on the auth
* object. If neither are available, this function errors w/ an argument error.
*/
function _withDefaultResolver(auth, resolverOverride) {
if (resolverOverride) {
return index._getInstance(resolverOverride);
}
index._assert(auth._popupRedirectResolver, auth, "argument-error" /* AuthErrorCode.ARGUMENT_ERROR */);
return auth._popupRedirectResolver;
}
/**
* @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.
*/
class IdpCredential extends index.AuthCredential {
constructor(params) {
super("custom" /* ProviderId.CUSTOM */, "custom" /* ProviderId.CUSTOM */);
this.params = params;
}
_getIdTokenResponse(auth) {
return index.signInWithIdp(auth, this._buildIdpRequest());
}
_linkToIdToken(auth, idToken) {
return index.signInWithIdp(auth, this._buildIdpRequest(idToken));
}
_getReauthenticationResolver(auth) {
return index.signInWithIdp(auth, this._buildIdpRequest());
}
_buildIdpRequest(idToken) {
const request = {
requestUri: this.params.requestUri,
sessionId: this.params.sessionId,
postBody: this.params.postBody,
tenantId: this.params.tenantId,
pendingToken: this.params.pendingToken,
returnSecureToken: true,
returnIdpCredential: true
};
if (idToken) {
request.idToken = idToken;
}
return request;
}
}
function _signIn(params) {
return index._signInWithCredential(params.auth, new IdpCredential(params), params.bypassAuthState);
}
function _reauth(params) {
const { auth, user } = params;
index._assert(user, auth, "internal-error" /* AuthErrorCode.INTERNAL_ERROR */);
return index._reauthenticate(user, new IdpCredential(params), params.bypassAuthState);
}
async function _link(params) {
const { auth, user } = params;
index._assert(user, auth, "internal-error" /* AuthErrorCode.INTERNAL_ERROR */);
return index._link(user, new IdpCredential(params), params.bypassAuthState);
}
/**
* @license
* Copyright 2020 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.
*/
/**
* Popup event manager. Handles the popup's entire lifecycle; listens to auth
* events
*/
class AbstractPopupRedirectOperation {
constructor(auth, filter, resolver, user, bypassAuthState = false) {
this.auth = auth;
this.resolver = resolver;
this.user = user;
this.bypassAuthState = bypassAuthState;
this.pendingPromise = null;
this.eventManager = null;
this.filter = Array.isArray(filter) ? filter : [filter];
}
execute() {
return new Promise(async (resolve, reject) => {
this.pendingPromise = { resolve, reject };
try {
this.eventManager = await this.resolver._initialize(this.auth);
await this.onExecution();
this.eventManager.registerConsumer(this);
}
catch (e) {
this.reject(e);
}
});
}
async onAuthEvent(event) {
const { urlResponse, sessionId, postBody, tenantId, error, type } = event;
if (error) {
this.reject(error);
return;
}
const params = {
auth: this.auth,
requestUri: urlResponse,
sessionId: sessionId,
tenantId: tenantId || undefined,
postBody: postBody || undefined,
user: this.user,
bypassAuthState: this.bypassAuthState
};
try {
this.resolve(await this.getIdpTask(type)(params));
}
catch (e) {
this.reject(e);
}
}
onError(error) {
this.reject(error);
}
getIdpTask(type) {
switch (type) {
case "signInViaPopup" /* AuthEventType.SIGN_IN_VIA_POPUP */:
case "signInViaRedirect" /* AuthEventType.SIGN_IN_VIA_REDIRECT */:
return _signIn;
case "linkViaPopup" /* AuthEventType.LINK_VIA_POPUP */:
case "linkViaRedirect" /* AuthEventType.LINK_VIA_REDIRECT */:
return _link;
case "reauthViaPopup" /* AuthEventType.REAUTH_VIA_POPUP */:
case "reauthViaRedirect" /* AuthEventType.REAUTH_VIA_REDIRECT */:
return _reauth;
default:
index._fail(this.auth, "internal-error" /* AuthErrorCode.INTERNAL_ERROR */);
}
}
resolve(cred) {
index.debugAssert(this.pendingPromise, 'Pending promise was never set');
this.pendingPromise.resolve(cred);
this.unregisterAndCleanUp();
}
reject(error) {
index.debugAssert(this.pendingPromise, 'Pending promise was never set');
this.pendingPromise.reject(error);
this.unregisterAndCleanUp();
}
unregisterAndCleanUp() {
if (this.eventManager) {
this.eventManager.unregisterConsumer(this);
}
this.pendingPromise = null;
this.cleanUp();
}
}
/**
* @license
* Copyright 2020 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 _POLL_WINDOW_CLOSE_TIMEOUT = new index.Delay(2000, 10000);
/**
* Authenticates a Firebase client using a popup-based OAuth authentication flow.
*
* @remarks
* If succeeds, returns the signed in user along with the provider's credential. If sign in was
* unsuccessful, returns an error object containing additional information about the error.
*
* This method does not work in a Node.js environment or with {@link Auth} instances created with a
* {@link @firebase/app#FirebaseServerApp}.
*
* @example
* ```javascript
* // Sign in using a popup.
* const provider = new FacebookAuthProvider();
* const result = await signInWithPopup(auth, provider);
*
* // The signed-in user info.
* const user = result.user;
* // This gives you a Facebook Access Token.
* const credential = provider.credentialFromResult(auth, result);
* const token = credential.accessToken;
* ```
*
* @param auth - The {@link Auth} instance.
* @param provider - The provider to authenticate. The provider has to be an {@link OAuthProvider}.
* Non-OAuth providers like {@link EmailAuthProvider} will throw an error.
* @param resolver - An instance of {@link PopupRedirectResolver}, optional
* if already supplied to {@link initializeAuth} or provided by {@link getAuth}.
*
* @public
*/
async function signInWithPopup(auth, provider, resolver) {
if (app._isFirebaseServerApp(auth.app)) {
return Promise.reject(index._createError(auth, "operation-not-supported-in-this-environment" /* AuthErrorCode.OPERATION_NOT_SUPPORTED */));
}
const authInternal = index._castAuth(auth);
index._assertInstanceOf(auth, provider, index.FederatedAuthProvider);
const resolverInternal = _withDefaultResolver(authInternal, resolver);
const action = new PopupOperation(authInternal, "signInViaPopup" /* AuthEventType.SIGN_IN_VIA_POPUP */, provider, resolverInternal);
return action.executeNotNull();
}
/**
* Reauthenticates the current user with the specified {@link OAuthProvider} using a pop-up based
* OAuth flow.
*
* @remarks
* If the reauthentication is successful, the returned result will contain the user and the
* provider's credential.
*
* This method does not work in a Node.js environment or on any {@link User} signed in by
* {@link Auth} instances created with a {@link @firebase/app#FirebaseServerApp}.
*
* @example
* ```javascript
* // Sign in using a popup.
* const provider = new FacebookAuthProvider();
* const result = await signInWithPopup(auth, provider);
* // Reauthenticate using a popup.
* await reauthenticateWithPopup(result.user, provider);
* ```
*
* @param user - The user.
* @param provider - The provider to authenticate. The provider has to be an {@link OAuthProvider}.
* Non-OAuth providers like {@link EmailAuthProvider} will throw an error.
* @param resolver - An instance of {@link PopupRedirectResolver}, optional
* if already supplied to {@link initializeAuth} or provided by {@link getAuth}.
*
* @public
*/
async function reauthenticateWithPopup(user, provider, resol