@oxyhq/core 12.11.0 → 12.11.1

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.
@@ -56,11 +56,30 @@ const CSRF_FETCH_MAX_ATTEMPTS = 2;
56
56
  const CSRF_FETCH_RETRY_DELAY_MS = 500;
57
57
  /**
58
58
  * Cooldown (ms) applied after a failed access-token refresh before another
59
- * refresh is attempted. Prevents a refresh storm (and server hammering) when
59
+ * refresh is attempted while the CURRENT token is still valid (a proactive,
60
+ * near-expiry refresh). Prevents a refresh storm (and server hammering) when
60
61
  * the auth refresh handler is failing — every in-flight request that
61
- * hits a 401 would otherwise trigger its own refresh.
62
+ * hits a 401 would otherwise trigger its own refresh. A still-valid token can
63
+ * afford to wait this out; the request keeps carrying it in the meantime.
62
64
  */
63
65
  const TOKEN_REFRESH_COOLDOWN_MS = 15000;
66
+ /**
67
+ * Cooldown (ms) applied after a failed refresh when the CURRENT access token is
68
+ * already past its `exp`. Much shorter than {@link TOKEN_REFRESH_COOLDOWN_MS}:
69
+ * an expired token is UNUSABLE, so the client must re-mint as soon as the mint
70
+ * endpoint is reachable again (e.g. a few seconds after an ECS rolling-deploy
71
+ * blip drains/restarts a task) instead of waiting out the full proactive
72
+ * cooldown while every request forwards or omits a stale bearer → server 401.
73
+ *
74
+ * Still NON-ZERO on purpose: it bounds the request-driven retry rate to at most
75
+ * one attempt per this interval so a PROLONGED outage cannot become a tight
76
+ * network storm. Combined with the process-wide single-flight below (concurrent
77
+ * requests coalesce to one in-flight mint) and the refresh handler's own
78
+ * terminal-state handling (a genuinely revoked session clears its device
79
+ * credential and stops issuing network mints), this recovers a transient blip
80
+ * ~15× faster without weakening the storm guard.
81
+ */
82
+ const EXPIRED_TOKEN_REFRESH_COOLDOWN_MS = 1000;
64
83
  /**
65
84
  * Lead time (seconds) before access-token expiry at which a preflight refresh
66
85
  * is triggered. A token within this window of `exp` is treated as effectively
@@ -133,7 +152,15 @@ class TokenStore {
133
152
  class HttpService {
134
153
  constructor(config) {
135
154
  this.tokenRefreshPromise = null;
136
- this.tokenRefreshCooldownUntil = 0;
155
+ /**
156
+ * Epoch ms of the last FAILED refresh (0 = none since the last success). The
157
+ * post-failure cooldown is measured from here; its length depends on whether
158
+ * the current token is still valid ({@link TOKEN_REFRESH_COOLDOWN_MS}) or
159
+ * already expired ({@link EXPIRED_TOKEN_REFRESH_COOLDOWN_MS}), so an expired
160
+ * token recovers promptly the instant it crosses `exp` — without storing a
161
+ * fixed deadline that could not shrink once the token expired mid-cooldown.
162
+ */
163
+ this.lastRefreshFailureAt = 0;
137
164
  this.authRefreshHandler = null;
138
165
  this.accessTokenProvider = null;
139
166
  this.deviceSecretMintInFlight = null;
