diff --git a/packages/app-check/src/api.test.ts b/packages/app-check/src/api.test.ts index 3e435f5b0cb..8f30d4b7b91 100644 --- a/packages/app-check/src/api.test.ts +++ b/packages/app-check/src/api.test.ts @@ -38,10 +38,13 @@ import * as logger from './logger'; import * as client from './client'; import * as storage from './storage'; import * as internalApi from './internal-api'; +import * as indexeddb from './indexeddb'; +import * as debug from './debug'; import { deleteApp, FirebaseApp } from '@firebase/app'; import { CustomProvider, ReCaptchaV3Provider } from './providers'; import { AppCheckService } from './factory'; import { AppCheckToken } from './public-types'; +import { getDebugToken } from './debug'; describe('api', () => { let app: FirebaseApp; @@ -118,6 +121,49 @@ describe('api', () => { }) ).to.equal(appCheckInstance); }); + it('starts debug mode on first call', async () => { + let token: string = ''; + const fakeWrite = (tokenToWrite: string): Promise => { + token = tokenToWrite; + return Promise.resolve(); + }; + stub(indexeddb, 'writeDebugTokenToIndexedDB').callsFake(fakeWrite); + stub(indexeddb, 'readDebugTokenFromIndexedDB').resolves(token); + const consoleStub = stub(console, 'log'); + self.FIREBASE_APPCHECK_DEBUG_TOKEN = true; + initializeAppCheck(app, { + provider: new ReCaptchaV3Provider(FAKE_SITE_KEY) + }); + // Ensure getDebugToken() call inside `initializeAppCheck()` + // has time to resolve, and double check its value matches that + // written to indexedDB. + expect(await getDebugToken()).to.equal(token); + expect(consoleStub.args[0][0]).to.include(token); + self.FIREBASE_APPCHECK_DEBUG_TOKEN = undefined; + }); + it('does not call initializeDebugMode on second call', async () => { + self.FIREBASE_APPCHECK_DEBUG_TOKEN = 'abcdefg'; + const consoleStub = stub(console, 'log'); + const initializeDebugModeSpy = spy(debug, 'initializeDebugMode'); + // First call, should call initializeDebugMode() + initializeAppCheck(app, { + provider: new ReCaptchaV3Provider(FAKE_SITE_KEY) + }); + expect(initializeDebugModeSpy).to.be.called; + initializeDebugModeSpy.resetHistory(); + // Second call, should not call initializeDebugMode() + initializeAppCheck(app, { + provider: new ReCaptchaV3Provider(FAKE_SITE_KEY) + }); + const token = await getDebugToken(); + expect(token).to.equal('abcdefg'); + // Two console logs of the token, for each initializeAppCheck call. + expect(consoleStub.args[0][0]).to.include(token); + expect(consoleStub.args[1][0]).to.include(token); + expect(consoleStub.args[1][0]).to.equal(consoleStub.args[0][0]); + expect(initializeDebugModeSpy).to.not.be.called; + self.FIREBASE_APPCHECK_DEBUG_TOKEN = undefined; + }); it('initialize reCAPTCHA when a ReCaptchaV3Provider is provided', () => { const initReCAPTCHAStub = stub(reCAPTCHA, 'initialize').returns( diff --git a/packages/app-check/src/api.ts b/packages/app-check/src/api.ts index 48ed326018c..b962f756ddf 100644 --- a/packages/app-check/src/api.ts +++ b/packages/app-check/src/api.ts @@ -23,7 +23,7 @@ import { PartialObserver } from './public-types'; import { ERROR_FACTORY, AppCheckError } from './errors'; -import { getState, setState, AppCheckState } from './state'; +import { getState, setState, AppCheckState, getDebugState } from './state'; import { FirebaseApp, getApp, _getProvider } from '@firebase/app'; import { getModularInstance, ErrorFn, NextFn } from '@firebase/util'; import { AppCheckService } from './factory'; @@ -35,6 +35,7 @@ import { isValid } from './internal-api'; import { readTokenFromStorage } from './storage'; +import { getDebugToken, initializeDebugMode, isDebugMode } from './debug'; declare module '@firebase/component' { interface NameServiceMapping { @@ -57,6 +58,23 @@ export function initializeAppCheck( app = getModularInstance(app); const provider = _getProvider(app, 'app-check'); + // Ensure initializeDebugMode() is only called once. + if (!getDebugState().initialized) { + initializeDebugMode(); + } + + // Log a message containing the debug token when `initializeAppCheck()` + // is called in debug mode. + if (isDebugMode()) { + // Do not block initialization to get the token for the message. + void getDebugToken().then(token => + // Not using logger because I don't think we ever want this accidentally hidden. + console.log( + `App Check debug token: ${token}. You will need to add it to your app's App Check settings in the Firebase console for it to work.` + ) + ); + } + if (provider.isInitialized()) { const existingInstance = provider.getImmediate(); const initialOptions = provider.getOptions() as unknown as AppCheckOptions; diff --git a/packages/app-check/src/debug.ts b/packages/app-check/src/debug.ts index da6cffbfcc4..864e21a169b 100644 --- a/packages/app-check/src/debug.ts +++ b/packages/app-check/src/debug.ts @@ -46,6 +46,11 @@ export async function getDebugToken(): Promise { export function initializeDebugMode(): void { const globals = getGlobal(); + const debugState = getDebugState(); + // Set to true if this function has been called, whether or not + // it enabled debug mode. + debugState.initialized = true; + if ( typeof globals.FIREBASE_APPCHECK_DEBUG_TOKEN !== 'string' && globals.FIREBASE_APPCHECK_DEBUG_TOKEN !== true @@ -53,7 +58,6 @@ export function initializeDebugMode(): void { return; } - const debugState = getDebugState(); debugState.enabled = true; const deferredToken = new Deferred(); debugState.token = deferredToken; diff --git a/packages/app-check/src/index.ts b/packages/app-check/src/index.ts index 8272f8fbece..96e7e82eb01 100644 --- a/packages/app-check/src/index.ts +++ b/packages/app-check/src/index.ts @@ -28,7 +28,6 @@ import { } from '@firebase/component'; import { _AppCheckComponentName } from './public-types'; import { factory, internalFactory } from './factory'; -import { initializeDebugMode } from './debug'; import { _AppCheckInternalComponentName } from './types'; import { name, version } from '../package.json'; @@ -82,4 +81,3 @@ function registerAppCheck(): void { } registerAppCheck(); -initializeDebugMode(); diff --git a/packages/app-check/src/state.ts b/packages/app-check/src/state.ts index 36788d37cc2..27d9de08816 100644 --- a/packages/app-check/src/state.ts +++ b/packages/app-check/src/state.ts @@ -41,6 +41,7 @@ export interface ReCAPTCHAState { } export interface DebugState { + initialized: boolean; enabled: boolean; token?: Deferred; } @@ -52,6 +53,7 @@ export const DEFAULT_STATE: AppCheckState = { }; const DEBUG_STATE: DebugState = { + initialized: false, enabled: false }; @@ -68,6 +70,7 @@ export function clearState(): void { APP_CHECK_STATES.clear(); DEBUG_STATE.enabled = false; DEBUG_STATE.token = undefined; + DEBUG_STATE.initialized = false; } export function getDebugState(): DebugState { diff --git a/packages/app-check/src/storage.ts b/packages/app-check/src/storage.ts index c83a32fbb43..5dfd854e339 100644 --- a/packages/app-check/src/storage.ts +++ b/packages/app-check/src/storage.ts @@ -87,10 +87,6 @@ export async function readOrCreateDebugTokenFromStorage(): Promise { writeDebugTokenToIndexedDB(newToken).catch(e => logger.warn(`Failed to persist debug token to IndexedDB. Error: ${e}`) ); - // Not using logger because I don't think we ever want this accidentally hidden? - console.log( - `App Check debug token: ${newToken}. You will need to add it to your app's App Check settings in the Firebase console for it to work` - ); return newToken; } else { return existingDebugToken;