@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/dist/index.cjs.js CHANGED
@@ -78,7 +78,11 @@ var TOKEN_REFRESH_TIME = {
78
78
  * This is the maximum retrial wait, currently 16 minutes.
79
79
  */
80
80
  RETRIAL_MAX_WAIT: 16 * 60 * 1000
81
- };
81
+ };
82
+ /**
83
+ * One day in millis, for certain error code backoffs.
84
+ */
85
+ var ONE_DAY = 24 * 60 * 60 * 1000;
82
86
 
83
87
  /**
84
88
  * @license
@@ -242,6 +246,7 @@ var ERRORS = (_a = {},
242
246
  _a["storage-get" /* STORAGE_GET */] = 'Error thrown when reading from storage. Original error: {$originalErrorMessage}.',
243
247
  _a["storage-set" /* STORAGE_WRITE */] = 'Error thrown when writing to storage. Original error: {$originalErrorMessage}.',
244
248
  _a["recaptcha-error" /* RECAPTCHA_ERROR */] = 'ReCAPTCHA error.',
249
+ _a["throttled" /* THROTTLED */] = "Requests throttled due to {$httpStatus} error. Attempts allowed again after {$time}",
245
250
  _a);
246
251
  var ERROR_FACTORY = new util.ErrorFactory('appCheck', 'AppCheck', ERRORS);
247
252
 
@@ -284,6 +289,28 @@ function uuidv4() {
284
289
  var r = (Math.random() * 16) | 0, v = c === 'x' ? r : (r & 0x3) | 0x8;
285
290
  return v.toString(16);
286
291
  });
292
+ }
293
+ function getDurationString(durationInMillis) {
294
+ var totalSeconds = Math.round(durationInMillis / 1000);
295
+ var days = Math.floor(totalSeconds / (3600 * 24));
296
+ var hours = Math.floor((totalSeconds - days * 3600 * 24) / 3600);
297
+ var minutes = Math.floor((totalSeconds - days * 3600 * 24 - hours * 3600) / 60);
298
+ var seconds = totalSeconds - days * 3600 * 24 - hours * 3600 - minutes * 60;
299
+ var result = '';
300
+ if (days) {
301
+ result += pad(days) + 'd:';
302
+ }
303
+ if (hours) {
304
+ result += pad(hours) + 'h:';
305
+ }
306
+ result += pad(minutes) + 'm:' + pad(seconds) + 's';
307
+ return result;
308
+ }
309
+ function pad(value) {
310
+ if (value === 0) {
311
+ return '00';
312
+ }
313
+ return value >= 10 ? value.toString() : '0' + value;
287
314
  }
288
315
 
289
316
  /**
@@ -744,9 +771,9 @@ function formatDummyToken(tokenErrorData) {
744
771
  function getToken$2(appCheck, forceRefresh) {
745
772
  if (forceRefresh === void 0) { forceRefresh = false; }
746
773
  return tslib.__awaiter(this, void 0, void 0, function () {
747
- var app, state, token, error, cachedToken, tokenFromDebugExchange, _a, _b, _c, e_1, interopTokenResult;
748
- return tslib.__generator(this, function (_d) {
749
- switch (_d.label) {
774
+ var app, state, token, error, cachedToken, shouldCallListeners, _a, _b, _c, _d, tokenFromDebugExchange, e_1, interopTokenResult;
775
+ return tslib.__generator(this, function (_e) {
776
+ switch (_e.label) {
750
777
  case 0:
751
778
  app = appCheck.app;
752
779
  ensureActivated(app);
@@ -756,14 +783,11 @@ function getToken$2(appCheck, forceRefresh) {
756
783
  if (!!token) return [3 /*break*/, 2];
757
784
  return [4 /*yield*/, state.cachedTokenPromise];
758
785
  case 1:
759
- cachedToken = _d.sent();
786
+ cachedToken = _e.sent();
760
787
  if (cachedToken && isValid(cachedToken)) {
761
788
  token = cachedToken;
762
- setState(app, tslib.__assign(tslib.__assign({}, state), { token: token }));
763
- // notify all listeners with the cached token
764
- notifyTokenListeners(app, { token: token.token });
765
789
  }
