@sendoracloud/sdk-react-native 1.41.0 → 1.41.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/CHANGELOG.md +34 -0
- package/README.md +1 -1
- package/dist/index.cjs +26 -17
- package/dist/index.d.cts +14 -6
- package/dist/index.d.ts +14 -6
- package/dist/index.js +26 -17
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.41.2 — claiming or restoring a held guest works on a real phone
|
|
4
|
+
|
|
5
|
+
**Fix (reported by TapMap: `merge()` / `claimHeldGuest()` returned 401 on an iPhone after every sign-in).** Before the server fix (e09c86dc, already live), `/token/refresh` preferred the `sd_rt` refresh cookie over the token in the request body. A phone keeps a native cookie jar, and after a sign-in that jar holds the ACCOUNT's cookie — so refreshing a HELD guest's token was answered for the account, and the SDK wrote the account's token into the guest's slot. The device lost its only copy of the guest's credential; `claimHeldGuest()` / `merge()` then failed with 401 and `restoreHeldGuest()` released the guest. The guest's account was never touched on the server.
|
|
6
|
+
|
|
7
|
+
- **No cookies.** Every SDK request now sends `credentials: "omit"`, so React Native's native cookie jar is neither read nor written (iOS `HTTPShouldHandleCookies = NO`, Android `CookieJar.NO_COOKIES`). The SDK never used cookies — it authenticates with the API key header and tokens in the body.
|
|
8
|
+
- **Identity check on a held refresh.** A held guest's refresh must come back for that guest. A response for anyone else now throws `HELD_GUEST_IDENTITY_MISMATCH` and changes nothing: the slot keeps the guest's own token, no `/merge`, nothing installed, nothing released.
|
|
9
|
+
- No action needed for apps on 1.41.0 / 1.41.1: the server fix already covers them. Upgrade for the belt-and-braces. Same fix in web 3.24.2 / iOS 5.4.2 / Android 4.26.2.
|
|
10
|
+
|
|
11
|
+
## 1.41.1 — a dead session is `session_expired` again; the server's detail moves to `endReason`
|
|
12
|
+
|
|
13
|
+
**Fix to a 1.41.0 behaviour change (ADR-023: a new value is additive only for a NEW situation).**
|
|
14
|
+
A session that died on a server-rejected refresh was reported as `session_expired` through
|
|
15
|
+
1.40.0. 1.41.0 reported the server's own verdict as the reason instead (`revoked` / `deleted`),
|
|
16
|
+
which gave two old situations new names: "account deleted on another device" and "sessions
|
|
17
|
+
revoked". Adding a value to a union is additive for the compiler, not for a listener that
|
|
18
|
+
matched the old value. A listener that acted only on `session_expired` (the documented pattern, and
|
|
19
|
+
the RopoLabs template's session-death handler) silently ignored both, leaving the app on a
|
|
20
|
+
signed-in screen where every request fails. Reported by RopoLabs against the published
|
|
21
|
+
tarball before any production install.
|
|
22
|
+
|
|
23
|
+
- `signed_out.reason` is `session_expired` for EVERY server-rejected refresh, exactly as 1.40.0.
|
|
24
|
+
- New optional `signed_out.endReason`: the server's verdict (`revoked` | `deleted` | `expired` |
|
|
25
|
+
`reuse` | `unknown`; `evicted` reserved). Present only when the server sent one; absent on
|
|
26
|
+
`user` / `account_deleted` and from an older backend.
|
|
27
|
+
- `revoked` / `evicted` / `deleted` stay in `AuthSignedOutReason` so code written against 1.41.0
|
|
28
|
+
compiles; they are never emitted again. If you shipped 1.41.0 with a listener matching them,
|
|
29
|
+
it keeps compiling and stops firing — match `session_expired` (and read `endReason`).
|
|
30
|
+
- Nothing else changed: same storage, same wipe effects (no anon-id rotation on a session death),
|
|
31
|
+
same held-slot release rules.
|
|
32
|
+
|
|
33
|
+
Parity: web 3.24.1 / iOS 5.4.1 / Android 4.26.1. Guard: `test/auth-machine-conformance.test.ts`
|
|
34
|
+
`refresh_dead.current` (every endReason incl. `evicted` / `unknown` / absent → `session_expired` +
|
|
35
|
+
the exact `endReason`, key absent when the server sent none).
|
|
36
|
+
|
|
3
37
|
## 1.41.0 — sign-in no longer retires the guest: it is held on the device (ADR-033 wave 1)
|
|
4
38
|
|
|
5
39
|
**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`
|
|
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.
|
|
266
|
+
version: "1.41.2",
|
|
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",
|
|
@@ -320,7 +320,7 @@ var package_default = {
|
|
|
320
320
|
scripts: {
|
|
321
321
|
build: "tsup src/index.ts src/contact-widget.tsx --format esm,cjs --dts --clean --external react --external react-native --external react-native-webview",
|
|
322
322
|
typecheck: "tsc --noEmit",
|
|
323
|
-
test: "tsx test/secure-storage.test.ts && tsx test/integration-storage.test.ts && tsx test/plugin.test.ts && tsx test/game-auth.test.ts && tsx test/link.test.ts && tsx test/session-preservation.test.ts && tsx test/credential-collision.test.ts && tsx test/token-expiry.test.ts && tsx test/unlink.test.ts && tsx test/deferred-match.test.ts && tsx test/anon-id-stability.test.ts && tsx test/event-queue.test.ts && tsx test/anonymous-over-account.test.ts && tsx test/auth-machine-conformance.test.ts && tsx test/held-guests.test.ts"
|
|
323
|
+
test: "tsx test/secure-storage.test.ts && tsx test/integration-storage.test.ts && tsx test/plugin.test.ts && tsx test/game-auth.test.ts && tsx test/link.test.ts && tsx test/session-preservation.test.ts && tsx test/credential-collision.test.ts && tsx test/token-expiry.test.ts && tsx test/unlink.test.ts && tsx test/deferred-match.test.ts && tsx test/anon-id-stability.test.ts && tsx test/event-queue.test.ts && tsx test/anonymous-over-account.test.ts && tsx test/auth-machine-conformance.test.ts && tsx test/held-guests.test.ts && tsx test/held-identity.test.ts"
|
|
324
324
|
},
|
|
325
325
|
devDependencies: {
|
|
326
326
|
"@types/react": "^19.2.16",
|
|
@@ -391,7 +391,18 @@ async function request(opts, method, path, body, extraHeaders, reqOpts) {
|
|
|
391
391
|
...extraHeaders ?? {}
|
|
392
392
|
},
|
|
393
393
|
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
394
|
-
signal: controller.signal
|
|
394
|
+
signal: controller.signal,
|
|
395
|
+
// ⚠ NO COOKIES (s58.360). The SDK authenticates with the x-api-key header
|
|
396
|
+
// and tokens in the body — never a cookie. But RN's fetch keeps a NATIVE
|
|
397
|
+
// cookie jar unless told otherwise (`XMLHttpRequest.withCredentials`
|
|
398
|
+
// defaults to true → iOS `HTTPShouldHandleCookies`; Android's OkHttp jar),
|
|
399
|
+
// so every `sd_rt` the server set was replayed on the next
|
|
400
|
+
// /token/refresh. After a sign-in that cookie is the ACCOUNT's, and the
|
|
401
|
+
// server preferred it over the HELD GUEST's token in the body: a claim
|
|
402
|
+
// refreshed the account and wrote its token into the guest's slot. The
|
|
403
|
+
// server now answers for the body (e09c86dc); this keeps the SDK out of
|
|
404
|
+
// that class regardless of what any endpoint prefers.
|
|
405
|
+
credentials: "omit"
|
|
395
406
|
});
|
|
396
407
|
} catch (err) {
|
|
397
408
|
clearTimeout(timer);
|
|
@@ -2810,6 +2821,14 @@ var Auth = class {
|
|
|
2810
2821
|
const rotated = readRotatedTokens(parsed.data);
|
|
2811
2822
|
if (!rotated) throw new AuthError("PARSE_ERROR", "refresh response carried no token pair", { status: res.status });
|
|
2812
2823
|
const d = parsed.data;
|
|
2824
|
+
const answeredFor = isAuthUserShape(d?.user) ? d.user.id : this.userFromClaims(rotated.accessToken).id;
|
|
2825
|
+
if (answeredFor && answeredFor !== slot.userId) {
|
|
2826
|
+
throw new AuthError(
|
|
2827
|
+
"HELD_GUEST_IDENTITY_MISMATCH",
|
|
2828
|
+
"The held guest's refresh was answered for a different account; nothing was changed",
|
|
2829
|
+
{ kind: "unknown", status: res.status }
|
|
2830
|
+
);
|
|
2831
|
+
}
|
|
2813
2832
|
const user = isAuthUserShape(d?.user) ? d.user : { ...this.userFromClaims(rotated.accessToken), id: slot.userId, isAnonymous: true };
|
|
2814
2833
|
const persisted = this.commit({ kind: "slot_tokens", slot: i, sent, refreshToken: rotated.refreshToken });
|
|
2815
2834
|
if (persisted.status !== "applied") return { outcome: "kept" };
|
|
@@ -2931,7 +2950,7 @@ var Auth = class {
|
|
|
2931
2950
|
} catch (err) {
|
|
2932
2951
|
if (err instanceof AuthError && isDeadRefreshError(err.code)) {
|
|
2933
2952
|
if (this.hooks.storage.get(REFRESH_KEY) === sent) {
|
|
2934
|
-
await this.commitAsync({ kind: "clear", reason:
|
|
2953
|
+
await this.commitAsync({ kind: "clear", reason: "session_expired", endReason: err.endReason, expectRefresh: sent });
|
|
2935
2954
|
} else {
|
|
2936
2955
|
const i = this.held.findIndex((h) => h?.refreshToken === sent);
|
|
2937
2956
|
if (i !== -1 && err.endReason !== void 0 && RELEASE_HELD_ON_END_REASONS.includes(err.endReason)) {
|
|
@@ -3207,7 +3226,9 @@ var Auth = class {
|
|
|
3207
3226
|
if (reason === "stale_blob" || reason === "replaced") return { status: "applied", held: false };
|
|
3208
3227
|
if (reason === "user" && prevRefresh) this.revokeFireAndForget(prevRefresh);
|
|
3209
3228
|
this.pendingWipeHook = this.wipeLocalIdentity(reason);
|
|
3210
|
-
this.emitAuthState(
|
|
3229
|
+
this.emitAuthState(
|
|
3230
|
+
tx.endReason !== void 0 ? { event: "signed_out", user: null, reason, endReason: tx.endReason } : { event: "signed_out", user: null, reason }
|
|
3231
|
+
);
|
|
3211
3232
|
return { status: "applied", held: false };
|
|
3212
3233
|
}
|
|
3213
3234
|
/**
|
|
@@ -3390,18 +3411,6 @@ var Auth = class {
|
|
|
3390
3411
|
else this.hooks.storage.remove(key);
|
|
3391
3412
|
}
|
|
3392
3413
|
};
|
|
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
3414
|
function isDeadRefreshError(code) {
|
|
3406
3415
|
if (!code) return false;
|
|
3407
3416
|
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
|
|
765
|
-
*
|
|
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` —
|
|
768
|
-
*
|
|
769
|
-
*
|
|
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
|
|
765
|
-
*
|
|
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` —
|
|
768
|
-
*
|
|
769
|
-
*
|
|
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.
|
|
215
|
+
version: "1.41.2",
|
|
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",
|
|
@@ -269,7 +269,7 @@ var package_default = {
|
|
|
269
269
|
scripts: {
|
|
270
270
|
build: "tsup src/index.ts src/contact-widget.tsx --format esm,cjs --dts --clean --external react --external react-native --external react-native-webview",
|
|
271
271
|
typecheck: "tsc --noEmit",
|
|
272
|
-
test: "tsx test/secure-storage.test.ts && tsx test/integration-storage.test.ts && tsx test/plugin.test.ts && tsx test/game-auth.test.ts && tsx test/link.test.ts && tsx test/session-preservation.test.ts && tsx test/credential-collision.test.ts && tsx test/token-expiry.test.ts && tsx test/unlink.test.ts && tsx test/deferred-match.test.ts && tsx test/anon-id-stability.test.ts && tsx test/event-queue.test.ts && tsx test/anonymous-over-account.test.ts && tsx test/auth-machine-conformance.test.ts && tsx test/held-guests.test.ts"
|
|
272
|
+
test: "tsx test/secure-storage.test.ts && tsx test/integration-storage.test.ts && tsx test/plugin.test.ts && tsx test/game-auth.test.ts && tsx test/link.test.ts && tsx test/session-preservation.test.ts && tsx test/credential-collision.test.ts && tsx test/token-expiry.test.ts && tsx test/unlink.test.ts && tsx test/deferred-match.test.ts && tsx test/anon-id-stability.test.ts && tsx test/event-queue.test.ts && tsx test/anonymous-over-account.test.ts && tsx test/auth-machine-conformance.test.ts && tsx test/held-guests.test.ts && tsx test/held-identity.test.ts"
|
|
273
273
|
},
|
|
274
274
|
devDependencies: {
|
|
275
275
|
"@types/react": "^19.2.16",
|
|
@@ -340,7 +340,18 @@ async function request(opts, method, path, body, extraHeaders, reqOpts) {
|
|
|
340
340
|
...extraHeaders ?? {}
|
|
341
341
|
},
|
|
342
342
|
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
343
|
-
signal: controller.signal
|
|
343
|
+
signal: controller.signal,
|
|
344
|
+
// ⚠ NO COOKIES (s58.360). The SDK authenticates with the x-api-key header
|
|
345
|
+
// and tokens in the body — never a cookie. But RN's fetch keeps a NATIVE
|
|
346
|
+
// cookie jar unless told otherwise (`XMLHttpRequest.withCredentials`
|
|
347
|
+
// defaults to true → iOS `HTTPShouldHandleCookies`; Android's OkHttp jar),
|
|
348
|
+
// so every `sd_rt` the server set was replayed on the next
|
|
349
|
+
// /token/refresh. After a sign-in that cookie is the ACCOUNT's, and the
|
|
350
|
+
// server preferred it over the HELD GUEST's token in the body: a claim
|
|
351
|
+
// refreshed the account and wrote its token into the guest's slot. The
|
|
352
|
+
// server now answers for the body (e09c86dc); this keeps the SDK out of
|
|
353
|
+
// that class regardless of what any endpoint prefers.
|
|
354
|
+
credentials: "omit"
|
|
344
355
|
});
|
|
345
356
|
} catch (err) {
|
|
346
357
|
clearTimeout(timer);
|
|
@@ -2759,6 +2770,14 @@ var Auth = class {
|
|
|
2759
2770
|
const rotated = readRotatedTokens(parsed.data);
|
|
2760
2771
|
if (!rotated) throw new AuthError("PARSE_ERROR", "refresh response carried no token pair", { status: res.status });
|
|
2761
2772
|
const d = parsed.data;
|
|
2773
|
+
const answeredFor = isAuthUserShape(d?.user) ? d.user.id : this.userFromClaims(rotated.accessToken).id;
|
|
2774
|
+
if (answeredFor && answeredFor !== slot.userId) {
|
|
2775
|
+
throw new AuthError(
|
|
2776
|
+
"HELD_GUEST_IDENTITY_MISMATCH",
|
|
2777
|
+
"The held guest's refresh was answered for a different account; nothing was changed",
|
|
2778
|
+
{ kind: "unknown", status: res.status }
|
|
2779
|
+
);
|
|
2780
|
+
}
|
|
2762
2781
|
const user = isAuthUserShape(d?.user) ? d.user : { ...this.userFromClaims(rotated.accessToken), id: slot.userId, isAnonymous: true };
|
|
2763
2782
|
const persisted = this.commit({ kind: "slot_tokens", slot: i, sent, refreshToken: rotated.refreshToken });
|
|
2764
2783
|
if (persisted.status !== "applied") return { outcome: "kept" };
|
|
@@ -2880,7 +2899,7 @@ var Auth = class {
|
|
|
2880
2899
|
} catch (err) {
|
|
2881
2900
|
if (err instanceof AuthError && isDeadRefreshError(err.code)) {
|
|
2882
2901
|
if (this.hooks.storage.get(REFRESH_KEY) === sent) {
|
|
2883
|
-
await this.commitAsync({ kind: "clear", reason:
|
|
2902
|
+
await this.commitAsync({ kind: "clear", reason: "session_expired", endReason: err.endReason, expectRefresh: sent });
|
|
2884
2903
|
} else {
|
|
2885
2904
|
const i = this.held.findIndex((h) => h?.refreshToken === sent);
|
|
2886
2905
|
if (i !== -1 && err.endReason !== void 0 && RELEASE_HELD_ON_END_REASONS.includes(err.endReason)) {
|
|
@@ -3156,7 +3175,9 @@ var Auth = class {
|
|
|
3156
3175
|
if (reason === "stale_blob" || reason === "replaced") return { status: "applied", held: false };
|
|
3157
3176
|
if (reason === "user" && prevRefresh) this.revokeFireAndForget(prevRefresh);
|
|
3158
3177
|
this.pendingWipeHook = this.wipeLocalIdentity(reason);
|
|
3159
|
-
this.emitAuthState(
|
|
3178
|
+
this.emitAuthState(
|
|
3179
|
+
tx.endReason !== void 0 ? { event: "signed_out", user: null, reason, endReason: tx.endReason } : { event: "signed_out", user: null, reason }
|
|
3180
|
+
);
|
|
3160
3181
|
return { status: "applied", held: false };
|
|
3161
3182
|
}
|
|
3162
3183
|
/**
|
|
@@ -3339,18 +3360,6 @@ var Auth = class {
|
|
|
3339
3360
|
else this.hooks.storage.remove(key);
|
|
3340
3361
|
}
|
|
3341
3362
|
};
|
|
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
3363
|
function isDeadRefreshError(code) {
|
|
3355
3364
|
if (!code) return false;
|
|
3356
3365
|
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.
|
|
3
|
+
"version": "1.41.2",
|
|
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",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"scripts": {
|
|
58
58
|
"build": "tsup src/index.ts src/contact-widget.tsx --format esm,cjs --dts --clean --external react --external react-native --external react-native-webview",
|
|
59
59
|
"typecheck": "tsc --noEmit",
|
|
60
|
-
"test": "tsx test/secure-storage.test.ts && tsx test/integration-storage.test.ts && tsx test/plugin.test.ts && tsx test/game-auth.test.ts && tsx test/link.test.ts && tsx test/session-preservation.test.ts && tsx test/credential-collision.test.ts && tsx test/token-expiry.test.ts && tsx test/unlink.test.ts && tsx test/deferred-match.test.ts && tsx test/anon-id-stability.test.ts && tsx test/event-queue.test.ts && tsx test/anonymous-over-account.test.ts && tsx test/auth-machine-conformance.test.ts && tsx test/held-guests.test.ts"
|
|
60
|
+
"test": "tsx test/secure-storage.test.ts && tsx test/integration-storage.test.ts && tsx test/plugin.test.ts && tsx test/game-auth.test.ts && tsx test/link.test.ts && tsx test/session-preservation.test.ts && tsx test/credential-collision.test.ts && tsx test/token-expiry.test.ts && tsx test/unlink.test.ts && tsx test/deferred-match.test.ts && tsx test/anon-id-stability.test.ts && tsx test/event-queue.test.ts && tsx test/anonymous-over-account.test.ts && tsx test/auth-machine-conformance.test.ts && tsx test/held-guests.test.ts && tsx test/held-identity.test.ts"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@types/react": "^19.2.16",
|