@firebase/app-check 0.5.1 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @firebase/app-check
2
2
 
3
+ ## 0.5.2
4
+
5
+ ### Patch Changes
6
+
7
+ - [`6f0049e66`](https://github.com/firebase/firebase-js-sdk/commit/6f0049e66064809ae990a2d9461e28b2d6d08d19) [#5676](https://github.com/firebase/firebase-js-sdk/pull/5676) - Block exchange requests for certain periods of time after certain error codes to prevent overwhelming the endpoint. Start token listener when App Check is initialized to avoid extra wait time on first getToken() call.
8
+
3
9
  ## 0.5.1
4
10
 
5
11
  ### Patch Changes
@@ -169,6 +169,11 @@ export declare class ReCaptchaEnterpriseProvider implements AppCheckProvider {
169
169
  private _siteKey;
170
170
  private _app?;
171
171
  private _platformLoggerProvider?;
172
+ /**
173
+ * Throttle requests on certain error codes to prevent too many retries
174
+ * in a short time.
175
+ */
176
+ private _throttleData;
172
177
  /**
173
178
  * Create a ReCaptchaEnterpriseProvider instance.
174
179
  * @param siteKey - reCAPTCHA Enterprise score-based site key.
@@ -189,6 +194,11 @@ export declare class ReCaptchaV3Provider implements AppCheckProvider {
189
194
  private _siteKey;
190
195
  private _app?;
191
196
  private _platformLoggerProvider?;
197
+ /**
198
+ * Throttle requests on certain error codes to prevent too many retries
199
+ * in a short time.
200
+ */
201
+ private _throttleData;
192
202
  /**
193
203
  * Create a ReCaptchaV3Provider instance.
194
204
  * @param siteKey - ReCAPTCHA V3 siteKey.
@@ -191,6 +191,11 @@ export declare class ReCaptchaEnterpriseProvider implements AppCheckProvider {
191
191
  private _siteKey;
192
192
  private _app?;
193
193
  private _platformLoggerProvider?;
194
+ /**
195
+ * Throttle requests on certain error codes to prevent too many retries
196
+ * in a short time.
197
+ */
198
+ private _throttleData;
194
199
  /**
195
200
  * Create a ReCaptchaEnterpriseProvider instance.
196
201
  * @param siteKey - reCAPTCHA Enterprise score-based site key.
@@ -221,6 +226,11 @@ export declare class ReCaptchaV3Provider implements AppCheckProvider {
221
226
  private _siteKey;
222
227
  private _app?;
223
228
  private _platformLoggerProvider?;
229
+ /**
230
+ * Throttle requests on certain error codes to prevent too many retries
231
+ * in a short time.
232
+ */
233
+ private _throttleData;
224
234
  /**
225
235
  * Create a ReCaptchaV3Provider instance.
226
236
  * @param siteKey - ReCAPTCHA V3 siteKey.
@@ -1,7 +1,7 @@
1
1
  import { _getProvider, getApp, _registerComponent, registerVersion } from '@firebase/app';
2
2
  import { Component } from '@firebase/component';
3
3
  import { __awaiter, __generator, __assign, __spreadArray } from 'tslib';
4
- import { Deferred, ErrorFactory, isIndexedDBAvailable, getGlobal, base64, issuedAtTime, getModularInstance } from '@firebase/util';
4
+ import { Deferred, ErrorFactory, isIndexedDBAvailable, getGlobal, base64, issuedAtTime, calculateBackoffMillis, getModularInstance } from '@firebase/util';
5
5
  import { Logger } from '@firebase/logger';
6
6
 
7
7
  /**
@@ -74,7 +74,11 @@ var TOKEN_REFRESH_TIME = {
74
74
  * This is the maximum retrial wait, currently 16 minutes.
75
75
  */
76
76
  RETRIAL_MAX_WAIT: 16 * 60 * 1000
77
- };
77
+ };
78
+ /**
79
+ * One day in millis, for certain error code backoffs.
80
+ */
81
+ var ONE_DAY = 24 * 60 * 60 * 1000;
78
82
 
