@layers/amba-react-native 4.2.1 → 4.3.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 +1032 -299
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +47 -6
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +47 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1032 -299
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -56,6 +56,20 @@ var AsyncStoragePersistence = class {
|
|
|
56
56
|
if (v !== null) this.memoryCache.set(fullKey, v);
|
|
57
57
|
return v;
|
|
58
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Read `key` from storage again, past the in-memory copy, and keep what
|
|
61
|
+
* it holds now. For a record whose first read came back incomplete (see
|
|
62
|
+
* `IdentityStore.recordStillUnreadable`).
|
|
63
|
+
*/
|
|
64
|
+
async reread(key) {
|
|
65
|
+
const fullKey = `${this.prefix}:${key}`;
|
|
66
|
+
const storage = await getStorage();
|
|
67
|
+
if (!storage) return this.memoryCache.get(fullKey) ?? null;
|
|
68
|
+
const v = await storage.getItem(fullKey);
|
|
69
|
+
if (v !== null) this.memoryCache.set(fullKey, v);
|
|
70
|
+
else this.memoryCache.delete(fullKey);
|
|
71
|
+
return v;
|
|
72
|
+
}
|
|
59
73
|
async write(key, value) {
|
|
60
74
|
const fullKey = `${this.prefix}:${key}`;
|
|
61
75
|
const storage = await getStorage();
|
|
@@ -249,8 +263,35 @@ function withSignal(init, signal) {
|
|
|
249
263
|
//#region src/http-client.ts
|
|
250
264
|
/** Default amba API endpoint. */
|
|
251
265
|
const DEFAULT_BASE_URL = "https://api.amba.dev";
|
|
252
|
-
/**
|
|
253
|
-
|
|
266
|
+
/**
|
|
267
|
+
* Server codes for a session token the server no longer accepts.
|
|
268
|
+
* `INVALID_SESSION` is the code the auth routes that verify the session
|
|
269
|
+
* themselves (`/identifiers`, `/link`, `/unlink`) answer with.
|
|
270
|
+
*/
|
|
271
|
+
const SESSION_REFUSED_CODES = new Set([
|
|
272
|
+
"SESSION_REVOKED",
|
|
273
|
+
"INVALID_SESSION_TOKEN",
|
|
274
|
+
"INVALID_SESSION"
|
|
275
|
+
]);
|
|
276
|
+
/** True for a server error that refused the session token a call sent. */
|
|
277
|
+
function isSessionRefusedError(err) {
|
|
278
|
+
return err instanceof AmbaApiError && SESSION_REFUSED_CODES.has(err.code);
|
|
279
|
+
}
|
|
280
|
+
const AUTH_PATH_PREFIX = "/v1/client/auth/";
|
|
281
|
+
/**
|
|
282
|
+
* The auth routes that read the session from the `Authorization` header and
|
|
283
|
+
* mint nothing: a refused session on them recovers like it does on any
|
|
284
|
+
* other authenticated call. The other auth routes stay out of the
|
|
285
|
+
* recovery: `/refresh` is the recovery itself, `/logout` ends the session,
|
|
286
|
+
* the sign-in, sign-up, OTP and magic-link routes take credentials (a 401
|
|
287
|
+
* there is a credential error), and `/link` and `/unlink` carry the session
|
|
288
|
+
* in their body, so their callers recover and rebuild the body themselves.
|
|
289
|
+
*/
|
|
290
|
+
const SESSION_HEADER_AUTH_PATHS = new Set([`${AUTH_PATH_PREFIX}me`, `${AUTH_PATH_PREFIX}identifiers`]);
|
|
291
|
+
/** Whether a refused session on `path` goes through `onSessionRefused`. */
|
|
292
|
+
function recoversRefusedSession(path) {
|
|
293
|
+
return !path.startsWith(AUTH_PATH_PREFIX) || SESSION_HEADER_AUTH_PATHS.has(path);
|
|
294
|
+
}
|
|
254
295
|
var AmbaHttpClient = class {
|
|
255
296
|
apiKey;
|
|
256
297
|
baseUrl;
|
|
@@ -386,7 +427,7 @@ var AmbaHttpClient = class {
|
|
|
386
427
|
try {
|
|
387
428
|
return parseResponse(response, text);
|
|
388
429
|
} catch (err) {
|
|
389
|
-
if (!resent && storedToken && this.onSessionRefused &&
|
|
430
|
+
if (!resent && storedToken && this.onSessionRefused && recoversRefusedSession(path) && isSessionRefusedError(err) && await this.recoverSession(storedToken, storedUserId)) return this.request(method, path, body, query, void 0, true);
|
|
390
431
|
throw err;
|
|
391
432
|
}
|
|
392
433
|
}
|
|
@@ -580,11 +621,42 @@ var IdentityStore = class IdentityStore {
|
|
|
580
621
|
installId;
|
|
581
622
|
/** Whether the stored record carries `installId`. */
|
|
582
623
|
installIdStored;
|
|
583
|
-
|
|
624
|
+
/** See `IdentitySnapshot.signInRequired`. */
|
|
625
|
+
signInRequired;
|
|
626
|
+
/**
|
|
627
|
+
* Advanced by every committed write that installs or ends a session (a
|
|
628
|
+
* sign-in, a sign-out, an ended session). A refresh or a link keeps it.
|
|
629
|
+
*/
|
|
630
|
+
generation = 0;
|
|
631
|
+
/** Set by `dispose`: the store commits and persists nothing after it. */
|
|
632
|
+
disposed = false;
|
|
633
|
+
/**
|
|
634
|
+
* Set while the record's credential part could not be read (see
|
|
635
|
+
* `PersistedIdentity.credentials_unreadable`). Writes wait for a read
|
|
636
|
+
* that succeeds, so a session this launch never loaded is not replaced.
|
|
637
|
+
*/
|
|
638
|
+
recordUnreadable;
|
|
639
|
+
/**
|
|
640
|
+
* Set while the anonymous id is a stand-in for one the unreadable
|
|
641
|
+
* credential part holds. It is never persisted.
|
|
642
|
+
*/
|
|
643
|
+
temporaryAnonymousId;
|
|
644
|
+
/** See `onStoredSessionLoaded`. */
|
|
645
|
+
storedSessionLoaded = null;
|
|
646
|
+
/** See `onAnonymousIdKnown`. */
|
|
647
|
+
anonymousIdKnown = null;
|
|
648
|
+
/** The refresh token the store held once created (see `continuesLaunchSession`). */
|
|
649
|
+
launchRefreshToken = null;
|
|
650
|
+
/** Sign-ins committed since the store was created. */
|
|
651
|
+
signInsSinceLaunch = 0;
|
|
652
|
+
constructor(persistence, seed, vault, plaintext, vaultUnreadable, seq, recordUnreadable, temporaryAnonymousId) {
|
|
584
653
|
this.persistence = persistence;
|
|
585
654
|
this.vault = vault;
|
|
586
655
|
this.plaintext = plaintext;
|
|
587
656
|
this.vaultUnreadable = vaultUnreadable;
|
|
657
|
+
this.recordUnreadable = recordUnreadable;
|
|
658
|
+
this.temporaryAnonymousId = temporaryAnonymousId;
|
|
659
|
+
this.signInRequired = seed.sign_in_required;
|
|
588
660
|
this.seq = seq;
|
|
589
661
|
this.installId = seed.install_id ?? uuidv4();
|
|
590
662
|
this.installIdStored = seed.install_id !== null;
|
|
@@ -620,21 +692,17 @@ var IdentityStore = class IdentityStore {
|
|
|
620
692
|
assertSecureRandomAvailable();
|
|
621
693
|
const raw = await persistence.read(IDENTITY_KEY);
|
|
622
694
|
let recordPresent = false;
|
|
695
|
+
let recordUnreadable = false;
|
|
696
|
+
let temporaryAnonymousId = false;
|
|
623
697
|
let seed;
|
|
624
698
|
if (raw) try {
|
|
625
699
|
const parsed = JSON.parse(raw);
|
|
626
|
-
seed =
|
|
627
|
-
anonymous_id: parsed.anonymous_id ?? uuidv4(),
|
|
628
|
-
app_user_id: parsed.app_user_id ?? null,
|
|
629
|
-
session_token: parsed.session_token ?? null,
|
|
630
|
-
refresh_token: parsed.refresh_token ?? null,
|
|
631
|
-
expires_at: parsed.expires_at ?? null,
|
|
632
|
-
anonymous_session: typeof parsed.anonymous_session === "boolean" ? parsed.anonymous_session : null,
|
|
633
|
-
write_seq: seqOf(parsed.write_seq) ?? 0,
|
|
634
|
-
vault_seq: parsed.vault_seq === void 0 ? 0 : seqOf(parsed.vault_seq),
|
|
635
|
-
install_id: typeof parsed.install_id === "string" ? parsed.install_id : null
|
|
636
|
-
};
|
|
700
|
+
seed = seedFrom(parsed);
|
|
637
701
|
recordPresent = typeof parsed.anonymous_id === "string";
|
|
702
|
+
if (parsed.credentials_unreadable === true) {
|
|
703
|
+
recordUnreadable = true;
|
|
704
|
+
temporaryAnonymousId = !recordPresent;
|
|
705
|
+
}
|
|
638
706
|
} catch {
|
|
639
707
|
seed = freshSeed();
|
|
640
708
|
}
|
|
@@ -649,7 +717,8 @@ var IdentityStore = class IdentityStore {
|
|
|
649
717
|
} : null;
|
|
650
718
|
let vaultReadable = true;
|
|
651
719
|
let seq = seed.write_seq;
|
|
652
|
-
if (vault)
|
|
720
|
+
if (vault && recordUnreadable) vaultReadable = false;
|
|
721
|
+
else if (vault) {
|
|
653
722
|
let fromVault = null;
|
|
654
723
|
try {
|
|
655
724
|
fromVault = await vault.read();
|
|
@@ -668,17 +737,10 @@ var IdentityStore = class IdentityStore {
|
|
|
668
737
|
seed.vault_seq = null;
|
|
669
738
|
}
|
|
670
739
|
}
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
seed.app_user_id = null;
|
|
674
|
-
seed.anonymous_session = null;
|
|
675
|
-
}
|
|
676
|
-
if (isExpired(seed.expires_at)) {
|
|
677
|
-
seed.session_token = null;
|
|
678
|
-
seed.expires_at = null;
|
|
679
|
-
}
|
|
680
|
-
const store = new IdentityStore(persistence, seed, vault, plaintext, !vaultReadable, seq);
|
|
740
|
+
settleSeed(seed, vaultReadable && !recordUnreadable);
|
|
741
|
+
const store = new IdentityStore(persistence, seed, vault, plaintext, !vaultReadable, seq, recordUnreadable, temporaryAnonymousId);
|
|
681
742
|
await store.flush({ hydrating: true });
|
|
743
|
+
store.launchRefreshToken = store.refreshToken;
|
|
682
744
|
return store;
|
|
683
745
|
}
|
|
684
746
|
snapshot() {
|
|
@@ -688,9 +750,184 @@ var IdentityStore = class IdentityStore {
|
|
|
688
750
|
sessionToken: this.sessionToken,
|
|
689
751
|
refreshToken: this.refreshToken,
|
|
690
752
|
expiresAt: this.expiresAt,
|
|
691
|
-
anonymousSession: this.anonymousSession
|
|
753
|
+
anonymousSession: this.anonymousSession,
|
|
754
|
+
signInRequired: this.signInRequired
|
|
692
755
|
};
|
|
693
756
|
}
|
|
757
|
+
/** The current generation (see `CommitFence`). */
|
|
758
|
+
getGeneration() {
|
|
759
|
+
return this.generation;
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Whether the store still holds the session it was created with, renewed
|
|
763
|
+
* or ended since but never replaced by a sign-in: it was created holding
|
|
764
|
+
* `refreshToken`, and no sign-in has committed since.
|
|
765
|
+
*/
|
|
766
|
+
continuesLaunchSession(refreshToken) {
|
|
767
|
+
return this.launchRefreshToken === refreshToken && this.signInsSinceLaunch === 0;
|
|
768
|
+
}
|
|
769
|
+
/** See `IdentitySnapshot.signInRequired`. */
|
|
770
|
+
isSignInRequired() {
|
|
771
|
+
return this.signInRequired;
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Detach the store (the SDK was configured again). Nothing commits or
|
|
775
|
+
* persists after this; the promise settles once the writes already under
|
|
776
|
+
* way have landed.
|
|
777
|
+
*/
|
|
778
|
+
dispose() {
|
|
779
|
+
this.disposed = true;
|
|
780
|
+
return this.flushChain;
|
|
781
|
+
}
|
|
782
|
+
/** Undo `dispose` (a `configure()` that failed before it replaced this store). */
|
|
783
|
+
reattach() {
|
|
784
|
+
this.disposed = false;
|
|
785
|
+
}
|
|
786
|
+
/** Whether a write under `fence` may commit now. */
|
|
787
|
+
mayCommit(fence) {
|
|
788
|
+
if (this.disposed) return false;
|
|
789
|
+
return fence.ifGeneration === void 0 || fence.ifGeneration === this.generation;
|
|
790
|
+
}
|
|
791
|
+
/** Commit bookkeeping: advance the generation unless the fence keeps it. */
|
|
792
|
+
committed(fence) {
|
|
793
|
+
if (fence.advance !== false) this.generation += 1;
|
|
794
|
+
return this.generation;
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Called when a stored session that couldn't be read at launch (a locked
|
|
798
|
+
* vault or credential record) is loaded after it, so the SDK can tell its
|
|
799
|
+
* subscribers. A session loaded while the store is created is not
|
|
800
|
+
* reported.
|
|
801
|
+
*/
|
|
802
|
+
onStoredSessionLoaded(listener) {
|
|
803
|
+
this.storedSessionLoaded = listener;
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Called when the stand-in anonymous id gives way to the device's own (see
|
|
807
|
+
* `temporaryAnonymousId`): a later read found it, or a sign-in gave one.
|
|
808
|
+
* A sign-out that rotates the id is not reported: it starts over.
|
|
809
|
+
*/
|
|
810
|
+
onAnonymousIdKnown(listener) {
|
|
811
|
+
this.anonymousIdKnown = listener;
|
|
812
|
+
}
|
|
813
|
+
/** Whether the anonymous id is a stand-in (see `temporaryAnonymousId`). */
|
|
814
|
+
hasStandInAnonymousId() {
|
|
815
|
+
return this.temporaryAnonymousId;
|
|
816
|
+
}
|
|
817
|
+
/** The stand-in gives way to a known anonymous id (see `onAnonymousIdKnown`). */
|
|
818
|
+
anonymousIdNowKnown() {
|
|
819
|
+
if (!this.temporaryAnonymousId) return;
|
|
820
|
+
this.temporaryAnonymousId = false;
|
|
821
|
+
if (!this.disposed) this.anonymousIdKnown?.();
|
|
822
|
+
}
|
|
823
|
+
/** Report a session a late read loaded (see `onStoredSessionLoaded`). */
|
|
824
|
+
reportStoredSession() {
|
|
825
|
+
if (this.signInsSinceLaunch === 0 && this.launchRefreshToken === null) this.launchRefreshToken = this.refreshToken;
|
|
826
|
+
if (this.disposed) return;
|
|
827
|
+
if (this.sessionToken === null && this.refreshToken === null) return;
|
|
828
|
+
this.storedSessionLoaded?.();
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Whether the record's credential part still can't be read (see
|
|
832
|
+
* `PersistedIdentity.credentials_unreadable`). When the launch read
|
|
833
|
+
* failed, the record is read again first. A session it holds is loaded
|
|
834
|
+
* when nothing has replaced the launch state since; the stored anonymous
|
|
835
|
+
* id replaces the stand-in either way.
|
|
836
|
+
*/
|
|
837
|
+
async recordStillUnreadable() {
|
|
838
|
+
if (!this.recordUnreadable) return false;
|
|
839
|
+
let parsed = null;
|
|
840
|
+
try {
|
|
841
|
+
const raw = await this.persistence.reread(IDENTITY_KEY);
|
|
842
|
+
parsed = raw ? JSON.parse(raw) : null;
|
|
843
|
+
} catch {
|
|
844
|
+
return true;
|
|
845
|
+
}
|
|
846
|
+
if (!this.recordUnreadable) return false;
|
|
847
|
+
if (parsed?.credentials_unreadable === true) return true;
|
|
848
|
+
this.recordUnreadable = false;
|
|
849
|
+
const storedAnonymousId = parsed && typeof parsed.anonymous_id === "string" ? parsed.anonymous_id : null;
|
|
850
|
+
const untouched = this.generation === 0 && this.sessionToken === null && this.refreshToken === null;
|
|
851
|
+
if (parsed && storedAnonymousId !== null && untouched) {
|
|
852
|
+
if (!this.vault) {
|
|
853
|
+
const stored$1 = seedFrom(parsed);
|
|
854
|
+
settleSeed(stored$1, true);
|
|
855
|
+
this.anonymousId = stored$1.anonymous_id;
|
|
856
|
+
this.appUserId = stored$1.app_user_id;
|
|
857
|
+
this.sessionToken = stored$1.session_token;
|
|
858
|
+
this.refreshToken = stored$1.refresh_token;
|
|
859
|
+
this.expiresAt = stored$1.expires_at;
|
|
860
|
+
this.anonymousSession = stored$1.anonymous_session;
|
|
861
|
+
this.signInRequired = stored$1.sign_in_required;
|
|
862
|
+
this.seq = Math.max(this.seq, stored$1.write_seq);
|
|
863
|
+
this.reportStoredSession();
|
|
864
|
+
this.anonymousIdNowKnown();
|
|
865
|
+
return false;
|
|
866
|
+
}
|
|
867
|
+
const stored = seedFrom(parsed);
|
|
868
|
+
this.anonymousId = stored.anonymous_id;
|
|
869
|
+
const wasStandIn = this.temporaryAnonymousId;
|
|
870
|
+
this.temporaryAnonymousId = false;
|
|
871
|
+
this.appUserId = stored.app_user_id;
|
|
872
|
+
this.anonymousSession = stored.anonymous_session;
|
|
873
|
+
this.signInRequired = stored.sign_in_required;
|
|
874
|
+
this.recordSeq = stored.write_seq;
|
|
875
|
+
this.vaultSeq = stored.vault_seq;
|
|
876
|
+
this.seq = Math.max(this.seq, stored.write_seq);
|
|
877
|
+
await this.vaultStillUnreadable();
|
|
878
|
+
if (this.sessionToken === null && this.refreshToken === null && (stored.session_token !== null || stored.refresh_token !== null)) {
|
|
879
|
+
this.plaintext = {
|
|
880
|
+
appUserId: stored.app_user_id,
|
|
881
|
+
slice: {
|
|
882
|
+
session_token: stored.session_token,
|
|
883
|
+
refresh_token: stored.refresh_token,
|
|
884
|
+
expires_at: stored.expires_at
|
|
885
|
+
}
|
|
886
|
+
};
|
|
887
|
+
const expired = isExpired(stored.expires_at);
|
|
888
|
+
this.sessionToken = expired ? null : stored.session_token;
|
|
889
|
+
this.refreshToken = stored.refresh_token;
|
|
890
|
+
this.expiresAt = expired ? null : stored.expires_at;
|
|
891
|
+
this.reportStoredSession();
|
|
892
|
+
}
|
|
893
|
+
if (!this.vaultUnreadable && this.sessionToken === null && this.refreshToken === null) {
|
|
894
|
+
if (this.appUserId !== null && this.anonymousSession === false) this.anonymousId = uuidv4();
|
|
895
|
+
this.appUserId = null;
|
|
896
|
+
this.anonymousSession = null;
|
|
897
|
+
}
|
|
898
|
+
if (wasStandIn && !this.disposed) this.anonymousIdKnown?.();
|
|
899
|
+
return false;
|
|
900
|
+
}
|
|
901
|
+
if (this.temporaryAnonymousId && storedAnonymousId !== null) this.anonymousId = storedAnonymousId;
|
|
902
|
+
this.anonymousIdNowKnown();
|
|
903
|
+
return false;
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* Whether the stored session can't be read right now: the record's
|
|
907
|
+
* credential part (see `recordStillUnreadable`) or the vault (see
|
|
908
|
+
* `vaultStillUnreadable`). The record is read first: it binds the entry.
|
|
909
|
+
*/
|
|
910
|
+
async credentialsStillUnreadable() {
|
|
911
|
+
if (await this.recordStillUnreadable()) return true;
|
|
912
|
+
return this.vaultStillUnreadable();
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Whether the stored credentials were unreadable at the last attempt. A
|
|
916
|
+
* call that needs a session reports `SECURE_STORE_UNAVAILABLE` then.
|
|
917
|
+
*/
|
|
918
|
+
credentialsUnknown() {
|
|
919
|
+
return this.recordUnreadable || this.vaultUnreadable;
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* Whether the anonymous id is still a stand-in after the record is read
|
|
923
|
+
* again (see `temporaryAnonymousId`). Anything sent under it would belong
|
|
924
|
+
* to nobody.
|
|
925
|
+
*/
|
|
926
|
+
async anonymousIdStillUnknown() {
|
|
927
|
+
if (!this.temporaryAnonymousId) return false;
|
|
928
|
+
await this.recordStillUnreadable();
|
|
929
|
+
return this.temporaryAnonymousId;
|
|
930
|
+
}
|
|
694
931
|
/**
|
|
695
932
|
* Whether the vault still can't be read. When the launch read failed, the
|
|
696
933
|
* vault is read again first, and a session it holds for this install is
|
|
@@ -699,6 +936,7 @@ var IdentityStore = class IdentityStore {
|
|
|
699
936
|
*/
|
|
700
937
|
async vaultStillUnreadable() {
|
|
701
938
|
if (!this.vault || !this.vaultUnreadable) return false;
|
|
939
|
+
if (this.recordUnreadable) return true;
|
|
702
940
|
let stored;
|
|
703
941
|
try {
|
|
704
942
|
stored = await this.vault.read();
|
|
@@ -707,7 +945,10 @@ var IdentityStore = class IdentityStore {
|
|
|
707
945
|
}
|
|
708
946
|
this.vaultUnreadable = false;
|
|
709
947
|
this.noteStoredSeq(stored);
|
|
710
|
-
if (this.sessionToken === null && this.refreshToken === null)
|
|
948
|
+
if (this.sessionToken === null && this.refreshToken === null) {
|
|
949
|
+
this.adoptStored(stored);
|
|
950
|
+
this.reportStoredSession();
|
|
951
|
+
}
|
|
711
952
|
return false;
|
|
712
953
|
}
|
|
713
954
|
/** Keep the next sequence number above an entry read from the vault. */
|
|
@@ -744,30 +985,51 @@ var IdentityStore = class IdentityStore {
|
|
|
744
985
|
/**
|
|
745
986
|
* Store a session. `anonymous` records whether it is a purely anonymous
|
|
746
987
|
* one (true only for `/v1/client/auth/anonymous`); omit it to keep the
|
|
747
|
-
* current value (a token refresh of the same session).
|
|
988
|
+
* current value (a token refresh of the same session). A stored session
|
|
989
|
+
* ends a pending `signInRequired`.
|
|
990
|
+
*
|
|
991
|
+
* Resolves the generation the write committed at, or null when the fence
|
|
992
|
+
* refused it (nothing changed).
|
|
748
993
|
*/
|
|
749
|
-
async setSession(args) {
|
|
994
|
+
async setSession(args, fence = {}) {
|
|
995
|
+
if (!this.mayCommit(fence)) return null;
|
|
750
996
|
this.appUserId = args.appUserId;
|
|
751
997
|
this.sessionToken = args.sessionToken;
|
|
752
998
|
this.refreshToken = args.refreshToken;
|
|
753
999
|
this.expiresAt = args.expiresAt;
|
|
754
1000
|
if (args.anonymous !== void 0) this.anonymousSession = args.anonymous;
|
|
1001
|
+
this.signInRequired = false;
|
|
1002
|
+
if (fence.advance !== false) this.signInsSinceLaunch += 1;
|
|
1003
|
+
const generation = this.committed(fence);
|
|
755
1004
|
await this.flush();
|
|
1005
|
+
return generation;
|
|
756
1006
|
}
|
|
757
|
-
/**
|
|
758
|
-
|
|
759
|
-
|
|
1007
|
+
/**
|
|
1008
|
+
* The current session gained an identity (a link). False when the fence
|
|
1009
|
+
* refused the write.
|
|
1010
|
+
*/
|
|
1011
|
+
async markIdentified(fence = {}) {
|
|
1012
|
+
if (!this.mayCommit(fence)) return false;
|
|
1013
|
+
if (this.anonymousSession === false) return true;
|
|
760
1014
|
this.anonymousSession = false;
|
|
761
1015
|
await this.flush();
|
|
1016
|
+
return true;
|
|
762
1017
|
}
|
|
763
1018
|
/**
|
|
764
1019
|
* Adopt the anonymous id the server holds for this install's user: the id
|
|
765
1020
|
* an anonymous sign-in answered with, or the fresh id a link gave the user.
|
|
1021
|
+
* False when the fence refused the write.
|
|
766
1022
|
*/
|
|
767
|
-
async setAnonymousId(anonymousId) {
|
|
768
|
-
if (
|
|
769
|
-
this.anonymousId
|
|
770
|
-
|
|
1023
|
+
async setAnonymousId(anonymousId, fence = {}) {
|
|
1024
|
+
if (!this.mayCommit(fence)) return false;
|
|
1025
|
+
if (anonymousId !== this.anonymousId) {
|
|
1026
|
+
this.anonymousId = anonymousId;
|
|
1027
|
+
this.anonymousIdNowKnown();
|
|
1028
|
+
await this.flush();
|
|
1029
|
+
return true;
|
|
1030
|
+
}
|
|
1031
|
+
this.anonymousIdNowKnown();
|
|
1032
|
+
return true;
|
|
771
1033
|
}
|
|
772
1034
|
/**
|
|
773
1035
|
* End the session. The anonymous id is rotated when `rotateAnonymousId`
|
|
@@ -779,16 +1041,24 @@ var IdentityStore = class IdentityStore {
|
|
|
779
1041
|
* anonymous sign-in).
|
|
780
1042
|
*/
|
|
781
1043
|
async clearSession(rotateAnonymousId, opts = {}) {
|
|
1044
|
+
if (!this.mayCommit({ ifGeneration: opts.ifGeneration })) return false;
|
|
782
1045
|
const hadSession = this.sessionToken !== null || this.refreshToken !== null;
|
|
1046
|
+
const unreadSession = this.credentialsUnknown() && this.appUserId !== null;
|
|
1047
|
+
if (opts.unlessSignedInSince !== void 0 && opts.unlessSignedInSince !== this.generation && hadSession) return false;
|
|
783
1048
|
const identified = this.anonymousSession === false || this.anonymousSession === null && !opts.keepIdWhenKindUnknown;
|
|
784
|
-
const
|
|
1049
|
+
const rotate = rotateAnonymousId || (hadSession || unreadSession) && identified;
|
|
1050
|
+
const nextAnonymousId = rotate ? uuidv4() : this.anonymousId;
|
|
1051
|
+
if (rotate) this.temporaryAnonymousId = false;
|
|
785
1052
|
this.appUserId = null;
|
|
786
1053
|
this.sessionToken = null;
|
|
787
1054
|
this.refreshToken = null;
|
|
788
1055
|
this.expiresAt = null;
|
|
789
1056
|
this.anonymousSession = null;
|
|
1057
|
+
this.signInRequired = opts.signInRequired === true;
|
|
790
1058
|
this.anonymousId = nextAnonymousId;
|
|
1059
|
+
this.committed({});
|
|
791
1060
|
await this.flush({ signingOut: true });
|
|
1061
|
+
return true;
|
|
792
1062
|
}
|
|
793
1063
|
/**
|
|
794
1064
|
* Persist the identity. `hydrating` marks the write that follows `create`:
|
|
@@ -798,11 +1068,13 @@ var IdentityStore = class IdentityStore {
|
|
|
798
1068
|
* to a new anonymous id so the entry left behind no longer belongs to it.
|
|
799
1069
|
*/
|
|
800
1070
|
flush(opts) {
|
|
1071
|
+
if (this.disposed) return Promise.resolve();
|
|
801
1072
|
const run = this.flushChain.then(() => this.flushNow(opts));
|
|
802
1073
|
this.flushChain = run.catch(() => void 0);
|
|
803
1074
|
return run;
|
|
804
1075
|
}
|
|
805
1076
|
async flushNow(opts) {
|
|
1077
|
+
if (await this.recordStillUnreadable() && this.generation === 0) return;
|
|
806
1078
|
if (!this.vault) {
|
|
807
1079
|
const seq$1 = ++this.seq;
|
|
808
1080
|
const recordSeq$1 = opts?.hydrating ? this.recordSeq : seq$1;
|
|
@@ -813,9 +1085,11 @@ var IdentityStore = class IdentityStore {
|
|
|
813
1085
|
refresh_token: this.refreshToken,
|
|
814
1086
|
expires_at: this.expiresAt,
|
|
815
1087
|
anonymous_session: this.anonymousSession,
|
|
1088
|
+
sign_in_required: this.signInRequired,
|
|
816
1089
|
write_seq: recordSeq$1,
|
|
817
1090
|
vault_seq: null,
|
|
818
|
-
install_id: this.installId
|
|
1091
|
+
install_id: this.installId,
|
|
1092
|
+
...this.temporaryAnonymousId ? { anonymous_id_unknown: true } : {}
|
|
819
1093
|
};
|
|
820
1094
|
await this.persistence.write(IDENTITY_KEY, JSON.stringify(payload));
|
|
821
1095
|
this.recordSeq = recordSeq$1;
|
|
@@ -894,9 +1168,11 @@ var IdentityStore = class IdentityStore {
|
|
|
894
1168
|
refresh_token: keep?.refresh_token ?? null,
|
|
895
1169
|
expires_at: keep?.expires_at ?? null,
|
|
896
1170
|
anonymous_session: this.anonymousSession,
|
|
1171
|
+
sign_in_required: this.signInRequired,
|
|
897
1172
|
write_seq: recordSeq,
|
|
898
1173
|
vault_seq: this.vaultSeq,
|
|
899
|
-
install_id: this.installId
|
|
1174
|
+
install_id: this.installId,
|
|
1175
|
+
...this.temporaryAnonymousId ? { anonymous_id_unknown: true } : {}
|
|
900
1176
|
};
|
|
901
1177
|
}
|
|
902
1178
|
};
|
|
@@ -908,11 +1184,47 @@ function freshSeed() {
|
|
|
908
1184
|
refresh_token: null,
|
|
909
1185
|
expires_at: null,
|
|
910
1186
|
anonymous_session: null,
|
|
1187
|
+
sign_in_required: false,
|
|
911
1188
|
write_seq: 0,
|
|
912
1189
|
vault_seq: null,
|
|
913
1190
|
install_id: null
|
|
914
1191
|
};
|
|
915
1192
|
}
|
|
1193
|
+
/** The identity a parsed record describes, with defaults for missing fields. */
|
|
1194
|
+
function seedFrom(parsed) {
|
|
1195
|
+
return {
|
|
1196
|
+
anonymous_id: parsed.anonymous_id ?? uuidv4(),
|
|
1197
|
+
app_user_id: parsed.app_user_id ?? null,
|
|
1198
|
+
session_token: parsed.session_token ?? null,
|
|
1199
|
+
refresh_token: parsed.refresh_token ?? null,
|
|
1200
|
+
expires_at: parsed.expires_at ?? null,
|
|
1201
|
+
anonymous_session: typeof parsed.anonymous_session === "boolean" ? parsed.anonymous_session : null,
|
|
1202
|
+
sign_in_required: parsed.sign_in_required === true,
|
|
1203
|
+
write_seq: seqOf(parsed.write_seq) ?? 0,
|
|
1204
|
+
vault_seq: parsed.vault_seq === void 0 ? 0 : seqOf(parsed.vault_seq),
|
|
1205
|
+
install_id: typeof parsed.install_id === "string" ? parsed.install_id : null
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* Settle a hydrated identity. With the credentials known (`readable`), a
|
|
1210
|
+
* user id with no token to act on is stale (memory storage after a
|
|
1211
|
+
* relaunch, a vault entry that was discarded): nobody is signed in, and the
|
|
1212
|
+
* session kind goes with it. A session that ended this way after it gained
|
|
1213
|
+
* an identity rotates the anonymous id, as `clearSession` would. An expired
|
|
1214
|
+
* session token is dropped; its refresh token stays so the session can be
|
|
1215
|
+
* renewed.
|
|
1216
|
+
*/
|
|
1217
|
+
function settleSeed(seed, readable) {
|
|
1218
|
+
if (readable && seed.session_token === null && seed.refresh_token === null) {
|
|
1219
|
+
if (seed.app_user_id !== null && seed.anonymous_session === false) seed.anonymous_id = uuidv4();
|
|
1220
|
+
seed.app_user_id = null;
|
|
1221
|
+
seed.anonymous_session = null;
|
|
1222
|
+
}
|
|
1223
|
+
if (isExpired(seed.expires_at)) {
|
|
1224
|
+
seed.session_token = null;
|
|
1225
|
+
seed.expires_at = null;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
916
1228
|
|
|
917
1229
|
//#endregion
|
|
918
1230
|
//#region src/token-storage.ts
|
|
@@ -1605,14 +1917,18 @@ var EventBatchQueue = class {
|
|
|
1605
1917
|
if (this.events.length) await this.store.set(this.storageKey, JSON.stringify(this.events));
|
|
1606
1918
|
else await this.store.delete(this.storageKey);
|
|
1607
1919
|
}
|
|
1608
|
-
|
|
1920
|
+
/**
|
|
1921
|
+
* Queue an event. `timestamp` is when it happened, for an event that waited
|
|
1922
|
+
* elsewhere before reaching the queue; it defaults to now.
|
|
1923
|
+
*/
|
|
1924
|
+
enqueue(event, properties, telemetry, timestamp) {
|
|
1609
1925
|
if (this.disposed) return Promise.reject(/* @__PURE__ */ new Error("Amba event queue disposed"));
|
|
1610
1926
|
const id = this.identity();
|
|
1611
1927
|
const item = {
|
|
1612
1928
|
event,
|
|
1613
1929
|
properties,
|
|
1614
1930
|
telemetry,
|
|
1615
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1931
|
+
timestamp: timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1616
1932
|
idempotency_key: newIdempotencyKey(),
|
|
1617
1933
|
userId: id.sessionToken ? id.userId : null,
|
|
1618
1934
|
anonymousId: id.anonymousId
|
|
@@ -2747,7 +3063,9 @@ function delay(ms) {
|
|
|
2747
3063
|
//#endregion
|
|
2748
3064
|
//#region src/index.ts
|
|
2749
3065
|
/** SDK semver — burned in at build time. Bump alongside package.json. */
|
|
2750
|
-
const SDK_VERSION = "4.
|
|
3066
|
+
const SDK_VERSION = "4.3.0";
|
|
3067
|
+
/** The most events held at once; the oldest goes first past it. */
|
|
3068
|
+
const MAX_HELD_EVENTS = 500;
|
|
2751
3069
|
/**
|
|
2752
3070
|
* Default host that serves deployed functions. `Amba.functions.invoke(name)`
|
|
2753
3071
|
* targets `https://<projectId>.<DEFAULT_FUNCTIONS_HOST>/<name>` unless a
|
|
@@ -2764,6 +3082,23 @@ let state = null;
|
|
|
2764
3082
|
* instead of disabling the now-active outbox.
|
|
2765
3083
|
*/
|
|
2766
3084
|
let _offlineEnableToken = 0;
|
|
3085
|
+
/** The `configure()` running now, if any (see `liveState`). */
|
|
3086
|
+
let configuring = null;
|
|
3087
|
+
/** Serializes `configure()` calls, so each detaches the one before it. */
|
|
3088
|
+
let configureChain = Promise.resolve();
|
|
3089
|
+
/**
|
|
3090
|
+
* The state calls act on. While a `configure()` is running, waits for it,
|
|
3091
|
+
* so a sign-in, a link or a sign-out made meanwhile lands on the state it
|
|
3092
|
+
* installs (or on the earlier one, when it fails).
|
|
3093
|
+
*/
|
|
3094
|
+
async function liveState() {
|
|
3095
|
+
while (configuring) await configuring.catch(() => void 0);
|
|
3096
|
+
return getState();
|
|
3097
|
+
}
|
|
3098
|
+
/** Whether two states talk to the same project with the same stored identity. */
|
|
3099
|
+
function sameIdentityTarget(a, b) {
|
|
3100
|
+
return a.projectId === b.projectId && a.clientKey === b.clientKey && a.baseUrl === b.baseUrl && a.persistence.prefix === b.persistence.prefix;
|
|
3101
|
+
}
|
|
2767
3102
|
function getState() {
|
|
2768
3103
|
if (!state) throw new AmbaApiError("NOT_INITIALIZED", "@layers/amba-react-native: SDK not configured. Call Amba.configure({ projectId, clientKey }) at app start.");
|
|
2769
3104
|
return state;
|
|
@@ -2856,10 +3191,16 @@ function snapshotSession(user) {
|
|
|
2856
3191
|
expiresAt: ""
|
|
2857
3192
|
};
|
|
2858
3193
|
}
|
|
2859
|
-
|
|
3194
|
+
/**
|
|
3195
|
+
* Tell `onAuthStateChange` subscribers about an auth change of `state_`.
|
|
3196
|
+
* A state a later `configure()` replaced tells no one.
|
|
3197
|
+
*/
|
|
3198
|
+
function notifyAuthSubscribers(state_, session, reason) {
|
|
3199
|
+
if (state_ !== state || state_.disposed) return;
|
|
2860
3200
|
if (session === null) experimentReporter.reset();
|
|
3201
|
+
const event = { reason };
|
|
2861
3202
|
for (const cb of authSubscribers) try {
|
|
2862
|
-
cb(session);
|
|
3203
|
+
cb(session, event);
|
|
2863
3204
|
} catch (err) {
|
|
2864
3205
|
if (typeof console !== "undefined") console.error("[amba] onAuthStateChange callback threw", err);
|
|
2865
3206
|
}
|
|
@@ -2916,21 +3257,50 @@ function normalizeAuthResult(result) {
|
|
|
2916
3257
|
user: normalizeUserAuthProviders(result.user)
|
|
2917
3258
|
};
|
|
2918
3259
|
}
|
|
2919
|
-
|
|
2920
|
-
notifyAuthSubscribers(snapshotSession(result.user));
|
|
3260
|
+
function notifyFromAuthResult(state_, result, reason) {
|
|
3261
|
+
notifyAuthSubscribers(state_, snapshotSession(result.user), reason);
|
|
3262
|
+
}
|
|
3263
|
+
/** A call needs a session while the user must sign in again. */
|
|
3264
|
+
function signInRequiredError() {
|
|
3265
|
+
return new AmbaApiError("SIGN_IN_REQUIRED", "the session ended and the user must sign in again; call a signIn* method, or Amba.auth.signInAnonymously() to start a new anonymous user");
|
|
3266
|
+
}
|
|
3267
|
+
function secureStoreUnavailableError() {
|
|
3268
|
+
return new AmbaApiError("SECURE_STORE_UNAVAILABLE", "the stored session can't be read yet (the device may be locked); retry once it is unlocked");
|
|
3269
|
+
}
|
|
3270
|
+
function sessionChangedError(message) {
|
|
3271
|
+
return new AmbaApiError("SESSION_CHANGED", message);
|
|
3272
|
+
}
|
|
3273
|
+
function isSessionChanged(err) {
|
|
3274
|
+
return err instanceof AmbaApiError && err.code === "SESSION_CHANGED";
|
|
3275
|
+
}
|
|
3276
|
+
/**
|
|
3277
|
+
* The stored session token, for a call that needs one. Without a session
|
|
3278
|
+
* the call fails with `SIGN_IN_REQUIRED` while the user must sign in again,
|
|
3279
|
+
* and with `UNAUTHORIZED` otherwise.
|
|
3280
|
+
*/
|
|
3281
|
+
function requireSession(state_) {
|
|
3282
|
+
const token = state_.identity.getSessionToken();
|
|
3283
|
+
if (token) return token;
|
|
3284
|
+
if (state_.identity.isSignInRequired()) throw signInRequiredError();
|
|
3285
|
+
if (state_.identity.credentialsUnknown()) throw secureStoreUnavailableError();
|
|
3286
|
+
throw new AmbaApiError("UNAUTHORIZED", "no active session — call signIn* first");
|
|
2921
3287
|
}
|
|
2922
3288
|
function expiresAtFromResult(result) {
|
|
2923
3289
|
if (result.expires_at && typeof result.expires_at === "string") return result.expires_at;
|
|
2924
3290
|
return new Date(Date.now() + 3600 * 1e3).toISOString();
|
|
2925
3291
|
}
|
|
2926
|
-
|
|
2927
|
-
|
|
3292
|
+
/**
|
|
3293
|
+
* Store an auth result under `fence`. Resolves the generation it committed
|
|
3294
|
+
* at, or null when the fence refused it.
|
|
3295
|
+
*/
|
|
3296
|
+
function persistAuthResult(state_, result, anonymous, fence = {}) {
|
|
3297
|
+
return state_.identity.setSession({
|
|
2928
3298
|
appUserId: result.user.id,
|
|
2929
3299
|
sessionToken: result.session_token,
|
|
2930
3300
|
refreshToken: result.refresh_token,
|
|
2931
3301
|
expiresAt: expiresAtFromResult(result),
|
|
2932
3302
|
anonymous
|
|
2933
|
-
});
|
|
3303
|
+
}, fence);
|
|
2934
3304
|
}
|
|
2935
3305
|
function nonEmptyString(value) {
|
|
2936
3306
|
return typeof value === "string" && value.length > 0 ? value : null;
|
|
@@ -2942,14 +3312,31 @@ function nonEmptyString(value) {
|
|
|
2942
3312
|
* request carried none). A link adopts the account's rotated anonymous id.
|
|
2943
3313
|
* A refresh keeps the session's kind; every other sign-in records an
|
|
2944
3314
|
* identified session.
|
|
3315
|
+
*
|
|
3316
|
+
* A sign-in the app asked for stores its session whatever happened
|
|
3317
|
+
* meanwhile: it is the newest thing the app asked for. The automatic
|
|
3318
|
+
* anonymous sign-in and a link pass a `fence` and fail with
|
|
3319
|
+
* `SESSION_CHANGED` (storing nothing) when the session changed while they
|
|
3320
|
+
* were in flight.
|
|
2945
3321
|
*/
|
|
2946
|
-
async function postAuth(state_, path, body) {
|
|
3322
|
+
async function postAuth(state_, path, body, fence = {}) {
|
|
2947
3323
|
const result = normalizeAuthResult((await state_.http.post(path, body)).data);
|
|
2948
|
-
|
|
3324
|
+
const anonymous = path === P.authAnonymous ? true : path === P.authRefresh ? void 0 : false;
|
|
3325
|
+
let target = state_;
|
|
3326
|
+
let committedAt = await persistAuthResult(state_, result, anonymous, fence);
|
|
3327
|
+
if (committedAt === null && fence.ifGeneration === void 0 && state_.disposed) {
|
|
3328
|
+
const next = await liveState();
|
|
3329
|
+
if (next === state_ ? !next.disposed : sameIdentityTarget(next, state_)) {
|
|
3330
|
+
target = next;
|
|
3331
|
+
committedAt = await persistAuthResult(next, result, anonymous);
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3334
|
+
if (committedAt === null) throw sessionChangedError("the session changed while this sign-in was in flight; the newer session stays");
|
|
3335
|
+
state_ = target;
|
|
2949
3336
|
const adoptedAnonymousId = path === P.authAnonymous ? nonEmptyString(result.anonymous_id) ?? nonEmptyString(result.user?.anonymous_id) : path === P.authLink ? nonEmptyString(result.user?.anonymous_id) : null;
|
|
2950
|
-
if (adoptedAnonymousId) await adoptAnonymousId(state_, adoptedAnonymousId);
|
|
3337
|
+
if (adoptedAnonymousId) await adoptAnonymousId(state_, adoptedAnonymousId, committedAt);
|
|
2951
3338
|
state_.eventQueue?.flush();
|
|
2952
|
-
|
|
3339
|
+
notifyFromAuthResult(state_, result, "signed_in");
|
|
2953
3340
|
return result;
|
|
2954
3341
|
}
|
|
2955
3342
|
/**
|
|
@@ -2958,16 +3345,86 @@ async function postAuth(state_, path, body) {
|
|
|
2958
3345
|
* session; they are handed to the signed-in user first, so the id change
|
|
2959
3346
|
* leaves them replayable.
|
|
2960
3347
|
*/
|
|
2961
|
-
async function adoptAnonymousId(state_, anonymousId) {
|
|
3348
|
+
async function adoptAnonymousId(state_, anonymousId, generation) {
|
|
2962
3349
|
const previous = state_.identity.getAnonymousId();
|
|
2963
3350
|
if (anonymousId === previous) return;
|
|
3351
|
+
if (state_.identity.getGeneration() !== generation) return;
|
|
2964
3352
|
const appUserId = state_.identity.getAppUserId();
|
|
2965
3353
|
if (appUserId && state_.offlineEnabled) try {
|
|
2966
3354
|
await getOfflineQueue(state_).claimAnonymousOps(previous, appUserId);
|
|
2967
3355
|
} catch (err) {
|
|
2968
3356
|
if (typeof console !== "undefined") console.warn("[amba] could not reassign queued offline writes", err);
|
|
2969
3357
|
}
|
|
2970
|
-
await state_.identity.setAnonymousId(anonymousId);
|
|
3358
|
+
await state_.identity.setAnonymousId(anonymousId, { ifGeneration: generation });
|
|
3359
|
+
}
|
|
3360
|
+
/**
|
|
3361
|
+
* Hold an event `track()` got while the stored identity couldn't be read.
|
|
3362
|
+
* It keeps its call time and a copy of its properties; past
|
|
3363
|
+
* `MAX_HELD_EVENTS` the oldest is dropped.
|
|
3364
|
+
*/
|
|
3365
|
+
function holdEvent(state_, event, properties, options) {
|
|
3366
|
+
const held = state_.heldEvents ??= [];
|
|
3367
|
+
if (held.length >= MAX_HELD_EVENTS) {
|
|
3368
|
+
held.shift();
|
|
3369
|
+
state_.heldEventsDropped = (state_.heldEventsDropped ?? 0) + 1;
|
|
3370
|
+
}
|
|
3371
|
+
held.push({
|
|
3372
|
+
event,
|
|
3373
|
+
properties: snapshotProperties(properties),
|
|
3374
|
+
options,
|
|
3375
|
+
occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3376
|
+
generation: state_.identity.getGeneration()
|
|
3377
|
+
});
|
|
3378
|
+
}
|
|
3379
|
+
/** Report the held events dropped for `MAX_HELD_EVENTS`, once. */
|
|
3380
|
+
function reportHeldEventsDropped(state_) {
|
|
3381
|
+
const dropped = state_.heldEventsDropped ?? 0;
|
|
3382
|
+
state_.heldEventsDropped = 0;
|
|
3383
|
+
if (dropped > 0 && typeof console !== "undefined") console.warn(`[amba] dropped ${dropped} event(s) tracked while the secure store was locked: more than ${MAX_HELD_EVENTS} were waiting`);
|
|
3384
|
+
}
|
|
3385
|
+
/** Drop the held events, with one warning that says why. */
|
|
3386
|
+
function dropHeldEvents(state_, why) {
|
|
3387
|
+
const count = (state_.heldEvents ?? []).length + (state_.heldEventsDropped ?? 0);
|
|
3388
|
+
state_.heldEvents = [];
|
|
3389
|
+
state_.heldEventsDropped = 0;
|
|
3390
|
+
if (count > 0 && typeof console !== "undefined") console.warn(`[amba] dropped ${count} event(s) tracked while the secure store was locked: ${why}`);
|
|
3391
|
+
}
|
|
3392
|
+
/**
|
|
3393
|
+
* Send the held events once the stored identity is known, oldest first,
|
|
3394
|
+
* through the path `track()` uses, under the identity that loaded. A session
|
|
3395
|
+
* renewal the load started finishes first. An event that fails is reported
|
|
3396
|
+
* and the rest go on; events tracked meanwhile join the end of the line.
|
|
3397
|
+
* An event goes out only while the session it belongs to is still the
|
|
3398
|
+
* stored one: the session that loaded for events held before, the session
|
|
3399
|
+
* of the moment for events tracked during the release. When the session
|
|
3400
|
+
* changed since (the renewal is refused and the session ends, a sign-in or
|
|
3401
|
+
* sign-out lands), the event is dropped, and one warning counts them.
|
|
3402
|
+
*/
|
|
3403
|
+
async function releaseHeldEvents(state_) {
|
|
3404
|
+
if (state_.releasingHeldEvents || !state_.heldEvents?.length) return;
|
|
3405
|
+
state_.releasingHeldEvents = true;
|
|
3406
|
+
let stale = 0;
|
|
3407
|
+
try {
|
|
3408
|
+
const loadedAt = state_.identity.getGeneration();
|
|
3409
|
+
await settleSessionInFlight(state_);
|
|
3410
|
+
reportHeldEventsDropped(state_);
|
|
3411
|
+
while (state_.heldEvents?.length && !state_.disposed) {
|
|
3412
|
+
const next = state_.heldEvents.shift();
|
|
3413
|
+
const belongsTo = Math.max(next.generation, loadedAt);
|
|
3414
|
+
if (state_.identity.getGeneration() !== belongsTo) {
|
|
3415
|
+
stale += 1;
|
|
3416
|
+
continue;
|
|
3417
|
+
}
|
|
3418
|
+
try {
|
|
3419
|
+
await sendEvent(state_, next.event, next.properties, next.options, next.occurredAt);
|
|
3420
|
+
} catch (err) {
|
|
3421
|
+
if (typeof console !== "undefined") console.warn(`[amba] could not send "${next.event}", tracked while the secure store was locked`, err);
|
|
3422
|
+
}
|
|
3423
|
+
}
|
|
3424
|
+
} finally {
|
|
3425
|
+
state_.releasingHeldEvents = false;
|
|
3426
|
+
if (stale > 0 && typeof console !== "undefined") console.warn(`[amba] dropped ${stale} event(s) tracked while the secure store was locked: the session they belong to changed before they could be sent`);
|
|
3427
|
+
}
|
|
2971
3428
|
}
|
|
2972
3429
|
/** Paths that must NOT trigger lazy anonymous sign-in. */
|
|
2973
3430
|
function pathSkipsLazySession(path) {
|
|
@@ -2978,21 +3435,41 @@ function pathSkipsLazySession(path) {
|
|
|
2978
3435
|
if (path === P.flags || path.startsWith(`${P.flags}/`)) return true;
|
|
2979
3436
|
return false;
|
|
2980
3437
|
}
|
|
3438
|
+
/**
|
|
3439
|
+
* Make sure a session is stored before an authenticated call. While the user
|
|
3440
|
+
* must sign in again (`sign_in_required`), the call fails with
|
|
3441
|
+
* `SIGN_IN_REQUIRED` and no anonymous user starts.
|
|
3442
|
+
*/
|
|
2981
3443
|
async function ensureAnonymousSession(state_) {
|
|
2982
3444
|
if (state_.identity.getSessionToken()) return;
|
|
3445
|
+
if (state_.identity.isSignInRequired()) throw signInRequiredError();
|
|
2983
3446
|
if (state_.anonSignInInflight) {
|
|
2984
3447
|
await state_.anonSignInInflight.catch(() => {});
|
|
2985
3448
|
if (state_.identity.getSessionToken()) return;
|
|
3449
|
+
if (state_.identity.isSignInRequired()) throw signInRequiredError();
|
|
3450
|
+
}
|
|
3451
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
3452
|
+
if (state_.identity.getSessionToken() || state_.disposed) return;
|
|
3453
|
+
if (state_.identity.isSignInRequired()) throw signInRequiredError();
|
|
3454
|
+
await anonymousSignInOnce(state_);
|
|
2986
3455
|
}
|
|
3456
|
+
if (!state_.identity.getSessionToken() && state_.identity.isSignInRequired()) throw signInRequiredError();
|
|
3457
|
+
}
|
|
3458
|
+
/** One run of the lazy sign-in, shared by every caller that waits for it. */
|
|
3459
|
+
function anonymousSignInOnce(state_) {
|
|
2987
3460
|
if (!state_.anonSignInInflight) state_.anonSignInInflight = (async () => {
|
|
2988
3461
|
try {
|
|
3462
|
+
await state_.identity.vaultStillUnreadable();
|
|
3463
|
+
if (await state_.identity.anonymousIdStillUnknown()) throw secureStoreUnavailableError();
|
|
3464
|
+
if (state_.identity.getSessionToken()) return;
|
|
2989
3465
|
if (await resumeWithRefreshToken(state_)) return;
|
|
2990
|
-
|
|
3466
|
+
if (state_.identity.getSessionToken() || state_.identity.isSignInRequired()) return;
|
|
3467
|
+
await signInAnonymouslyAutomatic(state_);
|
|
2991
3468
|
} finally {
|
|
2992
3469
|
state_.anonSignInInflight = null;
|
|
2993
3470
|
}
|
|
2994
3471
|
})();
|
|
2995
|
-
|
|
3472
|
+
return state_.anonSignInInflight;
|
|
2996
3473
|
}
|
|
2997
3474
|
/**
|
|
2998
3475
|
* Restore a stored session whose session token expired while the app was
|
|
@@ -3000,7 +3477,7 @@ async function ensureAnonymousSession(state_) {
|
|
|
3000
3477
|
* `onAuthStateChange` show the signed-in user without waiting for an
|
|
3001
3478
|
* authenticated call. Shares the in-flight slot with the lazy sign-in, so a
|
|
3002
3479
|
* concurrent authenticated call waits for it. A rejected refresh token ends
|
|
3003
|
-
* the session (
|
|
3480
|
+
* the session (`endLostSession`); this never signs in anonymously.
|
|
3004
3481
|
*/
|
|
3005
3482
|
function restoreStoredSession(state_) {
|
|
3006
3483
|
if (!state_.anonSignInInflight) state_.anonSignInInflight = (async () => {
|
|
@@ -3013,11 +3490,16 @@ function restoreStoredSession(state_) {
|
|
|
3013
3490
|
return state_.anonSignInInflight;
|
|
3014
3491
|
}
|
|
3015
3492
|
/**
|
|
3016
|
-
*
|
|
3017
|
-
*
|
|
3018
|
-
* `
|
|
3493
|
+
* Answers that mean the refresh token's session is over: the server
|
|
3494
|
+
* rejected or revoked it, or it renewed a different user than the stored
|
|
3495
|
+
* one (`OWNER_MISMATCH`). A 401 without the server's envelope (a proxy, a
|
|
3496
|
+
* captive portal) maps to `UNAUTHORIZED` and leaves the session in place.
|
|
3019
3497
|
*/
|
|
3020
|
-
const SESSION_ENDED_CODES = new Set([
|
|
3498
|
+
const SESSION_ENDED_CODES = new Set([
|
|
3499
|
+
"INVALID_TOKEN",
|
|
3500
|
+
"SESSION_REVOKED",
|
|
3501
|
+
"OWNER_MISMATCH"
|
|
3502
|
+
]);
|
|
3021
3503
|
function isSessionEnded(err) {
|
|
3022
3504
|
return err instanceof AmbaApiError && SESSION_ENDED_CODES.has(err.code);
|
|
3023
3505
|
}
|
|
@@ -3033,15 +3515,19 @@ function trackLink(state_, link) {
|
|
|
3033
3515
|
}
|
|
3034
3516
|
/**
|
|
3035
3517
|
* After the server refused the stored session token on an authenticated
|
|
3036
|
-
* call (`SESSION_REVOKED`, `INVALID_SESSION_TOKEN`).
|
|
3518
|
+
* call (`SESSION_REVOKED`, `INVALID_SESSION_TOKEN`, `INVALID_SESSION`). The
|
|
3519
|
+
* HTTP client runs it for calls that send the session in the
|
|
3520
|
+
* `Authorization` header, `/v1/client/auth/me` and `/identifiers`
|
|
3521
|
+
* included; `withBodySession` runs it for the link and unlink calls, which
|
|
3522
|
+
* send the session in their body. True when a usable
|
|
3037
3523
|
* session for the user the call was sent for is stored afterwards, so the
|
|
3038
3524
|
* call is sent once more as that user:
|
|
3039
3525
|
* - a link in flight on this device stores the pair it returns, which is
|
|
3040
3526
|
* waited for first;
|
|
3041
3527
|
* - a session another flow stored after the call left is used as is;
|
|
3042
3528
|
* - otherwise the refresh token continues the session. A rejected refresh
|
|
3043
|
-
* token ends it (
|
|
3044
|
-
*
|
|
3529
|
+
* token ends it (`endLostSession`) and the call's own error reaches the
|
|
3530
|
+
* app.
|
|
3045
3531
|
* A session of any other user (a sign-in meanwhile, or the anonymous user
|
|
3046
3532
|
* a lazy sign-in starts after the session ended) answers false, and the
|
|
3047
3533
|
* call fails with the server's code. Offline or a server error keeps the
|
|
@@ -3067,68 +3553,160 @@ async function recoverRefusedSession(state_, usedToken, usedUserId) {
|
|
|
3067
3553
|
return usableForCaller(state_.identity.getSessionToken());
|
|
3068
3554
|
}
|
|
3069
3555
|
/**
|
|
3556
|
+
* Send an auth call that carries the stored session token in its body
|
|
3557
|
+
* (`/link`, `/unlink`). `send` builds the request from the token it is
|
|
3558
|
+
* given. When the server refuses that token, the session recovers through
|
|
3559
|
+
* `recoverRefusedSession` (one refresh shared with every other refused
|
|
3560
|
+
* call) and the call is sent once more with the recovered token; a second
|
|
3561
|
+
* refusal reaches the app. A link waits here outside `trackLink`: its own
|
|
3562
|
+
* failed attempt has already left `linksInflight` (the removal is the first
|
|
3563
|
+
* reaction on that promise), so recovery never waits on itself.
|
|
3564
|
+
*/
|
|
3565
|
+
async function withBodySession(state_, send) {
|
|
3566
|
+
await loadUnreadSession(state_);
|
|
3567
|
+
const usedToken = requireSession(state_);
|
|
3568
|
+
const usedUserId = state_.identity.getAppUserId();
|
|
3569
|
+
try {
|
|
3570
|
+
return await send(usedToken);
|
|
3571
|
+
} catch (err) {
|
|
3572
|
+
if (!isSessionRefusedError(err)) throw err;
|
|
3573
|
+
if (!await recoverRefusedSession(state_, usedToken, usedUserId)) throw err;
|
|
3574
|
+
const recovered = state_.identity.getSessionToken();
|
|
3575
|
+
if (!recovered) throw err;
|
|
3576
|
+
return send(recovered);
|
|
3577
|
+
}
|
|
3578
|
+
}
|
|
3579
|
+
/**
|
|
3580
|
+
* Read a store the launch couldn't read (a locked device) again. When that
|
|
3581
|
+
* loads a session, wait for the renewal it starts, so the caller acts on
|
|
3582
|
+
* the loaded session (a sign-out revokes it, a link sends it).
|
|
3583
|
+
*/
|
|
3584
|
+
async function loadUnreadSession(state_) {
|
|
3585
|
+
if (!state_.identity.credentialsUnknown()) return;
|
|
3586
|
+
await state_.identity.credentialsStillUnreadable();
|
|
3587
|
+
if (!state_.identity.credentialsUnknown()) await settleSessionInFlight(state_);
|
|
3588
|
+
}
|
|
3589
|
+
/**
|
|
3590
|
+
* Before a read of the current session (`me`, `getSession`,
|
|
3591
|
+
* `getCurrentIdentifiers`) while no session token is stored: load a stored
|
|
3592
|
+
* session the launch couldn't read yet, then wait for the launch restore or
|
|
3593
|
+
* sign-in in flight, so a check made at launch sees the session it
|
|
3594
|
+
* restores.
|
|
3595
|
+
*/
|
|
3596
|
+
async function settleSessionInFlight(state_) {
|
|
3597
|
+
if (state_.identity.getSessionToken()) return;
|
|
3598
|
+
await state_.identity.credentialsStillUnreadable();
|
|
3599
|
+
if (state_.identity.getSessionToken() || !state_.anonSignInInflight) return;
|
|
3600
|
+
await state_.anonSignInInflight.catch(() => {});
|
|
3601
|
+
}
|
|
3602
|
+
/**
|
|
3070
3603
|
* Continue the stored session with its refresh token. True when it did.
|
|
3071
|
-
* False when there is no refresh token, or the
|
|
3072
|
-
*
|
|
3073
|
-
*
|
|
3074
|
-
*
|
|
3604
|
+
* False when there is no refresh token, or the session is over (the server
|
|
3605
|
+
* rejected the refresh token, or it renewed another user): the session then
|
|
3606
|
+
* ends through `endLostSession`. Any other failure (offline, a 5xx) is
|
|
3607
|
+
* thrown, so a flaky network never ends a session or starts a new user.
|
|
3075
3608
|
*/
|
|
3076
3609
|
async function resumeWithRefreshToken(state_) {
|
|
3077
3610
|
const used = state_.identity.getRefreshToken();
|
|
3078
3611
|
if (!used) return false;
|
|
3612
|
+
const startedAt = state_.identity.getGeneration();
|
|
3079
3613
|
try {
|
|
3080
3614
|
await refreshSession(state_);
|
|
3081
3615
|
return true;
|
|
3082
3616
|
} catch (err) {
|
|
3083
3617
|
if (state_.identity.getRefreshToken() !== used) return state_.identity.getSessionToken() !== null;
|
|
3084
3618
|
if (!isSessionEnded(err)) throw err;
|
|
3085
|
-
await state_
|
|
3086
|
-
notifyAuthSubscribers(null);
|
|
3619
|
+
await endLostSession(state_, startedAt);
|
|
3087
3620
|
return false;
|
|
3088
3621
|
}
|
|
3089
3622
|
}
|
|
3090
3623
|
/**
|
|
3091
|
-
*
|
|
3092
|
-
*
|
|
3093
|
-
*
|
|
3094
|
-
*
|
|
3624
|
+
* End a session that is over without a sign-out, once, unless the session
|
|
3625
|
+
* changed since `startedAt`.
|
|
3626
|
+
*
|
|
3627
|
+
* - A session that gained an identity (a sign-in other than anonymous, or a
|
|
3628
|
+
* link) leaves the user to sign in again: its tokens are cleared, the
|
|
3629
|
+
* anonymous id rotates (it belongs to the identified account), and
|
|
3630
|
+
* subscribers hear `null` with `sign_in_required`. No new anonymous user
|
|
3631
|
+
* starts; calls that need a session fail with `SIGN_IN_REQUIRED`.
|
|
3632
|
+
* - A purely anonymous session keeps its anonymous id, so the next call that
|
|
3633
|
+
* needs a session resumes the same anonymous user through `/anonymous`
|
|
3634
|
+
* (or the server starts a new one). Subscribers hear `null` with
|
|
3635
|
+
* `session_ended`.
|
|
3636
|
+
* - A session of unknown kind (stored by an SDK before 4.0.14) is treated
|
|
3637
|
+
* like an anonymous one: `/anonymous` resumes a still-anonymous user, and
|
|
3638
|
+
* answers `IDENTITY_SIGN_IN_REQUIRED` for one who linked an identity,
|
|
3639
|
+
* which then leaves the user to sign in again.
|
|
3640
|
+
*/
|
|
3641
|
+
async function endLostSession(state_, startedAt) {
|
|
3642
|
+
if (state_.identity.snapshot().anonymousSession === false) {
|
|
3643
|
+
if (await state_.identity.clearSession(false, {
|
|
3644
|
+
signInRequired: true,
|
|
3645
|
+
ifGeneration: startedAt
|
|
3646
|
+
})) notifyAuthSubscribers(state_, null, "sign_in_required");
|
|
3647
|
+
return;
|
|
3648
|
+
}
|
|
3649
|
+
if (await state_.identity.clearSession(false, {
|
|
3650
|
+
keepIdWhenKindUnknown: true,
|
|
3651
|
+
ifGeneration: startedAt
|
|
3652
|
+
})) notifyAuthSubscribers(state_, null, "session_ended");
|
|
3653
|
+
}
|
|
3654
|
+
/**
|
|
3655
|
+
* The anonymous sign-in the SDK starts on its own before an authenticated
|
|
3656
|
+
* call: `/v1/client/auth/anonymous` with the device's anonymous id. It
|
|
3657
|
+
* stores its session only when nothing replaced the stored identity while
|
|
3658
|
+
* it was in flight; a sign-in that landed first wins and this answer is
|
|
3659
|
+
* dropped.
|
|
3660
|
+
*
|
|
3661
|
+
* When the server answers `IDENTITY_SIGN_IN_REQUIRED` (the id belongs to a
|
|
3662
|
+
* user who signs in with a linked identity), the stored session continues
|
|
3663
|
+
* when this device holds its refresh token. Otherwise the user must sign in
|
|
3664
|
+
* again (`sign_in_required`): no new anonymous user starts on its own.
|
|
3095
3665
|
*/
|
|
3096
|
-
async function
|
|
3666
|
+
async function signInAnonymouslyAutomatic(state_) {
|
|
3667
|
+
if (state_.disposed) return;
|
|
3668
|
+
const startedAt = state_.identity.getGeneration();
|
|
3097
3669
|
try {
|
|
3098
|
-
|
|
3670
|
+
await postAuth(state_, P.authAnonymous, { anonymous_id: state_.identity.getAnonymousId() }, { ifGeneration: startedAt });
|
|
3099
3671
|
} catch (err) {
|
|
3672
|
+
if (isSessionChanged(err)) return;
|
|
3100
3673
|
if (!(err instanceof AmbaApiError) || err.code !== IDENTITY_SIGN_IN_REQUIRED) throw err;
|
|
3101
|
-
|
|
3674
|
+
if (await continueHeldSession(state_)) return;
|
|
3675
|
+
if (await state_.identity.clearSession(true, {
|
|
3676
|
+
signInRequired: true,
|
|
3677
|
+
ifGeneration: startedAt
|
|
3678
|
+
})) notifyAuthSubscribers(state_, null, "sign_in_required");
|
|
3102
3679
|
}
|
|
3103
3680
|
}
|
|
3104
3681
|
const IDENTITY_SIGN_IN_REQUIRED = "IDENTITY_SIGN_IN_REQUIRED";
|
|
3105
3682
|
/**
|
|
3106
3683
|
* After `/anonymous` answered `IDENTITY_SIGN_IN_REQUIRED`: continue the
|
|
3107
|
-
* stored session when this device holds its refresh token
|
|
3108
|
-
*
|
|
3109
|
-
*
|
|
3110
|
-
*
|
|
3111
|
-
* `SECURE_STORE_UNAVAILABLE` and replaces nothing.
|
|
3684
|
+
* stored session when this device holds its refresh token. Null when it
|
|
3685
|
+
* holds none, or the server rejected it. While the stored session can't be
|
|
3686
|
+
* read (an iOS device not yet unlocked since a restart), it is unknown, so
|
|
3687
|
+
* the call fails with `SECURE_STORE_UNAVAILABLE` and replaces nothing.
|
|
3112
3688
|
*/
|
|
3113
|
-
async function
|
|
3114
|
-
if (await state_.identity.
|
|
3689
|
+
async function continueHeldSession(state_) {
|
|
3690
|
+
if (await state_.identity.credentialsStillUnreadable()) throw secureStoreUnavailableError();
|
|
3115
3691
|
const used = state_.identity.getRefreshToken();
|
|
3116
|
-
if (used)
|
|
3692
|
+
if (!used) return null;
|
|
3693
|
+
try {
|
|
3117
3694
|
return await refreshSession(state_);
|
|
3118
3695
|
} catch (refreshErr) {
|
|
3119
3696
|
if (!isSessionEnded(refreshErr)) throw refreshErr;
|
|
3120
3697
|
const current = state_.identity.getRefreshToken();
|
|
3121
3698
|
if (current !== null && current !== used) throw refreshErr;
|
|
3699
|
+
return null;
|
|
3122
3700
|
}
|
|
3123
|
-
await state_.identity.clearSession(true);
|
|
3124
|
-
return postAuth(state_, P.authAnonymous, { anonymous_id: state_.identity.getAnonymousId() });
|
|
3125
3701
|
}
|
|
3126
3702
|
/**
|
|
3127
3703
|
* Rotate the session pair with the stored refresh token. The session keeps
|
|
3128
3704
|
* its user and its kind (anonymous or identified), and auth-state
|
|
3129
3705
|
* subscribers hear the refreshed session. The server includes the user; an
|
|
3130
3706
|
* older server that leaves it out keeps the stored user id, and the result's
|
|
3131
|
-
* user then carries only that id.
|
|
3707
|
+
* user then carries only that id. A pair that renews a different user than
|
|
3708
|
+
* the stored one is never stored: the session ends (`endLostSession`) and
|
|
3709
|
+
* the call fails with `OWNER_MISMATCH`.
|
|
3132
3710
|
*/
|
|
3133
3711
|
function refreshSession(state_) {
|
|
3134
3712
|
if (state_.refreshInflight) return state_.refreshInflight;
|
|
@@ -3141,32 +3719,66 @@ function refreshSession(state_) {
|
|
|
3141
3719
|
return inflight;
|
|
3142
3720
|
}
|
|
3143
3721
|
async function refreshSessionOnce(state_) {
|
|
3722
|
+
if (state_.disposed) throw sessionChangedError("the SDK was configured again");
|
|
3144
3723
|
const refreshToken = state_.identity.getRefreshToken();
|
|
3724
|
+
if (!refreshToken && state_.identity.isSignInRequired()) throw signInRequiredError();
|
|
3145
3725
|
if (!refreshToken) throw new AmbaApiError("UNAUTHORIZED", "no refresh token available — call signIn* first");
|
|
3146
|
-
const
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
user
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3726
|
+
const startedAt = state_.identity.getGeneration();
|
|
3727
|
+
const exchange$1 = refreshExchangeFor(state_, refreshToken);
|
|
3728
|
+
try {
|
|
3729
|
+
const env = await exchange$1.response;
|
|
3730
|
+
if (state_.identity.getRefreshToken() !== refreshToken || state_.identity.getGeneration() !== startedAt) throw sessionChangedError("the session changed while it was being refreshed");
|
|
3731
|
+
const storedUserId = state_.identity.getAppUserId();
|
|
3732
|
+
const renewedUserId = nonEmptyString(env.data.user?.id);
|
|
3733
|
+
if (renewedUserId !== null && storedUserId !== null && renewedUserId !== storedUserId) {
|
|
3734
|
+
await endLostSession(state_, startedAt);
|
|
3735
|
+
throw new AmbaApiError("OWNER_MISMATCH", "the refreshed session belongs to a different user than the stored one; sign in again");
|
|
3736
|
+
}
|
|
3737
|
+
const appUserId = renewedUserId ?? storedUserId;
|
|
3738
|
+
if (!appUserId) throw new AmbaApiError("UNAUTHORIZED", "the refreshed session names no user — call signIn* first");
|
|
3739
|
+
const user = env.data.user ? normalizeUserAuthProviders(env.data.user) : {
|
|
3740
|
+
id: appUserId,
|
|
3741
|
+
auth_providers: [],
|
|
3742
|
+
properties: {}
|
|
3743
|
+
};
|
|
3744
|
+
const result = {
|
|
3745
|
+
session_token: env.data.session_token,
|
|
3746
|
+
refresh_token: env.data.refresh_token,
|
|
3747
|
+
user,
|
|
3748
|
+
expires_at: env.data.expires_at ?? null
|
|
3749
|
+
};
|
|
3750
|
+
const committedAt = await persistAuthResult(state_, result, void 0, {
|
|
3751
|
+
ifGeneration: startedAt,
|
|
3752
|
+
advance: false
|
|
3753
|
+
});
|
|
3754
|
+
if (committedAt === null) throw sessionChangedError("the session changed while it was being refreshed");
|
|
3755
|
+
const anonymousId = nonEmptyString(user.anonymous_id);
|
|
3756
|
+
if (anonymousId && anonymousId !== state_.identity.getAnonymousId() && state_.identity.getRefreshToken() === result.refresh_token) {
|
|
3757
|
+
if (state_.identity.snapshot().anonymousSession === true) await state_.identity.markIdentified({ ifGeneration: committedAt });
|
|
3758
|
+
await adoptAnonymousId(state_, anonymousId, committedAt);
|
|
3759
|
+
}
|
|
3760
|
+
state_.eventQueue?.flush();
|
|
3761
|
+
notifyFromAuthResult(state_, result, "token_refreshed");
|
|
3762
|
+
return result;
|
|
3763
|
+
} finally {
|
|
3764
|
+
if (state_.refreshExchange === exchange$1) state_.refreshExchange = null;
|
|
3166
3765
|
}
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3766
|
+
}
|
|
3767
|
+
/**
|
|
3768
|
+
* The `/refresh` request for `refreshToken`: the one the previous state had
|
|
3769
|
+
* in flight for the same token when `configure()` ran again, or a new one.
|
|
3770
|
+
* It stays on the state until its answer is stored, so a `configure()`
|
|
3771
|
+
* meanwhile hands it on instead of presenting the token again.
|
|
3772
|
+
*/
|
|
3773
|
+
function refreshExchangeFor(state_, refreshToken) {
|
|
3774
|
+
const inherited = state_.inheritedRefresh;
|
|
3775
|
+
if (inherited) state_.inheritedRefresh = null;
|
|
3776
|
+
const exchange$1 = inherited && inherited.token === refreshToken ? inherited : {
|
|
3777
|
+
token: refreshToken,
|
|
3778
|
+
response: state_.http.post(P.authRefresh, { refresh_token: refreshToken })
|
|
3779
|
+
};
|
|
3780
|
+
state_.refreshExchange = exchange$1;
|
|
3781
|
+
return exchange$1;
|
|
3170
3782
|
}
|
|
3171
3783
|
/**
|
|
3172
3784
|
* POST an OTP or email + password link. The request asks the server to
|
|
@@ -3177,9 +3789,17 @@ async function refreshSessionOnce(state_) {
|
|
|
3177
3789
|
* account's rotated anonymous id is adopted.
|
|
3178
3790
|
*/
|
|
3179
3791
|
function postLink(state_, body) {
|
|
3180
|
-
return trackLink(state_, postLinkOnce(state_,
|
|
3792
|
+
return withBodySession(state_, (sessionToken) => trackLink(state_, postLinkOnce(state_, {
|
|
3793
|
+
...body,
|
|
3794
|
+
session_token: sessionToken
|
|
3795
|
+
})));
|
|
3181
3796
|
}
|
|
3182
3797
|
async function postLinkOnce(state_, body) {
|
|
3798
|
+
const startedAt = state_.identity.getGeneration();
|
|
3799
|
+
const fence = {
|
|
3800
|
+
ifGeneration: startedAt,
|
|
3801
|
+
advance: false
|
|
3802
|
+
};
|
|
3183
3803
|
const env = await state_.http.post(P.authLink, {
|
|
3184
3804
|
...body,
|
|
3185
3805
|
rotate_session: true
|
|
@@ -3187,7 +3807,7 @@ async function postLinkOnce(state_, body) {
|
|
|
3187
3807
|
const user = normalizeUserAuthProviders(env.data.user);
|
|
3188
3808
|
const sessionToken = nonEmptyString(env.data.session_token);
|
|
3189
3809
|
const refreshToken = nonEmptyString(env.data.refresh_token);
|
|
3190
|
-
if (sessionToken && refreshToken
|
|
3810
|
+
if (!(sessionToken && refreshToken ? await state_.identity.setSession({
|
|
3191
3811
|
appUserId: user.id,
|
|
3192
3812
|
sessionToken,
|
|
3193
3813
|
refreshToken,
|
|
@@ -3198,10 +3818,9 @@ async function postLinkOnce(state_, body) {
|
|
|
3198
3818
|
expires_at: env.data.expires_at ?? null
|
|
3199
3819
|
}),
|
|
3200
3820
|
anonymous: false
|
|
3201
|
-
});
|
|
3202
|
-
else await state_.identity.markIdentified();
|
|
3821
|
+
}, fence) !== null : await state_.identity.markIdentified(fence))) throw sessionChangedError("the session changed while the link was in flight; the newer session stays");
|
|
3203
3822
|
const anonymousId = nonEmptyString(user.anonymous_id);
|
|
3204
|
-
if (anonymousId) await adoptAnonymousId(state_, anonymousId);
|
|
3823
|
+
if (anonymousId) await adoptAnonymousId(state_, anonymousId, startedAt);
|
|
3205
3824
|
return user;
|
|
3206
3825
|
}
|
|
3207
3826
|
/**
|
|
@@ -3212,13 +3831,11 @@ async function postUserNoRotate(state_, path, body) {
|
|
|
3212
3831
|
return normalizeUserAuthProviders((await state_.http.post(path, body)).data.user);
|
|
3213
3832
|
}
|
|
3214
3833
|
async function unlinkProvider(provider) {
|
|
3215
|
-
const s =
|
|
3216
|
-
|
|
3217
|
-
if (!sessionToken) throw new AmbaApiError("UNAUTHORIZED", "no active session — call signIn* first");
|
|
3218
|
-
return postUserNoRotate(s, P.authUnlink, {
|
|
3834
|
+
const s = await liveState();
|
|
3835
|
+
return withBodySession(s, (sessionToken) => postUserNoRotate(s, P.authUnlink, {
|
|
3219
3836
|
provider,
|
|
3220
3837
|
session_token: sessionToken
|
|
3221
|
-
});
|
|
3838
|
+
}));
|
|
3222
3839
|
}
|
|
3223
3840
|
function requireEmail(email) {
|
|
3224
3841
|
const trimmed = email.trim();
|
|
@@ -3471,107 +4088,238 @@ function detectDeviceRegion() {
|
|
|
3471
4088
|
}
|
|
3472
4089
|
}
|
|
3473
4090
|
/**
|
|
3474
|
-
*
|
|
4091
|
+
* The send path of `events.track()`: the event batch queue when batching is
|
|
4092
|
+
* on, otherwise the live request (with the offline outbox behind it when
|
|
4093
|
+
* enabled). `occurredAtOverride` is when the event happened, for one that
|
|
4094
|
+
* waited (see `releaseHeldEvents`).
|
|
3475
4095
|
*/
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
4096
|
+
async function sendEvent(s, event, properties, options, occurredAtOverride) {
|
|
4097
|
+
if (s.eventQueue && !options?.immediate) return s.eventQueue.enqueue(event, properties, options?.telemetry === true, occurredAtOverride);
|
|
4098
|
+
const occurredAt = occurredAtOverride ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
4099
|
+
const props = s.offlineEnabled ? snapshotProperties(properties) : properties;
|
|
4100
|
+
const body = {
|
|
4101
|
+
event,
|
|
4102
|
+
properties: props ?? void 0,
|
|
4103
|
+
anonymous_id: s.identity.getAnonymousId(),
|
|
4104
|
+
timestamp: occurredAt
|
|
4105
|
+
};
|
|
4106
|
+
if (options?.telemetry) body["telemetry"] = true;
|
|
4107
|
+
const idempotencyKey = s.offlineEnabled ? newIdempotencyKey() : void 0;
|
|
4108
|
+
if (idempotencyKey && keyedLiveTrack(props, options?.telemetry)) body["idempotency_key"] = idempotencyKey;
|
|
4109
|
+
const outboxTicket = s.offlineEnabled ? getOfflineQueue(s).ticket() : void 0;
|
|
4110
|
+
try {
|
|
4111
|
+
await s.http.post(P.events, body);
|
|
4112
|
+
} catch (err) {
|
|
4113
|
+
if (s.offlineEnabled && isOfflineError(err)) {
|
|
4114
|
+
await getOfflineQueue(s).enqueue({
|
|
4115
|
+
kind: "track",
|
|
4116
|
+
event,
|
|
4117
|
+
properties: props,
|
|
4118
|
+
telemetry: options?.telemetry,
|
|
4119
|
+
occurredAt,
|
|
4120
|
+
idempotencyKey
|
|
4121
|
+
}, outboxTicket);
|
|
4122
|
+
return;
|
|
3487
4123
|
}
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
4124
|
+
throw err;
|
|
4125
|
+
}
|
|
4126
|
+
}
|
|
4127
|
+
/**
|
|
4128
|
+
* The local half of a sign-out on `state_`: clear the session (unless a
|
|
4129
|
+
* sign-in stored a newer one since `startedAt`), drop the signed-out
|
|
4130
|
+
* user's cached values and queued writes, and tell subscribers. False when
|
|
4131
|
+
* nothing was cleared.
|
|
4132
|
+
*/
|
|
4133
|
+
async function endSignedOutSession(state_, rotateAnonymousId, startedAt) {
|
|
4134
|
+
const signedOutUser = state_.identity.getAppUserId();
|
|
4135
|
+
if (!await state_.identity.clearSession(rotateAnonymousId, { unlessSignedInSince: startedAt })) return false;
|
|
4136
|
+
dropHeldEvents(state_, "the device signed out before its identity could be read");
|
|
4137
|
+
state_.remoteValues.clearForSignOut(signedOutUser);
|
|
4138
|
+
await state_.remoteValues.flushed();
|
|
4139
|
+
try {
|
|
4140
|
+
await getOfflineQueue(state_).purge();
|
|
4141
|
+
} catch (err) {
|
|
4142
|
+
if (typeof console !== "undefined") console.warn("[amba] signOut could not clear the offline outbox", err);
|
|
4143
|
+
}
|
|
4144
|
+
state_.eventQueue?.flush();
|
|
4145
|
+
notifyAuthSubscribers(state_, null, "signed_out");
|
|
4146
|
+
return true;
|
|
4147
|
+
}
|
|
4148
|
+
/**
|
|
4149
|
+
* One `configure()`: build the state for `config` and install it in place
|
|
4150
|
+
* of the previous one (see `Amba.configure`).
|
|
4151
|
+
*/
|
|
4152
|
+
async function configureOnce(config) {
|
|
4153
|
+
if (!config.projectId || config.projectId.trim() === "") throw new Error("Amba.configure: projectId is required");
|
|
4154
|
+
if (!config.clientKey || config.clientKey.trim() === "") throw new Error("Amba.configure: clientKey is required");
|
|
4155
|
+
const batchSize = config.events?.batchSize ?? 100;
|
|
4156
|
+
const flushIntervalMs = config.events?.flushIntervalMs ?? 6e4;
|
|
4157
|
+
const maxQueue = config.events?.maxQueue ?? 1e4;
|
|
4158
|
+
if (config.events?.batching) {
|
|
4159
|
+
if (!Number.isInteger(batchSize) || batchSize < 1 || batchSize > 500) throw new Error("Amba.configure: events.batchSize must be 1–500");
|
|
4160
|
+
if (!Number.isInteger(flushIntervalMs) || flushIntervalMs < 0) throw new Error("Amba.configure: events.flushIntervalMs must be a non-negative integer");
|
|
4161
|
+
if (!Number.isInteger(maxQueue) || maxQueue < 1) throw new Error("Amba.configure: events.maxQueue must be positive");
|
|
4162
|
+
}
|
|
4163
|
+
const deadlines = resolveRequestDeadlines(config);
|
|
4164
|
+
const tokenStorage = config.tokenStorage ?? "secure";
|
|
4165
|
+
if (!TOKEN_STORAGE_MODES.includes(tokenStorage)) throw new Error("Amba.configure: tokenStorage must be \"secure\", \"memory\" or \"asyncStorage\"");
|
|
4166
|
+
const secureStorage = config.secureStorage;
|
|
4167
|
+
if (secureStorage !== void 0 && (typeof secureStorage?.getItem !== "function" || typeof secureStorage?.setItem !== "function" || typeof secureStorage?.removeItem !== "function")) throw new Error("Amba.configure: secureStorage must provide getItem, setItem and removeItem");
|
|
4168
|
+
const configDefaults = readConfigDefaults(config.configDefaults);
|
|
4169
|
+
_pendingConfigDefaults = configDefaults;
|
|
4170
|
+
const storagePrefix = config.storagePrefix ?? "amba";
|
|
4171
|
+
const persistence = new AsyncStoragePersistence(storagePrefix);
|
|
4172
|
+
if (config.events?.batching) await persistence.assertDurable();
|
|
4173
|
+
await persistence.hydrate();
|
|
4174
|
+
const vault = await resolveTokenVault(tokenStorage, storagePrefix, secureStorage);
|
|
4175
|
+
const prior = state;
|
|
4176
|
+
let inheritedRefresh = null;
|
|
4177
|
+
const detached = prior !== null && !prior.disposed;
|
|
4178
|
+
if (prior && detached) {
|
|
4179
|
+
prior.disposed = true;
|
|
4180
|
+
inheritedRefresh = prior.refreshExchange ?? prior.inheritedRefresh ?? null;
|
|
4181
|
+
await prior.identity.dispose();
|
|
4182
|
+
}
|
|
4183
|
+
const baseUrl = (config.apiUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
4184
|
+
let identity;
|
|
4185
|
+
let remoteValues;
|
|
4186
|
+
try {
|
|
4187
|
+
identity = await IdentityStore.create(persistence, vault);
|
|
4188
|
+
const store = identity;
|
|
4189
|
+
remoteValues = new RemoteValuesCache(persistence, recordKey(baseUrl, config.clientKey), configDefaults ?? {}, () => store.getAppUserId());
|
|
3503
4190
|
await remoteValues.load();
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
4191
|
+
} catch (err) {
|
|
4192
|
+
if (prior && detached && state === prior) {
|
|
4193
|
+
prior.disposed = false;
|
|
4194
|
+
prior.identity.reattach();
|
|
4195
|
+
prior.inheritedRefresh = inheritedRefresh;
|
|
4196
|
+
if (inheritedRefresh && prior.identity.getRefreshToken() === inheritedRefresh.token) restoreStoredSession(prior).catch(() => {});
|
|
4197
|
+
}
|
|
4198
|
+
throw err;
|
|
4199
|
+
}
|
|
4200
|
+
const sdkVersionHeader = `amba-sdk/${SDK_VERSION} ${`amba-react-native/${SDK_VERSION}`}`;
|
|
4201
|
+
const http = new AmbaHttpClient({
|
|
4202
|
+
apiKey: config.clientKey,
|
|
4203
|
+
baseUrl,
|
|
4204
|
+
sdkPlatform: "react-native",
|
|
4205
|
+
sdkVersionHeader,
|
|
4206
|
+
appVersion: normalizeAppVersion(config.appVersion),
|
|
4207
|
+
deviceRegion: detectDeviceRegion(),
|
|
4208
|
+
deviceOs: detectDeviceOs(),
|
|
4209
|
+
identity,
|
|
4210
|
+
requestTimeoutMs: deadlines.requestMs,
|
|
4211
|
+
longRequestTimeoutMs: deadlines.longMs,
|
|
4212
|
+
ensureSession: async (path) => {
|
|
4213
|
+
if (pathSkipsLazySession(path)) return;
|
|
4214
|
+
if (state?.identity.isSignInRequired()) throw signInRequiredError();
|
|
4215
|
+
if (config.autoAnonymousSession === false) return;
|
|
4216
|
+
await ensureAnonymousSession(getState());
|
|
4217
|
+
},
|
|
4218
|
+
onSessionRefused: (usedToken, usedUserId) => recoverRefusedSession(getState(), usedToken, usedUserId)
|
|
4219
|
+
});
|
|
4220
|
+
_offlineEnableToken += 1;
|
|
4221
|
+
if (prior?.offlineQueue) await prior.offlineQueue.dispose();
|
|
4222
|
+
prior?.detachEventAppState?.();
|
|
4223
|
+
prior?.detachIdentityAppState?.();
|
|
4224
|
+
if (prior?.eventQueue) await prior.eventQueue.dispose();
|
|
4225
|
+
experimentReporter.reset();
|
|
4226
|
+
const nextState = {
|
|
4227
|
+
http,
|
|
4228
|
+
identity,
|
|
4229
|
+
debug: config.debug ?? false,
|
|
4230
|
+
projectId: config.projectId,
|
|
4231
|
+
clientKey: config.clientKey,
|
|
4232
|
+
baseUrl,
|
|
4233
|
+
functionsBaseUrl: config.functionsBaseUrl ?? null,
|
|
4234
|
+
resolvedFunctionsProjectId: null,
|
|
4235
|
+
functionsProjectIdInflight: null,
|
|
4236
|
+
persistence,
|
|
4237
|
+
remoteValues,
|
|
4238
|
+
refreshedFor: {
|
|
4239
|
+
config: null,
|
|
4240
|
+
flags: null
|
|
4241
|
+
},
|
|
4242
|
+
offlineEnabled: false,
|
|
4243
|
+
offlineQueue: null,
|
|
4244
|
+
eventQueue: null,
|
|
4245
|
+
inheritedRefresh
|
|
4246
|
+
};
|
|
4247
|
+
if (prior?.heldEvents?.length || prior?.heldEventsDropped) if (sameIdentityTarget(prior, nextState)) {
|
|
4248
|
+
const launchedAt = identity.getGeneration();
|
|
4249
|
+
nextState.heldEvents = (prior.heldEvents ?? []).map((held) => ({
|
|
4250
|
+
...held,
|
|
4251
|
+
generation: launchedAt
|
|
4252
|
+
}));
|
|
4253
|
+
nextState.heldEventsDropped = prior.heldEventsDropped;
|
|
4254
|
+
prior.heldEvents = [];
|
|
4255
|
+
prior.heldEventsDropped = 0;
|
|
4256
|
+
} else dropHeldEvents(prior, "the SDK was configured for another project");
|
|
4257
|
+
state = nextState;
|
|
4258
|
+
identity.onAnonymousIdKnown(() => {
|
|
4259
|
+
releaseHeldEvents(nextState);
|
|
4260
|
+
});
|
|
4261
|
+
identity.onStoredSessionLoaded(() => {
|
|
4262
|
+
const userId = identity.getAppUserId();
|
|
4263
|
+
if (identity.getSessionToken() && userId) notifyAuthSubscribers(nextState, snapshotSession({
|
|
4264
|
+
id: userId,
|
|
4265
|
+
auth_providers: [],
|
|
4266
|
+
properties: {}
|
|
4267
|
+
}), "session_restored");
|
|
4268
|
+
else if (config.autoAnonymousSession !== false && identity.getRefreshToken()) restoreStoredSession(nextState).catch(() => {});
|
|
4269
|
+
});
|
|
4270
|
+
try {
|
|
4271
|
+
const { AppState } = require("react-native");
|
|
4272
|
+
const sub = AppState.addEventListener("change", (status) => {
|
|
4273
|
+
if (status === "active" && identity.credentialsUnknown()) identity.credentialsStillUnreadable();
|
|
3521
4274
|
});
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
4275
|
+
nextState.detachIdentityAppState = () => sub.remove();
|
|
4276
|
+
} catch {}
|
|
4277
|
+
if (config.events?.batching) {
|
|
4278
|
+
const queue = new EventBatchQueue(offlineStore(nextState), `event_outbox:${nextState.projectId}`, {
|
|
4279
|
+
batchSize,
|
|
4280
|
+
flushIntervalMs,
|
|
4281
|
+
maxQueue,
|
|
4282
|
+
onDrop: config.events.onDrop
|
|
4283
|
+
}, () => ({
|
|
4284
|
+
userId: nextState.identity.getAppUserId(),
|
|
4285
|
+
anonymousId: nextState.identity.getAnonymousId(),
|
|
4286
|
+
sessionToken: nextState.identity.getSessionToken()
|
|
4287
|
+
}), (body, token) => nextState.http.postWithSession("/v1/client/events/batch", body, token), (body, token) => nextState.http.postWithSession(P.events, body, token));
|
|
4288
|
+
nextState.eventQueue = queue;
|
|
4289
|
+
await queue.start();
|
|
4290
|
+
try {
|
|
4291
|
+
const { AppState } = require("react-native");
|
|
4292
|
+
const sub = AppState.addEventListener("change", (status) => {
|
|
4293
|
+
if (status === "background") queue.flush();
|
|
4294
|
+
});
|
|
4295
|
+
nextState.detachEventAppState = () => sub.remove();
|
|
4296
|
+
} catch {}
|
|
4297
|
+
}
|
|
4298
|
+
if (!nextState.functionsBaseUrl && !PROJECT_ID_PATTERN.test(nextState.projectId)) resolveFunctionsProjectId(nextState).catch(() => {});
|
|
4299
|
+
if (nextState.heldEvents?.length && !identity.hasStandInAnonymousId()) releaseHeldEvents(nextState);
|
|
4300
|
+
if (inheritedRefresh !== null && identity.getRefreshToken() === inheritedRefresh.token || config.autoAnonymousSession !== false && !identity.getSessionToken() && identity.getRefreshToken()) restoreStoredSession(nextState).catch(() => {});
|
|
4301
|
+
}
|
|
4302
|
+
/**
|
|
4303
|
+
* Top-level SDK namespace for React Native.
|
|
4304
|
+
*/
|
|
4305
|
+
const Amba = {
|
|
4306
|
+
configure: (config) => {
|
|
4307
|
+
const run = configuring ? configureChain.then(() => configureOnce(config)) : configureOnce(config);
|
|
4308
|
+
configureChain = run.catch(() => void 0);
|
|
4309
|
+
configuring = run;
|
|
4310
|
+
const settle = () => {
|
|
4311
|
+
if (configuring === run) configuring = null;
|
|
3547
4312
|
};
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
const queue = new EventBatchQueue(offlineStore(nextState), `event_outbox:${nextState.projectId}`, {
|
|
3551
|
-
batchSize,
|
|
3552
|
-
flushIntervalMs,
|
|
3553
|
-
maxQueue,
|
|
3554
|
-
onDrop: config.events.onDrop
|
|
3555
|
-
}, () => ({
|
|
3556
|
-
userId: nextState.identity.getAppUserId(),
|
|
3557
|
-
anonymousId: nextState.identity.getAnonymousId(),
|
|
3558
|
-
sessionToken: nextState.identity.getSessionToken()
|
|
3559
|
-
}), (body, token) => nextState.http.postWithSession("/v1/client/events/batch", body, token), (body, token) => nextState.http.postWithSession(P.events, body, token));
|
|
3560
|
-
nextState.eventQueue = queue;
|
|
3561
|
-
await queue.start();
|
|
3562
|
-
try {
|
|
3563
|
-
const { AppState } = require("react-native");
|
|
3564
|
-
const sub = AppState.addEventListener("change", (status) => {
|
|
3565
|
-
if (status === "background") queue.flush();
|
|
3566
|
-
});
|
|
3567
|
-
nextState.detachEventAppState = () => sub.remove();
|
|
3568
|
-
} catch {}
|
|
3569
|
-
}
|
|
3570
|
-
if (!nextState.functionsBaseUrl && !PROJECT_ID_PATTERN.test(nextState.projectId)) resolveFunctionsProjectId(nextState).catch(() => {});
|
|
3571
|
-
if (config.autoAnonymousSession !== false && !identity.getSessionToken() && identity.getRefreshToken()) restoreStoredSession(nextState).catch(() => {});
|
|
4313
|
+
run.then(settle, settle);
|
|
4314
|
+
return run;
|
|
3572
4315
|
},
|
|
3573
4316
|
_reset: () => {
|
|
4317
|
+
if (state) {
|
|
4318
|
+
state.disposed = true;
|
|
4319
|
+
state.identity.dispose();
|
|
4320
|
+
}
|
|
3574
4321
|
state?.detachEventAppState?.();
|
|
4322
|
+
state?.detachIdentityAppState?.();
|
|
3575
4323
|
state?.eventQueue?.dispose();
|
|
3576
4324
|
state = null;
|
|
3577
4325
|
authSubscribers.clear();
|
|
@@ -3605,35 +4353,18 @@ const Amba = {
|
|
|
3605
4353
|
},
|
|
3606
4354
|
events: {
|
|
3607
4355
|
track: async (event, properties, options) => {
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
properties: props ?? void 0,
|
|
3615
|
-
anonymous_id: s.identity.getAnonymousId(),
|
|
3616
|
-
timestamp: occurredAt
|
|
3617
|
-
};
|
|
3618
|
-
if (options?.telemetry) body["telemetry"] = true;
|
|
3619
|
-
const idempotencyKey = s.offlineEnabled ? newIdempotencyKey() : void 0;
|
|
3620
|
-
if (idempotencyKey && keyedLiveTrack(props, options?.telemetry)) body["idempotency_key"] = idempotencyKey;
|
|
3621
|
-
const outboxTicket = s.offlineEnabled ? getOfflineQueue(s).ticket() : void 0;
|
|
3622
|
-
try {
|
|
3623
|
-
await s.http.post(P.events, body);
|
|
3624
|
-
} catch (err) {
|
|
3625
|
-
if (s.offlineEnabled && isOfflineError(err)) {
|
|
3626
|
-
await getOfflineQueue(s).enqueue({
|
|
3627
|
-
kind: "track",
|
|
3628
|
-
event,
|
|
3629
|
-
properties: props,
|
|
3630
|
-
telemetry: options?.telemetry,
|
|
3631
|
-
occurredAt,
|
|
3632
|
-
idempotencyKey
|
|
3633
|
-
}, outboxTicket);
|
|
3634
|
-
return;
|
|
4356
|
+
let s = getState();
|
|
4357
|
+
for (let attempt = 0;; attempt += 1) {
|
|
4358
|
+
const hold = (s.heldEvents?.length ?? 0) > 0 || await s.identity.anonymousIdStillUnknown();
|
|
4359
|
+
if (s.disposed && attempt === 0) {
|
|
4360
|
+
s = await liveState();
|
|
4361
|
+
continue;
|
|
3635
4362
|
}
|
|
3636
|
-
|
|
4363
|
+
if (!hold) return sendEvent(s, event, properties, options);
|
|
4364
|
+
if (typeof event !== "string" || event.trim() === "") throw new AmbaApiError("VALIDATION_ERROR", "event name must be non-empty");
|
|
4365
|
+
holdEvent(s, event, properties, options);
|
|
4366
|
+
if (!s.identity.hasStandInAnonymousId()) releaseHeldEvents(s);
|
|
4367
|
+
return;
|
|
3637
4368
|
}
|
|
3638
4369
|
},
|
|
3639
4370
|
flush: async () => getState().eventQueue?.flush() ?? {
|
|
@@ -3655,18 +4386,24 @@ const Amba = {
|
|
|
3655
4386
|
},
|
|
3656
4387
|
auth: {
|
|
3657
4388
|
signInAnonymously: async () => {
|
|
3658
|
-
const s =
|
|
4389
|
+
const s = await liveState();
|
|
4390
|
+
await loadUnreadSession(s);
|
|
4391
|
+
if (await s.identity.anonymousIdStillUnknown()) throw secureStoreUnavailableError();
|
|
4392
|
+
const startedAt = s.identity.getGeneration();
|
|
3659
4393
|
try {
|
|
3660
4394
|
return await postAuth(s, P.authAnonymous, { anonymous_id: s.identity.getAnonymousId() });
|
|
3661
4395
|
} catch (err) {
|
|
3662
4396
|
if (!(err instanceof AmbaApiError) || err.code !== IDENTITY_SIGN_IN_REQUIRED) throw err;
|
|
3663
|
-
|
|
4397
|
+
const held = await continueHeldSession(s);
|
|
4398
|
+
if (held) return held;
|
|
4399
|
+
if (!await s.identity.clearSession(true, { unlessSignedInSince: startedAt })) throw sessionChangedError("a sign-in stored a newer session while this call was in flight");
|
|
4400
|
+
return postAuth(s, P.authAnonymous, { anonymous_id: s.identity.getAnonymousId() });
|
|
3664
4401
|
}
|
|
3665
4402
|
},
|
|
3666
4403
|
signInWithEmail: async (email, password) => {
|
|
3667
4404
|
requireEmail(email);
|
|
3668
4405
|
if (password === "") throw new AmbaApiError("VALIDATION_ERROR", "password must not be empty");
|
|
3669
|
-
return postAuth(
|
|
4406
|
+
return postAuth(await liveState(), P.authEmailLogin, {
|
|
3670
4407
|
email,
|
|
3671
4408
|
password
|
|
3672
4409
|
});
|
|
@@ -3674,14 +4411,14 @@ const Amba = {
|
|
|
3674
4411
|
signUpWithEmail: async (email, password) => {
|
|
3675
4412
|
requireEmail(email);
|
|
3676
4413
|
requirePassword(password);
|
|
3677
|
-
return postAuth(
|
|
4414
|
+
return postAuth(await liveState(), P.authEmailSignup, {
|
|
3678
4415
|
email,
|
|
3679
4416
|
password
|
|
3680
4417
|
});
|
|
3681
4418
|
},
|
|
3682
4419
|
signInWithApple: async (identityToken, options) => {
|
|
3683
4420
|
if (identityToken.trim() === "") throw new AmbaApiError("VALIDATION_ERROR", "identity_token must not be empty");
|
|
3684
|
-
return postAuth(
|
|
4421
|
+
return postAuth(await liveState(), P.authSocial, {
|
|
3685
4422
|
provider: "apple",
|
|
3686
4423
|
id_token: identityToken,
|
|
3687
4424
|
...nonceField(options)
|
|
@@ -3689,7 +4426,7 @@ const Amba = {
|
|
|
3689
4426
|
},
|
|
3690
4427
|
signInWithGoogle: async (idToken, options) => {
|
|
3691
4428
|
if (idToken.trim() === "") throw new AmbaApiError("VALIDATION_ERROR", "id_token must not be empty");
|
|
3692
|
-
return postAuth(
|
|
4429
|
+
return postAuth(await liveState(), P.authSocial, {
|
|
3693
4430
|
provider: "google",
|
|
3694
4431
|
id_token: idToken,
|
|
3695
4432
|
...nonceField(options)
|
|
@@ -3702,7 +4439,7 @@ const Amba = {
|
|
|
3702
4439
|
verifyEmailOtp: async (email, code) => {
|
|
3703
4440
|
requireEmail(email);
|
|
3704
4441
|
requireOtpCode(code);
|
|
3705
|
-
return postAuth(
|
|
4442
|
+
return postAuth(await liveState(), P.authEmailOtpVerify, {
|
|
3706
4443
|
email,
|
|
3707
4444
|
code
|
|
3708
4445
|
});
|
|
@@ -3714,7 +4451,7 @@ const Amba = {
|
|
|
3714
4451
|
verifySmsOtp: async (phone, code) => {
|
|
3715
4452
|
requireE164Phone(phone);
|
|
3716
4453
|
requireOtpCode(code);
|
|
3717
|
-
return postAuth(
|
|
4454
|
+
return postAuth(await liveState(), P.authSmsOtpVerify, {
|
|
3718
4455
|
phone,
|
|
3719
4456
|
code
|
|
3720
4457
|
});
|
|
@@ -3725,59 +4462,48 @@ const Amba = {
|
|
|
3725
4462
|
},
|
|
3726
4463
|
verifyMagicLink: async (token) => {
|
|
3727
4464
|
if (token.trim() === "") throw new AmbaApiError("VALIDATION_ERROR", "token must not be empty");
|
|
3728
|
-
return postAuth(
|
|
4465
|
+
return postAuth(await liveState(), P.authMagicLinkVerify, { token });
|
|
3729
4466
|
},
|
|
3730
4467
|
linkAccount: async (provider, credential, options) => {
|
|
3731
4468
|
const providerTrimmed = provider?.trim() ?? "";
|
|
3732
4469
|
if (providerTrimmed === "") throw new AmbaApiError("VALIDATION_ERROR", "provider must not be empty");
|
|
3733
4470
|
if (typeof credential !== "string" || credential.trim() === "") throw new AmbaApiError("VALIDATION_ERROR", "credential must not be empty");
|
|
3734
|
-
const s =
|
|
3735
|
-
|
|
3736
|
-
if (!sessionToken) throw new AmbaApiError("UNAUTHORIZED", "no active session — call signIn* first");
|
|
3737
|
-
return trackLink(s, postAuth(s, P.authLink, {
|
|
4471
|
+
const s = await liveState();
|
|
4472
|
+
return withBodySession(s, (sessionToken) => trackLink(s, postAuth(s, P.authLink, {
|
|
3738
4473
|
provider: providerTrimmed,
|
|
3739
4474
|
token: credential,
|
|
3740
4475
|
session_token: sessionToken,
|
|
3741
4476
|
...nonceField(options)
|
|
3742
|
-
}
|
|
4477
|
+
}, {
|
|
4478
|
+
ifGeneration: s.identity.getGeneration(),
|
|
4479
|
+
advance: false
|
|
4480
|
+
})));
|
|
3743
4481
|
},
|
|
3744
4482
|
linkSmsOtp: async (phone, code) => {
|
|
3745
4483
|
requireE164Phone(phone);
|
|
3746
4484
|
requireOtpCode(code);
|
|
3747
|
-
|
|
3748
|
-
const sessionToken = s.identity.getSessionToken();
|
|
3749
|
-
if (!sessionToken) throw new AmbaApiError("UNAUTHORIZED", "no active session — call signIn* first");
|
|
3750
|
-
return postLink(s, {
|
|
4485
|
+
return postLink(await liveState(), {
|
|
3751
4486
|
provider: "sms_otp",
|
|
3752
4487
|
phone,
|
|
3753
|
-
code
|
|
3754
|
-
session_token: sessionToken
|
|
4488
|
+
code
|
|
3755
4489
|
});
|
|
3756
4490
|
},
|
|
3757
4491
|
linkEmailOtp: async (email, code) => {
|
|
3758
4492
|
requireEmail(email);
|
|
3759
4493
|
requireOtpCode(code);
|
|
3760
|
-
|
|
3761
|
-
const sessionToken = s.identity.getSessionToken();
|
|
3762
|
-
if (!sessionToken) throw new AmbaApiError("UNAUTHORIZED", "no active session — call signIn* first");
|
|
3763
|
-
return postLink(s, {
|
|
4494
|
+
return postLink(await liveState(), {
|
|
3764
4495
|
provider: "email_otp",
|
|
3765
4496
|
email,
|
|
3766
|
-
code
|
|
3767
|
-
session_token: sessionToken
|
|
4497
|
+
code
|
|
3768
4498
|
});
|
|
3769
4499
|
},
|
|
3770
4500
|
linkEmailPassword: async (email, password) => {
|
|
3771
4501
|
requireEmail(email);
|
|
3772
4502
|
requirePassword(password);
|
|
3773
|
-
|
|
3774
|
-
const sessionToken = s.identity.getSessionToken();
|
|
3775
|
-
if (!sessionToken) throw new AmbaApiError("UNAUTHORIZED", "no active session — call signIn* first");
|
|
3776
|
-
return postLink(s, {
|
|
4503
|
+
return postLink(await liveState(), {
|
|
3777
4504
|
provider: "email_password",
|
|
3778
4505
|
email,
|
|
3779
|
-
password
|
|
3780
|
-
session_token: sessionToken
|
|
4506
|
+
password
|
|
3781
4507
|
});
|
|
3782
4508
|
},
|
|
3783
4509
|
unlinkSmsOtp: async () => unlinkProvider("sms_otp"),
|
|
@@ -3786,42 +4512,47 @@ const Amba = {
|
|
|
3786
4512
|
unlinkGoogle: async () => unlinkProvider("google"),
|
|
3787
4513
|
getCurrentIdentifiers: async () => {
|
|
3788
4514
|
const s = getState();
|
|
3789
|
-
|
|
4515
|
+
await settleSessionInFlight(s);
|
|
4516
|
+
requireSession(s);
|
|
3790
4517
|
return (await s.http.get(P.authIdentifiers)).data.identifiers;
|
|
3791
4518
|
},
|
|
3792
4519
|
signOut: async (rotateAnonymousId = false) => {
|
|
3793
|
-
const s =
|
|
4520
|
+
const s = await liveState();
|
|
4521
|
+
await loadUnreadSession(s);
|
|
4522
|
+
const startedAt = s.identity.getGeneration();
|
|
4523
|
+
const revoked = s.identity.getRefreshToken();
|
|
3794
4524
|
if (s.identity.getSessionToken()) try {
|
|
3795
|
-
|
|
3796
|
-
await s.http.post(P.authLogout, refreshToken ? { refresh_token: refreshToken } : {});
|
|
4525
|
+
await s.http.post(P.authLogout, revoked ? { refresh_token: revoked } : {});
|
|
3797
4526
|
} catch (err) {
|
|
3798
4527
|
if (typeof console !== "undefined") console.warn("[amba] signOut server call failed", err);
|
|
3799
4528
|
}
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
try {
|
|
3805
|
-
await getOfflineQueue(s).purge();
|
|
3806
|
-
} catch (err) {
|
|
3807
|
-
if (typeof console !== "undefined") console.warn("[amba] signOut could not clear the offline outbox", err);
|
|
3808
|
-
}
|
|
3809
|
-
s.eventQueue?.flush();
|
|
3810
|
-
notifyAuthSubscribers(null);
|
|
4529
|
+
if (await endSignedOutSession(s, rotateAnonymousId, startedAt)) return;
|
|
4530
|
+
if (!s.disposed || revoked === null) return;
|
|
4531
|
+
const next = await liveState();
|
|
4532
|
+
if (next !== s && !next.disposed && sameIdentityTarget(next, s) && next.identity.continuesLaunchSession(revoked)) await endSignedOutSession(next, rotateAnonymousId, next.identity.getGeneration());
|
|
3811
4533
|
},
|
|
3812
|
-
refresh: async () => refreshSession(
|
|
4534
|
+
refresh: async () => refreshSession(await liveState()),
|
|
3813
4535
|
me: async () => {
|
|
3814
4536
|
const s = getState();
|
|
3815
|
-
|
|
4537
|
+
await settleSessionInFlight(s);
|
|
4538
|
+
requireSession(s);
|
|
3816
4539
|
return normalizeUserAuthProviders((await s.http.get(P.authMe)).data);
|
|
3817
4540
|
},
|
|
3818
4541
|
getSession: async () => {
|
|
3819
4542
|
const s = getState();
|
|
4543
|
+
await settleSessionInFlight(s);
|
|
3820
4544
|
if (!s.identity.isAuthenticated()) return null;
|
|
3821
|
-
|
|
4545
|
+
try {
|
|
4546
|
+
return snapshotSession(normalizeUserAuthProviders((await s.http.get(P.authMe)).data));
|
|
4547
|
+
} catch (err) {
|
|
4548
|
+
if (isSessionRefusedError(err) && !s.identity.isAuthenticated()) return null;
|
|
4549
|
+
throw err;
|
|
4550
|
+
}
|
|
3822
4551
|
},
|
|
3823
4552
|
getAnonymousId: async () => {
|
|
3824
|
-
|
|
4553
|
+
const s = getState();
|
|
4554
|
+
if (await s.identity.anonymousIdStillUnknown()) throw secureStoreUnavailableError();
|
|
4555
|
+
return s.identity.getAnonymousId();
|
|
3825
4556
|
},
|
|
3826
4557
|
onAuthStateChange: (cb) => {
|
|
3827
4558
|
authSubscribers.add(cb);
|
|
@@ -3833,17 +4564,19 @@ const Amba = {
|
|
|
3833
4564
|
users: {
|
|
3834
4565
|
get: async (userId) => {
|
|
3835
4566
|
const s = getState();
|
|
3836
|
-
|
|
4567
|
+
await settleSessionInFlight(s);
|
|
4568
|
+
requireSession(s);
|
|
3837
4569
|
const path = userId === void 0 || userId === null || userId === "" ? P.usersMe : `${P.users}/${encodePathSegment(userId)}`;
|
|
3838
4570
|
return normalizeUserAuthProviders((await s.http.get(path)).data);
|
|
3839
4571
|
},
|
|
3840
|
-
update: ((arg1, arg2) => {
|
|
4572
|
+
update: (async (arg1, arg2) => {
|
|
3841
4573
|
const s = getState();
|
|
3842
|
-
|
|
4574
|
+
await settleSessionInFlight(s);
|
|
4575
|
+
requireSession(s);
|
|
3843
4576
|
const isById = typeof arg1 === "string";
|
|
3844
4577
|
const patch = isById ? arg2 ?? {} : arg1;
|
|
3845
4578
|
const path = isById ? `${P.users}/${encodePathSegment(arg1)}` : `${P.users}/me`;
|
|
3846
|
-
return s.http.patch(path, patch)
|
|
4579
|
+
return normalizeUserAuthProviders((await s.http.patch(path, patch)).data);
|
|
3847
4580
|
})
|
|
3848
4581
|
},
|
|
3849
4582
|
sessions: {
|