766
- _d.label = 2;
790
+ _e.label = 2;
767
791
  case 2:
768
792
  // Return the cached token (from either memory or indexedDB) if it's valid
769
793
  if (!forceRefresh && token && isValid(token)) {
@@ -771,44 +795,69 @@ function getToken$2(appCheck, forceRefresh) {
771
795
  token: token.token
772
796
  }];
773
797
  }
774
- if (!isDebugMode()) return [3 /*break*/, 6];
775
- _a = exchangeToken;
776
- _b = getExchangeDebugTokenRequest;
777
- _c = [app];
798
+ shouldCallListeners = false;
799
+ if (!isDebugMode()) return [3 /*break*/, 7];
800
+ if (!!state.exchangeTokenPromise) return [3 /*break*/, 4];
801
+ _a = state;
802
+ _b = exchangeToken;
803
+ _c = getExchangeDebugTokenRequest;
804
+ _d = [app];
778
805
  return [4 /*yield*/, getDebugToken()];
779
- case 3: return [4 /*yield*/, _a.apply(void 0, [_b.apply(void 0, _c.concat([_d.sent()])), appCheck.platformLoggerProvider])];
780
- case 4:
781
- tokenFromDebugExchange = _d.sent();
806
+ case 3:
807
+ _a.exchangeTokenPromise = _b.apply(void 0, [_c.apply(void 0, _d.concat([_e.sent()])), appCheck.platformLoggerProvider]).then(function (token) {
808
+ state.exchangeTokenPromise = undefined;
809
+ return token;
810
+ });
811
+ shouldCallListeners = true;
812
+ _e.label = 4;
813
+ case 4: return [4 /*yield*/, state.exchangeTokenPromise];
814
+ case 5:
815
+ tokenFromDebugExchange = _e.sent();
782
816
  // Write debug token to indexedDB.
783
817
  return [4 /*yield*/, writeTokenToStorage(app, tokenFromDebugExchange)];
784
- case 5:
818
+ case 6:
785
819
  // Write debug token to indexedDB.
786
- _d.sent();
820
+ _e.sent();
787
821
  // Write debug token to state.
788
822
  setState(app, tslib.__assign(tslib.__assign({}, state), { token: tokenFromDebugExchange }));
789
823
  return [2 /*return*/, { token: tokenFromDebugExchange.token }];
790
- case 6:
791
- _d.trys.push([6, 8, , 9]);
792
- return [4 /*yield*/, state.provider.getToken()];
793
824
  case 7:
794
- // state.provider is populated in initializeAppCheck()
795
- // ensureActivated() at the top of this function checks that
796
- // initializeAppCheck() has been called.
797
- token = _d.sent();
798
- return [3 /*break*/, 9];
825
+ _e.trys.push([7, 9, , 10]);
826
+ // Avoid making another call to the exchange endpoint if one is in flight.
827
+ if (!state.exchangeTokenPromise) {
828
+ // state.provider is populated in initializeAppCheck()
829
+ // ensureActivated() at the top of this function checks that
830
+ // initializeAppCheck() has been called.
831
+ state.exchangeTokenPromise = state.provider.getToken().then(function (token) {
832
+ state.exchangeTokenPromise = undefined;
833
+ return token;
834
+ });
835
+ shouldCallListeners = true;
836
+ }
837
+ return [4 /*yield*/, state.exchangeTokenPromise];
799
838
  case 8:
800
- e_1 = _d.sent();
801
- // `getToken()` should never throw, but logging error text to console will aid debugging.
802
- logger.error(e_1);
803
- error = e_1;
804
- return [3 /*break*/, 9];
839
+ token = _e.sent();
840
+ return [3 /*break*/, 10];
805
841
  case 9:
806
- if (!!token) return [3 /*break*/, 10];
842
+ e_1 = _e.sent();
843
+ if (e_1.code === "appCheck/" + "throttled" /* THROTTLED */) {
844
+ // Warn if throttled, but do not treat it as an error.
845
+ logger.warn(e_1.message);
846
+ }
847
+ else {
848
+ // `getToken()` should never throw, but logging error text to console will aid debugging.
849
+ logger.error(e_1);
850
+ }
851
+ // Always save error to be added to dummy token.
852
+ error = e_1;
853
+ return [3 /*break*/, 10];
854
+ case 10:
855
+ if (!!token) return [3 /*break*/, 11];
807
856
  // if token is undefined, there must be an error.
