@sendoracloud/sdk-react-native 1.41.0 → 1.41.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.41.1 — a dead session is `session_expired` again; the server's detail moves to `endReason`
4
+
5
+ **Fix to a 1.41.0 behaviour change (ADR-023: a new value is additive only for a NEW situation).**
6
+ A session that died on a server-rejected refresh was reported as `session_expired` through
7
+ 1.40.0. 1.41.0 reported the server's own verdict as the reason instead (`revoked` / `deleted`),
8
+ which gave two old situations new names: "account deleted on another device" and "sessions
9
+ revoked". Adding a value to a union is additive for the compiler, not for a listener that
10
+ matched the old value. A listener that acted only on `session_expired` (the documented pattern, and
11
+ the RopoLabs template's session-death handler) silently ignored both, leaving the app on a
12
+ signed-in screen where every request fails. Reported by RopoLabs against the published
13
+ tarball before any production install.
14
+
15
+ - `signed_out.reason` is `session_expired` for EVERY server-rejected refresh, exactly as 1.40.0.
16
+ - New optional `signed_out.endReason`: the server's verdict (`revoked` | `deleted` | `expired` |
17
+ `reuse` | `unknown`; `evicted` reserved). Present only when the server sent one; absent on
18
+ `user` / `account_deleted` and from an older backend.
19
+ - `revoked` / `evicted` / `deleted` stay in `AuthSignedOutReason` so code written against 1.41.0
20
+ compiles; they are never emitted again. If you shipped 1.41.0 with a listener matching them,
21
+ it keeps compiling and stops firing — match `session_expired` (and read `endReason`).
22
+ - Nothing else changed: same storage, same wipe effects (no anon-id rotation on a session death),
23
+ same held-slot release rules.
24
+
25
+ Parity: web 3.24.1 / iOS 5.4.1 / Android 4.26.1. Guard: `test/auth-machine-conformance.test.ts`
26
+ `refresh_dead.current` (every endReason incl. `evicted` / `unknown` / absent → `session_expired` +
27
+ the exact `endReason`, key absent when the server sent none).
28
+
3
29
  ## 1.41.0 — sign-in no longer retires the guest: it is held on the device (ADR-033 wave 1)
4
30
 
5
31
  **Two semantic changes, both strictly less destructive than before; everything else is additive
package/README.md CHANGED
@@ -303,7 +303,7 @@ This SDK is on the **1.x line**. Patch bumps (1.x.y → 1.x.z) are backwards-com
303
303
 
304
304
  ### Auth state is a transition stream, not a health signal
305
305
 