79
83
  /**
80
84
  * @license
@@ -238,6 +242,7 @@ var ERRORS = (_a = {},
238
242
  _a["storage-get" /* STORAGE_GET */] = 'Error thrown when reading from storage. Original error: {$originalErrorMessage}.',
239
243
  _a["storage-set" /* STORAGE_WRITE */] = 'Error thrown when writing to storage. Original error: {$originalErrorMessage}.',
240
244
  _a["recaptcha-error" /* RECAPTCHA_ERROR */] = 'ReCAPTCHA error.',
245
+ _a["throttled" /* THROTTLED */] = "Requests throttled due to {$httpStatus} error. Attempts allowed again after {$time}",
241
246
  _a);
242
247
  var ERROR_FACTORY = new ErrorFactory('appCheck', 'AppCheck', ERRORS);
243
248
 
@@ -280,6 +285,28 @@ function uuidv4() {
280
285
  var r = (Math.random() * 16) | 0, v = c === 'x' ? r : (r & 0x3) | 0x8;
281
286
  return v.toString(16);
282
287
  });
288
+ }
289
+ function getDurationString(durationInMillis) {
290
+ var totalSeconds = Math.round(durationInMillis / 1000);
291
+ var days = Math.floor(totalSeconds / (3600 * 24));
292
+ var hours = Math.floor((totalSeconds - days * 3600 * 24) / 3600);
293
+ var minutes = Math.floor((totalSeconds - days * 3600 * 24 - hours * 3600) / 60);
294
+ var seconds = totalSeconds - days * 3600 * 24 - hours * 3600 - minutes * 60;
295
+ var result = '';
296
+ if (days) {
297
+ result += pad(days) + 'd:';
298
+ }
299
+ if (hours) {
300
+ result += pad(hours) + 'h:';
301
+ }
302
+ result += pad(minutes) + 'm:' + pad(seconds) + 's';
303
+ return result;
304
+ }
305
+ function pad(value) {
306
+ if (value === 0) {
307
+ return '00';
308
+ }
309
+ return value >= 10 ? value.toString() : '0' + value;
283
310
  }
284
311
 
285
312
  /**
@@ -740,9 +767,9 @@ function formatDummyToken(tokenErrorData) {
740
767
  function getToken$2(appCheck, forceRefresh) {
741
768
  if (forceRefresh === void 0) { forceRefresh = false; }
742
769
  return __awaiter(this, void 0, void 0, function () {
743
- var app, state, token, error, cachedToken, tokenFromDebugExchange, _a, _b, _c, e_1, interopTokenResult;
744
- return __generator(this, function (_d) {
745
- switch (_d.label) {
770
+ var app, state, token, error, cachedToken, shouldCallListeners, _a, _b, _c, _d, tokenFromDebugExchange, e_1, interopTokenResult;
771
+ return __generator(this, function (_e) {
772
+ switch (_e.label) {
746
773
  case 0:
747
774
  app = appCheck.app;
748
775
  ensureActivated(app);
@@ -752,14 +779,11 @@ function getToken$2(appCheck, forceRefresh) {
752
779
  if (!!token) return [3 /*break*/, 2];
753
780
  return [4 /*yield*/, state.cachedTokenPromise];
754
781
  case 1:
755
- cachedToken = _d.sent();
782
+ cachedToken = _e.sent();
756
783
  if (cachedToken && isValid(cachedToken)) {
757
784
  token = cachedToken;
758
- setState(app, __assign(__assign({}, state), { token: token }));
759
- // notify all listeners with the cached token
760
- notifyTokenListeners(app, { token: token.token });
761
785
  }
762
- _d.label = 2;
786
+ _e.label = 2;
763
787
  case 2:
764
788
  // Return the cached token (from either memory or indexedDB) if it's valid
765
789
  if (!forceRefresh && token && isValid(token)) {
@@ -767,44 +791,69 @@ function getToken$2(appCheck, forceRefresh) {
767
791
  token: token.token
768
792
  }];
769
793
  }
