@sendoracloud/sdk-react-native 0.12.0 → 0.13.0

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 CHANGED
@@ -719,6 +719,20 @@ var Auth = class {
719
719
  this.inflight = next.catch(() => void 0);
720
720
  return next;
721
721
  }
722
+ /**
723
+ * Hot-path token refresh. Single-flight via `refreshInflight` so a
724
+ * UI batch firing 10 simultaneous reads never produces 10 /refresh
725
+ * round-trips. Returns null on any failure so callers can decide
726
+ * whether to surface a sign-in prompt or proceed anonymously.
727
+ *
728
+ * s58.46 — on a `INVALID_REFRESH_TOKEN` (new backend error code) or
729
+ * HTTP 401 we WIPE the stored refresh token. Pre-s58.46 we caught
730
+ * the error and returned null but kept the stored token intact, so
731
+ * the very next caller would re-attempt the same dead token in an
732
+ * infinite loop (Pulse News iOS hammered /refresh ~1×/s for hours).
733
+ * The wipe forces the next op to fall through to /anonymous or
734
+ * surface NOT_SIGNED_IN to the host app instead of looping.
735
+ */
722
736
  async refreshAccessToken() {
723
737
  if (this.refreshInflight) return this.refreshInflight;
724
738
  const refresh = this.hooks.storage.get(REFRESH_KEY);
@@ -727,14 +741,18 @@ var Auth = class {
727
741
  try {
728
742
  const data = await this.callAuth("/api/v1/auth-service/token/refresh", {
729
743
  refreshToken: refresh
730
- }).catch(() => null);
731
- if (!data) return null;
744
+ });
732
745
  this.accessToken = data.tokens.accessToken;
733
746
  this.accessExpiresAt = Date.now() + data.tokens.expiresIn * 1e3;
734
747
  this.hooks.storage.set(TOKEN_KEY, data.tokens.accessToken);
735
748
  this.hooks.storage.set(TOKEN_EXPIRES_KEY, String(this.accessExpiresAt));
736
749
  this.hooks.storage.set(REFRESH_KEY, data.tokens.refreshToken);
737
750
  return data.tokens.accessToken;
751
+ } catch (err) {
752
+ if (err instanceof AuthError && isDeadRefreshError(err.code)) {
753
+ await this.wipeLocalIdentity().catch(() => void 0);
754
+ }
755
+ return null;
738
756
  } finally {
739
757
  this.refreshInflight = null;
740
758
  }
@@ -785,6 +803,10 @@ var Auth = class {
785
803
  await this.hooks.onAnonymousWipe();
786
804
  }
787
805
  };
