Skip to content

Cache App Check debug token #5055

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jun 29, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/loud-lamps-camp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@firebase/app-check': patch
---

Fix an error causing App Check to log `HTTP status 429` errors in debug mode.
44 changes: 21 additions & 23 deletions packages/app-check/src/api.test.ts
Original file line number Diff line number Diff line change
@@ -28,6 +28,7 @@ import {
getFakeApp,
getFakeCustomTokenProvider,
getFakePlatformLoggingProvider,
getFakeGreCAPTCHA,
removegreCAPTCHAScriptsOnPage
} from '../test/util';
import { clearState, getState } from './state';
@@ -37,8 +38,12 @@ import * as internalApi from './internal-api';
import * as client from './client';
import * as storage from './storage';
import * as logger from './logger';
import * as util from './util';

describe('api', () => {
beforeEach(() => {
stub(util, 'getRecaptcha').returns(getFakeGreCAPTCHA());
});
describe('activate()', () => {
let app: FirebaseApp;

@@ -126,25 +131,29 @@ describe('api', () => {
});
});
describe('onTokenChanged()', () => {
const fakePlatformLoggingProvider = getFakePlatformLoggingProvider();
const fakeRecaptchaToken = 'fake-recaptcha-token';
const fakeRecaptchaAppCheckToken = {
token: 'fake-recaptcha-app-check-token',
expireTimeMillis: Date.now() + 60000,
issuedAtTimeMillis: 0
};

beforeEach(() => {
stub(storage, 'readTokenFromStorage').resolves(undefined);
stub(storage, 'writeTokenToStorage');
});
afterEach(() => {
clearState();
removegreCAPTCHAScriptsOnPage();
});
it('Listeners work when using top-level parameters pattern', async () => {
const app = getFakeApp({ automaticDataCollectionEnabled: true });
activate(app, FAKE_SITE_KEY, true);
const fakePlatformLoggingProvider = getFakePlatformLoggingProvider();
const fakeRecaptchaToken = 'fake-recaptcha-token';
const fakeRecaptchaAppCheckToken = {
token: 'fake-recaptcha-app-check-token',
expireTimeMillis: 123,
issuedAtTimeMillis: 0
};
const app = getFakeApp();
activate(app, FAKE_SITE_KEY, false);
stub(reCAPTCHA, 'getToken').returns(Promise.resolve(fakeRecaptchaToken));
stub(client, 'exchangeToken').returns(
Promise.resolve(fakeRecaptchaAppCheckToken)
);
stub(storage, 'writeTokenToStorage').returns(Promise.resolve(undefined));

const listener1 = (): void => {
throw new Error();
@@ -183,20 +192,12 @@ describe('api', () => {
});

it('Listeners work when using Observer pattern', async () => {
const app = getFakeApp({ automaticDataCollectionEnabled: true });
activate(app, FAKE_SITE_KEY, true);
const fakePlatformLoggingProvider = getFakePlatformLoggingProvider();
const fakeRecaptchaToken = 'fake-recaptcha-token';
const fakeRecaptchaAppCheckToken = {
token: 'fake-recaptcha-app-check-token',
expireTimeMillis: 123,
issuedAtTimeMillis: 0
};
const app = getFakeApp();
activate(app, FAKE_SITE_KEY, false);
stub(reCAPTCHA, 'getToken').returns(Promise.resolve(fakeRecaptchaToken));
stub(client, 'exchangeToken').returns(
Promise.resolve(fakeRecaptchaAppCheckToken)
);
stub(storage, 'writeTokenToStorage').returns(Promise.resolve(undefined));

const listener1 = (): void => {
throw new Error();
@@ -238,11 +239,8 @@ describe('api', () => {
stub(logger.logger, 'error');
const app = getFakeApp();
activate(app, FAKE_SITE_KEY, false);
const fakePlatformLoggingProvider = getFakePlatformLoggingProvider();
const fakeRecaptchaToken = 'fake-recaptcha-token';
stub(reCAPTCHA, 'getToken').returns(Promise.resolve(fakeRecaptchaToken));
stub(client, 'exchangeToken').rejects('exchange error');
stub(storage, 'writeTokenToStorage').returns(Promise.resolve(undefined));

const listener1 = spy();

4 changes: 3 additions & 1 deletion packages/app-check/src/client.test.ts
Original file line number Diff line number Diff line change
@@ -52,7 +52,8 @@ describe('client', () => {
});

it('returns a AppCheck token', async () => {
useFakeTimers();
// To get a consistent expireTime/issuedAtTime.
const clock = useFakeTimers();
fetchStub.returns(
Promise.resolve({
status: 200,
@@ -77,6 +78,7 @@ describe('client', () => {
expireTimeMillis: 3600,
issuedAtTimeMillis: 0
});
clock.restore();
});

it('throws when there is a network error', async () => {
18 changes: 15 additions & 3 deletions packages/app-check/src/factory.ts
Original file line number Diff line number Diff line change
@@ -36,18 +36,21 @@ import {
import { Provider } from '@firebase/component';
import { PartialObserver } from '@firebase/util';

import { FirebaseService } from '@firebase/app-types/private';
import { getState } from './state';

export function factory(
app: FirebaseApp,
platformLoggerProvider: Provider<'platform-logger'>
): FirebaseAppCheck {
): FirebaseAppCheck & FirebaseService {
return {
app,
activate: (
siteKeyOrProvider: string | AppCheckProvider,
isTokenAutoRefreshEnabled?: boolean
) => activate(app, siteKeyOrProvider, isTokenAutoRefreshEnabled),
setTokenAutoRefreshEnabled: (isTokenAutoRefreshEnabled: boolean) =>
setTokenAutoRefreshEnabled(app, isTokenAutoRefreshEnabled),

getToken: forceRefresh =>
getToken(app, platformLoggerProvider, forceRefresh),
onTokenChanged: (
@@ -68,7 +71,16 @@ export function factory(
onNextOrObserver as (tokenResult: AppCheckTokenResult) => void,
onError,
onCompletion
)
),
INTERNAL: {
delete: () => {
const { tokenObservers } = getState(app);
for (const tokenObserver of tokenObservers) {
removeTokenListener(app, tokenObserver.next);
}
return Promise.resolve();
}
}
};
}

196 changes: 92 additions & 104 deletions packages/app-check/src/internal-api.test.ts
Original file line number Diff line number Diff line change
@@ -23,6 +23,7 @@ import {
FAKE_SITE_KEY,
getFakeApp,
getFakeCustomTokenProvider,
getFakeGreCAPTCHA,
getFakePlatformLoggingProvider,
removegreCAPTCHAScriptsOnPage
} from '../test/util';
@@ -38,20 +39,26 @@ import * as reCAPTCHA from './recaptcha';
import * as logger from './logger';
import * as client from './client';
import * as storage from './storage';
import * as util from './util';
import { getState, clearState, setState, getDebugState } from './state';
import { AppCheckTokenResult } from '@firebase/app-check-interop-types';
import { Deferred } from '@firebase/util';
import { AppCheckTokenResult } from '../../app-check-interop-types';

const fakePlatformLoggingProvider = getFakePlatformLoggingProvider();

describe('internal api', () => {
let app: FirebaseApp;
let storageReadStub: SinonStub;
let storageWriteStub: SinonStub;

beforeEach(() => {
app = getFakeApp();
storageReadStub = stub(storage, 'readTokenFromStorage').resolves(undefined);
storageWriteStub = stub(storage, 'writeTokenToStorage');
stub(util, 'getRecaptcha').returns(getFakeGreCAPTCHA());
});

afterEach(() => {
afterEach(async () => {
clearState();
removegreCAPTCHAScriptsOnPage();
});
@@ -60,18 +67,19 @@ describe('internal api', () => {
const fakeRecaptchaToken = 'fake-recaptcha-token';
const fakeRecaptchaAppCheckToken = {
token: 'fake-recaptcha-app-check-token',
expireTimeMillis: 123,
// This makes isValid(token) true.
expireTimeMillis: Date.now() + 60000,
issuedAtTimeMillis: 0
};

const fakeCachedAppCheckToken = {
token: 'fake-cached-app-check-token',
expireTimeMillis: 123,
// This makes isValid(token) true.
expireTimeMillis: Date.now() + 60000,
issuedAtTimeMillis: 0
};

it('uses customTokenProvider to get an AppCheck token', async () => {
const clock = useFakeTimers();
const customTokenProvider = getFakeCustomTokenProvider();
const customProviderSpy = spy(customTokenProvider, 'getToken');

@@ -82,20 +90,18 @@ describe('internal api', () => {
expect(token).to.deep.equal({
token: 'fake-custom-app-check-token'
});

clock.restore();
});

it('uses reCAPTCHA token to exchange for AppCheck token if no customTokenProvider is provided', async () => {
activate(app, FAKE_SITE_KEY);

const reCAPTCHASpy = stub(reCAPTCHA, 'getToken').returns(
Promise.resolve(fakeRecaptchaToken)
const reCAPTCHASpy = stub(reCAPTCHA, 'getToken').resolves(
fakeRecaptchaToken
);
const exchangeTokenStub: SinonStub = stub(
client,
'exchangeToken'
).returns(Promise.resolve(fakeRecaptchaAppCheckToken));
).resolves(fakeRecaptchaAppCheckToken);

const token = await getToken(app, fakePlatformLoggingProvider);

@@ -111,12 +117,12 @@ describe('internal api', () => {
const errorStub = stub(console, 'error');
activate(app, FAKE_SITE_KEY, true);

const reCAPTCHASpy = stub(reCAPTCHA, 'getToken').returns(
Promise.resolve(fakeRecaptchaToken)
const reCAPTCHASpy = stub(reCAPTCHA, 'getToken').resolves(
fakeRecaptchaToken
);

const error = new Error('oops, something went wrong');
stub(client, 'exchangeToken').returns(Promise.reject(error));
stub(client, 'exchangeToken').rejects(error);

const token = await getToken(app, fakePlatformLoggingProvider);

@@ -132,12 +138,8 @@ describe('internal api', () => {
});

it('notifies listeners using cached token', async () => {
activate(app, FAKE_SITE_KEY, true);

const clock = useFakeTimers();
stub(storage, 'readTokenFromStorage').returns(
Promise.resolve(fakeCachedAppCheckToken)
);
activate(app, FAKE_SITE_KEY, false);
storageReadStub.resolves(fakeCachedAppCheckToken);

const listener1 = spy();
const listener2 = spy();
@@ -152,18 +154,13 @@ describe('internal api', () => {
expect(listener2).to.be.calledWith({
token: fakeCachedAppCheckToken.token
});

clock.restore();
});

it('notifies listeners using new token', async () => {
activate(app, FAKE_SITE_KEY, true);
activate(app, FAKE_SITE_KEY, false);

stub(storage, 'readTokenFromStorage').returns(Promise.resolve(undefined));
stub(reCAPTCHA, 'getToken').returns(Promise.resolve(fakeRecaptchaToken));
stub(client, 'exchangeToken').returns(
Promise.resolve(fakeRecaptchaAppCheckToken)
);
stub(reCAPTCHA, 'getToken').resolves(fakeRecaptchaToken);
stub(client, 'exchangeToken').resolves(fakeRecaptchaAppCheckToken);

const listener1 = spy();
const listener2 = spy();
@@ -182,8 +179,8 @@ describe('internal api', () => {

it('calls optional error handler if there is an error getting a token', async () => {
stub(logger.logger, 'error');
activate(app, FAKE_SITE_KEY, true);
stub(reCAPTCHA, 'getToken').returns(Promise.resolve(fakeRecaptchaToken));
activate(app, FAKE_SITE_KEY, false);
stub(reCAPTCHA, 'getToken').resolves(fakeRecaptchaToken);
stub(client, 'exchangeToken').rejects('exchange error');
const listener1 = spy();

@@ -197,35 +194,46 @@ describe('internal api', () => {
expect(errorFn1.args[0][0].name).to.include('exchange error');
});

it('ignores listeners that throw', async () => {
activate(app, FAKE_SITE_KEY, false);
stub(reCAPTCHA, 'getToken').resolves(fakeRecaptchaToken);
stub(client, 'exchangeToken').resolves(fakeRecaptchaAppCheckToken);
const listener1 = stub().throws(new Error());
const listener2 = spy();

const errorFn1 = spy();

addTokenListener(app, fakePlatformLoggingProvider, listener1, errorFn1);
addTokenListener(app, fakePlatformLoggingProvider, listener2);

await getToken(app, fakePlatformLoggingProvider);

expect(errorFn1).not.to.be.called;
expect(listener1).to.be.called;
expect(listener2).to.be.called;
});

it('loads persisted token to memory and returns it', async () => {
const clock = useFakeTimers();
activate(app, FAKE_SITE_KEY);

stub(storage, 'readTokenFromStorage').returns(
Promise.resolve(fakeCachedAppCheckToken)
);
storageReadStub.resolves(fakeCachedAppCheckToken);

const clientStub = stub(client, 'exchangeToken');

expect(getState(app).token).to.equal(undefined);
expect(await getToken(app, fakePlatformLoggingProvider)).to.deep.equal({
const result = await getToken(app, fakePlatformLoggingProvider);
expect(result).to.deep.equal({
token: fakeCachedAppCheckToken.token
});
expect(getState(app).token).to.equal(fakeCachedAppCheckToken);
expect(clientStub).has.not.been.called;

clock.restore();
});

it('persists token to storage', async () => {
activate(app, FAKE_SITE_KEY);
activate(app, FAKE_SITE_KEY, false);

stub(storage, 'readTokenFromStorage').returns(Promise.resolve(undefined));
stub(reCAPTCHA, 'getToken').returns(Promise.resolve(fakeRecaptchaToken));
stub(client, 'exchangeToken').returns(
Promise.resolve(fakeRecaptchaAppCheckToken)
);
const storageWriteStub = stub(storage, 'writeTokenToStorage');
stub(reCAPTCHA, 'getToken').resolves(fakeRecaptchaToken);
stub(client, 'exchangeToken').resolves(fakeRecaptchaAppCheckToken);
const result = await getToken(app, fakePlatformLoggingProvider);
expect(result).to.deep.equal({ token: fakeRecaptchaAppCheckToken.token });
expect(storageWriteStub).has.been.calledWith(
@@ -235,27 +243,23 @@ describe('internal api', () => {
});

it('returns the valid token in memory without making network request', async () => {
const clock = useFakeTimers();
activate(app, FAKE_SITE_KEY);
setState(app, { ...getState(app), token: fakeRecaptchaAppCheckToken });

const clientStub = stub(client, 'exchangeToken');
expect(await getToken(app, fakePlatformLoggingProvider)).to.deep.equal({
const result = await getToken(app, fakePlatformLoggingProvider);
expect(result).to.deep.equal({
token: fakeRecaptchaAppCheckToken.token
});
expect(clientStub).to.not.have.been.called;

clock.restore();
});

it('force to get new token when forceRefresh is true', async () => {
activate(app, FAKE_SITE_KEY);
setState(app, { ...getState(app), token: fakeRecaptchaAppCheckToken });

stub(reCAPTCHA, 'getToken').returns(Promise.resolve(fakeRecaptchaToken));
stub(client, 'exchangeToken').returns(
Promise.resolve(fakeRecaptchaAppCheckToken)
);
stub(reCAPTCHA, 'getToken').resolves(fakeRecaptchaToken);
stub(client, 'exchangeToken').resolves(fakeRecaptchaAppCheckToken);

expect(
await getToken(app, fakePlatformLoggingProvider, true)
@@ -264,11 +268,11 @@ describe('internal api', () => {
});
});

it('exchanges debug token if in debug mode', async () => {
it('exchanges debug token if in debug mode and there is no cached token', async () => {
const exchangeTokenStub: SinonStub = stub(
client,
'exchangeToken'
).returns(Promise.resolve(fakeRecaptchaAppCheckToken));
).resolves(fakeRecaptchaAppCheckToken);
const debugState = getDebugState();
debugState.enabled = true;
debugState.token = new Deferred();
@@ -284,8 +288,15 @@ describe('internal api', () => {
});

describe('addTokenListener', () => {
const fakeRecaptchaAppCheckToken = {
token: 'fake-recaptcha-app-check-token',
// This makes isValid(token) true.
expireTimeMillis: Date.now() + 60000,
issuedAtTimeMillis: 0
};
it('adds token listeners', () => {
const listener = (): void => {};
stub(client, 'exchangeToken').resolves(fakeRecaptchaAppCheckToken);

addTokenListener(app, fakePlatformLoggingProvider, listener);

@@ -294,93 +305,69 @@ describe('internal api', () => {

it('starts proactively refreshing token after adding the first listener', () => {
const listener = (): void => {};
stub(client, 'exchangeToken').resolves(fakeRecaptchaAppCheckToken);
setState(app, { ...getState(app), isTokenAutoRefreshEnabled: true });
expect(getState(app).tokenObservers.length).to.equal(0);
expect(getState(app).tokenRefresher).to.equal(undefined);

addTokenListener(app, fakePlatformLoggingProvider, listener);

expect(getState(app).tokenRefresher?.isRunning()).to.be.true;

removeTokenListener(app, listener);
});

it('notifies the listener with the valid token in memory immediately', done => {
it('notifies the listener with the valid token in memory immediately', async () => {
const clock = useFakeTimers();
const fakeListener = (token: AppCheckTokenResult): void => {
expect(token).to.deep.equal({
token: `fake-memory-app-check-token`
});
clock.restore();
done();
};
const listener = stub();

setState(app, {
...getState(app),
token: {
token: `fake-memory-app-check-token`,
expireTimeMillis: 123,
expireTimeMillis: Date.now() + 60000,
issuedAtTimeMillis: 0
}
});

addTokenListener(app, fakePlatformLoggingProvider, fakeListener);
addTokenListener(app, fakePlatformLoggingProvider, listener);
await clock.runAllAsync();
expect(listener).to.be.calledWith({
token: 'fake-memory-app-check-token'
});
clock.restore();
});

it('notifies the listener with the valid token in storage', done => {
const clock = useFakeTimers();
activate(app, FAKE_SITE_KEY, true);
stub(storage, 'readTokenFromStorage').returns(
Promise.resolve({
token: `fake-cached-app-check-token`,
expireTimeMillis: 123,
issuedAtTimeMillis: 0
})
);
activate(app, FAKE_SITE_KEY);
storageReadStub.resolves({
token: `fake-cached-app-check-token`,
expireTimeMillis: Date.now() + 60000,
issuedAtTimeMillis: 0
});

// Need to use done() if the callback will be called by the
// refresher.
const fakeListener = (token: AppCheckTokenResult): void => {
expect(token).to.deep.equal({
token: `fake-cached-app-check-token`
});
clock.restore();
done();
};

addTokenListener(app, fakePlatformLoggingProvider, fakeListener);
clock.tick(1);
});

it('notifies the listener with the debug token immediately', done => {
const fakeListener = (token: AppCheckTokenResult): void => {
expect(token).to.deep.equal({
token: `my-debug-token`
});
done();
};

const debugState = getDebugState();
debugState.enabled = true;
debugState.token = new Deferred();
debugState.token.resolve('my-debug-token');

activate(app, FAKE_SITE_KEY, true);
addTokenListener(app, fakePlatformLoggingProvider, fakeListener);
});

it('does NOT start token refresher in debug mode', () => {
const debugState = getDebugState();
debugState.enabled = true;
debugState.token = new Deferred();
debugState.token.resolve('my-debug-token');

activate(app, FAKE_SITE_KEY, true);
addTokenListener(app, fakePlatformLoggingProvider, () => {});

const state = getState(app);
expect(state.tokenRefresher).is.undefined;
});
});

describe('removeTokenListener', () => {
const fakeRecaptchaAppCheckToken = {
token: 'fake-recaptcha-app-check-token',
// This makes isValid(token) true.
expireTimeMillis: Date.now() + 60000,
issuedAtTimeMillis: 0
};
it('should remove token listeners', () => {
stub(client, 'exchangeToken').resolves(fakeRecaptchaAppCheckToken);
const listener = (): void => {};
addTokenListener(app, fakePlatformLoggingProvider, listener);
expect(getState(app).tokenObservers.length).to.equal(1);
@@ -390,6 +377,7 @@ describe('internal api', () => {
});

it('should stop proactively refreshing token after deleting the last listener', () => {
stub(client, 'exchangeToken').resolves(fakeRecaptchaAppCheckToken);
const listener = (): void => {};
setState(app, { ...getState(app), isTokenAutoRefreshEnabled: true });

98 changes: 44 additions & 54 deletions packages/app-check/src/internal-api.ts
Original file line number Diff line number Diff line change
@@ -24,7 +24,6 @@ import {
import {
AppCheckTokenInternal,
AppCheckTokenObserver,
getDebugState,
getState,
setState
} from './state';
@@ -72,25 +71,17 @@ export async function getToken(
forceRefresh = false
): Promise<AppCheckTokenResult> {
ensureActivated(app);
/**
* DEBUG MODE
* return the debug token directly
*/
if (isDebugMode()) {
const tokenFromDebugExchange: AppCheckTokenInternal = await exchangeToken(
getExchangeDebugTokenRequest(app, await getDebugToken()),
platformLoggerProvider
);
return { token: tokenFromDebugExchange.token };
}

const state = getState(app);

/**
* First check if there is a token in memory from a previous `getToken()` call.
*/
let token: AppCheckTokenInternal | undefined = state.token;
let error: Error | undefined = undefined;

/**
* try to load token from indexedDB if it's the first time this function is called
* If there is no token in memory, try to load token from indexedDB.
*/
if (!token) {
// readTokenFromStorage() always resolves. In case of an error, it resolves with `undefined`.
@@ -104,13 +95,30 @@ export async function getToken(
}
}

// return the cached token if it's valid
// Return the cached token (from either memory or indexedDB) if it's valid
if (!forceRefresh && token && isValid(token)) {
return {
token: token.token
};
}

/**
* DEBUG MODE
* If debug mode is set, and there is no cached token, fetch a new App
* Check token using the debug token, and return it directly.
*/
if (isDebugMode()) {
const tokenFromDebugExchange: AppCheckTokenInternal = await exchangeToken(
getExchangeDebugTokenRequest(app, await getDebugToken()),
platformLoggerProvider
);
// Write debug token to indexedDB.
await writeTokenToStorage(app, tokenFromDebugExchange);
// Write debug token to state.
setState(app, { ...state, token: tokenFromDebugExchange });
return { token: tokenFromDebugExchange.token };
}

/**
* request a new token
*/
@@ -155,7 +163,7 @@ export async function getToken(
interopTokenResult = {
token: token.token
};
// write the new token to the memory state as well ashe persistent storage.
// write the new token to the memory state as well as the persistent storage.
// Only do it if we got a valid new token
setState(app, { ...state, token });
await writeTokenToStorage(app, token);
@@ -182,48 +190,30 @@ export function addTokenListener(
};

/**
* DEBUG MODE
*
* invoke the listener once with the debug token.
* Invoke the listener with the valid token, then start the token refresher
*/
if (isDebugMode()) {
const debugState = getDebugState();
if (debugState.enabled && debugState.token) {
debugState.token.promise
.then(token => listener({ token }))
.catch(() => {
/** Ignore errors in listeners. */
});
}
} else {
/**
* PROD MODE
*
* invoke the listener with the valid token, then start the token refresher
*/
if (!newState.tokenRefresher) {
const tokenRefresher = createTokenRefresher(app, platformLoggerProvider);
newState.tokenRefresher = tokenRefresher;
}
if (!newState.tokenRefresher) {
const tokenRefresher = createTokenRefresher(app, platformLoggerProvider);
newState.tokenRefresher = tokenRefresher;
}

// Create the refresher but don't start it if `isTokenAutoRefreshEnabled`
// is not true.
if (
!newState.tokenRefresher.isRunning() &&
state.isTokenAutoRefreshEnabled === true
) {
newState.tokenRefresher.start();
}
// Create the refresher but don't start it if `isTokenAutoRefreshEnabled`
// is not true.
if (
!newState.tokenRefresher.isRunning() &&
state.isTokenAutoRefreshEnabled === true
) {
newState.tokenRefresher.start();
}

// invoke the listener async immediately if there is a valid token
if (state.token && isValid(state.token)) {
const validToken = state.token;
Promise.resolve()
.then(() => listener({ token: validToken.token }))
.catch(() => {
/** Ignore errors in listeners. */
});
}
// invoke the listener async immediately if there is a valid token
if (state.token && isValid(state.token)) {
const validToken = state.token;
Promise.resolve()
.then(() => listener({ token: validToken.token }))
.catch(() => {
/** Ignore errors in listeners. */
});
}

setState(app, newState);
1 change: 1 addition & 0 deletions packages/app-check/src/state.ts
Original file line number Diff line number Diff line change
@@ -54,6 +54,7 @@ export interface ReCAPTCHAState {

export interface DebugState {
enabled: boolean;
// This is the debug token string the user interacts with.
token?: Deferred<string>;
}