770
- if (!isDebugMode()) return [3 /*break*/, 6];
771
- _a = exchangeToken;
772
- _b = getExchangeDebugTokenRequest;
773
- _c = [app];
794
+ shouldCallListeners = false;
795
+ if (!isDebugMode()) return [3 /*break*/, 7];
796
+ if (!!state.exchangeTokenPromise) return [3 /*break*/, 4];
797
+ _a = state;
798
+ _b = exchangeToken;
799
+ _c = getExchangeDebugTokenRequest;
800
+ _d = [app];
774
801
  return [4 /*yield*/, getDebugToken()];
775
- case 3: return [4 /*yield*/, _a.apply(void 0, [_b.apply(void 0, _c.concat([_d.sent()])), appCheck.platformLoggerProvider])];
776
- case 4:
777
- tokenFromDebugExchange = _d.sent();
802
+ case 3:
803
+ _a.exchangeTokenPromise = _b.apply(void 0, [_c.apply(void 0, _d.concat([_e.sent()])), appCheck.platformLoggerProvider]).then(function (token) {
804
+ state.exchangeTokenPromise = undefined;
805
+ return token;
806
+ });
807
+ shouldCallListeners = true;
808
+ _e.label = 4;
809
+ case 4: return [4 /*yield*/, state.exchangeTokenPromise];
810
+ case 5:
811
+ tokenFromDebugExchange = _e.sent();
778
812
  // Write debug token to indexedDB.
779
813
  return [4 /*yield*/, writeTokenToStorage(app, tokenFromDebugExchange)];
780
- case 5:
814
+ case 6:
781
815
  // Write debug token to indexedDB.
782
- _d.sent();
816
+ _e.sent();
783
817
  // Write debug token to state.
784
818
  setState(app, __assign(__assign({}, state), { token: tokenFromDebugExchange }));
785
819
  return [2 /*return*/, { token: tokenFromDebugExchange.token }];
786
- case 6:
787
- _d.trys.push([6, 8, , 9]);
788
- return [4 /*yield*/, state.provider.getToken()];
789
820
  case 7:
790
- // state.provider is populated in initializeAppCheck()
791
- // ensureActivated() at the top of this function checks that
792
- // initializeAppCheck() has been called.
793
- token = _d.sent();
794
- return [3 /*break*/, 9];
821
+ _e.trys.push([7, 9, , 10]);
822
+ // Avoid making another call to the exchange endpoint if one is in flight.
823
+ if (!state.exchangeTokenPromise) {
824
+ // state.provider is populated in initializeAppCheck()
825
+ // ensureActivated() at the top of this function checks that
826
+ // initializeAppCheck() has been called.
827
+ state.exchangeTokenPromise = state.provider.getToken().then(function (token) {
828
+ state.exchangeTokenPromise = undefined;
829
+ return token;
830
+ });
831
+ shouldCallListeners = true;
832
+ }
833
+ return [4 /*yield*/, state.exchangeTokenPromise];
795
834
  case 8:
796
- e_1 = _d.sent();
797
- // `getToken()` should never throw, but logging error text to console will aid debugging.
798
- logger.error(e_1);
799
- error = e_1;
800
- return [3 /*break*/, 9];
835
+ token = _e.sent();
836
+ return [3 /*break*/, 10];
801
837
  case 9:
802
- if (!!token) return [3 /*break*/, 10];
838
+ e_1 = _e.sent();
839
+ if (e_1.code === "appCheck/" + "throttled" /* THROTTLED */) {
840
+ // Warn if throttled, but do not treat it as an error.
841
+ logger.warn(e_1.message);
842
+ }
843
+ else {
844
+ // `getToken()` should never throw, but logging error text to console will aid debugging.
845
+ logger.error(e_1);
846
+ }
847
+ // Always save error to be added to dummy token.
848
+ error = e_1;
849
+ return [3 /*break*/, 10];
850
+ case 10:
851
+ if (!!token) return [3 /*break*/, 11];
803
852
  // if token is undefined, there must be an error.
804
853
  // we return a dummy token along with the error
805
854
  interopTokenResult = makeDummyTokenResult(error);