808
857
  // we return a dummy token along with the error
809
858
  interopTokenResult = makeDummyTokenResult(error);
810
- return [3 /*break*/, 12];
811
- case 10:
859
+ return [3 /*break*/, 13];
860
+ case 11:
812
861
  interopTokenResult = {
813
862
  token: token.token
814
863
  };
@@ -816,11 +865,13 @@ function getToken$2(appCheck, forceRefresh) {
816
865
  // Only do it if we got a valid new token
817
866
  setState(app, tslib.__assign(tslib.__assign({}, state), { token: token }));
818
867
  return [4 /*yield*/, writeTokenToStorage(app, token)];
819
- case 11:
820
- _d.sent();
821
- _d.label = 12;
822
868
  case 12:
823
- notifyTokenListeners(app, interopTokenResult);
869
+ _e.sent();
870
+ _e.label = 13;
871
+ case 13:
872
+ if (shouldCallListeners) {
873
+ notifyTokenListeners(app, interopTokenResult);
874
+ }
824
875
  return [2 /*return*/, interopTokenResult];
825
876
  }
826
877
  });
@@ -834,44 +885,31 @@ function addTokenListener(appCheck, type, listener, onError) {
834
885
  error: onError,
835
886
  type: type
836
887
  };
837
- var newState = tslib.__assign(tslib.__assign({}, state), { tokenObservers: tslib.__spreadArray(tslib.__spreadArray([], state.tokenObservers), [tokenObserver]) });
838
- /**
839
- * Invoke the listener with the valid token, then start the token refresher
840
- */
841
- if (!newState.tokenRefresher) {
842
- var tokenRefresher = createTokenRefresher(appCheck);
843
- newState.tokenRefresher = tokenRefresher;
844
- }
845
- // Create the refresher but don't start it if `isTokenAutoRefreshEnabled`
846
- // is not true.
847
- if (!newState.tokenRefresher.isRunning() && state.isTokenAutoRefreshEnabled) {
848
- newState.tokenRefresher.start();
849
- }
888
+ setState(app, tslib.__assign(tslib.__assign({}, state), { tokenObservers: tslib.__spreadArray(tslib.__spreadArray([], state.tokenObservers), [tokenObserver]) }));
850
889
  // Invoke the listener async immediately if there is a valid token
851
890
  // in memory.
852
891
  if (state.token && isValid(state.token)) {
853
892
  var validToken_1 = state.token;
854
893
  Promise.resolve()
855
- .then(function () { return listener({ token: validToken_1.token }); })
856
- .catch(function () {
857
- /* we don't care about exceptions thrown in listeners */
858
- });
859
- }
860
- else if (state.token == null) {
861
- // Only check cache if there was no token. If the token was invalid,
862
- // skip this and rely on exchange endpoint.
863
- void state
864
- .cachedTokenPromise // Storage token promise. Always populated in `activate()`.
865
- .then(function (cachedToken) {
866
- if (cachedToken && isValid(cachedToken)) {
867
- listener({ token: cachedToken.token });
868
- }
894
+ .then(function () {
895
+ listener({ token: validToken_1.token });
896
+ initTokenRefresher(appCheck);
869
897
  })
870
898
  .catch(function () {
871
- /** Ignore errors in listeners. */
899
+ /* we don't care about exceptions thrown in listeners */
872
900
  });
873
901
  }
874
- setState(app, newState);
902
+ /**
903
+ * Wait for any cached token promise to resolve before starting the token
904
+ * refresher. The refresher checks to see if there is an existing token
905
+ * in state and calls the exchange endpoint if not. We should first let the
906
+ * IndexedDB check have a chance to populate state if it can.
907
+ *
908
+ * Listener call isn't needed here because cachedTokenPromise will call any
909
+ * listeners that exist when it resolves.
910
+ */
911
+ // state.cachedTokenPromise is always populated in `activate()`.
912
+ void state.cachedTokenPromise.then(function () { return initTokenRefresher(appCheck); });
875
913
  }
876
914
  function removeTokenListener(app, listener) {
877
915
  var state = getState(app);
@@ -883,6 +921,23 @@ function removeTokenListener(app, listener) {
883
921
  }
884
922
  setState(app, tslib.__assign(tslib.__assign({}, state), { tokenObservers: newObservers }));
885
923
  }
924
+ /**
925
+ * Logic to create and start refresher as needed.
926
+ */
927
+ function initTokenRefresher(appCheck) {
928
+ var app = appCheck.app;
929
+ var state = getState(app);
930
+ // Create the refresher but don't start it if `isTokenAutoRefreshEnabled`
931
+ // is not true.
932
+ var refresher = state.tokenRefresher;
933
+ if (!refresher) {
934
+ refresher = createTokenRefresher(appCheck);
935
+ setState(app, tslib.__assign(tslib.__assign({}, state), { tokenRefresher: refresher }));
936
+ }
937
+ if (!refresher.isRunning() && state.isTokenAutoRefreshEnabled) {
938
+ refresher.start();
939
+ }
940
+ }
886
941
  function createTokenRefresher(appCheck) {
887
942
  var _this = this;
888
943
  var app = appCheck.app;
@@ -913,7 +968,6 @@ function createTokenRefresher(appCheck) {
913
968
  }
914
969
  });
