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