806
- return [3 /*break*/, 12];
807
- case 10:
855
+ return [3 /*break*/, 13];
856
+ case 11:
808
857
  interopTokenResult = {
809
858
  token: token.token
810
859
  };
@@ -812,11 +861,13 @@ function getToken$2(appCheck, forceRefresh) {
812
861
  // Only do it if we got a valid new token
813
862
  setState(app, __assign(__assign({}, state), { token: token }));
814
863
  return [4 /*yield*/, writeTokenToStorage(app, token)];
815
- case 11:
816
- _d.sent();
817
- _d.label = 12;
818
864
  case 12:
819
- notifyTokenListeners(app, interopTokenResult);
865
+ _e.sent();
866
+ _e.label = 13;
867
+ case 13:
868
+ if (shouldCallListeners) {
869
+ notifyTokenListeners(app, interopTokenResult);
870
+ }
820
871
  return [2 /*return*/, interopTokenResult];
821
872
  }
822
873
  });
@@ -830,44 +881,31 @@ function addTokenListener(appCheck, type, listener, onError) {
830
881
  error: onError,
831
882
  type: type
832
883
  };
833
- var newState = __assign(__assign({}, state), { tokenObservers: __spreadArray(__spreadArray([], state.tokenObservers), [tokenObserver]) });
834
- /**
835
- * Invoke the listener with the valid token, then start the token refresher
836
- */
837
- if (!newState.tokenRefresher) {
838
- var tokenRefresher = createTokenRefresher(appCheck);
839
- newState.tokenRefresher = tokenRefresher;
840
- }
841
- // Create the refresher but don't start it if `isTokenAutoRefreshEnabled`
842
- // is not true.
843
- if (!newState.tokenRefresher.isRunning() && state.isTokenAutoRefreshEnabled) {
844
- newState.tokenRefresher.start();
845
- }
884
+ setState(app, __assign(__assign({}, state), { tokenObservers: __spreadArray(__spreadArray([], state.tokenObservers), [tokenObserver]) }));
846
885
  // Invoke the listener async immediately if there is a valid token
847
886
  // in memory.
848
887
  if (state.token && isValid(state.token)) {
849
888
  var validToken_1 = state.token;
850
889
  Promise.resolve()
851
- .then(function () { return listener({ token: validToken_1.token }); })
852
- .catch(function () {
853
- /* we don't care about exceptions thrown in listeners */
854
- });
855
- }
856
- else if (state.token == null) {
857
- // Only check cache if there was no token. If the token was invalid,
858
- // skip this and rely on exchange endpoint.
859
- void state
860
- .cachedTokenPromise // Storage token promise. Always populated in `activate()`.
861
- .then(function (cachedToken) {
862
- if (cachedToken && isValid(cachedToken)) {
863
- listener({ token: cachedToken.token });
864
- }
890
+ .then(function () {
891
+ listener({ token: validToken_1.token });
892
+ initTokenRefresher(appCheck);
865
893
  })
866
894
  .catch(function () {
867
- /** Ignore errors in listeners. */
895
+ /* we don't care about exceptions thrown in listeners */
868
896
  });
869
897
  }
870
- setState(app, newState);
898
+ /**
899
+ * Wait for any cached token promise to resolve before starting the token
900
+ * refresher. The refresher checks to see if there is an existing token
901
+ * in state and calls the exchange endpoint if not. We should first let the
902
+ * IndexedDB check have a chance to populate state if it can.
903
+ *
904
+ * Listener call isn't needed here because cachedTokenPromise will call any
905
+ * listeners that exist when it resolves.
906
+ */
907
+ // state.cachedTokenPromise is always populated in `activate()`.
908
+ void state.cachedTokenPromise.then(function () { return initTokenRefresher(appCheck); });
871
909
  }
872
910
  function removeTokenListener(app, listener) {
873
911
  var state = getState(app);
@@ -879,6 +917,23 @@ function removeTokenListener(app, listener) {
879
917
  }
880
918
  setState(app, __assign(__assign({}, state), { tokenObservers: newObservers }));
881
919
  }
920
+ /**
921
+ * Logic to create and start refresher as needed.
922
+ */
923
+ function initTokenRefresher(appCheck) {
924
+ var app = appCheck.app;
925
+ var state = getState(app);
926
+ // Create the refresher but don't start it if `isTokenAutoRefreshEnabled`
927
+ // is not true.
928
+ var refresher = state.tokenRefresher;
929
+ if (!refresher) {
930
+ refresher = createTokenRefresher(appCheck);
931
+ setState(app, __assign(__assign({}, state), { tokenRefresher: refresher }));
932
+ }
933
+ if (!refresher.isRunning() && state.isTokenAutoRefreshEnabled) {
934
+ refresher.start();
935
+ }
936
+ }
882
937
  function createTokenRefresher(appCheck) {
883
938
  var _this = this;
884
939
  var app = appCheck.app;
@@ -909,7 +964,6 @@ function createTokenRefresher(appCheck) {
909
964
  }
910
965
  });