@@ -823,26 +850,38 @@ class HttpService {
823
850
  if (!this.authRefreshHandler) {
824
851
  return null;
825
852
  }
826
- if (Date.now() < this.tokenRefreshCooldownUntil) {
853
+ // Post-failure cooldown. A genuinely EXPIRED current token uses a much
854
+ // shorter cooldown than a still-valid (proactive, near-expiry) one: an
855
+ // expired token is unusable, so re-mint as soon as the endpoint is reachable
856
+ // again rather than waiting out the full window while requests carry a stale
857
+ // bearer. Both cooldowns are measured from the last failure, so the moment a
858
+ // still-valid token crosses `exp` mid-cooldown the shorter window applies.
859
+ const cooldownMs = this.isAccessTokenExpired()
860
+ ? EXPIRED_TOKEN_REFRESH_COOLDOWN_MS
861
+ : TOKEN_REFRESH_COOLDOWN_MS;
862
+ if (Date.now() - this.lastRefreshFailureAt < cooldownMs) {
827
863
  return null;
828
864
  }
829
865
  if (!this.tokenRefreshPromise) {
830
866
  this.tokenRefreshPromise = this.authRefreshHandler(reason)
831
867
  .then((newToken) => {
832
868
  if (!newToken) {
833
- this.tokenRefreshCooldownUntil = Date.now() + TOKEN_REFRESH_COOLDOWN_MS;
869
+ this.lastRefreshFailureAt = Date.now();
834
870
  return null;
835
871
  }
836
872
  if (this.tokenStore.getAccessToken() !== newToken) {
837
873
  this.tokenStore.setTokens(newToken);
838
874
  this.notifyTokenChange();
839
875
  }
876
+ // A success clears the failure timestamp so the next refresh is never
877
+ // throttled by a stale cooldown.
878
+ this.lastRefreshFailureAt = 0;
840
879
  this.logger.debug('Token refreshed via the auth refresh handler');
841
880
  return newToken;
842
881
  })
843
882
  .catch((error) => {
844
883
  this.logger.warn('Token refresh failed:', error);
845
- this.tokenRefreshCooldownUntil = Date.now() + TOKEN_REFRESH_COOLDOWN_MS;
884
+ this.lastRefreshFailureAt = Date.now();
846
885
  return null;
847
886
  })
848
887
  .finally(() => {
@@ -851,6 +890,27 @@ class HttpService {
851
890
  }
852
891
  return this.tokenRefreshPromise;
853
892
  }
893
+ /**
894
+ * Whether the CURRENT stored access token is already past its `exp`. Drives
895
+ * the shorter post-failure refresh cooldown ({@link EXPIRED_TOKEN_REFRESH_COOLDOWN_MS}):
896
+ * a still-valid (near-expiry) token can wait out the full cooldown, but an
897
+ * expired one must re-mint promptly. Returns `false` for an absent or
898
+ * opaque/no-`exp` token — no proof it is expired, so keep the conservative
899
+ * (longer) cooldown and avoid an unnecessary retry loop.
900
+ */
901
+ isAccessTokenExpired() {
902
+ const token = this.tokenStore.getAccessToken();
903
+ if (!token) {
904
+ return false;
905
+ }
906
+ try {
907
+ const decoded = (0, jwt_decode_1.jwtDecode)(token);
908
+ return typeof decoded.exp === 'number' && decoded.exp <= Math.floor(Date.now() / 1000);
909
+ }
910
+ catch {
911
+ return false;
912
+ }
913
+ }
854
914
  /**
855
915
  * PROCESS-WIDE single-flight for the rotating device-secret mint
856
916
  * (`POST /session/device/token`).
@@ -152,6 +152,9 @@ function OxyServicesPrivacyMixin(Base) {
152
152
  cache: false,
153
153
  });
154
154
  this.clearCacheEntry('GET:/privacy/restricted');
155
+ // The restriction changed the viewer's graph (`restrictedIds`) — bust the
156
+ // cached consolidated `GET /users/me/graph` so the next read reflects it.
157
+ this.clearCacheEntry('GET:/users/me/graph');
155
158
  return result;
156
159
  }
157
160
  catch (error) {
@@ -175,6 +178,9 @@ function OxyServicesPrivacyMixin(Base) {
175
178
  cache: false,
176
179
  });
177
180
  this.clearCacheEntry('GET:/privacy/restricted');
181
+ // Symmetric to restrictUser: the unrestrict changed the viewer's
182
+ // `restrictedIds`, so bust the consolidated `GET /users/me/graph` cache.
183
+ this.clearCacheEntry('GET:/users/me/graph');
178
184
  return result;
179
185
  }
180
186
  catch (error) {
@@ -821,6 +821,7 @@ function OxyServicesUserMixin(Base) {
821
821
  followingIds: graph?.followingIds || [],
822
822
  mutualIds: graph?.mutualIds || [],
823
823
  blockedIds: graph?.blockedIds || [],
824
+ restrictedIds: graph?.restrictedIds || [],
824
825
  };
825
826
  }
826
827
  catch (error) {