806
+ function isDeadRefreshError(code) {
807
+ if (!code) return false;
808
+ return code === "INVALID_REFRESH_TOKEN" || code === "HTTP_401" || code === "UNAUTHORIZED" || code === "RATE_LIMIT_EXCEEDED" || code === "RATE_LIMIT";
809
+ }
788
810
  function decodeJwtPayload(token) {
789
811
  try {
790
812
  const parts = token.split(".");
package/dist/index.d.cts CHANGED
@@ -270,6 +270,20 @@ declare class Auth {
270
270
  */
271
271
  signOut(): Promise<void>;
272
272
  private serialize;
273
+ /**
274
+ * Hot-path token refresh. Single-flight via `refreshInflight` so a
275
+ * UI batch firing 10 simultaneous reads never produces 10 /refresh
276
+ * round-trips. Returns null on any failure so callers can decide
277
+ * whether to surface a sign-in prompt or proceed anonymously.
278
+ *
279
+ * s58.46 — on a `INVALID_REFRESH_TOKEN` (new backend error code) or
280
+ * HTTP 401 we WIPE the stored refresh token. Pre-s58.46 we caught
281
+ * the error and returned null but kept the stored token intact, so
282
+ * the very next caller would re-attempt the same dead token in an
283
+ * infinite loop (Pulse News iOS hammered /refresh ~1×/s for hours).
284
+ * The wipe forces the next op to fall through to /anonymous or
285
+ * surface NOT_SIGNED_IN to the host app instead of looping.
286
+ */
273
287
  private refreshAccessToken;
274
288
  private callAuth;
275
289
  private persist;
package/dist/index.d.ts CHANGED
@@ -270,6 +270,20 @@ declare class Auth {
270
270
  */
271
271
  signOut(): Promise<void>;
272
272
  private serialize;
273
+ /**
274
+ * Hot-path token refresh. Single-flight via `refreshInflight` so a
275
+ * UI batch firing 10 simultaneous reads never produces 10 /refresh
276
+ * round-trips. Returns null on any failure so callers can decide
277
+ * whether to surface a sign-in prompt or proceed anonymously.
278
+ *
279
+ * s58.46 — on a `INVALID_REFRESH_TOKEN` (new backend error code) or
280
+ * HTTP 401 we WIPE the stored refresh token. Pre-s58.46 we caught
281
+ * the error and returned null but kept the stored token intact, so
282
+ * the very next caller would re-attempt the same dead token in an
283
+ * infinite loop (Pulse News iOS hammered /refresh ~1×/s for hours).
284
+ * The wipe forces the next op to fall through to /anonymous or
285
+ * surface NOT_SIGNED_IN to the host app instead of looping.
286
+ */
273
287
  private refreshAccessToken;
274
288
  private callAuth;
275
289
  private persist;
package/dist/index.js CHANGED
@@ -679,6 +679,20 @@ var Auth = class {
679
679
  this.inflight = next.catch(() => void 0);
680
680
  return next;
681
681
  }
682
+ /**
683
+ * Hot-path token refresh. Single-flight via `refreshInflight` so a
684
+ * UI batch firing 10 simultaneous reads never produces 10 /refresh
685
+ * round-trips. Returns null on any failure so callers can decide
686
+ * whether to surface a sign-in prompt or proceed anonymously.
687
+ *
688
+ * s58.46 — on a `INVALID_REFRESH_TOKEN` (new backend error code) or
689
+ * HTTP 401 we WIPE the stored refresh token. Pre-s58.46 we caught
690
+ * the error and returned null but kept the stored token intact, so
691
+ * the very next caller would re-attempt the same dead token in an
692
+ * infinite loop (Pulse News iOS hammered /refresh ~1×/s for hours).
693
+ * The wipe forces the next op to fall through to /anonymous or
694
+ * surface NOT_SIGNED_IN to the host app instead of looping.
695
+ */
682
696
  async refreshAccessToken() {
683
697
  if (this.refreshInflight) return this.refreshInflight;
684
698
  const refresh = this.hooks.storage.get(REFRESH_KEY);
@@ -687,14 +701,18 @@ var Auth = class {
687
701
  try {
688
702
  const data = await this.callAuth("/api/v1/auth-service/token/refresh", {
689
703
  refreshToken: refresh
690
- }).catch(() => null);
691
- if (!data) return null;
704
+ });
692
705
  this.accessToken = data.tokens.accessToken;
693
706
  this.accessExpiresAt = Date.now() + data.tokens.expiresIn * 1e3;
694
707
  this.hooks.storage.set(TOKEN_KEY, data.tokens.accessToken);
695
708
  this.hooks.storage.set(TOKEN_EXPIRES_KEY, String(this.accessExpiresAt));
696
709
  this.hooks.storage.set(REFRESH_KEY, data.tokens.refreshToken);
697
710
  return data.tokens.accessToken;
711
+ } catch (err) {
712
+ if (err instanceof AuthError && isDeadRefreshError(err.code)) {
713
+ await this.wipeLocalIdentity().catch(() => void 0);
714
+ }
715
+ return null;
698
716
  } finally {
699
717
  this.refreshInflight = null;
700
718
  }
@@ -745,6 +763,10 @@ var Auth = class {
745
763
  await this.hooks.onAnonymousWipe();
746
764
  }
747
765
  };
766
+ function isDeadRefreshError(code) {
767
+ if (!code) return false;
768
+ return code === "INVALID_REFRESH_TOKEN" || code === "HTTP_401" || code === "UNAUTHORIZED" || code === "RATE_LIMIT_EXCEEDED" || code === "RATE_LIMIT";
769
+ }
748
770
  function decodeJwtPayload(token) {
749
771
  try {
750
772
  const parts = token.split(".");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendoracloud/sdk-react-native",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Sendora Cloud React Native + Expo SDK — analytics, identity, push-token registration, auth. Expo Go compatible, no native modules beyond AsyncStorage.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",