@sendoracloud/sdk-react-native 0.12.0 → 0.14.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 +51 -9
- package/dist/index.d.cts +32 -4
- package/dist/index.d.ts +32 -4
- package/dist/index.js +51 -9
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -235,21 +235,41 @@ var Auth = class {
|
|
|
235
235
|
this.resolveReady = res;
|
|
236
236
|
});
|
|
237
237
|
}
|
|
238
|
-
/**
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
238
|
+
/**
|
|
239
|
+
* Re-hydrate session from storage. Called by the parent SDK during
|
|
240
|
+
* init() after Storage.hydrate() has run. Marks the internal ready
|
|
241
|
+
* promise as resolved so queued auth calls unblock.
|
|
242
|
+
*
|
|
243
|
+
* s58.47 — restore the user even when the cached ACCESS token is
|
|
244
|
+
* missing/expired, as long as USER_KEY + REFRESH_KEY are present.
|
|
245
|
+
* Pre-s58.47 this required BOTH USER_KEY and TOKEN_KEY, which
|
|
246
|
+
* caused a cold-start regression: if the access token rotated out
|
|
247
|
+
* of storage (TTL elapsed, app killed between persists, partial
|
|
248
|
+
* write, AsyncStorage eviction) the user appeared signed out even
|
|
249
|
+
* though their refresh chain was still alive in storage. Hosts
|
|
250
|
+
* then called `signInAnonymously()` on every launch and minted a
|
|
251
|
+
* fresh anon user, fragmenting analytics across sessions
|
|
252
|
+
* (Pulse News symptom). With the refresh token present, the next
|
|
253
|
+
* `getAccessToken()` call will silently refresh + restore a valid
|
|
254
|
+
* access token without disturbing identity.
|
|
255
|
+
*/
|
|
242
256
|
hydrate() {
|
|
243
257
|
const cachedUser = this.hooks.storage.get(USER_KEY);
|
|
244
258
|
const cachedToken = this.hooks.storage.get(TOKEN_KEY);
|
|
259
|
+
const cachedRefresh = this.hooks.storage.get(REFRESH_KEY);
|
|
245
260
|
const cachedExpires = this.hooks.storage.get(TOKEN_EXPIRES_KEY);
|
|
246
|
-
if (cachedUser && cachedToken) {
|
|
261
|
+
if (cachedUser && (cachedToken || cachedRefresh)) {
|
|
247
262
|
try {
|
|
248
263
|
const parsed = JSON.parse(cachedUser);
|
|
249
264
|
if (typeof parsed.id !== "string" || parsed.id.length === 0) throw new Error("invalid cache");
|
|
250
265
|
this.user = parsed;
|
|
251
|
-
|
|
252
|
-
|
|
266
|
+
if (cachedToken) {
|
|
267
|
+
this.accessToken = cachedToken;
|
|
268
|
+
this.accessExpiresAt = cachedExpires ? Number(cachedExpires) : 0;
|
|
269
|
+
} else {
|
|
270
|
+
this.accessToken = null;
|
|
271
|
+
this.accessExpiresAt = 0;
|
|
272
|
+
}
|
|
253
273
|
this.hooks.onIdentityChange(this.user.id);
|
|
254
274
|
} catch {
|
|
255
275
|
this.hooks.storage.remove(USER_KEY);
|
|
@@ -719,6 +739,20 @@ var Auth = class {
|
|
|
719
739
|
this.inflight = next.catch(() => void 0);
|
|
720
740
|
return next;
|
|
721
741
|
}
|
|
742
|
+
/**
|
|
743
|
+
* Hot-path token refresh. Single-flight via `refreshInflight` so a
|
|
744
|
+
* UI batch firing 10 simultaneous reads never produces 10 /refresh
|
|
745
|
+
* round-trips. Returns null on any failure so callers can decide
|
|
746
|
+
* whether to surface a sign-in prompt or proceed anonymously.
|
|
747
|
+
*
|
|
748
|
+
* s58.46 — on a `INVALID_REFRESH_TOKEN` (new backend error code) or
|
|
749
|
+
* HTTP 401 we WIPE the stored refresh token. Pre-s58.46 we caught
|
|
750
|
+
* the error and returned null but kept the stored token intact, so
|
|
751
|
+
* the very next caller would re-attempt the same dead token in an
|
|
752
|
+
* infinite loop (Pulse News iOS hammered /refresh ~1×/s for hours).
|
|
753
|
+
* The wipe forces the next op to fall through to /anonymous or
|
|
754
|
+
* surface NOT_SIGNED_IN to the host app instead of looping.
|
|
755
|
+
*/
|
|
722
756
|
async refreshAccessToken() {
|
|
723
757
|
if (this.refreshInflight) return this.refreshInflight;
|
|
724
758
|
const refresh = this.hooks.storage.get(REFRESH_KEY);
|
|
@@ -727,14 +761,18 @@ var Auth = class {
|
|
|
727
761
|
try {
|
|
728
762
|
const data = await this.callAuth("/api/v1/auth-service/token/refresh", {
|
|
729
763
|
refreshToken: refresh
|
|
730
|
-
})
|
|
731
|
-
if (!data) return null;
|
|
764
|
+
});
|
|
732
765
|
this.accessToken = data.tokens.accessToken;
|
|
733
766
|
this.accessExpiresAt = Date.now() + data.tokens.expiresIn * 1e3;
|
|
734
767
|
this.hooks.storage.set(TOKEN_KEY, data.tokens.accessToken);
|
|
735
768
|
this.hooks.storage.set(TOKEN_EXPIRES_KEY, String(this.accessExpiresAt));
|
|
736
769
|
this.hooks.storage.set(REFRESH_KEY, data.tokens.refreshToken);
|
|
737
770
|
return data.tokens.accessToken;
|
|
771
|
+
} catch (err) {
|
|
772
|
+
if (err instanceof AuthError && isDeadRefreshError(err.code)) {
|
|
773
|
+
await this.wipeLocalIdentity().catch(() => void 0);
|
|
774
|
+
}
|
|
775
|
+
return null;
|
|
738
776
|
} finally {
|
|
739
777
|
this.refreshInflight = null;
|
|
740
778
|
}
|
|
@@ -785,6 +823,10 @@ var Auth = class {
|
|
|
785
823
|
await this.hooks.onAnonymousWipe();
|
|
786
824
|
}
|
|
787
825
|
};
|
|
826
|
+
function isDeadRefreshError(code) {
|
|
827
|
+
if (!code) return false;
|
|
828
|
+
return code === "INVALID_REFRESH_TOKEN" || code === "HTTP_401" || code === "UNAUTHORIZED" || code === "RATE_LIMIT_EXCEEDED" || code === "RATE_LIMIT";
|
|
829
|
+
}
|
|
788
830
|
function decodeJwtPayload(token) {
|
|
789
831
|
try {
|
|
790
832
|
const parts = token.split(".");
|
package/dist/index.d.cts
CHANGED
|
@@ -122,10 +122,24 @@ declare class Auth {
|
|
|
122
122
|
private readyPromise;
|
|
123
123
|
private resolveReady;
|
|
124
124
|
constructor(hooks: AuthHooks);
|
|
125
|
-
/**
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
125
|
+
/**
|
|
126
|
+
* Re-hydrate session from storage. Called by the parent SDK during
|
|
127
|
+
* init() after Storage.hydrate() has run. Marks the internal ready
|
|
128
|
+
* promise as resolved so queued auth calls unblock.
|
|
129
|
+
*
|
|
130
|
+
* s58.47 — restore the user even when the cached ACCESS token is
|
|
131
|
+
* missing/expired, as long as USER_KEY + REFRESH_KEY are present.
|
|
132
|
+
* Pre-s58.47 this required BOTH USER_KEY and TOKEN_KEY, which
|
|
133
|
+
* caused a cold-start regression: if the access token rotated out
|
|
134
|
+
* of storage (TTL elapsed, app killed between persists, partial
|
|
135
|
+
* write, AsyncStorage eviction) the user appeared signed out even
|
|
136
|
+
* though their refresh chain was still alive in storage. Hosts
|
|
137
|
+
* then called `signInAnonymously()` on every launch and minted a
|
|
138
|
+
* fresh anon user, fragmenting analytics across sessions
|
|
139
|
+
* (Pulse News symptom). With the refresh token present, the next
|
|
140
|
+
* `getAccessToken()` call will silently refresh + restore a valid
|
|
141
|
+
* access token without disturbing identity.
|
|
142
|
+
*/
|
|
129
143
|
hydrate(): void;
|
|
130
144
|
getCurrentUser(): AuthUser | null;
|
|
131
145
|
/**
|
|
@@ -270,6 +284,20 @@ declare class Auth {
|
|
|
270
284
|
*/
|
|
271
285
|
signOut(): Promise<void>;
|
|
272
286
|
private serialize;
|
|
287
|
+
/**
|
|
288
|
+
* Hot-path token refresh. Single-flight via `refreshInflight` so a
|
|
289
|
+
* UI batch firing 10 simultaneous reads never produces 10 /refresh
|
|
290
|
+
* round-trips. Returns null on any failure so callers can decide
|
|
291
|
+
* whether to surface a sign-in prompt or proceed anonymously.
|
|
292
|
+
*
|
|
293
|
+
* s58.46 — on a `INVALID_REFRESH_TOKEN` (new backend error code) or
|
|
294
|
+
* HTTP 401 we WIPE the stored refresh token. Pre-s58.46 we caught
|
|
295
|
+
* the error and returned null but kept the stored token intact, so
|
|
296
|
+
* the very next caller would re-attempt the same dead token in an
|
|
297
|
+
* infinite loop (Pulse News iOS hammered /refresh ~1×/s for hours).
|
|
298
|
+
* The wipe forces the next op to fall through to /anonymous or
|
|
299
|
+
* surface NOT_SIGNED_IN to the host app instead of looping.
|
|
300
|
+
*/
|
|
273
301
|
private refreshAccessToken;
|
|
274
302
|
private callAuth;
|
|
275
303
|
private persist;
|
package/dist/index.d.ts
CHANGED
|
@@ -122,10 +122,24 @@ declare class Auth {
|
|
|
122
122
|
private readyPromise;
|
|
123
123
|
private resolveReady;
|
|
124
124
|
constructor(hooks: AuthHooks);
|
|
125
|
-
/**
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
125
|
+
/**
|
|
126
|
+
* Re-hydrate session from storage. Called by the parent SDK during
|
|
127
|
+
* init() after Storage.hydrate() has run. Marks the internal ready
|
|
128
|
+
* promise as resolved so queued auth calls unblock.
|
|
129
|
+
*
|
|
130
|
+
* s58.47 — restore the user even when the cached ACCESS token is
|
|
131
|
+
* missing/expired, as long as USER_KEY + REFRESH_KEY are present.
|
|
132
|
+
* Pre-s58.47 this required BOTH USER_KEY and TOKEN_KEY, which
|
|
133
|
+
* caused a cold-start regression: if the access token rotated out
|
|
134
|
+
* of storage (TTL elapsed, app killed between persists, partial
|
|
135
|
+
* write, AsyncStorage eviction) the user appeared signed out even
|
|
136
|
+
* though their refresh chain was still alive in storage. Hosts
|
|
137
|
+
* then called `signInAnonymously()` on every launch and minted a
|
|
138
|
+
* fresh anon user, fragmenting analytics across sessions
|
|
139
|
+
* (Pulse News symptom). With the refresh token present, the next
|
|
140
|
+
* `getAccessToken()` call will silently refresh + restore a valid
|
|
141
|
+
* access token without disturbing identity.
|
|
142
|
+
*/
|
|
129
143
|
hydrate(): void;
|
|
130
144
|
getCurrentUser(): AuthUser | null;
|
|
131
145
|
/**
|
|
@@ -270,6 +284,20 @@ declare class Auth {
|
|
|
270
284
|
*/
|
|
271
285
|
signOut(): Promise<void>;
|
|
272
286
|
private serialize;
|
|
287
|
+
/**
|
|
288
|
+
* Hot-path token refresh. Single-flight via `refreshInflight` so a
|
|
289
|
+
* UI batch firing 10 simultaneous reads never produces 10 /refresh
|
|
290
|
+
* round-trips. Returns null on any failure so callers can decide
|
|
291
|
+
* whether to surface a sign-in prompt or proceed anonymously.
|
|
292
|
+
*
|
|
293
|
+
* s58.46 — on a `INVALID_REFRESH_TOKEN` (new backend error code) or
|
|
294
|
+
* HTTP 401 we WIPE the stored refresh token. Pre-s58.46 we caught
|
|
295
|
+
* the error and returned null but kept the stored token intact, so
|
|
296
|
+
* the very next caller would re-attempt the same dead token in an
|
|
297
|
+
* infinite loop (Pulse News iOS hammered /refresh ~1×/s for hours).
|
|
298
|
+
* The wipe forces the next op to fall through to /anonymous or
|
|
299
|
+
* surface NOT_SIGNED_IN to the host app instead of looping.
|
|
300
|
+
*/
|
|
273
301
|
private refreshAccessToken;
|
|
274
302
|
private callAuth;
|
|
275
303
|
private persist;
|
package/dist/index.js
CHANGED
|
@@ -195,21 +195,41 @@ var Auth = class {
|
|
|
195
195
|
this.resolveReady = res;
|
|
196
196
|
});
|
|
197
197
|
}
|
|
198
|
-
/**
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
198
|
+
/**
|
|
199
|
+
* Re-hydrate session from storage. Called by the parent SDK during
|
|
200
|
+
* init() after Storage.hydrate() has run. Marks the internal ready
|
|
201
|
+
* promise as resolved so queued auth calls unblock.
|
|
202
|
+
*
|
|
203
|
+
* s58.47 — restore the user even when the cached ACCESS token is
|
|
204
|
+
* missing/expired, as long as USER_KEY + REFRESH_KEY are present.
|
|
205
|
+
* Pre-s58.47 this required BOTH USER_KEY and TOKEN_KEY, which
|
|
206
|
+
* caused a cold-start regression: if the access token rotated out
|
|
207
|
+
* of storage (TTL elapsed, app killed between persists, partial
|
|
208
|
+
* write, AsyncStorage eviction) the user appeared signed out even
|
|
209
|
+
* though their refresh chain was still alive in storage. Hosts
|
|
210
|
+
* then called `signInAnonymously()` on every launch and minted a
|
|
211
|
+
* fresh anon user, fragmenting analytics across sessions
|
|
212
|
+
* (Pulse News symptom). With the refresh token present, the next
|
|
213
|
+
* `getAccessToken()` call will silently refresh + restore a valid
|
|
214
|
+
* access token without disturbing identity.
|
|
215
|
+
*/
|
|
202
216
|
hydrate() {
|
|
203
217
|
const cachedUser = this.hooks.storage.get(USER_KEY);
|
|
204
218
|
const cachedToken = this.hooks.storage.get(TOKEN_KEY);
|
|
219
|
+
const cachedRefresh = this.hooks.storage.get(REFRESH_KEY);
|
|
205
220
|
const cachedExpires = this.hooks.storage.get(TOKEN_EXPIRES_KEY);
|
|
206
|
-
if (cachedUser && cachedToken) {
|
|
221
|
+
if (cachedUser && (cachedToken || cachedRefresh)) {
|
|
207
222
|
try {
|
|
208
223
|
const parsed = JSON.parse(cachedUser);
|
|
209
224
|
if (typeof parsed.id !== "string" || parsed.id.length === 0) throw new Error("invalid cache");
|
|
210
225
|
this.user = parsed;
|
|
211
|
-
|
|
212
|
-
|
|
226
|
+
if (cachedToken) {
|
|
227
|
+
this.accessToken = cachedToken;
|
|
228
|
+
this.accessExpiresAt = cachedExpires ? Number(cachedExpires) : 0;
|
|
229
|
+
} else {
|
|
230
|
+
this.accessToken = null;
|
|
231
|
+
this.accessExpiresAt = 0;
|
|
232
|
+
}
|
|
213
233
|
this.hooks.onIdentityChange(this.user.id);
|
|
214
234
|
} catch {
|
|
215
235
|
this.hooks.storage.remove(USER_KEY);
|
|
@@ -679,6 +699,20 @@ var Auth = class {
|
|
|
679
699
|
this.inflight = next.catch(() => void 0);
|
|
680
700
|
return next;
|
|
681
701
|
}
|
|
702
|
+
/**
|
|
703
|
+
* Hot-path token refresh. Single-flight via `refreshInflight` so a
|
|
704
|
+
* UI batch firing 10 simultaneous reads never produces 10 /refresh
|
|
705
|
+
* round-trips. Returns null on any failure so callers can decide
|
|
706
|
+
* whether to surface a sign-in prompt or proceed anonymously.
|
|
707
|
+
*
|
|
708
|
+
* s58.46 — on a `INVALID_REFRESH_TOKEN` (new backend error code) or
|
|
709
|
+
* HTTP 401 we WIPE the stored refresh token. Pre-s58.46 we caught
|
|
710
|
+
* the error and returned null but kept the stored token intact, so
|
|
711
|
+
* the very next caller would re-attempt the same dead token in an
|
|
712
|
+
* infinite loop (Pulse News iOS hammered /refresh ~1×/s for hours).
|
|
713
|
+
* The wipe forces the next op to fall through to /anonymous or
|
|
714
|
+
* surface NOT_SIGNED_IN to the host app instead of looping.
|
|
715
|
+
*/
|
|
682
716
|
async refreshAccessToken() {
|
|
683
717
|
if (this.refreshInflight) return this.refreshInflight;
|
|
684
718
|
const refresh = this.hooks.storage.get(REFRESH_KEY);
|
|
@@ -687,14 +721,18 @@ var Auth = class {
|
|
|
687
721
|
try {
|
|
688
722
|
const data = await this.callAuth("/api/v1/auth-service/token/refresh", {
|
|
689
723
|
refreshToken: refresh
|
|
690
|
-
})
|
|
691
|
-
if (!data) return null;
|
|
724
|
+
});
|
|
692
725
|
this.accessToken = data.tokens.accessToken;
|
|
693
726
|
this.accessExpiresAt = Date.now() + data.tokens.expiresIn * 1e3;
|
|
694
727
|
this.hooks.storage.set(TOKEN_KEY, data.tokens.accessToken);
|
|
695
728
|
this.hooks.storage.set(TOKEN_EXPIRES_KEY, String(this.accessExpiresAt));
|
|
696
729
|
this.hooks.storage.set(REFRESH_KEY, data.tokens.refreshToken);
|
|
697
730
|
return data.tokens.accessToken;
|
|
731
|
+
} catch (err) {
|
|
732
|
+
if (err instanceof AuthError && isDeadRefreshError(err.code)) {
|
|
733
|
+
await this.wipeLocalIdentity().catch(() => void 0);
|
|
734
|
+
}
|
|
735
|
+
return null;
|
|
698
736
|
} finally {
|
|
699
737
|
this.refreshInflight = null;
|
|
700
738
|
}
|
|
@@ -745,6 +783,10 @@ var Auth = class {
|
|
|
745
783
|
await this.hooks.onAnonymousWipe();
|
|
746
784
|
}
|
|
747
785
|
};
|
|
786
|
+
function isDeadRefreshError(code) {
|
|
787
|
+
if (!code) return false;
|
|
788
|
+
return code === "INVALID_REFRESH_TOKEN" || code === "HTTP_401" || code === "UNAUTHORIZED" || code === "RATE_LIMIT_EXCEEDED" || code === "RATE_LIMIT";
|
|
789
|
+
}
|
|
748
790
|
function decodeJwtPayload(token) {
|
|
749
791
|
try {
|
|
750
792
|
const parts = token.split(".");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sendoracloud/sdk-react-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.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",
|