mirror of
https://github.com/musix-org/musix-oss
synced 2025-01-12 18:14:51 +00:00
886 lines
33 KiB
JavaScript
886 lines
33 KiB
JavaScript
import firebase from '@firebase/app';
|
|
import { ErrorFactory, FirebaseError } from '@firebase/util';
|
|
import { openDb } from 'idb';
|
|
|
|
const version = "0.2.6";
|
|
|
|
/**
|
|
* @license
|
|
* Copyright 2019 Google Inc.
|
|
*
|
|
* 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}`;
|
|
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 Inc.
|
|
*
|
|
* 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 values.',
|
|
["create-installation-failed" /* CREATE_INSTALLATION_FAILED */]: 'Could not register Firebase Installation.',
|
|
["generate-token-failed" /* GENERATE_TOKEN_FAILED */]: 'Could not generate Auth Token.',
|
|
["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 = 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 Inc.
|
|
*
|
|
* 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 extractAppConfig(app) {
|
|
if (!app || !app.options) {
|
|
throw ERROR_FACTORY.create("missing-app-config-values" /* MISSING_APP_CONFIG_VALUES */);
|
|
}
|
|
const appName = app.name;
|
|
const { projectId, apiKey, appId } = app.options;
|
|
if (!appName || !projectId || !apiKey || !appId) {
|
|
throw ERROR_FACTORY.create("missing-app-config-values" /* MISSING_APP_CONFIG_VALUES */);
|
|
}
|
|
return { appName, projectId, apiKey, appId };
|
|
}
|
|
|
|
/**
|
|
* @license
|
|
* Copyright 2019 Google Inc.
|
|
*
|
|
* 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.create("request-failed" /* REQUEST_FAILED */, {
|
|
requestName,
|
|
serverCode: errorData.code,
|
|
serverMessage: errorData.message,
|
|
serverStatus: errorData.status
|
|
});
|
|
}
|
|
function getHeaders({ apiKey }) {
|
|
return new Headers({
|
|
'Content-Type': 'application/json',
|
|
Accept: 'application/json',
|
|
'x-goog-api-key': apiKey
|
|
});
|
|
}
|
|
function getHeadersWithAuth(appConfig, { refreshToken }) {
|
|
const headers = getHeaders(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 Inc.
|
|
*
|
|
* 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 createInstallation(appConfig, { fid }) {
|
|
const endpoint = getInstallationsEndpoint(appConfig);
|
|
const headers = getHeaders(appConfig);
|
|
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 Inc.
|
|
*
|
|
* 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 Inc.
|
|
*
|
|
* 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 Inc.
|
|
*
|
|
* 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 Inc.
|
|
*
|
|
* 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, upgradeDB => {
|
|
// 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 (upgradeDB.oldVersion) {
|
|
case 0:
|
|
upgradeDB.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');
|
|
await tx.objectStore(OBJECT_STORE_NAME).put(value, key);
|
|
await tx.complete;
|
|
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 === oldValue) {
|
|
return newValue;
|
|
}
|
|
if (newValue === undefined) {
|
|
await store.delete(key);
|
|
}
|
|
else {
|
|
await store.put(newValue, key);
|
|
}
|
|
await tx.complete;
|
|
return newValue;
|
|
}
|
|
function getKey(appConfig) {
|
|
return `${appConfig.appName}!${appConfig.appId}`;
|
|
}
|
|
|
|
/**
|
|
* @license
|
|
* Copyright 2019 Google Inc.
|
|
*
|
|
* 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(appConfig) {
|
|
let registrationPromise;
|
|
const installationEntry = await update(appConfig, (oldEntry) => {
|
|
const installationEntry = updateOrCreateInstallationEntry(oldEntry);
|
|
const entryWithPromise = triggerRegistrationIfNecessary(appConfig, 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
|
|
};
|
|
}
|
|
function updateOrCreateInstallationEntry(oldEntry) {
|
|
const entry = oldEntry || {
|
|
fid: generateFid(),
|
|
registrationStatus: 0 /* NOT_STARTED */
|
|
};
|
|
if (hasInstallationRequestTimedOut(entry)) {
|
|
return {
|
|
fid: entry.fid,
|
|
registrationStatus: 0 /* NOT_STARTED */
|
|
};
|
|
}
|
|
return entry;
|
|
}
|
|
/**
|
|
* If the Firebase Installation is not registered yet, this will trigger the registration
|
|
* and return an InProgressInstallationEntry.
|
|
*/
|
|
function triggerRegistrationIfNecessary(appConfig, installationEntry) {
|
|
if (installationEntry.registrationStatus === 0 /* NOT_STARTED */) {
|
|
if (!navigator.onLine) {
|
|
// Registration required but app is offline.
|
|
const registrationPromiseWithError = Promise.reject(ERROR_FACTORY.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(appConfig, inProgressEntry);
|
|
return { installationEntry: inProgressEntry, registrationPromise };
|
|
}
|
|
else if (installationEntry.registrationStatus === 1 /* IN_PROGRESS */) {
|
|
return {
|
|
installationEntry,
|
|
registrationPromise: waitUntilFidRegistration(appConfig)
|
|
};
|
|
}
|
|
else {
|
|
return { installationEntry };
|
|
}
|
|
}
|
|
/** This will be executed only once for each new Firebase Installation. */
|
|
async function registerInstallation(appConfig, installationEntry) {
|
|
try {
|
|
const registeredInstallationEntry = await createInstallation(appConfig, installationEntry);
|
|
return set(appConfig, registeredInstallationEntry);
|
|
}
|
|
catch (e) {
|
|
if (isServerError(e) && e.serverCode === 409) {
|
|
// Server returned a "FID can not be used" error.
|
|
// Generate a new ID next time.
|
|
await remove(appConfig);
|
|
}
|
|
else {
|
|
// Registration failed. Set FID as not registered.
|
|
await set(appConfig, {
|
|
fid: installationEntry.fid,
|
|
registrationStatus: 0 /* NOT_STARTED */
|
|
});
|
|
}
|
|
throw e;
|
|
}
|
|
}
|
|
/** Call if FID registration is pending. */
|
|
async function waitUntilFidRegistration(appConfig) {
|
|
// 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(appConfig);
|
|
while (entry.registrationStatus === 1 /* IN_PROGRESS */) {
|
|
// createInstallation request still in progress.
|
|
await sleep(100);
|
|
entry = await updateInstallationRequest(appConfig);
|
|
}
|
|
if (entry.registrationStatus === 0 /* NOT_STARTED */) {
|
|
throw ERROR_FACTORY.create("create-installation-failed" /* CREATE_INSTALLATION_FAILED */);
|
|
}
|
|
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.create("installation-not-found" /* INSTALLATION_NOT_FOUND */);
|
|
}
|
|
if (hasInstallationRequestTimedOut(oldEntry)) {
|
|
return {
|
|
fid: oldEntry.fid,
|
|
registrationStatus: 0 /* NOT_STARTED */
|
|
};
|
|
}
|
|
return oldEntry;
|
|
});
|
|
}
|
|
function hasInstallationRequestTimedOut(installationEntry) {
|
|
return (installationEntry.registrationStatus === 1 /* IN_PROGRESS */ &&
|
|
installationEntry.registrationTime + PENDING_TIMEOUT_MS < Date.now());
|
|
}
|
|
|
|
/**
|
|
* @license
|
|
* Copyright 2019 Google Inc.
|
|
*
|
|
* 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 generateAuthToken(appConfig, installationEntry) {
|
|
const endpoint = getGenerateAuthTokenEndpoint(appConfig, installationEntry);
|
|
const headers = getHeadersWithAuth(appConfig, installationEntry);
|
|
const body = {
|
|
installation: {
|
|
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 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 Inc.
|
|
*
|
|
* 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(appConfig) {
|
|
let tokenPromise;
|
|
const entry = await update(appConfig, (oldEntry) => {
|
|
if (!isEntryRegistered(oldEntry)) {
|
|
throw ERROR_FACTORY.create("not-registered" /* NOT_REGISTERED */);
|
|
}
|
|
const oldAuthToken = oldEntry.authToken;
|
|
if (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(appConfig);
|
|
return oldEntry;
|
|
}
|
|
else {
|
|
// No token or token expired.
|
|
if (!navigator.onLine) {
|
|
throw ERROR_FACTORY.create("app-offline" /* APP_OFFLINE */);
|
|
}
|
|
const inProgressEntry = makeAuthTokenRequestInProgressEntry(oldEntry);
|
|
tokenPromise = fetchAuthTokenFromServer(appConfig, inProgressEntry);
|
|
return inProgressEntry;
|
|
}
|
|
});
|
|
const authToken = tokenPromise
|
|
? await tokenPromise
|
|
: entry.authToken;
|
|
return authToken.token;
|
|
}
|
|
/**
|
|
* Call only if FID is registered and Auth Token request is in progress.
|
|
*/
|
|
async function waitUntilAuthTokenRequest(appConfig) {
|
|
// 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(appConfig);
|
|
while (entry.authToken.requestStatus === 1 /* IN_PROGRESS */) {
|
|
// generateAuthToken still in progress.
|
|
await sleep(100);
|
|
entry = await updateAuthTokenRequest(appConfig);
|
|
}
|
|
const authToken = entry.authToken;
|
|
if (authToken.requestStatus === 0 /* NOT_STARTED */) {
|
|
throw ERROR_FACTORY.create("generate-token-failed" /* GENERATE_TOKEN_FAILED */);
|
|
}
|
|
else {
|
|
return authToken;
|
|
}
|
|
}
|
|
/**
|
|
* Called only if there is a GenerateAuthToken request in progress.
|
|
*
|
|
* Updates the InstallationEntry in the DB based on the status of the
|
|
* GenerateAuthToken request.
|
|
*
|
|
* Returns the updated InstallationEntry.
|
|
*/
|
|
function updateAuthTokenRequest(appConfig) {
|
|
return update(appConfig, (oldEntry) => {
|
|
if (!isEntryRegistered(oldEntry)) {
|
|
throw ERROR_FACTORY.create("not-registered" /* NOT_REGISTERED */);
|
|
}
|
|
const oldAuthToken = oldEntry.authToken;
|
|
if (hasAuthTokenRequestTimedOut(oldAuthToken)) {
|
|
return Object.assign({}, oldEntry, { authToken: { requestStatus: 0 /* NOT_STARTED */ } });
|
|
}
|
|
return oldEntry;
|
|
});
|
|
}
|
|
async function fetchAuthTokenFromServer(appConfig, installationEntry) {
|
|
try {
|
|
const authToken = await generateAuthToken(appConfig, installationEntry);
|
|
const updatedInstallationEntry = Object.assign({}, installationEntry, { authToken });
|
|
await set(appConfig, updatedInstallationEntry);
|
|
return authToken;
|
|
}
|
|
catch (e) {
|
|
if (isServerError(e) && (e.serverCode === 401 || e.serverCode === 404)) {
|
|
// Server returned a "FID not found" or a "Invalid authentication" error.
|
|
// Generate a new ID next time.
|
|
await remove(appConfig);
|
|
}
|
|
else {
|
|
const updatedInstallationEntry = Object.assign({}, installationEntry, { authToken: { requestStatus: 0 /* NOT_STARTED */ } });
|
|
await set(appConfig, updatedInstallationEntry);
|
|
}
|
|
throw e;
|
|
}
|
|
}
|
|
function isEntryRegistered(installationEntry) {
|
|
return (installationEntry !== undefined &&
|
|
installationEntry.registrationStatus === 2 /* COMPLETED */);
|
|
}
|
|
function isAuthTokenValid(authToken) {
|
|
return (authToken.requestStatus === 2 /* COMPLETED */ &&
|
|
!isAuthTokenExpired(authToken));
|
|
}
|
|
function isAuthTokenExpired(authToken) {
|
|
const now = Date.now();
|
|
return (now < authToken.creationTime ||
|
|
authToken.creationTime + authToken.expiresIn < now + TOKEN_EXPIRATION_BUFFER);
|
|
}
|
|
/** Returns an updated InstallationEntry with an InProgressAuthToken. */
|
|
function makeAuthTokenRequestInProgressEntry(oldEntry) {
|
|
const inProgressAuthToken = {
|
|
requestStatus: 1 /* IN_PROGRESS */,
|
|
requestTime: Date.now()
|
|
};
|
|
return Object.assign({}, oldEntry, { authToken: inProgressAuthToken });
|
|
}
|
|
function hasAuthTokenRequestTimedOut(authToken) {
|
|
return (authToken.requestStatus === 1 /* IN_PROGRESS */ &&
|
|
authToken.requestTime + PENDING_TIMEOUT_MS < Date.now());
|
|
}
|
|
|
|
/**
|
|
* @license
|
|
* Copyright 2019 Google Inc.
|
|
*
|
|
* 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 getId(app) {
|
|
const appConfig = extractAppConfig(app);
|
|
const { installationEntry, registrationPromise } = await getInstallationEntry(appConfig);
|
|
if (registrationPromise) {
|
|
// Suppress registration errors as they are not a problem for getId.
|
|
registrationPromise.catch(() => { });
|
|
}
|
|
if (installationEntry.registrationStatus === 2 /* COMPLETED */) {
|
|
// If the installation is already registered, update the authentication
|
|
// token if needed. Suppress errors as they are not relevant to getId.
|
|
refreshAuthToken(appConfig).catch(() => { });
|
|
}
|
|
return installationEntry.fid;
|
|
}
|
|
|
|
/**
|
|
* @license
|
|
* Copyright 2019 Google Inc.
|
|
*
|
|
* 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 getToken(app) {
|
|
const appConfig = extractAppConfig(app);
|
|
await completeInstallationRegistration(appConfig);
|
|
// At this point we either have a Registered Installation in the DB, or we've
|
|
// already thrown an error.
|
|
return refreshAuthToken(appConfig);
|
|
}
|
|
async function completeInstallationRegistration(appConfig) {
|
|
const { installationEntry, registrationPromise } = await getInstallationEntry(appConfig);
|
|
if (registrationPromise) {
|
|
// A createInstallation request is in progress. Wait until it finishes.
|
|
await registrationPromise;
|
|
}
|
|
else if (installationEntry.registrationStatus !== 2 /* COMPLETED */) {
|
|
// Installation ID can't be registered.
|
|
throw ERROR_FACTORY.create("create-installation-failed" /* CREATE_INSTALLATION_FAILED */);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @license
|
|
* Copyright 2019 Google Inc.
|
|
*
|
|
* 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 deleteInstallation(appConfig, installationEntry) {
|
|
const endpoint = getDeleteEndpoint(appConfig, installationEntry);
|
|
const headers = getHeadersWithAuth(appConfig, installationEntry);
|
|
const request = {
|
|
method: 'DELETE',
|
|
headers
|
|
};
|
|
const response = await retryIfServerError(() => fetch(endpoint, request));
|
|
if (!response.ok) {
|
|
throw await getErrorFromResponse('Delete Installation', response);
|
|
}
|
|
}
|
|
function getDeleteEndpoint(appConfig, { fid }) {
|
|
return `${getInstallationsEndpoint(appConfig)}/${fid}`;
|
|
}
|
|
|
|
/**
|
|
* @license
|
|
* Copyright 2019 Google Inc.
|
|
*
|
|
* 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 deleteInstallation$1(app) {
|
|
const appConfig = extractAppConfig(app);
|
|
const entry = await update(appConfig, (oldEntry) => {
|
|
if (oldEntry && oldEntry.registrationStatus === 0 /* NOT_STARTED */) {
|
|
// Delete the unregistered entry without sending a deleteInstallation request.
|
|
return undefined;
|
|
}
|
|
return oldEntry;
|
|
});
|
|
if (entry) {
|
|
if (entry.registrationStatus === 1 /* IN_PROGRESS */) {
|
|
// Can't delete while trying to register.
|
|
throw ERROR_FACTORY.create("delete-pending-registration" /* DELETE_PENDING_REGISTRATION */);
|
|
}
|
|
else if (entry.registrationStatus === 2 /* COMPLETED */) {
|
|
if (!navigator.onLine) {
|
|
throw ERROR_FACTORY.create("app-offline" /* APP_OFFLINE */);
|
|
}
|
|
else {
|
|
await deleteInstallation(appConfig, entry);
|
|
await remove(appConfig);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @license
|
|
* Copyright 2019 Google Inc.
|
|
*
|
|
* 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 registerInstallations(instance) {
|
|
const installationsName = 'installations';
|
|
const factoryMethod = app => {
|
|
// Throws if app isn't configured properly.
|
|
extractAppConfig(app);
|
|
return {
|
|
app,
|
|
getId: () => getId(app),
|
|
getToken: () => getToken(app),
|
|
delete: () => deleteInstallation$1(app)
|
|
};
|
|
};
|
|
instance.INTERNAL.registerService(installationsName, factoryMethod);
|
|
}
|
|
registerInstallations(firebase);
|
|
|
|
export { registerInstallations };
|
|
//# sourceMappingURL=index.esm2017.js.map
|