911
966
  }); }, function () {
912
- // TODO: when should we retry?
913
967
  return true;
914
968
  }, function () {
915
969
  var state = getState(app);
@@ -1010,7 +1064,7 @@ function internalFactory(appCheck) {
1010
1064
  }
1011
1065
 
1012
1066
  var name = "@firebase/app-check";
1013
- var version = "0.5.1";
1067
+ var version = "0.5.2";
1014
1068
 
1015
1069
  /**
1016
1070
  * @license
@@ -1177,23 +1231,53 @@ var ReCaptchaV3Provider = /** @class */ (function () {
1177
1231
  */
1178
1232
  function ReCaptchaV3Provider(_siteKey) {
1179
1233
  this._siteKey = _siteKey;
1234
+ /**
1235
+ * Throttle requests on certain error codes to prevent too many retries
1236
+ * in a short time.
1237
+ */
1238
+ this._throttleData = null;
1180
1239
  }
1181
1240
  /**
1182
1241
  * Returns an App Check token.
1183
1242
  * @internal
1184
1243
  */
1185
1244
  ReCaptchaV3Provider.prototype.getToken = function () {
1245
+ var _a;
1186
1246
  return __awaiter(this, void 0, void 0, function () {
1187
- var attestedClaimsToken;
1188
- return __generator(this, function (_a) {
1189
- switch (_a.label) {
1190
- case 0: return [4 /*yield*/, getToken$1(this._app).catch(function (_e) {
1191
- // reCaptcha.execute() throws null which is not very descriptive.
1192
- throw ERROR_FACTORY.create("recaptcha-error" /* RECAPTCHA_ERROR */);
1193
- })];
1247
+ var attestedClaimsToken, result, e_1;
1248
+ return __generator(this, function (_b) {
1249
+ switch (_b.label) {
1250
+ case 0:
1251
+ throwIfThrottled(this._throttleData);
1252
+ return [4 /*yield*/, getToken$1(this._app).catch(function (_e) {
1253
+ // reCaptcha.execute() throws null which is not very descriptive.
1254
+ throw ERROR_FACTORY.create("recaptcha-error" /* RECAPTCHA_ERROR */);
1255
+ })];
1194
1256
  case 1:
1195
- attestedClaimsToken = _a.sent();
1196
- return [2 /*return*/, exchangeToken(getExchangeRecaptchaV3TokenRequest(this._app, attestedClaimsToken), this._platformLoggerProvider)];
1257
+ attestedClaimsToken = _b.sent();
1258
+ _b.label = 2;
1259
+ case 2:
1260
+ _b.trys.push([2, 4, , 5]);
1261
+ return [4 /*yield*/, exchangeToken(getExchangeRecaptchaV3TokenRequest(this._app, attestedClaimsToken), this._platformLoggerProvider)];
1262
+ case 3:
1263
+ result = _b.sent();
1264
+ return [3 /*break*/, 5];
1265
+ case 4:
1266
+ e_1 = _b.sent();
1267
+ if (e_1.code === "fetch-status-error" /* FETCH_STATUS_ERROR */) {
1268
+ this._throttleData = setBackoff(Number((_a = e_1.customData) === null || _a === void 0 ? void 0 : _a.httpStatus), this._throttleData);
1269
+ throw ERROR_FACTORY.create("throttled" /* THROTTLED */, {
1270
+ time: getDurationString(this._throttleData.allowRequestsAfter - Date.now()),
1271
+ httpStatus: this._throttleData.httpStatus
1272
+ });
1273
+ }
1274
+ else {
1275
+ throw e_1;
1276
+ }
1277
+ case 5:
1278
+ // If successful, clear throttle data.
1279
+ this._throttleData = null;
1280
+ return [2 /*return*/, result];
1197
1281
  }
1198
1282
  });
1199
1283
  });
@@ -1234,23 +1318,53 @@ var ReCaptchaEnterpriseProvider = /** @class */ (function () {
1234
1318
  */
1235
1319
  function ReCaptchaEnterpriseProvider(_siteKey) {
1236
1320
  this._siteKey = _siteKey;
1321
+ /**
1322
+ * Throttle requests on certain error codes to prevent too many retries
1323
+ * in a short time.
1324
+ */
1325
+ this._throttleData = null;
1237
1326
  }
1238
1327
  /**
1239
1328
  * Returns an App Check token.
1240
1329
  * @internal
1241
1330
  */
1242
1331
  ReCaptchaEnterpriseProvider.prototype.getToken = function () {
1332
+ var _a;
1243
1333
  return __awaiter(this, void 0, void 0, function () {
1244
- var attestedClaimsToken;
1245
- return __generator(this, function (_a) {
1246
- switch (_a.label) {
1247
- case 0: return [4 /*yield*/, getToken$1(this._app).catch(function (_e) {
1248
- // reCaptcha.execute() throws null which is not very descriptive.
1249
- throw ERROR_FACTORY.create("recaptcha-error" /* RECAPTCHA_ERROR */);
1250
- })];
1334
+ var attestedClaimsToken, result, e_2;
1335
+ return __generator(this, function (_b) {
1336
+ switch (_b.label) {
1337
+ case 0:
1338
+ throwIfThrottled(this._throttleData);
1339
+ return [4 /*yield*/, getToken$1(this._app).catch(function (_e) {
1340
+ // reCaptcha.execute() throws null which is not very descriptive.
1341
+ throw ERROR_FACTORY.create("recaptcha-error" /* RECAPTCHA_ERROR */);
1342
+ })];
1251
1343
  case 1:
1252
- attestedClaimsToken = _a.sent();
1253
- return [2 /*return*/, exchangeToken(getExchangeRecaptchaEnterpriseTokenRequest(this._app, attestedClaimsToken), this._platformLoggerProvider)];
1344
+ attestedClaimsToken = _b.sent();
1345
+ _b.label = 2;
1346
+ case 2:
1347
+ _b.trys.push([2, 4, , 5]);
1348
+ return [4 /*yield*/, exchangeToken(getExchangeRecaptchaEnterpriseTokenRequest(this._app, attestedClaimsToken), this._platformLoggerProvider)];
1349
+ case 3:
1350
+ result = _b.sent();
1351
+ return [3 /*break*/, 5];
1352
+ case 4:
1353
+ e_2 = _b.sent();
1354
+ if (e_2.code === "fetch-status-error" /* FETCH_STATUS_ERROR */) {
1355
+ this._throttleData = setBackoff(Number((_a = e_2.customData) === null || _a === void 0 ? void 0 : _a.httpStatus), this._throttleData);
1356
+ throw ERROR_FACTORY.create("throttled" /* THROTTLED */, {
1357
+ time: getDurationString(this._throttleData.allowRequestsAfter - Date.now()),
1358
+ httpStatus: this._throttleData.httpStatus
1359
+ });
1360
+ }
1361
+ else {
1362
+ throw e_2;
1363
+ }
1364
+ case 5:
1365
+ // If successful, clear throttle data.
1366
+ this._throttleData = null;
1367
+ return [2 /*return*/, result];
1254
1368
  }
1255
1369
  });
1256
1370
  });
@@ -1327,7 +1441,58 @@ var CustomProvider = /** @class */ (function () {
1327
1441
  }
1328
1442
  };
1329
1443
  return CustomProvider;
1330
- }());
1444
+ }());
1445
+ /**
1446
+ * Set throttle data to block requests until after a certain time
1447
+ * depending on the failed request's status code.
1448
+ * @param httpStatus - Status code of failed request.
1449
+ * @param throttleData - `ThrottleData` object containing previous throttle
1450
+ * data state.
1451
+ * @returns Data about current throttle state and expiration time.
1452
+ */
1453
+ function setBackoff(httpStatus, throttleData) {
1454
+ /**
1455
+ * Block retries for 1 day for the following error codes:
1456
+ *
1457
+ * 404: Likely malformed URL.
1458
+ *
1459
+ * 403:
1460
+ * - Attestation failed
1461
+ * - Wrong API key
1462
+ * - Project deleted
1463
+ */
1464
+ if (httpStatus === 404 || httpStatus === 403) {
1465
+ return {
1466
+ backoffCount: 1,
1467
+ allowRequestsAfter: Date.now() + ONE_DAY,
1468
+ httpStatus: httpStatus
1469
+ };
1470
+ }
1471
+ else {
1472
+ /**
1473
+ * For all other error codes, the time when it is ok to retry again
1474
+ * is based on exponential backoff.
1475
+ */
1476
+ var backoffCount = throttleData ? throttleData.backoffCount : 0;
1477
+ var backoffMillis = calculateBackoffMillis(backoffCount, 1000, 2);
1478
+ return {
1479
+ backoffCount: backoffCount + 1,
1480
+ allowRequestsAfter: Date.now() + backoffMillis,
1481
+ httpStatus: httpStatus
1482
+ };
1483
+ }
1484
+ }
1485
+ function throwIfThrottled(throttleData) {
1486
+ if (throttleData) {
1487
+ if (Date.now() - throttleData.allowRequestsAfter <= 0) {
1488
+ // If before, throw.
1489
+ throw ERROR_FACTORY.create("throttled" /* THROTTLED */, {
1490
+ time: getDurationString(throttleData.allowRequestsAfter - Date.now()),
1491
+ httpStatus: throttleData.httpStatus
1492
+ });
1493
+ }
1494
+ }
1495
+ }
1331
1496
 
1332
1497
  /**
1333
1498
  * @license
@@ -1384,6 +1549,17 @@ function initializeAppCheck(app, options) {
1384
1549
  }
1385
1550
  var appCheck = provider.initialize({ options: options });
1386
1551
  _activate(app, options.provider, options.isTokenAutoRefreshEnabled);
1552
+ // If isTokenAutoRefreshEnabled is false, do not send any requests to the
1553
+ // exchange endpoint without an explicit call from the user either directly
1554
+ // or through another Firebase library (storage, functions, etc.)
1555
+ if (getState(app).isTokenAutoRefreshEnabled) {
1556
+ // Adding a listener will start the refresher and fetch a token if needed.
1557
+ // This gets a token ready and prevents a delay when an internal library
1558
+ // requests the token.
1559
+ // Listener function does not need to do anything, its base functionality
1560
+ // of calling getToken() already fetches token and writes it to memory/storage.
1561
+ addTokenListener(appCheck, "INTERNAL" /* INTERNAL */, function () { });
1562
+ }
1387
1563
  return appCheck;
1388
1564
  }
1389
1565
  /**
@@ -1403,6 +1579,8 @@ function _activate(app, provider, isTokenAutoRefreshEnabled) {
1403
1579
  newState.cachedTokenPromise = readTokenFromStorage(app).then(function (cachedToken) {
1404
1580
  if (cachedToken && isValid(cachedToken)) {
1405
1581
  setState(app, __assign(__assign({}, getState(app)), { token: cachedToken }));
1582
+ // notify all listeners with the cached token
1583
+ notifyTokenListeners(app, { token: cachedToken.token });
1406
1584
  }
1407
1585
  return cachedToken;
1408
1586
  });