306
- `auth.onAuthStateChanged()` reports confirmed transitions only — `signed_in` (`cause`: `mint` | `sign_in` | `restore` | `link`), `signed_out` (reason `user` | `session_expired` | `account_deleted` | `revoked` | `evicted` | `deleted`), `user_updated`, `guest_held`, `guest_released`, `session_withheld`, `device_takeover`, `deletion_cancelled`. Each subscriber first receives one `initial{status, user}` (1.41.0); with a live session it is followed by the same `signed_in{user}` replay 1.28–1.40 sent (no `cause`), so existing listeners keep working. New code: read `initial` and treat a cause-less `signed_in` right after it as the replay. A transient failure of the background token refresh (offline, timeout, HTTP 429, 5xx) emits **nothing**: the stored refresh token is kept and the refresh retried with backoff, so no state changed. "No event" means "no confirmed transition", never "the session is fine". Only a server-rejected refresh token produces `signed_out(session_expired)`. For liveness, read `getAccessToken()` — `null` means no usable token right now.
306
+ `auth.onAuthStateChanged()` reports confirmed transitions only — `signed_in` (`cause`: `mint` | `sign_in` | `restore` | `link`), `signed_out` (reason `user` | `session_expired` | `account_deleted`; a session the server killed is always `session_expired`, with the server's verdict in `endReason` — `revoked` | `deleted` | `expired` | `reuse` | `unknown` — since 1.41.1), `user_updated`, `guest_held`, `guest_released`, `session_withheld`, `device_takeover`, `deletion_cancelled`. Each subscriber first receives one `initial{status, user}` (1.41.0); with a live session it is followed by the same `signed_in{user}` replay 1.28–1.40 sent (no `cause`), so existing listeners keep working. New code: read `initial` and treat a cause-less `signed_in` right after it as the replay. A transient failure of the background token refresh (offline, timeout, HTTP 429, 5xx) emits **nothing**: the stored refresh token is kept and the refresh retried with backoff, so no state changed. "No event" means "no confirmed transition", never "the session is fine". Only a server-rejected refresh token produces `signed_out(session_expired)`. For liveness, read `getAccessToken()` — `null` means no usable token right now.
307
307
 
308
308
  `signInAnonymously()` returns the **current** session when one exists, anonymous or identified — it never replaces a signed-in account with a guest. Pass `forceNew: true` to deliberately leave the current session and mint a fresh anonymous one (a guest left this way is held, not lost).
309
309
 
package/dist/index.cjs CHANGED
@@ -263,7 +263,7 @@ var Storage = class {
263
263
  // package.json
264
264
  var package_default = {
265
265
  name: "@sendoracloud/sdk-react-native",
266
- version: "1.41.0",
266
+ version: "1.41.1",
267
267
  description: "Sendora Cloud React Native + Expo SDK \u2014 analytics, identity, push-token registration, auth (incl. Game Center / Play Games), deep links (Branch / Firebase Dynamic Links parity). Auth + analytics + deep links work in Expo Go; push-token registration and the no-arg Game Center / Play Games sign-in require a Dev Client (EAS Build or `npx expo prebuild`).",
268
268
  type: "module",
269
269
  main: "./dist/index.cjs",
@@ -2931,7 +2931,7 @@ var Auth = class {
2931
2931
  } catch (err) {
2932
2932
  if (err instanceof AuthError && isDeadRefreshError(err.code)) {
2933
2933
  if (this.hooks.storage.get(REFRESH_KEY) === sent) {
2934
- await this.commitAsync({ kind: "clear", reason: signedOutReasonFor(err.endReason), expectRefresh: sent });
2934
+ await this.commitAsync({ kind: "clear", reason: "session_expired", endReason: err.endReason, expectRefresh: sent });
2935
2935
  } else {
2936
2936
  const i = this.held.findIndex((h) => h?.refreshToken === sent);
2937
2937
  if (i !== -1 && err.endReason !== void 0 && RELEASE_HELD_ON_END_REASONS.includes(err.endReason)) {
@@ -3207,7 +3207,9 @@ var Auth = class {
3207
3207
  if (reason === "stale_blob" || reason === "replaced") return { status: "applied", held: false };
3208
3208
  if (reason === "user" && prevRefresh) this.revokeFireAndForget(prevRefresh);
3209
3209
  this.pendingWipeHook = this.wipeLocalIdentity(reason);
3210
- this.emitAuthState({ event: "signed_out", user: null, reason });
3210
+ this.emitAuthState(
3211
+ tx.endReason !== void 0 ? { event: "signed_out", user: null, reason, endReason: tx.endReason } : { event: "signed_out", user: null, reason }
3212
+ );
3211
3213
  return { status: "applied", held: false };
3212
3214
  }
3213
3215
  /**
@@ -3390,18 +3392,6 @@ var Auth = class {
3390
3392
  else this.hooks.storage.remove(key);
3391
3393
  }
3392
3394
  };
3393
- function signedOutReasonFor(endReason) {
3394
- switch (endReason) {
3395
- case "revoked":
3396
- return "revoked";
3397
- case "deleted":
3398
- return "deleted";
3399
- case "evicted":
3400
- return "evicted";
3401
- default:
3402
- return "session_expired";
3403
- }
3404
- }
3405
3395
  function isDeadRefreshError(code) {
3406
3396
  if (!code) return false;
3407
3397
  return DEAD_CODES.includes(code);
package/dist/index.d.cts CHANGED
@@ -761,12 +761,17 @@ interface DeletionCancelledEvent {
761
761
  * Why the local session ended.
762
762
  *
763
763
  * - `user` — the app called `signOut()` (or `discardWithheld()`).
764
- * - `session_expired` — the server rejected the stored refresh token with no
765
- * finer reason (or `endReason: expired | reuse | unknown`).
764
+ * - `session_expired` — the server rejected the stored refresh token. EVERY
765
+ * dead verdict, whatever the server's detail: read the
766
+ * event's `endReason` (`revoked` / `deleted` / `expired` /
767
+ * `reuse` / `unknown`) for that.
766
768
  * - `account_deleted` — `deleteAccount()` succeeded.
767
- * - `revoked` / `evicted` / `deleted` — ADR-033 B11 (additive): the server's
768
- * own `endReason` on the dead-token verdict. `evicted` is
769
- * reserved and not emitted by the backend yet.
769
+ * - `revoked` / `evicted` / `deleted` — ⚠ emitted by 1.41.0 ONLY, never since
770
+ * 1.41.1. 1.41.0 put the server's `endReason` here, which
771
+ * renamed "account deleted elsewhere" / "sessions revoked"
772
+ * (both `session_expired` through 1.40.0) and silently
773
+ * broke listeners matching `session_expired`. Kept in the
774
+ * type so code written against 1.41.0 still compiles.
770
775
  *
771
776
  * Transient network failures and rate limits never produce any of these.
772
777
  * Consumers default-branch (§3.5 NORMATIVE).
@@ -792,10 +797,13 @@ type AuthStateChange = {
792
797
  event: "signed_in";
793
798
  user: AuthUser;
794
799
  cause?: AuthSignedInCause;
795
- } | {
800
+ }
801
+ /** `endReason` (1.41.1) = the server's own verdict on a dead refresh token; absent on `user` / `account_deleted` and from an older backend. */
802
+ | {
796
803
  event: "signed_out";
797
804
  user: null;
798
805
  reason: AuthSignedOutReason;
806
+ endReason?: RefreshEndReason;
799
807
  } | {
800
808
  event: "device_takeover";
801
809
  user: AuthUser | null;
package/dist/index.d.ts CHANGED
@@ -761,12 +761,17 @@ interface DeletionCancelledEvent {
761
761
  * Why the local session ended.
762
762
  *
763
763
  * - `user` — the app called `signOut()` (or `discardWithheld()`).
764
- * - `session_expired` — the server rejected the stored refresh token with no
765
- * finer reason (or `endReason: expired | reuse | unknown`).
764
+ * - `session_expired` — the server rejected the stored refresh token. EVERY
765
+ * dead verdict, whatever the server's detail: read the
766
+ * event's `endReason` (`revoked` / `deleted` / `expired` /
767
+ * `reuse` / `unknown`) for that.
766
768
  * - `account_deleted` — `deleteAccount()` succeeded.
767
- * - `revoked` / `evicted` / `deleted` — ADR-033 B11 (additive): the server's
768
- * own `endReason` on the dead-token verdict. `evicted` is
769
- * reserved and not emitted by the backend yet.
769
+ * - `revoked` / `evicted` / `deleted` — ⚠ emitted by 1.41.0 ONLY, never since
770
+ * 1.41.1. 1.41.0 put the server's `endReason` here, which
771
+ * renamed "account deleted elsewhere" / "sessions revoked"
772
+ * (both `session_expired` through 1.40.0) and silently
773
+ * broke listeners matching `session_expired`. Kept in the
774
+ * type so code written against 1.41.0 still compiles.
770
775
  *
771
776
  * Transient network failures and rate limits never produce any of these.
772
777
  * Consumers default-branch (§3.5 NORMATIVE).
@@ -792,10 +797,13 @@ type AuthStateChange = {
792
797
  event: "signed_in";
793
798
  user: AuthUser;
794
799
  cause?: AuthSignedInCause;
795
- } | {
800
+ }
801
+ /** `endReason` (1.41.1) = the server's own verdict on a dead refresh token; absent on `user` / `account_deleted` and from an older backend. */
802
+ | {
796
803
  event: "signed_out";
797
804
  user: null;
798
805
  reason: AuthSignedOutReason;
806
+ endReason?: RefreshEndReason;
799
807
  } | {
800
808
  event: "device_takeover";
801
809
  user: AuthUser | null;
package/dist/index.js CHANGED
@@ -212,7 +212,7 @@ var Storage = class {
212
212
  // package.json
213
213
  var package_default = {
214
214
  name: "@sendoracloud/sdk-react-native",
215
- version: "1.41.0",
215
+ version: "1.41.1",
216
216
  description: "Sendora Cloud React Native + Expo SDK \u2014 analytics, identity, push-token registration, auth (incl. Game Center / Play Games), deep links (Branch / Firebase Dynamic Links parity). Auth + analytics + deep links work in Expo Go; push-token registration and the no-arg Game Center / Play Games sign-in require a Dev Client (EAS Build or `npx expo prebuild`).",
217
217
  type: "module",
218
218
  main: "./dist/index.cjs",
@@ -2880,7 +2880,7 @@ var Auth = class {
2880
2880
  } catch (err) {
2881
2881
  if (err instanceof AuthError && isDeadRefreshError(err.code)) {
2882
2882
  if (this.hooks.storage.get(REFRESH_KEY) === sent) {
2883
- await this.commitAsync({ kind: "clear", reason: signedOutReasonFor(err.endReason), expectRefresh: sent });
2883
+ await this.commitAsync({ kind: "clear", reason: "session_expired", endReason: err.endReason, expectRefresh: sent });
2884
2884
  } else {
2885
2885
  const i = this.held.findIndex((h) => h?.refreshToken === sent);
2886
2886
  if (i !== -1 && err.endReason !== void 0 && RELEASE_HELD_ON_END_REASONS.includes(err.endReason)) {
@@ -3156,7 +3156,9 @@ var Auth = class {
3156
3156
  if (reason === "stale_blob" || reason === "replaced") return { status: "applied", held: false };
3157
3157
  if (reason === "user" && prevRefresh) this.revokeFireAndForget(prevRefresh);
3158
3158
  this.pendingWipeHook = this.wipeLocalIdentity(reason);
3159
- this.emitAuthState({ event: "signed_out", user: null, reason });
3159
+ this.emitAuthState(
3160
+ tx.endReason !== void 0 ? { event: "signed_out", user: null, reason, endReason: tx.endReason } : { event: "signed_out", user: null, reason }
3161
+ );
3160
3162
  return { status: "applied", held: false };
3161
3163
  }
3162
3164
  /**
@@ -3339,18 +3341,6 @@ var Auth = class {
3339
3341
  else this.hooks.storage.remove(key);
3340
3342
  }
3341
3343
  };
3342
- function signedOutReasonFor(endReason) {
3343
- switch (endReason) {
3344
- case "revoked":
3345
- return "revoked";
3346
- case "deleted":
3347
- return "deleted";
3348
- case "evicted":
3349
- return "evicted";
3350
- default:
3351
- return "session_expired";
3352
- }
3353
- }
3354
3344
  function isDeadRefreshError(code) {
3355
3345
  if (!code) return false;
3356
3346
  return DEAD_CODES.includes(code);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendoracloud/sdk-react-native",
3
- "version": "1.41.0",
3
+ "version": "1.41.1",
4
4
  "description": "Sendora Cloud React Native + Expo SDK — analytics, identity, push-token registration, auth (incl. Game Center / Play Games), deep links (Branch / Firebase Dynamic Links parity). Auth + analytics + deep links work in Expo Go; push-token registration and the no-arg Game Center / Play Games sign-in require a Dev Client (EAS Build or `npx expo prebuild`).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",