diff --git a/.changeset/nine-clouds-hammer.md b/.changeset/nine-clouds-hammer.md new file mode 100644 index 00000000000..be025e0c5a4 --- /dev/null +++ b/.changeset/nine-clouds-hammer.md @@ -0,0 +1,8 @@ +--- +'@firebase/installations': minor +'@firebase/app': minor +'firebase': minor +'@firebase/remote-config': patch +--- + +Add `installationsAuthToken` as an optional FirebaseServerApp variable. If present, then Installations `getId` and `getToken` will use the provided value instead of initializing the Installations SDK to retrieve those values dynamically. This should unlock SDKs that require these Installations values in a server environment where the Installations SDK isn't supported. diff --git a/common/api-review/app.api.md b/common/api-review/app.api.md index bdfb2a681f1..06b026e34ad 100644 --- a/common/api-review/app.api.md +++ b/common/api-review/app.api.md @@ -73,6 +73,7 @@ export interface FirebaseOptions { // @public export interface FirebaseServerApp extends FirebaseApp { + readonly installationsId: string | null; name: string; readonly settings: FirebaseServerAppSettings; } @@ -80,6 +81,7 @@ export interface FirebaseServerApp extends FirebaseApp { // @public export interface FirebaseServerAppSettings extends Omit { authIdToken?: string; + installationsAuthToken?: string; releaseOnDeref?: object; } diff --git a/docs-devsite/app.firebaseserverapp.md b/docs-devsite/app.firebaseserverapp.md index 66b51c45fb2..7a9f4522649 100644 --- a/docs-devsite/app.firebaseserverapp.md +++ b/docs-devsite/app.firebaseserverapp.md @@ -25,9 +25,20 @@ export interface FirebaseServerApp extends FirebaseApp | Property | Type | Description | | --- | --- | --- | +| [installationsId](./app.firebaseserverapp.md#firebaseserverappinstallationsid) | string \| null | The parsed Firebase Installations Id token if a installationsAuthToken was provided to [initializeServerApp()](./app.md#initializeserverapp_30ab697). Null otherwise. | | [name](./app.firebaseserverapp.md#firebaseserverappname) | string | There is no getApp() operation for FirebaseServerApp, so the name is not relevant for applications. However, it may be used internally, and is declared here so that FirebaseServerApp conforms to the FirebaseApp interface. | | [settings](./app.firebaseserverapp.md#firebaseserverappsettings) | [FirebaseServerAppSettings](./app.firebaseserverappsettings.md#firebaseserverappsettings_interface) | The (read-only) configuration settings for this server app. These are the original parameters given in [initializeServerApp()](./app.md#initializeserverapp_30ab697). | +## FirebaseServerApp.installationsId + +The parsed Firebase Installations Id token if a `installationsAuthToken` was provided to [initializeServerApp()](./app.md#initializeserverapp_30ab697). Null otherwise. + +Signature: + +```typescript +readonly installationsId: string | null; +``` + ## FirebaseServerApp.name There is no `getApp()` operation for `FirebaseServerApp`, so the name is not relevant for applications. However, it may be used internally, and is declared here so that `FirebaseServerApp` conforms to the `FirebaseApp` interface. diff --git a/docs-devsite/app.firebaseserverappsettings.md b/docs-devsite/app.firebaseserverappsettings.md index bc46c5292d0..4fd27c75d7c 100644 --- a/docs-devsite/app.firebaseserverappsettings.md +++ b/docs-devsite/app.firebaseserverappsettings.md @@ -24,6 +24,7 @@ export interface FirebaseServerAppSettings extends OmitInvoking getAuth with a FirebaseServerApp configured with a validated authIdToken causes an automatic attempt to sign in the user that the authIdToken represents. The token needs to have been recently minted for this operation to succeed.If the token fails local verification, or if the Auth service has failed to validate it when the Auth SDK is initialized, then a warning is logged to the console and the Auth SDK will not sign in a user on initialization.If a user is successfully signed in, then the Auth instance's onAuthStateChanged callback is invoked with the User object as per standard Auth flows. However, User objects created via an authIdToken do not have a refresh token. Attempted refreshToken operations fail. | +| [installationsAuthToken](./app.firebaseserverappsettings.md#firebaseserverappsettingsinstallationsauthtoken) | string | An optional Installations Auth token which allows the use of Remote Config SDK in SSR enviornments.If provided, the FirebaseServerApp will attempt to parse the Installations id from the token.If the token is deemed to be malformed then an error will be thrown during the invocation of initializeServerApp.If the the Installations Id and the provided installationsAuthToken are successfully parsed, then they will be used by the Installations implementation when getToken and getId are invoked.Attempting to use Remote Config without providing an installationsAuthToken here will cause Installations to throw errors when Remote Config attempts to query the Installations id and authToken. | | [releaseOnDeref](./app.firebaseserverappsettings.md#firebaseserverappsettingsreleaseonderef) | object | An optional object. If provided, the Firebase SDK uses a FinalizationRegistry object to monitor the garbage collection status of the provided object. The Firebase SDK releases its reference on the FirebaseServerApp instance when the provided releaseOnDeref object is garbage collected.You can use this field to reduce memory management overhead for your application. If provided, an app running in a SSR pass does not need to perform FirebaseServerApp cleanup, so long as the reference object is deleted (by falling out of SSR scope, for instance.)If an object is not provided then the application must clean up the FirebaseServerApp instance by invoking deleteApp.If the application provides an object in this parameter, but the application is executed in a JavaScript engine that predates the support of FinalizationRegistry (introduced in node v14.6.0, for instance), then an error is thrown at FirebaseServerApp initialization. | ## FirebaseServerAppSettings.authIdToken @@ -42,6 +43,24 @@ If a user is successfully signed in, then the Auth instance's `onAuthStateChange authIdToken?: string; ``` +## FirebaseServerAppSettings.installationsAuthToken + +An optional Installations Auth token which allows the use of Remote Config SDK in SSR enviornments. + +If provided, the `FirebaseServerApp` will attempt to parse the Installations id from the token. + +If the token is deemed to be malformed then an error will be thrown during the invocation of `initializeServerApp`. + +If the the Installations Id and the provided `installationsAuthToken` are successfully parsed, then they will be used by the Installations implementation when `getToken` and `getId` are invoked. + +Attempting to use Remote Config without providing an `installationsAuthToken` here will cause Installations to throw errors when Remote Config attempts to query the Installations id and authToken. + +Signature: + +```typescript +installationsAuthToken?: string; +``` + ## FirebaseServerAppSettings.releaseOnDeref An optional object. If provided, the Firebase SDK uses a `FinalizationRegistry` object to monitor the garbage collection status of the provided object. The Firebase SDK releases its reference on the `FirebaseServerApp` instance when the provided `releaseOnDeref` object is garbage collected. diff --git a/packages/app/src/errors.ts b/packages/app/src/errors.ts index 0149ef3dcb1..d3fb2add33d 100644 --- a/packages/app/src/errors.ts +++ b/packages/app/src/errors.ts @@ -31,7 +31,8 @@ export const enum AppError { IDB_WRITE = 'idb-set', IDB_DELETE = 'idb-delete', FINALIZATION_REGISTRY_NOT_SUPPORTED = 'finalization-registry-not-supported', - INVALID_SERVER_APP_ENVIRONMENT = 'invalid-server-app-environment' + INVALID_SERVER_APP_ENVIRONMENT = 'invalid-server-app-environment', + INVALID_SERVER_APP_INSTALLATIONS_AUTH_TOKEN = 'invalid-server-installations-auth-token' } const ERRORS: ErrorMap = { @@ -61,7 +62,9 @@ const ERRORS: ErrorMap = { [AppError.FINALIZATION_REGISTRY_NOT_SUPPORTED]: 'FirebaseServerApp deleteOnDeref field defined but the JS runtime does not support FinalizationRegistry.', [AppError.INVALID_SERVER_APP_ENVIRONMENT]: - 'FirebaseServerApp is not for use in browser environments.' + 'FirebaseServerApp is not for use in browser environments.', + [AppError.INVALID_SERVER_APP_INSTALLATIONS_AUTH_TOKEN]: + 'FirebaseServerApp could not initialize due to an invalid Installations auth token' }; interface ErrorParams { diff --git a/packages/app/src/firebaseServerApp.test.ts b/packages/app/src/firebaseServerApp.test.ts index bf2da5c06d5..689d76cfae0 100644 --- a/packages/app/src/firebaseServerApp.test.ts +++ b/packages/app/src/firebaseServerApp.test.ts @@ -19,7 +19,19 @@ import { expect } from 'chai'; import '../test/setup'; import { ComponentContainer } from '@firebase/component'; import { FirebaseServerAppImpl } from './firebaseServerApp'; -import { FirebaseServerAppSettings } from './public-types'; +import { FirebaseApp, FirebaseServerAppSettings } from './public-types'; + +const VALID_INSTATLLATIONS_AUTH_TOKEN_SECONDPART: string = + 'foo.eyJhcHBJZCI6IjE6MDAwMDAwMDAwMDAwOndlYjowMDAwMDAwMDAwMDAwMDAwMDAwMDAwIiwiZXhwIjoiMDAwMDAwMD' + + 'AwMCIsImZpZCI6IjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAiLCJwcm9qZWN0TnVtYmVyIjoiMDAwMDAwMDAwMDAwIn0.foo'; + +const INVALID_INSTATLLATIONS_AUTH_TOKEN: string = + 'foo.eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9eyJhcHBJZCI6IjE6MDAwMDAwMDAwMDAwOndlYjowMDAwMDAwMDAwMD' + + 'AwMDAwMDAwMDAwIiwiZXhwIjowMDAwMDAwMDAwLCJwcm9qZWN0TnVtYmVyIjowMDAwMDAwMDAwMDB9.foo'; + +const INVALID_INSTATLLATIONS_AUTH_TOKEN_ONE_PART: string = + 'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9eyJhcHBJZCI6IjE6MDAwMDAwMDAwMDAwOndlYjowMDAwMDAwMDAwMD' + + 'AwMDAwMDAwMDAwIiwiZXhwIjowMDAwMDAwMDAwLCJwcm9qZWN0TnVtYmVyIjowMDAwMDAwMDAwMDB9'; describe('FirebaseServerApp', () => { it('has various accessors', () => { @@ -155,4 +167,74 @@ describe('FirebaseServerApp', () => { expect(JSON.stringify(app)).to.eql(undefined); }); + + it('parses valid installationsAuthToken', () => { + const options = { + apiKey: 'APIKEY' + }; + + const serverAppSettings: FirebaseServerAppSettings = { + automaticDataCollectionEnabled: false, + installationsAuthToken: VALID_INSTATLLATIONS_AUTH_TOKEN_SECONDPART + }; + + let app: FirebaseApp | null = null; + try { + app = new FirebaseServerAppImpl( + options, + serverAppSettings, + 'testName', + new ComponentContainer('test') + ); + } catch (e) {} + expect(app).to.not.be.null; + }); + + it('invalid installationsAuthToken throws', () => { + const options = { + apiKey: 'APIKEY' + }; + + const serverAppSettings: FirebaseServerAppSettings = { + automaticDataCollectionEnabled: false, + installationsAuthToken: INVALID_INSTATLLATIONS_AUTH_TOKEN + }; + + let failed = false; + try { + new FirebaseServerAppImpl( + options, + serverAppSettings, + 'testName', + new ComponentContainer('test') + ); + } catch (e) { + failed = true; + } + expect(failed).to.be.true; + }); + + it('invalid single part installationsAuthToken throws', () => { + const options = { + apiKey: 'APIKEY' + }; + + const serverAppSettings: FirebaseServerAppSettings = { + automaticDataCollectionEnabled: false, + installationsAuthToken: INVALID_INSTATLLATIONS_AUTH_TOKEN_ONE_PART + }; + + let failed = false; + try { + new FirebaseServerAppImpl( + options, + serverAppSettings, + 'testName', + new ComponentContainer('test') + ); + } catch (e) { + failed = true; + } + expect(failed).to.be.true; + }); }); diff --git a/packages/app/src/firebaseServerApp.ts b/packages/app/src/firebaseServerApp.ts index 0c41d4cd607..bfa6a99d16c 100644 --- a/packages/app/src/firebaseServerApp.ts +++ b/packages/app/src/firebaseServerApp.ts @@ -26,6 +26,7 @@ import { ComponentContainer } from '@firebase/component'; import { FirebaseAppImpl } from './firebaseApp'; import { ERROR_FACTORY, AppError } from './errors'; import { name as packageName, version } from '../package.json'; +import { base64Decode } from '@firebase/util'; export class FirebaseServerAppImpl extends FirebaseAppImpl @@ -34,6 +35,7 @@ export class FirebaseServerAppImpl private readonly _serverConfig: FirebaseServerAppSettings; private _finalizationRegistry: FinalizationRegistry | null; private _refCount: number; + private _installationsId: string | null; constructor( options: FirebaseOptions | FirebaseAppImpl, @@ -67,6 +69,26 @@ export class FirebaseServerAppImpl ...serverConfig }; + // Parse the installationAuthToken if provided. + // TODO: kick off the token verification process. + this._installationsId = null; + if (this._serverConfig.installationsAuthToken !== undefined) { + try { + const decodedToken = base64Decode( + this._serverConfig.installationsAuthToken.split('.')[1] + ); + const tokenJSON = JSON.parse(decodedToken ? decodedToken : ''); + this._installationsId = tokenJSON.fid; + } catch (e) { + console.warn(e); + } + if (this._installationsId === null) { + throw ERROR_FACTORY.create( + AppError.INVALID_SERVER_APP_INSTALLATIONS_AUTH_TOKEN + ); + } + } + this._finalizationRegistry = null; if (typeof FinalizationRegistry !== 'undefined') { this._finalizationRegistry = new FinalizationRegistry(() => { @@ -125,6 +147,11 @@ export class FirebaseServerAppImpl return this._serverConfig; } + get installationsId(): string | null { + this.checkDestroyed(); + return this._installationsId; + } + /** * This function will throw an Error if the App has already been deleted - * use before performing API actions on the App. diff --git a/packages/app/src/public-types.ts b/packages/app/src/public-types.ts index ff25de93a46..c5423dd144f 100644 --- a/packages/app/src/public-types.ts +++ b/packages/app/src/public-types.ts @@ -100,6 +100,12 @@ export interface FirebaseServerApp extends FirebaseApp { * ``` */ readonly settings: FirebaseServerAppSettings; + + /** + * The parsed Firebase Installations Id token if a `installationsAuthToken` was provided to + * {@link (initializeServerApp:1) | initializeServerApp()}. Null otherwise. + */ + readonly installationsId: string | null; } /** @@ -196,6 +202,26 @@ export interface FirebaseServerAppSettings */ authIdToken?: string; + /** + * An optional Installations Auth token which allows the use of Remote Config SDK in + * SSR enviornments. + * + * If provided, the `FirebaseServerApp` will attempt to parse the Installations id + * from the token. + * + * If the token is deemed to be malformed then an error will be + * thrown during the invocation of `initializeServerApp`. + * + * If the the Installations Id and the provided `installationsAuthToken` are successfully parsed, + * then they will be used by the Installations implementation when `getToken` and `getId` are + * invoked. + * + * Attempting to use Remote Config without providing an `installationsAuthToken` here will cause + * Installations to throw errors when Remote Config attempts to query the Installations id and + * authToken. + */ + installationsAuthToken?: string; + /** * An optional object. If provided, the Firebase SDK uses a `FinalizationRegistry` * object to monitor the garbage collection status of the provided object. The diff --git a/packages/installations/karma.conf.js b/packages/installations/karma.conf.js index 1699a0681ec..71ad8b9362f 100644 --- a/packages/installations/karma.conf.js +++ b/packages/installations/karma.conf.js @@ -24,6 +24,7 @@ module.exports = function (config) { ...karmaBase, // files to load into karma files, + exclude: ['src/**/*-server-app.test.ts'], // frameworks to use // available frameworks: https://npmjs.org/browse/keyword/karma-adapter frameworks: ['mocha'] diff --git a/packages/installations/package.json b/packages/installations/package.json index d6240ba4666..bfb03b9028a 100644 --- a/packages/installations/package.json +++ b/packages/installations/package.json @@ -25,9 +25,11 @@ "build:deps": "lerna run --scope @firebase/installations --include-dependencies build", "build:release": "yarn build && yarn typings:public", "dev": "rollup -c -w", - "test": "yarn type-check && yarn test:karma && yarn lint", - "test:ci": "node ../../scripts/run_tests_in_ci.js", - "test:karma": "karma start", + "test": "yarn type-check && yarn test:all && yarn lint", + "test:ci": "node ../../scripts/run_tests_in_ci.js -s test:all", + "test:all": "run-p --npm-path npm test:browser test:node", + "test:browser" : "karma start", + "test:node": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' nyc --reporter lcovonly -- mocha src/**/*server-app.test.ts --config ../../config/mocharc.node.js", "test:debug": "karma start --browsers=Chrome --auto-watch", "trusted-type-check": "tsec -p tsconfig.json --noEmit", "type-check": "tsc -p . --noEmit", diff --git a/packages/installations/src/api/get-id-server-app.test.ts b/packages/installations/src/api/get-id-server-app.test.ts new file mode 100644 index 00000000000..b29b018eff6 --- /dev/null +++ b/packages/installations/src/api/get-id-server-app.test.ts @@ -0,0 +1,46 @@ +/** + * @license + * Copyright 2024 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. + */ + +import { expect } from 'chai'; +import { getId } from './get-id'; +import { + FAKE_INSTALLATIONS_ID, + getFakeInstallations, + getFakeServerApp +} from '../testing/fake-generators'; + +describe('getId-serverapp', () => { + it('getId with firebaseServerApp with authIdToken returns valid id', async () => { + const installationsAuthToken = 'fakeToken'; + const serverApp = getFakeServerApp(installationsAuthToken); + const installations = getFakeInstallations(serverApp); + const fid = await getId(installations); + expect(fid).to.equal(FAKE_INSTALLATIONS_ID); + }); + it('getId with firebaseServerApp without authIdToken throws', async () => { + const serverApp = getFakeServerApp(); + const installations = getFakeInstallations(serverApp); + let fails = false; + try { + await getId(installations); + } catch (e) { + console.error(e); + fails = true; + } + expect(fails).to.be.true; + }); +}); diff --git a/packages/installations/src/api/get-id.ts b/packages/installations/src/api/get-id.ts index 589e8b49550..7ccfe618749 100644 --- a/packages/installations/src/api/get-id.ts +++ b/packages/installations/src/api/get-id.ts @@ -19,6 +19,8 @@ import { getInstallationEntry } from '../helpers/get-installation-entry'; import { refreshAuthToken } from '../helpers/refresh-auth-token'; import { FirebaseInstallationsImpl } from '../interfaces/installation-impl'; import { Installations } from '../interfaces/public-types'; +import { _isFirebaseServerApp } from '@firebase/app'; +import { ERROR_FACTORY, ErrorCode } from '../util/errors'; /** * Creates a Firebase Installation if there isn't one for the app and @@ -28,6 +30,15 @@ import { Installations } from '../interfaces/public-types'; * @public */ export async function getId(installations: Installations): Promise { + if (_isFirebaseServerApp(installations.app)) { + if (!installations.app.installationsId) { + throw ERROR_FACTORY.create( + ErrorCode.SERVER_APP_MISSING_INSTALLATIONS_AUTH_TOKEN + ); + } + return installations.app.installationsId; + } + const installationsImpl = installations as FirebaseInstallationsImpl; const { installationEntry, registrationPromise } = await getInstallationEntry( installationsImpl diff --git a/packages/installations/src/api/get-token-server-app.test.ts b/packages/installations/src/api/get-token-server-app.test.ts new file mode 100644 index 00000000000..d4d8b3ffa35 --- /dev/null +++ b/packages/installations/src/api/get-token-server-app.test.ts @@ -0,0 +1,47 @@ +/** + * @license + * Copyright 2024 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. + */ + +import { expect, use } from 'chai'; +import { getToken } from './get-token'; +import { + getFakeInstallations, + getFakeServerApp +} from '../testing/fake-generators'; +import chaiAsPromised from 'chai-as-promised'; + +use(chaiAsPromised); + +describe('getToken-serverapp', () => { + it('getToken with firebaseServerApp with authIdToken returns valid token', async () => { + const installationsAuthToken = 'fakeToken'; + const serverApp = getFakeServerApp(installationsAuthToken); + const installations = getFakeInstallations(serverApp); + const token = await getToken(installations); + expect(token).to.equal(installationsAuthToken); + }); + it('getToken with firebaseServerApp without authIdToken throws', async () => { + const serverApp = getFakeServerApp(); + const installations = getFakeInstallations(serverApp); + let fails = false; + try { + await getToken(installations); + } catch (e) { + fails = true; + } + expect(fails).to.be.true; + }); +}); diff --git a/packages/installations/src/api/get-token.ts b/packages/installations/src/api/get-token.ts index 10e009e4a3a..b470ce1d863 100644 --- a/packages/installations/src/api/get-token.ts +++ b/packages/installations/src/api/get-token.ts @@ -19,6 +19,8 @@ import { getInstallationEntry } from '../helpers/get-installation-entry'; import { refreshAuthToken } from '../helpers/refresh-auth-token'; import { FirebaseInstallationsImpl } from '../interfaces/installation-impl'; import { Installations } from '../interfaces/public-types'; +import { _isFirebaseServerApp } from '@firebase/app'; +import { ERROR_FACTORY, ErrorCode } from '../util/errors'; /** * Returns a Firebase Installations auth token, identifying the current @@ -32,6 +34,14 @@ export async function getToken( installations: Installations, forceRefresh = false ): Promise { + if (_isFirebaseServerApp(installations.app)) { + if (!installations.app.settings.installationsAuthToken) { + throw ERROR_FACTORY.create( + ErrorCode.SERVER_APP_MISSING_INSTALLATIONS_AUTH_TOKEN + ); + } + return installations.app.settings.installationsAuthToken; + } const installationsImpl = installations as FirebaseInstallationsImpl; await completeInstallationRegistration(installationsImpl); diff --git a/packages/installations/src/testing/fake-generators.ts b/packages/installations/src/testing/fake-generators.ts index 6309228d72b..a29f0b12eb6 100644 --- a/packages/installations/src/testing/fake-generators.ts +++ b/packages/installations/src/testing/fake-generators.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { FirebaseApp } from '@firebase/app'; +import { FirebaseApp, FirebaseServerApp } from '@firebase/app'; import { Component, ComponentContainer, @@ -27,6 +27,8 @@ import { AppConfig } from '../interfaces/installation-impl'; +export const FAKE_INSTALLATIONS_ID = 'abc123'; + export function getFakeApp(): FirebaseApp { return { name: 'appName', @@ -43,13 +45,29 @@ export function getFakeApp(): FirebaseApp { }; } +export function getFakeServerApp( + installationsAuthToken: string | null = null +): FirebaseServerApp { + const app = getFakeApp() as any; + app.settings = { + automaticDataCollectionEnabled: true + }; + if (installationsAuthToken !== null) { + app.settings.installationsAuthToken = installationsAuthToken; + app.installationsId = FAKE_INSTALLATIONS_ID; + } + return app; +} + export function getFakeAppConfig( customValues: Partial = {} ): AppConfig { return { ...extractAppConfig(getFakeApp()), ...customValues }; } -export function getFakeInstallations(): FirebaseInstallationsImpl { +export function getFakeInstallations( + app?: FirebaseApp +): FirebaseInstallationsImpl { const container = new ComponentContainer('test'); container.addComponent( new Component( @@ -61,9 +79,9 @@ export function getFakeInstallations(): FirebaseInstallationsImpl { ComponentType.PRIVATE ) ); - + const configuredApp: FirebaseApp = app ? app : getFakeApp(); return { - app: getFakeApp(), + app: configuredApp, appConfig: getFakeAppConfig(), heartbeatServiceProvider: container.getProvider('heartbeat'), _delete: () => { diff --git a/packages/installations/src/util/errors.ts b/packages/installations/src/util/errors.ts index 1a82753c3d2..a09b7f22ee8 100644 --- a/packages/installations/src/util/errors.ts +++ b/packages/installations/src/util/errors.ts @@ -24,7 +24,8 @@ export const enum ErrorCode { INSTALLATION_NOT_FOUND = 'installation-not-found', REQUEST_FAILED = 'request-failed', APP_OFFLINE = 'app-offline', - DELETE_PENDING_REGISTRATION = 'delete-pending-registration' + DELETE_PENDING_REGISTRATION = 'delete-pending-registration', + SERVER_APP_MISSING_INSTALLATIONS_AUTH_TOKEN = 'server-app-missing-installations-auth-token' } const ERROR_DESCRIPTION_MAP: { readonly [key in ErrorCode]: string } = { @@ -36,7 +37,9 @@ const ERROR_DESCRIPTION_MAP: { readonly [key in ErrorCode]: string } = { '{$requestName} request failed with error "{$serverCode} {$serverStatus}: {$serverMessage}"', [ErrorCode.APP_OFFLINE]: 'Could not process request. Application offline.', [ErrorCode.DELETE_PENDING_REGISTRATION]: - "Can't delete installation while there is a pending registration request." + "Can't delete installation while there is a pending registration request.", + [ErrorCode.SERVER_APP_MISSING_INSTALLATIONS_AUTH_TOKEN]: + 'The instance of FirebaseServerApp was not initialized with a valid installationsAuthToken' }; interface ErrorParams { diff --git a/packages/remote-config/src/client/rest_client.ts b/packages/remote-config/src/client/rest_client.ts index 87fdae3c3d6..773824e916c 100644 --- a/packages/remote-config/src/client/rest_client.ts +++ b/packages/remote-config/src/client/rest_client.ts @@ -24,6 +24,7 @@ import { import { ERROR_FACTORY, ErrorCode } from '../errors'; import { getUserLanguage } from '../language'; import { _FirebaseInstallationsInternal } from '@firebase/installations'; +import { isBrowser } from '@firebase/util'; /** * Defines request body parameters required to call the fetch API: @@ -65,8 +66,13 @@ export class RestClient implements RemoteConfigFetchClient { * @throws a {@link ErrorCode.FETCH_PARSE} error if {@link Response#json} can't parse the * fetch response. * @throws a {@link ErrorCode.FETCH_STATUS} error if the service returns an HTTP error status. + * @throws a {@link ErrorCode.REQUIRES_BROWSER_ENVIRONMENT} error if the invoked in a non browser + * environment. */ async fetch(request: FetchRequest): Promise { + if (!isBrowser()) { + throw ERROR_FACTORY.create(ErrorCode.REQUIRES_BROWSER_ENVIRONMENT); + } const [installationId, installationToken] = await Promise.all([ this.firebaseInstallations.getId(), this.firebaseInstallations.getToken() diff --git a/packages/remote-config/src/errors.ts b/packages/remote-config/src/errors.ts index eac9a25657b..21f035dc823 100644 --- a/packages/remote-config/src/errors.ts +++ b/packages/remote-config/src/errors.ts @@ -31,7 +31,8 @@ export const enum ErrorCode { FETCH_THROTTLE = 'fetch-throttle', FETCH_PARSE = 'fetch-client-parse', FETCH_STATUS = 'fetch-status', - INDEXED_DB_UNAVAILABLE = 'indexed-db-unavailable' + INDEXED_DB_UNAVAILABLE = 'indexed-db-unavailable', + REQUIRES_BROWSER_ENVIRONMENT = 'requires-browser-environment' } const ERROR_DESCRIPTION_MAP: { readonly [key in ErrorCode]: string } = { @@ -67,7 +68,9 @@ const ERROR_DESCRIPTION_MAP: { readonly [key in ErrorCode]: string } = { [ErrorCode.FETCH_STATUS]: 'Fetch server returned an HTTP error status. HTTP status: {$httpStatus}.', [ErrorCode.INDEXED_DB_UNAVAILABLE]: - 'Indexed DB is not supported by current browser' + 'Indexed DB is not supported by current browser', + [ErrorCode.REQUIRES_BROWSER_ENVIRONMENT]: + 'The requested operation must be executed in a browser environment' }; // Note this is effectively a type system binding a code to params. This approach overlaps with the