915
970
  }); }, function () {
916
- // TODO: when should we retry?
917
971
  return true;
918
972
  }, function () {
919
973
  var state = getState(app);
@@ -1014,7 +1068,7 @@ function internalFactory(appCheck) {
1014
1068
  }
1015
1069
 
1016
1070
  var name = "@firebase/app-check";
1017
- var version = "0.5.1";
1071
+ var version = "0.5.2";
1018
1072
 
1019
1073
  /**
1020
1074
  * @license
@@ -1181,23 +1235,53 @@ var ReCaptchaV3Provider = /** @class */ (function () {
1181
1235
  */
1182
1236
  function ReCaptchaV3Provider(_siteKey) {
1183
1237
  this._siteKey = _siteKey;
1238
+ /**
1239
+ * Throttle requests on certain error codes to prevent too many retries
1240
+ * in a short time.
1241
+ */
1242
+ this._throttleData = null;
1184
1243
  }
1185
1244
  /**
1186
1245
  * Returns an App Check token.
1187
1246
  * @internal
1188
1247
  */
1189
1248
  ReCaptchaV3Provider.prototype.getToken = function () {
1249
+ var _a;
1190
1250
  return tslib.__awaiter(this, void 0, void 0, function () {
1191
- var attestedClaimsToken;
1192
- return tslib.__generator(this, function (_a) {
1193
- switch (_a.label) {
1194
- case 0: return [4 /*yield*/, getToken$1(this._app).catch(function (_e) {
1195
- // reCaptcha.execute() throws null which is not very descriptive.
1196
- throw ERROR_FACTORY.create("recaptcha-error" /* RECAPTCHA_ERROR */);
1197
- })];
1251
+ var attestedClaimsToken, result, e_1;
1252
+ return tslib.__generator(this, function (_b) {
1253
+ switch (_b.label) {
1254
+ case 0:
1255
+ throwIfThrottled(this._throttleData);
1256
+ return [4 /*yield*/, getToken$1(this._app).catch(function (_e) {
1257
+ // reCaptcha.execute() throws null which is not very descriptive.
1258
+ throw ERROR_FACTORY.create("recaptcha-error" /* RECAPTCHA_ERROR */);
1259
+ })];
1198
1260
  case 1:
1199
- attestedClaimsToken = _a.sent();
1200
- return [2 /*return*/, exchangeToken(getExchangeRecaptchaV3TokenRequest(this._app, attestedClaimsToken), this._platformLoggerProvider)];
1261
+ attestedClaimsToken = _b.sent();
1262
+ _b.label = 2;
1263
+ case 2:
1264
+ _b.trys.push([2, 4, , 5]);
1265
+ return [4 /*yield*/, exchangeToken(getExchangeRecaptchaV3TokenRequest(this._app, attestedClaimsToken), this._platformLoggerProvider)];
1266
+ case 3:
1267
+ result = _b.sent();
1268
+ return [3 /*break*/, 5];
1269
+ case 4:
1270
+ e_1 = _b.sent();
1271
+ if (e_1.code === "fetch-status-error" /* FETCH_STATUS_ERROR */) {
1272
+ this._throttleData = setBackoff(Number((_a = e_1.customData) === null || _a === void 0 ? void 0 : _a.httpStatus), this._throttleData);
1273
+ throw ERROR_FACTORY.create("throttled" /* THROTTLED */, {
1274
+ time: getDurationString(this._throttleData.allowRequestsAfter - Date.now()),
1275
+ httpStatus: this._throttleData.httpStatus
1276
+ });
1277
+ }
1278
+ else {
1279
+ throw e_1;
1280
+ }
1281
+ case 5:
1282
+ // If successful, clear throttle data.
1283
+ this._throttleData = null;
1284
+ return [2 /*return*/, result];
1201
1285
  }
1202
1286
  });
1203
1287
  });
@@ -1238,23 +1322,53 @@ var ReCaptchaEnterpriseProvider = /** @class */ (function () {
1238
1322
  */
1239
1323
  function ReCaptchaEnterpriseProvider(_siteKey) {
1240
1324
  this._siteKey = _siteKey;
1325
+ /**
1326
+ * Throttle requests on certain error codes to prevent too many retries
1327
+ * in a short time.
1328
+ */
1329
+ this._throttleData = null;
1241
1330
  }
1242
1331
  /**
1243
1332
  * Returns an App Check token.
1244
1333
  * @internal
1245
1334
  */
1246
1335
  ReCaptchaEnterpriseProvider.prototype.getToken = function () {
1336
+ var _a;
1247
1337
  return tslib.__awaiter(this, void 0, void 0, function () {
1248
- var attestedClaimsToken;
1249
- return tslib.__generator(this, function (_a) {
1250
- switch (_a.label) {
1251
- case 0: return [4 /*yield*/, getToken$1(this._app).catch(function (_e) {
1252
- // reCaptcha.execute() throws null which is not very descriptive.
1253
- throw ERROR_FACTORY.create("recaptcha-error" /* RECAPTCHA_ERROR */);
1254
- })];
1338
+ var attestedClaimsToken, result, e_2;
1339
+ return tslib.__generator(this, function (_b) {
1340
+ switch (_b.label) {
1341
+ case 0:
1342
+ throwIfThrottled(this._throttleData);
1343
+ return [4 /*yield*/, getToken$1(this._app).catch(function (_e) {
1344
+ // reCaptcha.execute() throws null which is not very descriptive.
1345
+ throw ERROR_FACTORY.create("recaptcha-error" /* RECAPTCHA_ERROR */);
1346
+ })];
1255
1347
  case 1:
1256
- attestedClaimsToken = _a.sent();
1257
- return [2 /*return*/, exchangeToken(getExchangeRecaptchaEnterpriseTokenRequest(this._app, attestedClaimsToken), this._platformLoggerProvider)];
1348
+ attestedClaimsToken = _b.sent();
1349
+ _b.label = 2;
1350
+ case 2:
1351
+ _b.trys.push([2, 4, , 5]);
1352
+ return [4 /*yield*/, exchangeToken(getExchangeRecaptchaEnterpriseTokenRequest(this._app, attestedClaimsToken), this._platformLoggerProvider)];
1353
+ case 3:
1354
+ result = _b.sent();
1355
+ return [3 /*break*/, 5];
1356
+ case 4:
1357
+ e_2 = _b.sent();
1358
+ if (e_2.code === "fetch-status-error" /* FETCH_STATUS_ERROR */) {
1359
+ this._throttleData = setBackoff(Number((_a = e_2.customData) === null || _a === void 0 ? void 0 : _a.httpStatus), this._throttleData);
1360
+ throw ERROR_FACTORY.create("throttled" /* THROTTLED */, {
1361
+ time: getDurationString(this._throttleData.allowRequestsAfter - Date.now()),
1362
+ httpStatus: this._throttleData.httpStatus
1363
+ });
1364
+ }
1365
+ else {
1366
+ throw e_2;
1367
+ }
1368
+ case 5:
1369
+ // If successful, clear throttle data.
1370
+ this._throttleData = null;
1371
+ return [2 /*return*/, result];
1258
1372
  }
1259
1373
  });
1260
1374
  });
@@ -1331,7 +1445,58 @@ var CustomProvider = /** @class */ (function () {
1331
1445
  }
1332
1446
  };
1333
1447
  return CustomProvider;
1334
- }());
1448
+ }());
1449
+ /**
1450
+ * Set throttle data to block requests until after a certain time
1451
+ * depending on the failed request's status code.
1452
+ * @param httpStatus - Status code of failed request.
1453
+ * @param throttleData - `ThrottleData` object containing previous throttle
1454
+ * data state.
1455
+ * @returns Data about current throttle state and expiration time.
1456
+ */
1457
+ function setBackoff(httpStatus, throttleData) {
1458
+ /**
1459
+ * Block retries for 1 day for the following error codes:
1460
+ *
1461
+ * 404: Likely malformed URL.
1462
+ *
1463
+ * 403:
1464
+ * - Attestation failed
1465
+ * - Wrong API key
1466
+ * - Project deleted
1467
+ */
1468
+ if (httpStatus === 404 || httpStatus === 403) {
1469
+ return {
1470
+ backoffCount: 1,
1471
+ allowRequestsAfter: Date.now() + ONE_DAY,
1472
+ httpStatus: httpStatus
1473
+ };
1474
+ }
1475
+ else {
1476
+ /**
1477
+ * For all other error codes, the time when it is ok to retry again
1478
+ * is based on exponential backoff.
1479
+ */
1480
+ var backoffCount = throttleData ? throttleData.backoffCount : 0;
1481
+ var backoffMillis = util.calculateBackoffMillis(backoffCount, 1000, 2);
1482
+ return {
1483
+ backoffCount: backoffCount + 1,
1484
+ allowRequestsAfter: Date.now() + backoffMillis,
1485
+ httpStatus: httpStatus
1486
+ };
1487
+ }
1488
+ }
1489
+ function throwIfThrottled(throttleData) {
1490
+ if (throttleData) {
1491
+ if (Date.now() - throttleData.allowRequestsAfter <= 0) {
1492
+ // If before, throw.
1493
+ throw ERROR_FACTORY.create("throttled" /* THROTTLED */, {
1494
+ time: getDurationString(throttleData.allowRequestsAfter - Date.now()),
1495
+ httpStatus: throttleData.httpStatus
1496
+ });
1497
+ }
1498
+ }
1499
+ }
1335
1500
 
1336
1501
  /**
1337
1502
  * @license
@@ -1388,6 +1553,17 @@ function initializeAppCheck(app$1, options) {
1388
1553
  }
1389
1554
  var appCheck = provider.initialize({ options: options });
1390
1555
  _activate(app$1, options.provider, options.isTokenAutoRefreshEnabled);
1556
+ // If isTokenAutoRefreshEnabled is false, do not send any requests to the
1557
+ // exchange endpoint without an explicit call from the user either directly
1558
+ // or through another Firebase library (storage, functions, etc.)
1559
+ if (getState(app$1).isTokenAutoRefreshEnabled) {
1560
+ // Adding a listener will start the refresher and fetch a token if needed.
1561
+ // This gets a token ready and prevents a delay when an internal library
1562
+ // requests the token.
1563
+ // Listener function does not need to do anything, its base functionality
1564
+ // of calling getToken() already fetches token and writes it to memory/storage.
1565
+ addTokenListener(appCheck, "INTERNAL" /* INTERNAL */, function () { });
1566
+ }
1391
1567
  return appCheck;
1392
1568
  }
1393
1569
  /**
@@ -1407,6 +1583,8 @@ function _activate(app, provider, isTokenAutoRefreshEnabled) {
1407
1583
  newState.cachedTokenPromise = readTokenFromStorage(app).then(function (cachedToken) {
1408
1584
  if (cachedToken && isValid(cachedToken)) {
1409
1585
  setState(app, tslib.__assign(tslib.__assign({}, getState(app)), { token: cachedToken }));
1586
+ // notify all listeners with the cached token
1587
+ notifyTokenListeners(app, { token: cachedToken.token });
1410
1588
  }
1411
1589
  return cachedToken;
1412
1590
  });