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