@layers/amba-react-native 4.2.2 → 4.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/index.cjs +1003 -276
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +53 -6
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +53 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1003 -276
- package/dist/index.js.map +1 -1
- package/package.json +4 -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();
|
|
@@ -496,17 +510,71 @@ function mapHttpError(response, rawBody, isJson) {
|
|
|
496
510
|
}
|
|
497
511
|
}
|
|
498
512
|
|
|
513
|
+
//#endregion
|
|
514
|
+
//#region src/secure-random.ts
|
|
515
|
+
/**
|
|
516
|
+
* The production loader. The `require` call must stay a string literal; see
|
|
517
|
+
* the module comment.
|
|
518
|
+
*/
|
|
519
|
+
function loadGetRandomValuesPolyfill() {
|
|
520
|
+
require("react-native-get-random-values");
|
|
521
|
+
}
|
|
522
|
+
function globalObject() {
|
|
523
|
+
return globalThis;
|
|
524
|
+
}
|
|
525
|
+
function currentGetRandomValues() {
|
|
526
|
+
const crypto = globalObject().crypto;
|
|
527
|
+
const grv = crypto?.getRandomValues;
|
|
528
|
+
return crypto && typeof grv === "function" ? grv : null;
|
|
529
|
+
}
|
|
530
|
+
function probe(grv) {
|
|
531
|
+
try {
|
|
532
|
+
const crypto = globalObject().crypto;
|
|
533
|
+
return grv.call(crypto, new Uint8Array(1)) instanceof Uint8Array;
|
|
534
|
+
} catch {
|
|
535
|
+
return false;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
let attempted = false;
|
|
539
|
+
/**
|
|
540
|
+
* Makes `globalThis.crypto.getRandomValues` available when the runtime can
|
|
541
|
+
* supply a secure one. Returns whether it is available afterwards.
|
|
542
|
+
*
|
|
543
|
+
* The polyfill is loaded at most once per JS runtime. A later call still
|
|
544
|
+
* reports true when the app installs a secure implementation itself.
|
|
545
|
+
*/
|
|
546
|
+
function ensureSecureRandom(load = loadGetRandomValuesPolyfill) {
|
|
547
|
+
if (currentGetRandomValues()) return true;
|
|
548
|
+
if (attempted) return false;
|
|
549
|
+
attempted = true;
|
|
550
|
+
const g = globalObject();
|
|
551
|
+
const hadCrypto = Object.prototype.hasOwnProperty.call(g, "crypto");
|
|
552
|
+
const priorCrypto = g.crypto;
|
|
553
|
+
try {
|
|
554
|
+
load();
|
|
555
|
+
} catch {}
|
|
556
|
+
const installed = currentGetRandomValues();
|
|
557
|
+
if (installed && probe(installed)) return true;
|
|
558
|
+
try {
|
|
559
|
+
if (!hadCrypto) delete g.crypto;
|
|
560
|
+
else if (g.crypto === priorCrypto && priorCrypto) delete priorCrypto.getRandomValues;
|
|
561
|
+
else g.crypto = priorCrypto;
|
|
562
|
+
} catch {}
|
|
563
|
+
return false;
|
|
564
|
+
}
|
|
565
|
+
|
|
499
566
|
//#endregion
|
|
500
567
|
//#region src/uuid.ts
|
|
501
568
|
/** Thrown when the runtime has no `crypto.getRandomValues`. */
|
|
502
569
|
var SecureRandomUnavailableError = class extends Error {
|
|
503
570
|
code = "SECURE_RANDOM_UNAVAILABLE";
|
|
504
571
|
constructor() {
|
|
505
|
-
super("Amba needs crypto.getRandomValues to generate identifiers, and this runtime does not provide it.
|
|
572
|
+
super("Amba needs crypto.getRandomValues to generate identifiers, and this runtime does not provide it. The SDK loads react-native-get-random-values for you, and its native module is missing from this build. Add the package to your app's package.json so the native module is linked (Expo: `npx expo install react-native-get-random-values`; bare React Native: `npm install react-native-get-random-values`, then `npx pod-install`) and rebuild the native app. No import is needed.");
|
|
506
573
|
this.name = "SecureRandomUnavailableError";
|
|
507
574
|
}
|
|
508
575
|
};
|
|
509
576
|
function secureGetRandomValues() {
|
|
577
|
+
ensureSecureRandom();
|
|
510
578
|
const crypto = globalThis.crypto;
|
|
511
579
|
const grv = crypto?.getRandomValues;
|
|
512
580
|
if (!crypto || typeof grv !== "function") throw new SecureRandomUnavailableError();
|
|
@@ -613,11 +681,42 @@ var IdentityStore = class IdentityStore {
|
|
|
613
681
|
installId;
|
|
614
682
|
/** Whether the stored record carries `installId`. */
|
|
615
683
|
installIdStored;
|
|
616
|
-
|
|
684
|
+
/** See `IdentitySnapshot.signInRequired`. */
|
|
685
|
+
signInRequired;
|
|
686
|
+
/**
|
|
687
|
+
* Advanced by every committed write that installs or ends a session (a
|
|
688
|
+
* sign-in, a sign-out, an ended session). A refresh or a link keeps it.
|
|
689
|
+
*/
|
|
690
|
+
generation = 0;
|
|
691
|
+
/** Set by `dispose`: the store commits and persists nothing after it. */
|
|
692
|
+
disposed = false;
|
|
693
|
+
/**
|
|
694
|
+
* Set while the record's credential part could not be read (see
|
|
695
|
+
* `PersistedIdentity.credentials_unreadable`). Writes wait for a read
|
|
696
|
+
* that succeeds, so a session this launch never loaded is not replaced.
|
|
697
|
+
*/
|
|
698
|
+
recordUnreadable;
|
|
699
|
+
/**
|
|
700
|
+
* Set while the anonymous id is a stand-in for one the unreadable
|
|
701
|
+
* credential part holds. It is never persisted.
|
|
702
|
+
*/
|
|
703
|
+
temporaryAnonymousId;
|
|
704
|
+
/** See `onStoredSessionLoaded`. */
|
|
705
|
+
storedSessionLoaded = null;
|
|
706
|
+
/** See `onAnonymousIdKnown`. */
|
|
707
|
+
anonymousIdKnown = null;
|
|
708
|
+
/** The refresh token the store held once created (see `continuesLaunchSession`). */
|
|
709
|
+
launchRefreshToken = null;
|
|
710
|
+
/** Sign-ins committed since the store was created. */
|
|
711
|
+
signInsSinceLaunch = 0;
|
|
712
|
+
constructor(persistence, seed, vault, plaintext, vaultUnreadable, seq, recordUnreadable, temporaryAnonymousId) {
|
|
617
713
|
this.persistence = persistence;
|
|
618
714
|
this.vault = vault;
|
|
619
715
|
this.plaintext = plaintext;
|
|
620
716
|
this.vaultUnreadable = vaultUnreadable;
|
|
717
|
+
this.recordUnreadable = recordUnreadable;
|
|
718
|
+
this.temporaryAnonymousId = temporaryAnonymousId;
|
|
719
|
+
this.signInRequired = seed.sign_in_required;
|
|
621
720
|
this.seq = seq;
|
|
622
721
|
this.installId = seed.install_id ?? uuidv4();
|
|
623
722
|
this.installIdStored = seed.install_id !== null;
|
|
@@ -653,21 +752,17 @@ var IdentityStore = class IdentityStore {
|
|
|
653
752
|
assertSecureRandomAvailable();
|
|
654
753
|
const raw = await persistence.read(IDENTITY_KEY);
|
|
655
754
|
let recordPresent = false;
|
|
755
|
+
let recordUnreadable = false;
|
|
756
|
+
let temporaryAnonymousId = false;
|
|
656
757
|
let seed;
|
|
657
758
|
if (raw) try {
|
|
658
759
|
const parsed = JSON.parse(raw);
|
|
659
|
-
seed =
|
|
660
|
-
anonymous_id: parsed.anonymous_id ?? uuidv4(),
|
|
661
|
-
app_user_id: parsed.app_user_id ?? null,
|
|
662
|
-
session_token: parsed.session_token ?? null,
|
|
663
|
-
refresh_token: parsed.refresh_token ?? null,
|
|
664
|
-
expires_at: parsed.expires_at ?? null,
|
|
665
|
-
anonymous_session: typeof parsed.anonymous_session === "boolean" ? parsed.anonymous_session : null,
|
|
666
|
-
write_seq: seqOf(parsed.write_seq) ?? 0,
|
|
667
|
-
vault_seq: parsed.vault_seq === void 0 ? 0 : seqOf(parsed.vault_seq),
|
|
668
|
-
install_id: typeof parsed.install_id === "string" ? parsed.install_id : null
|
|
669
|
-
};
|
|
760
|
+
seed = seedFrom(parsed);
|
|
670
761
|
recordPresent = typeof parsed.anonymous_id === "string";
|
|
762
|
+
if (parsed.credentials_unreadable === true) {
|
|
763
|
+
recordUnreadable = true;
|
|
764
|
+
temporaryAnonymousId = !recordPresent;
|
|
765
|
+
}
|
|
671
766
|
} catch {
|
|
672
767
|
seed = freshSeed();
|
|
673
768
|
}
|
|
@@ -682,7 +777,8 @@ var IdentityStore = class IdentityStore {
|
|
|
682
777
|
} : null;
|
|
683
778
|
let vaultReadable = true;
|
|
684
779
|
let seq = seed.write_seq;
|
|
685
|
-
if (vault)
|
|
780
|
+
if (vault && recordUnreadable) vaultReadable = false;
|
|
781
|
+
else if (vault) {
|
|
686
782
|
let fromVault = null;
|
|
687
783
|
try {
|
|
688
784
|
fromVault = await vault.read();
|
|
@@ -701,17 +797,10 @@ var IdentityStore = class IdentityStore {
|
|
|
701
797
|
seed.vault_seq = null;
|
|
702
798
|
}
|
|
703
799
|
}
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
seed.app_user_id = null;
|
|
707
|
-
seed.anonymous_session = null;
|
|
708
|
-
}
|
|
709
|
-
if (isExpired(seed.expires_at)) {
|
|
710
|
-
seed.session_token = null;
|
|
711
|
-
seed.expires_at = null;
|
|
712
|
-
}
|
|
713
|
-
const store = new IdentityStore(persistence, seed, vault, plaintext, !vaultReadable, seq);
|
|
800
|
+
settleSeed(seed, vaultReadable && !recordUnreadable);
|
|
801
|
+
const store = new IdentityStore(persistence, seed, vault, plaintext, !vaultReadable, seq, recordUnreadable, temporaryAnonymousId);
|
|
714
802
|
await store.flush({ hydrating: true });
|
|
803
|
+
store.launchRefreshToken = store.refreshToken;
|
|
715
804
|
return store;
|
|
716
805
|
}
|
|
717
806
|
snapshot() {
|
|
@@ -721,9 +810,184 @@ var IdentityStore = class IdentityStore {
|
|
|
721
810
|
sessionToken: this.sessionToken,
|
|
722
811
|
refreshToken: this.refreshToken,
|
|
723
812
|
expiresAt: this.expiresAt,
|
|
724
|
-
anonymousSession: this.anonymousSession
|
|
813
|
+
anonymousSession: this.anonymousSession,
|
|
814
|
+
signInRequired: this.signInRequired
|
|
725
815
|
};
|
|
726
816
|
}
|
|
817
|
+
/** The current generation (see `CommitFence`). */
|
|
818
|
+
getGeneration() {
|
|
819
|
+
return this.generation;
|
|
820
|
+
}
|
|
821
|
+
/**
|
|
822
|
+
* Whether the store still holds the session it was created with, renewed
|
|
823
|
+
* or ended since but never replaced by a sign-in: it was created holding
|
|
824
|
+
* `refreshToken`, and no sign-in has committed since.
|
|
825
|
+
*/
|
|
826
|
+
continuesLaunchSession(refreshToken) {
|
|
827
|
+
return this.launchRefreshToken === refreshToken && this.signInsSinceLaunch === 0;
|
|
828
|
+
}
|
|
829
|
+
/** See `IdentitySnapshot.signInRequired`. */
|
|
830
|
+
isSignInRequired() {
|
|
831
|
+
return this.signInRequired;
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Detach the store (the SDK was configured again). Nothing commits or
|
|
835
|
+
* persists after this; the promise settles once the writes already under
|
|
836
|
+
* way have landed.
|
|
837
|
+
*/
|
|
838
|
+
dispose() {
|
|
839
|
+
this.disposed = true;
|
|
840
|
+
return this.flushChain;
|
|
841
|
+
}
|
|
842
|
+
/** Undo `dispose` (a `configure()` that failed before it replaced this store). */
|
|
843
|
+
reattach() {
|
|
844
|
+
this.disposed = false;
|
|
845
|
+
}
|
|
846
|
+
/** Whether a write under `fence` may commit now. */
|
|
847
|
+
mayCommit(fence) {
|
|
848
|
+
if (this.disposed) return false;
|
|
849
|
+
return fence.ifGeneration === void 0 || fence.ifGeneration === this.generation;
|
|
850
|
+
}
|
|
851
|
+
/** Commit bookkeeping: advance the generation unless the fence keeps it. */
|
|
852
|
+
committed(fence) {
|
|
853
|
+
if (fence.advance !== false) this.generation += 1;
|
|
854
|
+
return this.generation;
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Called when a stored session that couldn't be read at launch (a locked
|
|
858
|
+
* vault or credential record) is loaded after it, so the SDK can tell its
|
|
859
|
+
* subscribers. A session loaded while the store is created is not
|
|
860
|
+
* reported.
|
|
861
|
+
*/
|
|
862
|
+
onStoredSessionLoaded(listener) {
|
|
863
|
+
this.storedSessionLoaded = listener;
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Called when the stand-in anonymous id gives way to the device's own (see
|
|
867
|
+
* `temporaryAnonymousId`): a later read found it, or a sign-in gave one.
|
|
868
|
+
* A sign-out that rotates the id is not reported: it starts over.
|
|
869
|
+
*/
|
|
870
|
+
onAnonymousIdKnown(listener) {
|
|
871
|
+
this.anonymousIdKnown = listener;
|
|
872
|
+
}
|
|
873
|
+
/** Whether the anonymous id is a stand-in (see `temporaryAnonymousId`). */
|
|
874
|
+
hasStandInAnonymousId() {
|
|
875
|
+
return this.temporaryAnonymousId;
|
|
876
|
+
}
|
|
877
|
+
/** The stand-in gives way to a known anonymous id (see `onAnonymousIdKnown`). */
|
|
878
|
+
anonymousIdNowKnown() {
|
|
879
|
+
if (!this.temporaryAnonymousId) return;
|
|
880
|
+
this.temporaryAnonymousId = false;
|
|
881
|
+
if (!this.disposed) this.anonymousIdKnown?.();
|
|
882
|
+
}
|
|
883
|
+
/** Report a session a late read loaded (see `onStoredSessionLoaded`). */
|
|
884
|
+
reportStoredSession() {
|
|
885
|
+
if (this.signInsSinceLaunch === 0 && this.launchRefreshToken === null) this.launchRefreshToken = this.refreshToken;
|
|
886
|
+
if (this.disposed) return;
|
|
887
|
+
if (this.sessionToken === null && this.refreshToken === null) return;
|
|
888
|
+
this.storedSessionLoaded?.();
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* Whether the record's credential part still can't be read (see
|
|
892
|
+
* `PersistedIdentity.credentials_unreadable`). When the launch read
|
|
893
|
+
* failed, the record is read again first. A session it holds is loaded
|
|
894
|
+
* when nothing has replaced the launch state since; the stored anonymous
|
|
895
|
+
* id replaces the stand-in either way.
|
|
896
|
+
*/
|
|
897
|
+
async recordStillUnreadable() {
|
|
898
|
+
if (!this.recordUnreadable) return false;
|
|
899
|
+
let parsed = null;
|
|
900
|
+
try {
|
|
901
|
+
const raw = await this.persistence.reread(IDENTITY_KEY);
|
|
902
|
+
parsed = raw ? JSON.parse(raw) : null;
|
|
903
|
+
} catch {
|
|
904
|
+
return true;
|
|
905
|
+
}
|
|
906
|
+
if (!this.recordUnreadable) return false;
|
|
907
|
+
if (parsed?.credentials_unreadable === true) return true;
|
|
908
|
+
this.recordUnreadable = false;
|
|
909
|
+
const storedAnonymousId = parsed && typeof parsed.anonymous_id === "string" ? parsed.anonymous_id : null;
|
|
910
|
+
const untouched = this.generation === 0 && this.sessionToken === null && this.refreshToken === null;
|
|
911
|
+
if (parsed && storedAnonymousId !== null && untouched) {
|
|
912
|
+
if (!this.vault) {
|
|
913
|
+
const stored$1 = seedFrom(parsed);
|
|
914
|
+
settleSeed(stored$1, true);
|
|
915
|
+
this.anonymousId = stored$1.anonymous_id;
|
|
916
|
+
this.appUserId = stored$1.app_user_id;
|
|
917
|
+
this.sessionToken = stored$1.session_token;
|
|
918
|
+
this.refreshToken = stored$1.refresh_token;
|
|
919
|
+
this.expiresAt = stored$1.expires_at;
|
|
920
|
+
this.anonymousSession = stored$1.anonymous_session;
|
|
921
|
+
this.signInRequired = stored$1.sign_in_required;
|
|
922
|
+
this.seq = Math.max(this.seq, stored$1.write_seq);
|
|
923
|
+
this.reportStoredSession();
|
|
924
|
+
this.anonymousIdNowKnown();
|
|
925
|
+
return false;
|
|
926
|
+
}
|
|
927
|
+
const stored = seedFrom(parsed);
|
|
928
|
+
this.anonymousId = stored.anonymous_id;
|
|
929
|
+
const wasStandIn = this.temporaryAnonymousId;
|
|
930
|
+
this.temporaryAnonymousId = false;
|
|
931
|
+
this.appUserId = stored.app_user_id;
|
|
932
|
+
this.anonymousSession = stored.anonymous_session;
|
|
933
|
+
this.signInRequired = stored.sign_in_required;
|
|
934
|
+
this.recordSeq = stored.write_seq;
|
|
935
|
+
this.vaultSeq = stored.vault_seq;
|
|
936
|
+
this.seq = Math.max(this.seq, stored.write_seq);
|
|
937
|
+
await this.vaultStillUnreadable();
|
|
938
|
+
if (this.sessionToken === null && this.refreshToken === null && (stored.session_token !== null || stored.refresh_token !== null)) {
|
|
939
|
+
this.plaintext = {
|
|
940
|
+
appUserId: stored.app_user_id,
|
|
941
|
+
slice: {
|
|
942
|
+
session_token: stored.session_token,
|
|
943
|
+
refresh_token: stored.refresh_token,
|
|
944
|
+
expires_at: stored.expires_at
|
|
945
|
+
}
|
|
946
|
+
};
|
|
947
|
+
const expired = isExpired(stored.expires_at);
|
|
948
|
+
this.sessionToken = expired ? null : stored.session_token;
|
|
949
|
+
this.refreshToken = stored.refresh_token;
|
|
950
|
+
this.expiresAt = expired ? null : stored.expires_at;
|
|
951
|
+
this.reportStoredSession();
|
|
952
|
+
}
|
|
953
|
+
if (!this.vaultUnreadable && this.sessionToken === null && this.refreshToken === null) {
|
|
954
|
+
if (this.appUserId !== null && this.anonymousSession === false) this.anonymousId = uuidv4();
|
|
955
|
+
this.appUserId = null;
|
|
956
|
+
this.anonymousSession = null;
|
|
957
|
+
}
|
|
958
|
+
if (wasStandIn && !this.disposed) this.anonymousIdKnown?.();
|
|
959
|
+
return false;
|
|
960
|
+
}
|
|
961
|
+
if (this.temporaryAnonymousId && storedAnonymousId !== null) this.anonymousId = storedAnonymousId;
|
|
962
|
+
this.anonymousIdNowKnown();
|
|
963
|
+
return false;
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Whether the stored session can't be read right now: the record's
|
|
967
|
+
* credential part (see `recordStillUnreadable`) or the vault (see
|
|
968
|
+
* `vaultStillUnreadable`). The record is read first: it binds the entry.
|
|
969
|
+
*/
|
|
970
|
+
async credentialsStillUnreadable() {
|
|
971
|
+
if (await this.recordStillUnreadable()) return true;
|
|
972
|
+
return this.vaultStillUnreadable();
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* Whether the stored credentials were unreadable at the last attempt. A
|
|
976
|
+
* call that needs a session reports `SECURE_STORE_UNAVAILABLE` then.
|
|
977
|
+
*/
|
|
978
|
+
credentialsUnknown() {
|
|
979
|
+
return this.recordUnreadable || this.vaultUnreadable;
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Whether the anonymous id is still a stand-in after the record is read
|
|
983
|
+
* again (see `temporaryAnonymousId`). Anything sent under it would belong
|
|
984
|
+
* to nobody.
|
|
985
|
+
*/
|
|
986
|
+
async anonymousIdStillUnknown() {
|
|
987
|
+
if (!this.temporaryAnonymousId) return false;
|
|
988
|
+
await this.recordStillUnreadable();
|
|
989
|
+
return this.temporaryAnonymousId;
|
|
990
|
+
}
|
|
727
991
|
/**
|
|
728
992
|
* Whether the vault still can't be read. When the launch read failed, the
|
|
729
993
|
* vault is read again first, and a session it holds for this install is
|
|
@@ -732,6 +996,7 @@ var IdentityStore = class IdentityStore {
|
|
|
732
996
|
*/
|
|
733
997
|
async vaultStillUnreadable() {
|
|
734
998
|
if (!this.vault || !this.vaultUnreadable) return false;
|
|
999
|
+
if (this.recordUnreadable) return true;
|
|
735
1000
|
let stored;
|
|
736
1001
|
try {
|
|
737
1002
|
stored = await this.vault.read();
|
|
@@ -740,7 +1005,10 @@ var IdentityStore = class IdentityStore {
|
|
|
740
1005
|
}
|
|
741
1006
|
this.vaultUnreadable = false;
|
|
742
1007
|
this.noteStoredSeq(stored);
|
|
743
|
-
if (this.sessionToken === null && this.refreshToken === null)
|
|
1008
|
+
if (this.sessionToken === null && this.refreshToken === null) {
|
|
1009
|
+
this.adoptStored(stored);
|
|
1010
|
+
this.reportStoredSession();
|
|
1011
|
+
}
|
|
744
1012
|
return false;
|
|
745
1013
|
}
|
|
746
1014
|
/** Keep the next sequence number above an entry read from the vault. */
|
|
@@ -777,30 +1045,51 @@ var IdentityStore = class IdentityStore {
|
|
|
777
1045
|
/**
|
|
778
1046
|
* Store a session. `anonymous` records whether it is a purely anonymous
|
|
779
1047
|
* one (true only for `/v1/client/auth/anonymous`); omit it to keep the
|
|
780
|
-
* current value (a token refresh of the same session).
|
|
1048
|
+
* current value (a token refresh of the same session). A stored session
|
|
1049
|
+
* ends a pending `signInRequired`.
|
|
1050
|
+
*
|
|
1051
|
+
* Resolves the generation the write committed at, or null when the fence
|
|
1052
|
+
* refused it (nothing changed).
|
|
781
1053
|
*/
|
|
782
|
-
async setSession(args) {
|
|
1054
|
+
async setSession(args, fence = {}) {
|
|
1055
|
+
if (!this.mayCommit(fence)) return null;
|
|
783
1056
|
this.appUserId = args.appUserId;
|
|
784
1057
|
this.sessionToken = args.sessionToken;
|
|
785
1058
|
this.refreshToken = args.refreshToken;
|
|
786
1059
|
this.expiresAt = args.expiresAt;
|
|
787
1060
|
if (args.anonymous !== void 0) this.anonymousSession = args.anonymous;
|
|
1061
|
+
this.signInRequired = false;
|
|
1062
|
+
if (fence.advance !== false) this.signInsSinceLaunch += 1;
|
|
1063
|
+
const generation = this.committed(fence);
|
|
788
1064
|
await this.flush();
|
|
1065
|
+
return generation;
|
|
789
1066
|
}
|
|
790
|
-
/**
|
|
791
|
-
|
|
792
|
-
|
|
1067
|
+
/**
|
|
1068
|
+
* The current session gained an identity (a link). False when the fence
|
|
1069
|
+
* refused the write.
|
|
1070
|
+
*/
|
|
1071
|
+
async markIdentified(fence = {}) {
|
|
1072
|
+
if (!this.mayCommit(fence)) return false;
|
|
1073
|
+
if (this.anonymousSession === false) return true;
|
|
793
1074
|
this.anonymousSession = false;
|
|
794
1075
|
await this.flush();
|
|
1076
|
+
return true;
|
|
795
1077
|
}
|
|
796
1078
|
/**
|
|
797
1079
|
* Adopt the anonymous id the server holds for this install's user: the id
|
|
798
1080
|
* an anonymous sign-in answered with, or the fresh id a link gave the user.
|
|
1081
|
+
* False when the fence refused the write.
|
|
799
1082
|
*/
|
|
800
|
-
async setAnonymousId(anonymousId) {
|
|
801
|
-
if (
|
|
802
|
-
this.anonymousId
|
|
803
|
-
|
|
1083
|
+
async setAnonymousId(anonymousId, fence = {}) {
|
|
1084
|
+
if (!this.mayCommit(fence)) return false;
|
|
1085
|
+
if (anonymousId !== this.anonymousId) {
|
|
1086
|
+
this.anonymousId = anonymousId;
|
|
1087
|
+
this.anonymousIdNowKnown();
|
|
1088
|
+
await this.flush();
|
|
1089
|
+
return true;
|
|
1090
|
+
}
|
|
1091
|
+
this.anonymousIdNowKnown();
|
|
1092
|
+
return true;
|
|
804
1093
|
}
|
|
805
1094
|
/**
|
|
806
1095
|
* End the session. The anonymous id is rotated when `rotateAnonymousId`
|
|
@@ -812,16 +1101,24 @@ var IdentityStore = class IdentityStore {
|
|
|
812
1101
|
* anonymous sign-in).
|
|
813
1102
|
*/
|
|
814
1103
|
async clearSession(rotateAnonymousId, opts = {}) {
|
|
1104
|
+
if (!this.mayCommit({ ifGeneration: opts.ifGeneration })) return false;
|
|
815
1105
|
const hadSession = this.sessionToken !== null || this.refreshToken !== null;
|
|
1106
|
+
const unreadSession = this.credentialsUnknown() && this.appUserId !== null;
|
|
1107
|
+
if (opts.unlessSignedInSince !== void 0 && opts.unlessSignedInSince !== this.generation && hadSession) return false;
|
|
816
1108
|
const identified = this.anonymousSession === false || this.anonymousSession === null && !opts.keepIdWhenKindUnknown;
|
|
817
|
-
const
|
|
1109
|
+
const rotate = rotateAnonymousId || (hadSession || unreadSession) && identified;
|
|
1110
|
+
const nextAnonymousId = rotate ? uuidv4() : this.anonymousId;
|
|
1111
|
+
if (rotate) this.temporaryAnonymousId = false;
|
|
818
1112
|
this.appUserId = null;
|
|
819
1113
|
this.sessionToken = null;
|
|
820
1114
|
this.refreshToken = null;
|
|
821
1115
|
this.expiresAt = null;
|
|
822
1116
|
this.anonymousSession = null;
|
|
1117
|
+
this.signInRequired = opts.signInRequired === true;
|
|
823
1118
|
this.anonymousId = nextAnonymousId;
|
|
1119
|
+
this.committed({});
|
|
824
1120
|
await this.flush({ signingOut: true });
|
|
1121
|
+
return true;
|
|
825
1122
|
}
|
|
826
1123
|
/**
|
|
827
1124
|
* Persist the identity. `hydrating` marks the write that follows `create`:
|
|
@@ -831,11 +1128,13 @@ var IdentityStore = class IdentityStore {
|
|
|
831
1128
|
* to a new anonymous id so the entry left behind no longer belongs to it.
|
|
832
1129
|
*/
|
|
833
1130
|
flush(opts) {
|
|
1131
|
+
if (this.disposed) return Promise.resolve();
|
|
834
1132
|
const run = this.flushChain.then(() => this.flushNow(opts));
|
|
835
1133
|
this.flushChain = run.catch(() => void 0);
|
|
836
1134
|
return run;
|
|
837
1135
|
}
|
|
838
1136
|
async flushNow(opts) {
|
|
1137
|
+
if (await this.recordStillUnreadable() && this.generation === 0) return;
|
|
839
1138
|
if (!this.vault) {
|
|
840
1139
|
const seq$1 = ++this.seq;
|
|
841
1140
|
const recordSeq$1 = opts?.hydrating ? this.recordSeq : seq$1;
|
|
@@ -846,9 +1145,11 @@ var IdentityStore = class IdentityStore {
|
|
|
846
1145
|
refresh_token: this.refreshToken,
|
|
847
1146
|
expires_at: this.expiresAt,
|
|
848
1147
|
anonymous_session: this.anonymousSession,
|
|
1148
|
+
sign_in_required: this.signInRequired,
|
|
849
1149
|
write_seq: recordSeq$1,
|
|
850
1150
|
vault_seq: null,
|
|
851
|
-
install_id: this.installId
|
|
1151
|
+
install_id: this.installId,
|
|
1152
|
+
...this.temporaryAnonymousId ? { anonymous_id_unknown: true } : {}
|
|
852
1153
|
};
|
|
853
1154
|
await this.persistence.write(IDENTITY_KEY, JSON.stringify(payload));
|
|
854
1155
|
this.recordSeq = recordSeq$1;
|
|
@@ -927,9 +1228,11 @@ var IdentityStore = class IdentityStore {
|
|
|
927
1228
|
refresh_token: keep?.refresh_token ?? null,
|
|
928
1229
|
expires_at: keep?.expires_at ?? null,
|
|
929
1230
|
anonymous_session: this.anonymousSession,
|
|
1231
|
+
sign_in_required: this.signInRequired,
|
|
930
1232
|
write_seq: recordSeq,
|
|
931
1233
|
vault_seq: this.vaultSeq,
|
|
932
|
-
install_id: this.installId
|
|
1234
|
+
install_id: this.installId,
|
|
1235
|
+
...this.temporaryAnonymousId ? { anonymous_id_unknown: true } : {}
|
|
933
1236
|
};
|
|
934
1237
|
}
|
|
935
1238
|
};
|
|
@@ -941,11 +1244,47 @@ function freshSeed() {
|
|
|
941
1244
|
refresh_token: null,
|
|
942
1245
|
expires_at: null,
|
|
943
1246
|
anonymous_session: null,
|
|
1247
|
+
sign_in_required: false,
|
|
944
1248
|
write_seq: 0,
|
|
945
1249
|
vault_seq: null,
|
|
946
1250
|
install_id: null
|
|
947
1251
|
};
|
|
948
1252
|
}
|
|
1253
|
+
/** The identity a parsed record describes, with defaults for missing fields. */
|
|
1254
|
+
function seedFrom(parsed) {
|
|
1255
|
+
return {
|
|
1256
|
+
anonymous_id: parsed.anonymous_id ?? uuidv4(),
|
|
1257
|
+
app_user_id: parsed.app_user_id ?? null,
|
|
1258
|
+
session_token: parsed.session_token ?? null,
|
|
1259
|
+
refresh_token: parsed.refresh_token ?? null,
|
|
1260
|
+
expires_at: parsed.expires_at ?? null,
|
|
1261
|
+
anonymous_session: typeof parsed.anonymous_session === "boolean" ? parsed.anonymous_session : null,
|
|
1262
|
+
sign_in_required: parsed.sign_in_required === true,
|
|
1263
|
+
write_seq: seqOf(parsed.write_seq) ?? 0,
|
|
1264
|
+
vault_seq: parsed.vault_seq === void 0 ? 0 : seqOf(parsed.vault_seq),
|
|
1265
|
+
install_id: typeof parsed.install_id === "string" ? parsed.install_id : null
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
/**
|
|
1269
|
+
* Settle a hydrated identity. With the credentials known (`readable`), a
|
|
1270
|
+
* user id with no token to act on is stale (memory storage after a
|
|
1271
|
+
* relaunch, a vault entry that was discarded): nobody is signed in, and the
|
|
1272
|
+
* session kind goes with it. A session that ended this way after it gained
|
|
1273
|
+
* an identity rotates the anonymous id, as `clearSession` would. An expired
|
|
1274
|
+
* session token is dropped; its refresh token stays so the session can be
|
|
1275
|
+
* renewed.
|
|
1276
|
+
*/
|
|
1277
|
+
function settleSeed(seed, readable) {
|
|
1278
|
+
if (readable && seed.session_token === null && seed.refresh_token === null) {
|
|
1279
|
+
if (seed.app_user_id !== null && seed.anonymous_session === false) seed.anonymous_id = uuidv4();
|
|
1280
|
+
seed.app_user_id = null;
|
|
1281
|
+
seed.anonymous_session = null;
|
|
1282
|
+
}
|
|
1283
|
+
if (isExpired(seed.expires_at)) {
|
|
1284
|
+
seed.session_token = null;
|
|
1285
|
+
seed.expires_at = null;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
949
1288
|
|
|
950
1289
|
//#endregion
|
|
951
1290
|
//#region src/token-storage.ts
|
|
@@ -1638,14 +1977,18 @@ var EventBatchQueue = class {
|
|
|
1638
1977
|
if (this.events.length) await this.store.set(this.storageKey, JSON.stringify(this.events));
|
|
1639
1978
|
else await this.store.delete(this.storageKey);
|
|
1640
1979
|
}
|
|
1641
|
-
|
|
1980
|
+
/**
|
|
1981
|
+
* Queue an event. `timestamp` is when it happened, for an event that waited
|
|
1982
|
+
* elsewhere before reaching the queue; it defaults to now.
|
|
1983
|
+
*/
|
|
1984
|
+
enqueue(event, properties, telemetry, timestamp) {
|
|
1642
1985
|
if (this.disposed) return Promise.reject(/* @__PURE__ */ new Error("Amba event queue disposed"));
|
|
1643
1986
|
const id = this.identity();
|
|
1644
1987
|
const item = {
|
|
1645
1988
|
event,
|
|
1646
1989
|
properties,
|
|
1647
1990
|
telemetry,
|
|
1648
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1991
|
+
timestamp: timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1649
1992
|
idempotency_key: newIdempotencyKey(),
|
|
1650
1993
|
userId: id.sessionToken ? id.userId : null,
|
|
1651
1994
|
anonymousId: id.anonymousId
|
|
@@ -2780,7 +3123,9 @@ function delay(ms) {
|
|
|
2780
3123
|
//#endregion
|
|
2781
3124
|
//#region src/index.ts
|
|
2782
3125
|
/** SDK semver — burned in at build time. Bump alongside package.json. */
|
|
2783
|
-
const SDK_VERSION = "4.
|
|
3126
|
+
const SDK_VERSION = "4.3.1";
|
|
3127
|
+
/** The most events held at once; the oldest goes first past it. */
|
|
3128
|
+
const MAX_HELD_EVENTS = 500;
|
|
2784
3129
|
/**
|
|
2785
3130
|
* Default host that serves deployed functions. `Amba.functions.invoke(name)`
|
|
2786
3131
|
* targets `https://<projectId>.<DEFAULT_FUNCTIONS_HOST>/<name>` unless a
|
|
@@ -2797,6 +3142,23 @@ let state = null;
|
|
|
2797
3142
|
* instead of disabling the now-active outbox.
|
|
2798
3143
|
*/
|
|
2799
3144
|
let _offlineEnableToken = 0;
|
|
3145
|
+
/** The `configure()` running now, if any (see `liveState`). */
|
|
3146
|
+
let configuring = null;
|
|
3147
|
+
/** Serializes `configure()` calls, so each detaches the one before it. */
|
|
3148
|
+
let configureChain = Promise.resolve();
|
|
3149
|
+
/**
|
|
3150
|
+
* The state calls act on. While a `configure()` is running, waits for it,
|
|
3151
|
+
* so a sign-in, a link or a sign-out made meanwhile lands on the state it
|
|
3152
|
+
* installs (or on the earlier one, when it fails).
|
|
3153
|
+
*/
|
|
3154
|
+
async function liveState() {
|
|
3155
|
+
while (configuring) await configuring.catch(() => void 0);
|
|
3156
|
+
return getState();
|
|
3157
|
+
}
|
|
3158
|
+
/** Whether two states talk to the same project with the same stored identity. */
|
|
3159
|
+
function sameIdentityTarget(a, b) {
|
|
3160
|
+
return a.projectId === b.projectId && a.clientKey === b.clientKey && a.baseUrl === b.baseUrl && a.persistence.prefix === b.persistence.prefix;
|
|
3161
|
+
}
|
|
2800
3162
|
function getState() {
|
|
2801
3163
|
if (!state) throw new AmbaApiError("NOT_INITIALIZED", "@layers/amba-react-native: SDK not configured. Call Amba.configure({ projectId, clientKey }) at app start.");
|
|
2802
3164
|
return state;
|
|
@@ -2889,10 +3251,16 @@ function snapshotSession(user) {
|
|
|
2889
3251
|
expiresAt: ""
|
|
2890
3252
|
};
|
|
2891
3253
|
}
|
|
2892
|
-
|
|
3254
|
+
/**
|
|
3255
|
+
* Tell `onAuthStateChange` subscribers about an auth change of `state_`.
|
|
3256
|
+
* A state a later `configure()` replaced tells no one.
|
|
3257
|
+
*/
|
|
3258
|
+
function notifyAuthSubscribers(state_, session, reason) {
|
|
3259
|
+
if (state_ !== state || state_.disposed) return;
|
|
2893
3260
|
if (session === null) experimentReporter.reset();
|
|
3261
|
+
const event = { reason };
|
|
2894
3262
|
for (const cb of authSubscribers) try {
|
|
2895
|
-
cb(session);
|
|
3263
|
+
cb(session, event);
|
|
2896
3264
|
} catch (err) {
|
|
2897
3265
|
if (typeof console !== "undefined") console.error("[amba] onAuthStateChange callback threw", err);
|
|
2898
3266
|
}
|
|
@@ -2949,21 +3317,50 @@ function normalizeAuthResult(result) {
|
|
|
2949
3317
|
user: normalizeUserAuthProviders(result.user)
|
|
2950
3318
|
};
|
|
2951
3319
|
}
|
|
2952
|
-
|
|
2953
|
-
notifyAuthSubscribers(snapshotSession(result.user));
|
|
3320
|
+
function notifyFromAuthResult(state_, result, reason) {
|
|
3321
|
+
notifyAuthSubscribers(state_, snapshotSession(result.user), reason);
|
|
3322
|
+
}
|
|
3323
|
+
/** A call needs a session while the user must sign in again. */
|
|
3324
|
+
function signInRequiredError() {
|
|
3325
|
+
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");
|
|
3326
|
+
}
|
|
3327
|
+
function secureStoreUnavailableError() {
|
|
3328
|
+
return new AmbaApiError("SECURE_STORE_UNAVAILABLE", "the stored session can't be read yet (the device may be locked); retry once it is unlocked");
|
|
3329
|
+
}
|
|
3330
|
+
function sessionChangedError(message) {
|
|
3331
|
+
return new AmbaApiError("SESSION_CHANGED", message);
|
|
3332
|
+
}
|
|
3333
|
+
function isSessionChanged(err) {
|
|
3334
|
+
return err instanceof AmbaApiError && err.code === "SESSION_CHANGED";
|
|
3335
|
+
}
|
|
3336
|
+
/**
|
|
3337
|
+
* The stored session token, for a call that needs one. Without a session
|
|
3338
|
+
* the call fails with `SIGN_IN_REQUIRED` while the user must sign in again,
|
|
3339
|
+
* and with `UNAUTHORIZED` otherwise.
|
|
3340
|
+
*/
|
|
3341
|
+
function requireSession(state_) {
|
|
3342
|
+
const token = state_.identity.getSessionToken();
|
|
3343
|
+
if (token) return token;
|
|
3344
|
+
if (state_.identity.isSignInRequired()) throw signInRequiredError();
|
|
3345
|
+
if (state_.identity.credentialsUnknown()) throw secureStoreUnavailableError();
|
|
3346
|
+
throw new AmbaApiError("UNAUTHORIZED", "no active session — call signIn* first");
|
|
2954
3347
|
}
|
|
2955
3348
|
function expiresAtFromResult(result) {
|
|
2956
3349
|
if (result.expires_at && typeof result.expires_at === "string") return result.expires_at;
|
|
2957
3350
|
return new Date(Date.now() + 3600 * 1e3).toISOString();
|
|
2958
3351
|
}
|
|
2959
|
-
|
|
2960
|
-
|
|
3352
|
+
/**
|
|
3353
|
+
* Store an auth result under `fence`. Resolves the generation it committed
|
|
3354
|
+
* at, or null when the fence refused it.
|
|
3355
|
+
*/
|
|
3356
|
+
function persistAuthResult(state_, result, anonymous, fence = {}) {
|
|
3357
|
+
return state_.identity.setSession({
|
|
2961
3358
|
appUserId: result.user.id,
|
|
2962
3359
|
sessionToken: result.session_token,
|
|
2963
3360
|
refreshToken: result.refresh_token,
|
|
2964
3361
|
expiresAt: expiresAtFromResult(result),
|
|
2965
3362
|
anonymous
|
|
2966
|
-
});
|
|
3363
|
+
}, fence);
|
|
2967
3364
|
}
|
|
2968
3365
|
function nonEmptyString(value) {
|
|
2969
3366
|
return typeof value === "string" && value.length > 0 ? value : null;
|
|
@@ -2975,14 +3372,31 @@ function nonEmptyString(value) {
|
|
|
2975
3372
|
* request carried none). A link adopts the account's rotated anonymous id.
|
|
2976
3373
|
* A refresh keeps the session's kind; every other sign-in records an
|
|
2977
3374
|
* identified session.
|
|
3375
|
+
*
|
|
3376
|
+
* A sign-in the app asked for stores its session whatever happened
|
|
3377
|
+
* meanwhile: it is the newest thing the app asked for. The automatic
|
|
3378
|
+
* anonymous sign-in and a link pass a `fence` and fail with
|
|
3379
|
+
* `SESSION_CHANGED` (storing nothing) when the session changed while they
|
|
3380
|
+
* were in flight.
|
|
2978
3381
|
*/
|
|
2979
|
-
async function postAuth(state_, path, body) {
|
|
3382
|
+
async function postAuth(state_, path, body, fence = {}) {
|
|
2980
3383
|
const result = normalizeAuthResult((await state_.http.post(path, body)).data);
|
|
2981
|
-
|
|
3384
|
+
const anonymous = path === P.authAnonymous ? true : path === P.authRefresh ? void 0 : false;
|
|
3385
|
+
let target = state_;
|
|
3386
|
+
let committedAt = await persistAuthResult(state_, result, anonymous, fence);
|
|
3387
|
+
if (committedAt === null && fence.ifGeneration === void 0 && state_.disposed) {
|
|
3388
|
+
const next = await liveState();
|
|
3389
|
+
if (next === state_ ? !next.disposed : sameIdentityTarget(next, state_)) {
|
|
3390
|
+
target = next;
|
|
3391
|
+
committedAt = await persistAuthResult(next, result, anonymous);
|
|
3392
|
+
}
|
|
3393
|
+
}
|
|
3394
|
+
if (committedAt === null) throw sessionChangedError("the session changed while this sign-in was in flight; the newer session stays");
|
|
3395
|
+
state_ = target;
|
|
2982
3396
|
const adoptedAnonymousId = path === P.authAnonymous ? nonEmptyString(result.anonymous_id) ?? nonEmptyString(result.user?.anonymous_id) : path === P.authLink ? nonEmptyString(result.user?.anonymous_id) : null;
|
|
2983
|
-
if (adoptedAnonymousId) await adoptAnonymousId(state_, adoptedAnonymousId);
|
|
3397
|
+
if (adoptedAnonymousId) await adoptAnonymousId(state_, adoptedAnonymousId, committedAt);
|
|
2984
3398
|
state_.eventQueue?.flush();
|
|
2985
|
-
|
|
3399
|
+
notifyFromAuthResult(state_, result, "signed_in");
|
|
2986
3400
|
return result;
|
|
2987
3401
|
}
|
|
2988
3402
|
/**
|
|
@@ -2991,16 +3405,86 @@ async function postAuth(state_, path, body) {
|
|
|
2991
3405
|
* session; they are handed to the signed-in user first, so the id change
|
|
2992
3406
|
* leaves them replayable.
|
|
2993
3407
|
*/
|
|
2994
|
-
async function adoptAnonymousId(state_, anonymousId) {
|
|
3408
|
+
async function adoptAnonymousId(state_, anonymousId, generation) {
|
|
2995
3409
|
const previous = state_.identity.getAnonymousId();
|
|
2996
3410
|
if (anonymousId === previous) return;
|
|
3411
|
+
if (state_.identity.getGeneration() !== generation) return;
|
|
2997
3412
|
const appUserId = state_.identity.getAppUserId();
|
|
2998
3413
|
if (appUserId && state_.offlineEnabled) try {
|
|
2999
3414
|
await getOfflineQueue(state_).claimAnonymousOps(previous, appUserId);
|
|
3000
3415
|
} catch (err) {
|
|
3001
3416
|
if (typeof console !== "undefined") console.warn("[amba] could not reassign queued offline writes", err);
|
|
3002
3417
|
}
|
|
3003
|
-
await state_.identity.setAnonymousId(anonymousId);
|
|
3418
|
+
await state_.identity.setAnonymousId(anonymousId, { ifGeneration: generation });
|
|
3419
|
+
}
|
|
3420
|
+
/**
|
|
3421
|
+
* Hold an event `track()` got while the stored identity couldn't be read.
|
|
3422
|
+
* It keeps its call time and a copy of its properties; past
|
|
3423
|
+
* `MAX_HELD_EVENTS` the oldest is dropped.
|
|
3424
|
+
*/
|
|
3425
|
+
function holdEvent(state_, event, properties, options) {
|
|
3426
|
+
const held = state_.heldEvents ??= [];
|
|
3427
|
+
if (held.length >= MAX_HELD_EVENTS) {
|
|
3428
|
+
held.shift();
|
|
3429
|
+
state_.heldEventsDropped = (state_.heldEventsDropped ?? 0) + 1;
|
|
3430
|
+
}
|
|
3431
|
+
held.push({
|
|
3432
|
+
event,
|
|
3433
|
+
properties: snapshotProperties(properties),
|
|
3434
|
+
options,
|
|
3435
|
+
occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3436
|
+
generation: state_.identity.getGeneration()
|
|
3437
|
+
});
|
|
3438
|
+
}
|
|
3439
|
+
/** Report the held events dropped for `MAX_HELD_EVENTS`, once. */
|
|
3440
|
+
function reportHeldEventsDropped(state_) {
|
|
3441
|
+
const dropped = state_.heldEventsDropped ?? 0;
|
|
3442
|
+
state_.heldEventsDropped = 0;
|
|
3443
|
+
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`);
|
|
3444
|
+
}
|
|
3445
|
+
/** Drop the held events, with one warning that says why. */
|
|
3446
|
+
function dropHeldEvents(state_, why) {
|
|
3447
|
+
const count = (state_.heldEvents ?? []).length + (state_.heldEventsDropped ?? 0);
|
|
3448
|
+
state_.heldEvents = [];
|
|
3449
|
+
state_.heldEventsDropped = 0;
|
|
3450
|
+
if (count > 0 && typeof console !== "undefined") console.warn(`[amba] dropped ${count} event(s) tracked while the secure store was locked: ${why}`);
|
|
3451
|
+
}
|
|
3452
|
+
/**
|
|
3453
|
+
* Send the held events once the stored identity is known, oldest first,
|
|
3454
|
+
* through the path `track()` uses, under the identity that loaded. A session
|
|
3455
|
+
* renewal the load started finishes first. An event that fails is reported
|
|
3456
|
+
* and the rest go on; events tracked meanwhile join the end of the line.
|
|
3457
|
+
* An event goes out only while the session it belongs to is still the
|
|
3458
|
+
* stored one: the session that loaded for events held before, the session
|
|
3459
|
+
* of the moment for events tracked during the release. When the session
|
|
3460
|
+
* changed since (the renewal is refused and the session ends, a sign-in or
|
|
3461
|
+
* sign-out lands), the event is dropped, and one warning counts them.
|
|
3462
|
+
*/
|
|
3463
|
+
async function releaseHeldEvents(state_) {
|
|
3464
|
+
if (state_.releasingHeldEvents || !state_.heldEvents?.length) return;
|
|
3465
|
+
state_.releasingHeldEvents = true;
|
|
3466
|
+
let stale = 0;
|
|
3467
|
+
try {
|
|
3468
|
+
const loadedAt = state_.identity.getGeneration();
|
|
3469
|
+
await settleSessionInFlight(state_);
|
|
3470
|
+
reportHeldEventsDropped(state_);
|
|
3471
|
+
while (state_.heldEvents?.length && !state_.disposed) {
|
|
3472
|
+
const next = state_.heldEvents.shift();
|
|
3473
|
+
const belongsTo = Math.max(next.generation, loadedAt);
|
|
3474
|
+
if (state_.identity.getGeneration() !== belongsTo) {
|
|
3475
|
+
stale += 1;
|
|
3476
|
+
continue;
|
|
3477
|
+
}
|
|
3478
|
+
try {
|
|
3479
|
+
await sendEvent(state_, next.event, next.properties, next.options, next.occurredAt);
|
|
3480
|
+
} catch (err) {
|
|
3481
|
+
if (typeof console !== "undefined") console.warn(`[amba] could not send "${next.event}", tracked while the secure store was locked`, err);
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
3484
|
+
} finally {
|
|
3485
|
+
state_.releasingHeldEvents = false;
|
|
3486
|
+
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`);
|
|
3487
|
+
}
|
|
3004
3488
|
}
|
|
3005
3489
|
/** Paths that must NOT trigger lazy anonymous sign-in. */
|
|
3006
3490
|
function pathSkipsLazySession(path) {
|
|
@@ -3011,21 +3495,41 @@ function pathSkipsLazySession(path) {
|
|
|
3011
3495
|
if (path === P.flags || path.startsWith(`${P.flags}/`)) return true;
|
|
3012
3496
|
return false;
|
|
3013
3497
|
}
|
|
3498
|
+
/**
|
|
3499
|
+
* Make sure a session is stored before an authenticated call. While the user
|
|
3500
|
+
* must sign in again (`sign_in_required`), the call fails with
|
|
3501
|
+
* `SIGN_IN_REQUIRED` and no anonymous user starts.
|
|
3502
|
+
*/
|
|
3014
3503
|
async function ensureAnonymousSession(state_) {
|
|
3015
3504
|
if (state_.identity.getSessionToken()) return;
|
|
3505
|
+
if (state_.identity.isSignInRequired()) throw signInRequiredError();
|
|
3016
3506
|
if (state_.anonSignInInflight) {
|
|
3017
3507
|
await state_.anonSignInInflight.catch(() => {});
|
|
3018
3508
|
if (state_.identity.getSessionToken()) return;
|
|
3509
|
+
if (state_.identity.isSignInRequired()) throw signInRequiredError();
|
|
3019
3510
|
}
|
|
3511
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
3512
|
+
if (state_.identity.getSessionToken() || state_.disposed) return;
|
|
3513
|
+
if (state_.identity.isSignInRequired()) throw signInRequiredError();
|
|
3514
|
+
await anonymousSignInOnce(state_);
|
|
3515
|
+
}
|
|
3516
|
+
if (!state_.identity.getSessionToken() && state_.identity.isSignInRequired()) throw signInRequiredError();
|
|
3517
|
+
}
|
|
3518
|
+
/** One run of the lazy sign-in, shared by every caller that waits for it. */
|
|
3519
|
+
function anonymousSignInOnce(state_) {
|
|
3020
3520
|
if (!state_.anonSignInInflight) state_.anonSignInInflight = (async () => {
|
|
3021
3521
|
try {
|
|
3522
|
+
await state_.identity.vaultStillUnreadable();
|
|
3523
|
+
if (await state_.identity.anonymousIdStillUnknown()) throw secureStoreUnavailableError();
|
|
3524
|
+
if (state_.identity.getSessionToken()) return;
|
|
3022
3525
|
if (await resumeWithRefreshToken(state_)) return;
|
|
3023
|
-
|
|
3526
|
+
if (state_.identity.getSessionToken() || state_.identity.isSignInRequired()) return;
|
|
3527
|
+
await signInAnonymouslyAutomatic(state_);
|
|
3024
3528
|
} finally {
|
|
3025
3529
|
state_.anonSignInInflight = null;
|
|
3026
3530
|
}
|
|
3027
3531
|
})();
|
|
3028
|
-
|
|
3532
|
+
return state_.anonSignInInflight;
|
|
3029
3533
|
}
|
|
3030
3534
|
/**
|
|
3031
3535
|
* Restore a stored session whose session token expired while the app was
|
|
@@ -3033,7 +3537,7 @@ async function ensureAnonymousSession(state_) {
|
|
|
3033
3537
|
* `onAuthStateChange` show the signed-in user without waiting for an
|
|
3034
3538
|
* authenticated call. Shares the in-flight slot with the lazy sign-in, so a
|
|
3035
3539
|
* concurrent authenticated call waits for it. A rejected refresh token ends
|
|
3036
|
-
* the session (
|
|
3540
|
+
* the session (`endLostSession`); this never signs in anonymously.
|
|
3037
3541
|
*/
|
|
3038
3542
|
function restoreStoredSession(state_) {
|
|
3039
3543
|
if (!state_.anonSignInInflight) state_.anonSignInInflight = (async () => {
|
|
@@ -3046,11 +3550,16 @@ function restoreStoredSession(state_) {
|
|
|
3046
3550
|
return state_.anonSignInInflight;
|
|
3047
3551
|
}
|
|
3048
3552
|
/**
|
|
3049
|
-
*
|
|
3050
|
-
*
|
|
3051
|
-
* `
|
|
3553
|
+
* Answers that mean the refresh token's session is over: the server
|
|
3554
|
+
* rejected or revoked it, or it renewed a different user than the stored
|
|
3555
|
+
* one (`OWNER_MISMATCH`). A 401 without the server's envelope (a proxy, a
|
|
3556
|
+
* captive portal) maps to `UNAUTHORIZED` and leaves the session in place.
|
|
3052
3557
|
*/
|
|
3053
|
-
const SESSION_ENDED_CODES = new Set([
|
|
3558
|
+
const SESSION_ENDED_CODES = new Set([
|
|
3559
|
+
"INVALID_TOKEN",
|
|
3560
|
+
"SESSION_REVOKED",
|
|
3561
|
+
"OWNER_MISMATCH"
|
|
3562
|
+
]);
|
|
3054
3563
|
function isSessionEnded(err) {
|
|
3055
3564
|
return err instanceof AmbaApiError && SESSION_ENDED_CODES.has(err.code);
|
|
3056
3565
|
}
|
|
@@ -3077,8 +3586,8 @@ function trackLink(state_, link) {
|
|
|
3077
3586
|
* waited for first;
|
|
3078
3587
|
* - a session another flow stored after the call left is used as is;
|
|
3079
3588
|
* - otherwise the refresh token continues the session. A rejected refresh
|
|
3080
|
-
* token ends it (
|
|
3081
|
-
*
|
|
3589
|
+
* token ends it (`endLostSession`) and the call's own error reaches the
|
|
3590
|
+
* app.
|
|
3082
3591
|
* A session of any other user (a sign-in meanwhile, or the anonymous user
|
|
3083
3592
|
* a lazy sign-in starts after the session ended) answers false, and the
|
|
3084
3593
|
* call fails with the server's code. Offline or a server error keeps the
|
|
@@ -3114,8 +3623,8 @@ async function recoverRefusedSession(state_, usedToken, usedUserId) {
|
|
|
3114
3623
|
* reaction on that promise), so recovery never waits on itself.
|
|
3115
3624
|
*/
|
|
3116
3625
|
async function withBodySession(state_, send) {
|
|
3117
|
-
|
|
3118
|
-
|
|
3626
|
+
await loadUnreadSession(state_);
|
|
3627
|
+
const usedToken = requireSession(state_);
|
|
3119
3628
|
const usedUserId = state_.identity.getAppUserId();
|
|
3120
3629
|
try {
|
|
3121
3630
|
return await send(usedToken);
|
|
@@ -3128,78 +3637,136 @@ async function withBodySession(state_, send) {
|
|
|
3128
3637
|
}
|
|
3129
3638
|
}
|
|
3130
3639
|
/**
|
|
3640
|
+
* Read a store the launch couldn't read (a locked device) again. When that
|
|
3641
|
+
* loads a session, wait for the renewal it starts, so the caller acts on
|
|
3642
|
+
* the loaded session (a sign-out revokes it, a link sends it).
|
|
3643
|
+
*/
|
|
3644
|
+
async function loadUnreadSession(state_) {
|
|
3645
|
+
if (!state_.identity.credentialsUnknown()) return;
|
|
3646
|
+
await state_.identity.credentialsStillUnreadable();
|
|
3647
|
+
if (!state_.identity.credentialsUnknown()) await settleSessionInFlight(state_);
|
|
3648
|
+
}
|
|
3649
|
+
/**
|
|
3131
3650
|
* Before a read of the current session (`me`, `getSession`,
|
|
3132
|
-
* `getCurrentIdentifiers`) while no session token is stored:
|
|
3133
|
-
*
|
|
3134
|
-
* session it
|
|
3651
|
+
* `getCurrentIdentifiers`) while no session token is stored: load a stored
|
|
3652
|
+
* session the launch couldn't read yet, then wait for the launch restore or
|
|
3653
|
+
* sign-in in flight, so a check made at launch sees the session it
|
|
3654
|
+
* restores.
|
|
3135
3655
|
*/
|
|
3136
3656
|
async function settleSessionInFlight(state_) {
|
|
3657
|
+
if (state_.identity.getSessionToken()) return;
|
|
3658
|
+
await state_.identity.credentialsStillUnreadable();
|
|
3137
3659
|
if (state_.identity.getSessionToken() || !state_.anonSignInInflight) return;
|
|
3138
3660
|
await state_.anonSignInInflight.catch(() => {});
|
|
3139
3661
|
}
|
|
3140
3662
|
/**
|
|
3141
3663
|
* Continue the stored session with its refresh token. True when it did.
|
|
3142
|
-
* False when there is no refresh token, or the
|
|
3143
|
-
*
|
|
3144
|
-
*
|
|
3145
|
-
*
|
|
3664
|
+
* False when there is no refresh token, or the session is over (the server
|
|
3665
|
+
* rejected the refresh token, or it renewed another user): the session then
|
|
3666
|
+
* ends through `endLostSession`. Any other failure (offline, a 5xx) is
|
|
3667
|
+
* thrown, so a flaky network never ends a session or starts a new user.
|
|
3146
3668
|
*/
|
|
3147
3669
|
async function resumeWithRefreshToken(state_) {
|
|
3148
3670
|
const used = state_.identity.getRefreshToken();
|
|
3149
3671
|
if (!used) return false;
|
|
3672
|
+
const startedAt = state_.identity.getGeneration();
|
|
3150
3673
|
try {
|
|
3151
3674
|
await refreshSession(state_);
|
|
3152
3675
|
return true;
|
|
3153
3676
|
} catch (err) {
|
|
3154
3677
|
if (state_.identity.getRefreshToken() !== used) return state_.identity.getSessionToken() !== null;
|
|
3155
3678
|
if (!isSessionEnded(err)) throw err;
|
|
3156
|
-
await state_
|
|
3157
|
-
notifyAuthSubscribers(null);
|
|
3679
|
+
await endLostSession(state_, startedAt);
|
|
3158
3680
|
return false;
|
|
3159
3681
|
}
|
|
3160
3682
|
}
|
|
3161
3683
|
/**
|
|
3162
|
-
*
|
|
3163
|
-
*
|
|
3164
|
-
*
|
|
3165
|
-
*
|
|
3684
|
+
* End a session that is over without a sign-out, once, unless the session
|
|
3685
|
+
* changed since `startedAt`.
|
|
3686
|
+
*
|
|
3687
|
+
* - A session that gained an identity (a sign-in other than anonymous, or a
|
|
3688
|
+
* link) leaves the user to sign in again: its tokens are cleared, the
|
|
3689
|
+
* anonymous id rotates (it belongs to the identified account), and
|
|
3690
|
+
* subscribers hear `null` with `sign_in_required`. No new anonymous user
|
|
3691
|
+
* starts; calls that need a session fail with `SIGN_IN_REQUIRED`.
|
|
3692
|
+
* - A purely anonymous session keeps its anonymous id, so the next call that
|
|
3693
|
+
* needs a session resumes the same anonymous user through `/anonymous`
|
|
3694
|
+
* (or the server starts a new one). Subscribers hear `null` with
|
|
3695
|
+
* `session_ended`.
|
|
3696
|
+
* - A session of unknown kind (stored by an SDK before 4.0.14) is treated
|
|
3697
|
+
* like an anonymous one: `/anonymous` resumes a still-anonymous user, and
|
|
3698
|
+
* answers `IDENTITY_SIGN_IN_REQUIRED` for one who linked an identity,
|
|
3699
|
+
* which then leaves the user to sign in again.
|
|
3700
|
+
*/
|
|
3701
|
+
async function endLostSession(state_, startedAt) {
|
|
3702
|
+
if (state_.identity.snapshot().anonymousSession === false) {
|
|
3703
|
+
if (await state_.identity.clearSession(false, {
|
|
3704
|
+
signInRequired: true,
|
|
3705
|
+
ifGeneration: startedAt
|
|
3706
|
+
})) notifyAuthSubscribers(state_, null, "sign_in_required");
|
|
3707
|
+
return;
|
|
3708
|
+
}
|
|
3709
|
+
if (await state_.identity.clearSession(false, {
|
|
3710
|
+
keepIdWhenKindUnknown: true,
|
|
3711
|
+
ifGeneration: startedAt
|
|
3712
|
+
})) notifyAuthSubscribers(state_, null, "session_ended");
|
|
3713
|
+
}
|
|
3714
|
+
/**
|
|
3715
|
+
* The anonymous sign-in the SDK starts on its own before an authenticated
|
|
3716
|
+
* call: `/v1/client/auth/anonymous` with the device's anonymous id. It
|
|
3717
|
+
* stores its session only when nothing replaced the stored identity while
|
|
3718
|
+
* it was in flight; a sign-in that landed first wins and this answer is
|
|
3719
|
+
* dropped.
|
|
3720
|
+
*
|
|
3721
|
+
* When the server answers `IDENTITY_SIGN_IN_REQUIRED` (the id belongs to a
|
|
3722
|
+
* user who signs in with a linked identity), the stored session continues
|
|
3723
|
+
* when this device holds its refresh token. Otherwise the user must sign in
|
|
3724
|
+
* again (`sign_in_required`): no new anonymous user starts on its own.
|
|
3166
3725
|
*/
|
|
3167
|
-
async function
|
|
3726
|
+
async function signInAnonymouslyAutomatic(state_) {
|
|
3727
|
+
if (state_.disposed) return;
|
|
3728
|
+
const startedAt = state_.identity.getGeneration();
|
|
3168
3729
|
try {
|
|
3169
|
-
|
|
3730
|
+
await postAuth(state_, P.authAnonymous, { anonymous_id: state_.identity.getAnonymousId() }, { ifGeneration: startedAt });
|
|
3170
3731
|
} catch (err) {
|
|
3732
|
+
if (isSessionChanged(err)) return;
|
|
3171
3733
|
if (!(err instanceof AmbaApiError) || err.code !== IDENTITY_SIGN_IN_REQUIRED) throw err;
|
|
3172
|
-
|
|
3734
|
+
if (await continueHeldSession(state_)) return;
|
|
3735
|
+
if (await state_.identity.clearSession(true, {
|
|
3736
|
+
signInRequired: true,
|
|
3737
|
+
ifGeneration: startedAt
|
|
3738
|
+
})) notifyAuthSubscribers(state_, null, "sign_in_required");
|
|
3173
3739
|
}
|
|
3174
3740
|
}
|
|
3175
3741
|
const IDENTITY_SIGN_IN_REQUIRED = "IDENTITY_SIGN_IN_REQUIRED";
|
|
3176
3742
|
/**
|
|
3177
3743
|
* After `/anonymous` answered `IDENTITY_SIGN_IN_REQUIRED`: continue the
|
|
3178
|
-
* stored session when this device holds its refresh token
|
|
3179
|
-
*
|
|
3180
|
-
*
|
|
3181
|
-
*
|
|
3182
|
-
* `SECURE_STORE_UNAVAILABLE` and replaces nothing.
|
|
3744
|
+
* stored session when this device holds its refresh token. Null when it
|
|
3745
|
+
* holds none, or the server rejected it. While the stored session can't be
|
|
3746
|
+
* read (an iOS device not yet unlocked since a restart), it is unknown, so
|
|
3747
|
+
* the call fails with `SECURE_STORE_UNAVAILABLE` and replaces nothing.
|
|
3183
3748
|
*/
|
|
3184
|
-
async function
|
|
3185
|
-
if (await state_.identity.
|
|
3749
|
+
async function continueHeldSession(state_) {
|
|
3750
|
+
if (await state_.identity.credentialsStillUnreadable()) throw secureStoreUnavailableError();
|
|
3186
3751
|
const used = state_.identity.getRefreshToken();
|
|
3187
|
-
if (used)
|
|
3752
|
+
if (!used) return null;
|
|
3753
|
+
try {
|
|
3188
3754
|
return await refreshSession(state_);
|
|
3189
3755
|
} catch (refreshErr) {
|
|
3190
3756
|
if (!isSessionEnded(refreshErr)) throw refreshErr;
|
|
3191
3757
|
const current = state_.identity.getRefreshToken();
|
|
3192
3758
|
if (current !== null && current !== used) throw refreshErr;
|
|
3759
|
+
return null;
|
|
3193
3760
|
}
|
|
3194
|
-
await state_.identity.clearSession(true);
|
|
3195
|
-
return postAuth(state_, P.authAnonymous, { anonymous_id: state_.identity.getAnonymousId() });
|
|
3196
3761
|
}
|
|
3197
3762
|
/**
|
|
3198
3763
|
* Rotate the session pair with the stored refresh token. The session keeps
|
|
3199
3764
|
* its user and its kind (anonymous or identified), and auth-state
|
|
3200
3765
|
* subscribers hear the refreshed session. The server includes the user; an
|
|
3201
3766
|
* older server that leaves it out keeps the stored user id, and the result's
|
|
3202
|
-
* user then carries only that id.
|
|
3767
|
+
* user then carries only that id. A pair that renews a different user than
|
|
3768
|
+
* the stored one is never stored: the session ends (`endLostSession`) and
|
|
3769
|
+
* the call fails with `OWNER_MISMATCH`.
|
|
3203
3770
|
*/
|
|
3204
3771
|
function refreshSession(state_) {
|
|
3205
3772
|
if (state_.refreshInflight) return state_.refreshInflight;
|
|
@@ -3212,32 +3779,66 @@ function refreshSession(state_) {
|
|
|
3212
3779
|
return inflight;
|
|
3213
3780
|
}
|
|
3214
3781
|
async function refreshSessionOnce(state_) {
|
|
3782
|
+
if (state_.disposed) throw sessionChangedError("the SDK was configured again");
|
|
3215
3783
|
const refreshToken = state_.identity.getRefreshToken();
|
|
3784
|
+
if (!refreshToken && state_.identity.isSignInRequired()) throw signInRequiredError();
|
|
3216
3785
|
if (!refreshToken) throw new AmbaApiError("UNAUTHORIZED", "no refresh token available — call signIn* first");
|
|
3217
|
-
const
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
user
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3786
|
+
const startedAt = state_.identity.getGeneration();
|
|
3787
|
+
const exchange$1 = refreshExchangeFor(state_, refreshToken);
|
|
3788
|
+
try {
|
|
3789
|
+
const env = await exchange$1.response;
|
|
3790
|
+
if (state_.identity.getRefreshToken() !== refreshToken || state_.identity.getGeneration() !== startedAt) throw sessionChangedError("the session changed while it was being refreshed");
|
|
3791
|
+
const storedUserId = state_.identity.getAppUserId();
|
|
3792
|
+
const renewedUserId = nonEmptyString(env.data.user?.id);
|
|
3793
|
+
if (renewedUserId !== null && storedUserId !== null && renewedUserId !== storedUserId) {
|
|
3794
|
+
await endLostSession(state_, startedAt);
|
|
3795
|
+
throw new AmbaApiError("OWNER_MISMATCH", "the refreshed session belongs to a different user than the stored one; sign in again");
|
|
3796
|
+
}
|
|
3797
|
+
const appUserId = renewedUserId ?? storedUserId;
|
|
3798
|
+
if (!appUserId) throw new AmbaApiError("UNAUTHORIZED", "the refreshed session names no user — call signIn* first");
|
|
3799
|
+
const user = env.data.user ? normalizeUserAuthProviders(env.data.user) : {
|
|
3800
|
+
id: appUserId,
|
|
3801
|
+
auth_providers: [],
|
|
3802
|
+
properties: {}
|
|
3803
|
+
};
|
|
3804
|
+
const result = {
|
|
3805
|
+
session_token: env.data.session_token,
|
|
3806
|
+
refresh_token: env.data.refresh_token,
|
|
3807
|
+
user,
|
|
3808
|
+
expires_at: env.data.expires_at ?? null
|
|
3809
|
+
};
|
|
3810
|
+
const committedAt = await persistAuthResult(state_, result, void 0, {
|
|
3811
|
+
ifGeneration: startedAt,
|
|
3812
|
+
advance: false
|
|
3813
|
+
});
|
|
3814
|
+
if (committedAt === null) throw sessionChangedError("the session changed while it was being refreshed");
|
|
3815
|
+
const anonymousId = nonEmptyString(user.anonymous_id);
|
|
3816
|
+
if (anonymousId && anonymousId !== state_.identity.getAnonymousId() && state_.identity.getRefreshToken() === result.refresh_token) {
|
|
3817
|
+
if (state_.identity.snapshot().anonymousSession === true) await state_.identity.markIdentified({ ifGeneration: committedAt });
|
|
3818
|
+
await adoptAnonymousId(state_, anonymousId, committedAt);
|
|
3819
|
+
}
|
|
3820
|
+
state_.eventQueue?.flush();
|
|
3821
|
+
notifyFromAuthResult(state_, result, "token_refreshed");
|
|
3822
|
+
return result;
|
|
3823
|
+
} finally {
|
|
3824
|
+
if (state_.refreshExchange === exchange$1) state_.refreshExchange = null;
|
|
3237
3825
|
}
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3826
|
+
}
|
|
3827
|
+
/**
|
|
3828
|
+
* The `/refresh` request for `refreshToken`: the one the previous state had
|
|
3829
|
+
* in flight for the same token when `configure()` ran again, or a new one.
|
|
3830
|
+
* It stays on the state until its answer is stored, so a `configure()`
|
|
3831
|
+
* meanwhile hands it on instead of presenting the token again.
|
|
3832
|
+
*/
|
|
3833
|
+
function refreshExchangeFor(state_, refreshToken) {
|
|
3834
|
+
const inherited = state_.inheritedRefresh;
|
|
3835
|
+
if (inherited) state_.inheritedRefresh = null;
|
|
3836
|
+
const exchange$1 = inherited && inherited.token === refreshToken ? inherited : {
|
|
3837
|
+
token: refreshToken,
|
|
3838
|
+
response: state_.http.post(P.authRefresh, { refresh_token: refreshToken })
|
|
3839
|
+
};
|
|
3840
|
+
state_.refreshExchange = exchange$1;
|
|
3841
|
+
return exchange$1;
|
|
3241
3842
|
}
|
|
3242
3843
|
/**
|
|
3243
3844
|
* POST an OTP or email + password link. The request asks the server to
|
|
@@ -3254,6 +3855,11 @@ function postLink(state_, body) {
|
|
|
3254
3855
|
})));
|
|
3255
3856
|
}
|
|
3256
3857
|
async function postLinkOnce(state_, body) {
|
|
3858
|
+
const startedAt = state_.identity.getGeneration();
|
|
3859
|
+
const fence = {
|
|
3860
|
+
ifGeneration: startedAt,
|
|
3861
|
+
advance: false
|
|
3862
|
+
};
|
|
3257
3863
|
const env = await state_.http.post(P.authLink, {
|
|
3258
3864
|
...body,
|
|
3259
3865
|
rotate_session: true
|
|
@@ -3261,7 +3867,7 @@ async function postLinkOnce(state_, body) {
|
|
|
3261
3867
|
const user = normalizeUserAuthProviders(env.data.user);
|
|
3262
3868
|
const sessionToken = nonEmptyString(env.data.session_token);
|
|
3263
3869
|
const refreshToken = nonEmptyString(env.data.refresh_token);
|
|
3264
|
-
if (sessionToken && refreshToken
|
|
3870
|
+
if (!(sessionToken && refreshToken ? await state_.identity.setSession({
|
|
3265
3871
|
appUserId: user.id,
|
|
3266
3872
|
sessionToken,
|
|
3267
3873
|
refreshToken,
|
|
@@ -3272,10 +3878,9 @@ async function postLinkOnce(state_, body) {
|
|
|
3272
3878
|
expires_at: env.data.expires_at ?? null
|
|
3273
3879
|
}),
|
|
3274
3880
|
anonymous: false
|
|
3275
|
-
});
|
|
3276
|
-
else await state_.identity.markIdentified();
|
|
3881
|
+
}, fence) !== null : await state_.identity.markIdentified(fence))) throw sessionChangedError("the session changed while the link was in flight; the newer session stays");
|
|
3277
3882
|
const anonymousId = nonEmptyString(user.anonymous_id);
|
|
3278
|
-
if (anonymousId) await adoptAnonymousId(state_, anonymousId);
|
|
3883
|
+
if (anonymousId) await adoptAnonymousId(state_, anonymousId, startedAt);
|
|
3279
3884
|
return user;
|
|
3280
3885
|
}
|
|
3281
3886
|
/**
|
|
@@ -3286,7 +3891,7 @@ async function postUserNoRotate(state_, path, body) {
|
|
|
3286
3891
|
return normalizeUserAuthProviders((await state_.http.post(path, body)).data.user);
|
|
3287
3892
|
}
|
|
3288
3893
|
async function unlinkProvider(provider) {
|
|
3289
|
-
const s =
|
|
3894
|
+
const s = await liveState();
|
|
3290
3895
|
return withBodySession(s, (sessionToken) => postUserNoRotate(s, P.authUnlink, {
|
|
3291
3896
|
provider,
|
|
3292
3897
|
session_token: sessionToken
|
|
@@ -3543,107 +4148,238 @@ function detectDeviceRegion() {
|
|
|
3543
4148
|
}
|
|
3544
4149
|
}
|
|
3545
4150
|
/**
|
|
3546
|
-
*
|
|
4151
|
+
* The send path of `events.track()`: the event batch queue when batching is
|
|
4152
|
+
* on, otherwise the live request (with the offline outbox behind it when
|
|
4153
|
+
* enabled). `occurredAtOverride` is when the event happened, for one that
|
|
4154
|
+
* waited (see `releaseHeldEvents`).
|
|
3547
4155
|
*/
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
4156
|
+
async function sendEvent(s, event, properties, options, occurredAtOverride) {
|
|
4157
|
+
if (s.eventQueue && !options?.immediate) return s.eventQueue.enqueue(event, properties, options?.telemetry === true, occurredAtOverride);
|
|
4158
|
+
const occurredAt = occurredAtOverride ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
4159
|
+
const props = s.offlineEnabled ? snapshotProperties(properties) : properties;
|
|
4160
|
+
const body = {
|
|
4161
|
+
event,
|
|
4162
|
+
properties: props ?? void 0,
|
|
4163
|
+
anonymous_id: s.identity.getAnonymousId(),
|
|
4164
|
+
timestamp: occurredAt
|
|
4165
|
+
};
|
|
4166
|
+
if (options?.telemetry) body["telemetry"] = true;
|
|
4167
|
+
const idempotencyKey = s.offlineEnabled ? newIdempotencyKey() : void 0;
|
|
4168
|
+
if (idempotencyKey && keyedLiveTrack(props, options?.telemetry)) body["idempotency_key"] = idempotencyKey;
|
|
4169
|
+
const outboxTicket = s.offlineEnabled ? getOfflineQueue(s).ticket() : void 0;
|
|
4170
|
+
try {
|
|
4171
|
+
await s.http.post(P.events, body);
|
|
4172
|
+
} catch (err) {
|
|
4173
|
+
if (s.offlineEnabled && isOfflineError(err)) {
|
|
4174
|
+
await getOfflineQueue(s).enqueue({
|
|
4175
|
+
kind: "track",
|
|
4176
|
+
event,
|
|
4177
|
+
properties: props,
|
|
4178
|
+
telemetry: options?.telemetry,
|
|
4179
|
+
occurredAt,
|
|
4180
|
+
idempotencyKey
|
|
4181
|
+
}, outboxTicket);
|
|
4182
|
+
return;
|
|
3559
4183
|
}
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
4184
|
+
throw err;
|
|
4185
|
+
}
|
|
4186
|
+
}
|
|
4187
|
+
/**
|
|
4188
|
+
* The local half of a sign-out on `state_`: clear the session (unless a
|
|
4189
|
+
* sign-in stored a newer one since `startedAt`), drop the signed-out
|
|
4190
|
+
* user's cached values and queued writes, and tell subscribers. False when
|
|
4191
|
+
* nothing was cleared.
|
|
4192
|
+
*/
|
|
4193
|
+
async function endSignedOutSession(state_, rotateAnonymousId, startedAt) {
|
|
4194
|
+
const signedOutUser = state_.identity.getAppUserId();
|
|
4195
|
+
if (!await state_.identity.clearSession(rotateAnonymousId, { unlessSignedInSince: startedAt })) return false;
|
|
4196
|
+
dropHeldEvents(state_, "the device signed out before its identity could be read");
|
|
4197
|
+
state_.remoteValues.clearForSignOut(signedOutUser);
|
|
4198
|
+
await state_.remoteValues.flushed();
|
|
4199
|
+
try {
|
|
4200
|
+
await getOfflineQueue(state_).purge();
|
|
4201
|
+
} catch (err) {
|
|
4202
|
+
if (typeof console !== "undefined") console.warn("[amba] signOut could not clear the offline outbox", err);
|
|
4203
|
+
}
|
|
4204
|
+
state_.eventQueue?.flush();
|
|
4205
|
+
notifyAuthSubscribers(state_, null, "signed_out");
|
|
4206
|
+
return true;
|
|
4207
|
+
}
|
|
4208
|
+
/**
|
|
4209
|
+
* One `configure()`: build the state for `config` and install it in place
|
|
4210
|
+
* of the previous one (see `Amba.configure`).
|
|
4211
|
+
*/
|
|
4212
|
+
async function configureOnce(config) {
|
|
4213
|
+
if (!config.projectId || config.projectId.trim() === "") throw new Error("Amba.configure: projectId is required");
|
|
4214
|
+
if (!config.clientKey || config.clientKey.trim() === "") throw new Error("Amba.configure: clientKey is required");
|
|
4215
|
+
const batchSize = config.events?.batchSize ?? 100;
|
|
4216
|
+
const flushIntervalMs = config.events?.flushIntervalMs ?? 6e4;
|
|
4217
|
+
const maxQueue = config.events?.maxQueue ?? 1e4;
|
|
4218
|
+
if (config.events?.batching) {
|
|
4219
|
+
if (!Number.isInteger(batchSize) || batchSize < 1 || batchSize > 500) throw new Error("Amba.configure: events.batchSize must be 1–500");
|
|
4220
|
+
if (!Number.isInteger(flushIntervalMs) || flushIntervalMs < 0) throw new Error("Amba.configure: events.flushIntervalMs must be a non-negative integer");
|
|
4221
|
+
if (!Number.isInteger(maxQueue) || maxQueue < 1) throw new Error("Amba.configure: events.maxQueue must be positive");
|
|
4222
|
+
}
|
|
4223
|
+
const deadlines = resolveRequestDeadlines(config);
|
|
4224
|
+
const tokenStorage = config.tokenStorage ?? "secure";
|
|
4225
|
+
if (!TOKEN_STORAGE_MODES.includes(tokenStorage)) throw new Error("Amba.configure: tokenStorage must be \"secure\", \"memory\" or \"asyncStorage\"");
|
|
4226
|
+
const secureStorage = config.secureStorage;
|
|
4227
|
+
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");
|
|
4228
|
+
const configDefaults = readConfigDefaults(config.configDefaults);
|
|
4229
|
+
_pendingConfigDefaults = configDefaults;
|
|
4230
|
+
const storagePrefix = config.storagePrefix ?? "amba";
|
|
4231
|
+
const persistence = new AsyncStoragePersistence(storagePrefix);
|
|
4232
|
+
if (config.events?.batching) await persistence.assertDurable();
|
|
4233
|
+
await persistence.hydrate();
|
|
4234
|
+
const vault = await resolveTokenVault(tokenStorage, storagePrefix, secureStorage);
|
|
4235
|
+
const prior = state;
|
|
4236
|
+
let inheritedRefresh = null;
|
|
4237
|
+
const detached = prior !== null && !prior.disposed;
|
|
4238
|
+
if (prior && detached) {
|
|
4239
|
+
prior.disposed = true;
|
|
4240
|
+
inheritedRefresh = prior.refreshExchange ?? prior.inheritedRefresh ?? null;
|
|
4241
|
+
await prior.identity.dispose();
|
|
4242
|
+
}
|
|
4243
|
+
const baseUrl = (config.apiUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
4244
|
+
let identity;
|
|
4245
|
+
let remoteValues;
|
|
4246
|
+
try {
|
|
4247
|
+
identity = await IdentityStore.create(persistence, vault);
|
|
4248
|
+
const store = identity;
|
|
4249
|
+
remoteValues = new RemoteValuesCache(persistence, recordKey(baseUrl, config.clientKey), configDefaults ?? {}, () => store.getAppUserId());
|
|
3575
4250
|
await remoteValues.load();
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
4251
|
+
} catch (err) {
|
|
4252
|
+
if (prior && detached && state === prior) {
|
|
4253
|
+
prior.disposed = false;
|
|
4254
|
+
prior.identity.reattach();
|
|
4255
|
+
prior.inheritedRefresh = inheritedRefresh;
|
|
4256
|
+
if (inheritedRefresh && prior.identity.getRefreshToken() === inheritedRefresh.token) restoreStoredSession(prior).catch(() => {});
|
|
4257
|
+
}
|
|
4258
|
+
throw err;
|
|
4259
|
+
}
|
|
4260
|
+
const sdkVersionHeader = `amba-sdk/${SDK_VERSION} ${`amba-react-native/${SDK_VERSION}`}`;
|
|
4261
|
+
const http = new AmbaHttpClient({
|
|
4262
|
+
apiKey: config.clientKey,
|
|
4263
|
+
baseUrl,
|
|
4264
|
+
sdkPlatform: "react-native",
|
|
4265
|
+
sdkVersionHeader,
|
|
4266
|
+
appVersion: normalizeAppVersion(config.appVersion),
|
|
4267
|
+
deviceRegion: detectDeviceRegion(),
|
|
4268
|
+
deviceOs: detectDeviceOs(),
|
|
4269
|
+
identity,
|
|
4270
|
+
requestTimeoutMs: deadlines.requestMs,
|
|
4271
|
+
longRequestTimeoutMs: deadlines.longMs,
|
|
4272
|
+
ensureSession: async (path) => {
|
|
4273
|
+
if (pathSkipsLazySession(path)) return;
|
|
4274
|
+
if (state?.identity.isSignInRequired()) throw signInRequiredError();
|
|
4275
|
+
if (config.autoAnonymousSession === false) return;
|
|
4276
|
+
await ensureAnonymousSession(getState());
|
|
4277
|
+
},
|
|
4278
|
+
onSessionRefused: (usedToken, usedUserId) => recoverRefusedSession(getState(), usedToken, usedUserId)
|
|
4279
|
+
});
|
|
4280
|
+
_offlineEnableToken += 1;
|
|
4281
|
+
if (prior?.offlineQueue) await prior.offlineQueue.dispose();
|
|
4282
|
+
prior?.detachEventAppState?.();
|
|
4283
|
+
prior?.detachIdentityAppState?.();
|
|
4284
|
+
if (prior?.eventQueue) await prior.eventQueue.dispose();
|
|
4285
|
+
experimentReporter.reset();
|
|
4286
|
+
const nextState = {
|
|
4287
|
+
http,
|
|
4288
|
+
identity,
|
|
4289
|
+
debug: config.debug ?? false,
|
|
4290
|
+
projectId: config.projectId,
|
|
4291
|
+
clientKey: config.clientKey,
|
|
4292
|
+
baseUrl,
|
|
4293
|
+
functionsBaseUrl: config.functionsBaseUrl ?? null,
|
|
4294
|
+
resolvedFunctionsProjectId: null,
|
|
4295
|
+
functionsProjectIdInflight: null,
|
|
4296
|
+
persistence,
|
|
4297
|
+
remoteValues,
|
|
4298
|
+
refreshedFor: {
|
|
4299
|
+
config: null,
|
|
4300
|
+
flags: null
|
|
4301
|
+
},
|
|
4302
|
+
offlineEnabled: false,
|
|
4303
|
+
offlineQueue: null,
|
|
4304
|
+
eventQueue: null,
|
|
4305
|
+
inheritedRefresh
|
|
4306
|
+
};
|
|
4307
|
+
if (prior?.heldEvents?.length || prior?.heldEventsDropped) if (sameIdentityTarget(prior, nextState)) {
|
|
4308
|
+
const launchedAt = identity.getGeneration();
|
|
4309
|
+
nextState.heldEvents = (prior.heldEvents ?? []).map((held) => ({
|
|
4310
|
+
...held,
|
|
4311
|
+
generation: launchedAt
|
|
4312
|
+
}));
|
|
4313
|
+
nextState.heldEventsDropped = prior.heldEventsDropped;
|
|
4314
|
+
prior.heldEvents = [];
|
|
4315
|
+
prior.heldEventsDropped = 0;
|
|
4316
|
+
} else dropHeldEvents(prior, "the SDK was configured for another project");
|
|
4317
|
+
state = nextState;
|
|
4318
|
+
identity.onAnonymousIdKnown(() => {
|
|
4319
|
+
releaseHeldEvents(nextState);
|
|
4320
|
+
});
|
|
4321
|
+
identity.onStoredSessionLoaded(() => {
|
|
4322
|
+
const userId = identity.getAppUserId();
|
|
4323
|
+
if (identity.getSessionToken() && userId) notifyAuthSubscribers(nextState, snapshotSession({
|
|
4324
|
+
id: userId,
|
|
4325
|
+
auth_providers: [],
|
|
4326
|
+
properties: {}
|
|
4327
|
+
}), "session_restored");
|
|
4328
|
+
else if (config.autoAnonymousSession !== false && identity.getRefreshToken()) restoreStoredSession(nextState).catch(() => {});
|
|
4329
|
+
});
|
|
4330
|
+
try {
|
|
4331
|
+
const { AppState } = require("react-native");
|
|
4332
|
+
const sub = AppState.addEventListener("change", (status) => {
|
|
4333
|
+
if (status === "active" && identity.credentialsUnknown()) identity.credentialsStillUnreadable();
|
|
3593
4334
|
});
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
4335
|
+
nextState.detachIdentityAppState = () => sub.remove();
|
|
4336
|
+
} catch {}
|
|
4337
|
+
if (config.events?.batching) {
|
|
4338
|
+
const queue = new EventBatchQueue(offlineStore(nextState), `event_outbox:${nextState.projectId}`, {
|
|
4339
|
+
batchSize,
|
|
4340
|
+
flushIntervalMs,
|
|
4341
|
+
maxQueue,
|
|
4342
|
+
onDrop: config.events.onDrop
|
|
4343
|
+
}, () => ({
|
|
4344
|
+
userId: nextState.identity.getAppUserId(),
|
|
4345
|
+
anonymousId: nextState.identity.getAnonymousId(),
|
|
4346
|
+
sessionToken: nextState.identity.getSessionToken()
|
|
4347
|
+
}), (body, token) => nextState.http.postWithSession("/v1/client/events/batch", body, token), (body, token) => nextState.http.postWithSession(P.events, body, token));
|
|
4348
|
+
nextState.eventQueue = queue;
|
|
4349
|
+
await queue.start();
|
|
4350
|
+
try {
|
|
4351
|
+
const { AppState } = require("react-native");
|
|
4352
|
+
const sub = AppState.addEventListener("change", (status) => {
|
|
4353
|
+
if (status === "background") queue.flush();
|
|
4354
|
+
});
|
|
4355
|
+
nextState.detachEventAppState = () => sub.remove();
|
|
4356
|
+
} catch {}
|
|
4357
|
+
}
|
|
4358
|
+
if (!nextState.functionsBaseUrl && !PROJECT_ID_PATTERN.test(nextState.projectId)) resolveFunctionsProjectId(nextState).catch(() => {});
|
|
4359
|
+
if (nextState.heldEvents?.length && !identity.hasStandInAnonymousId()) releaseHeldEvents(nextState);
|
|
4360
|
+
if (inheritedRefresh !== null && identity.getRefreshToken() === inheritedRefresh.token || config.autoAnonymousSession !== false && !identity.getSessionToken() && identity.getRefreshToken()) restoreStoredSession(nextState).catch(() => {});
|
|
4361
|
+
}
|
|
4362
|
+
/**
|
|
4363
|
+
* Top-level SDK namespace for React Native.
|
|
4364
|
+
*/
|
|
4365
|
+
const Amba = {
|
|
4366
|
+
configure: (config) => {
|
|
4367
|
+
const run = configuring ? configureChain.then(() => configureOnce(config)) : configureOnce(config);
|
|
4368
|
+
configureChain = run.catch(() => void 0);
|
|
4369
|
+
configuring = run;
|
|
4370
|
+
const settle = () => {
|
|
4371
|
+
if (configuring === run) configuring = null;
|
|
3619
4372
|
};
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
const queue = new EventBatchQueue(offlineStore(nextState), `event_outbox:${nextState.projectId}`, {
|
|
3623
|
-
batchSize,
|
|
3624
|
-
flushIntervalMs,
|
|
3625
|
-
maxQueue,
|
|
3626
|
-
onDrop: config.events.onDrop
|
|
3627
|
-
}, () => ({
|
|
3628
|
-
userId: nextState.identity.getAppUserId(),
|
|
3629
|
-
anonymousId: nextState.identity.getAnonymousId(),
|
|
3630
|
-
sessionToken: nextState.identity.getSessionToken()
|
|
3631
|
-
}), (body, token) => nextState.http.postWithSession("/v1/client/events/batch", body, token), (body, token) => nextState.http.postWithSession(P.events, body, token));
|
|
3632
|
-
nextState.eventQueue = queue;
|
|
3633
|
-
await queue.start();
|
|
3634
|
-
try {
|
|
3635
|
-
const { AppState } = require("react-native");
|
|
3636
|
-
const sub = AppState.addEventListener("change", (status) => {
|
|
3637
|
-
if (status === "background") queue.flush();
|
|
3638
|
-
});
|
|
3639
|
-
nextState.detachEventAppState = () => sub.remove();
|
|
3640
|
-
} catch {}
|
|
3641
|
-
}
|
|
3642
|
-
if (!nextState.functionsBaseUrl && !PROJECT_ID_PATTERN.test(nextState.projectId)) resolveFunctionsProjectId(nextState).catch(() => {});
|
|
3643
|
-
if (config.autoAnonymousSession !== false && !identity.getSessionToken() && identity.getRefreshToken()) restoreStoredSession(nextState).catch(() => {});
|
|
4373
|
+
run.then(settle, settle);
|
|
4374
|
+
return run;
|
|
3644
4375
|
},
|
|
3645
4376
|
_reset: () => {
|
|
4377
|
+
if (state) {
|
|
4378
|
+
state.disposed = true;
|
|
4379
|
+
state.identity.dispose();
|
|
4380
|
+
}
|
|
3646
4381
|
state?.detachEventAppState?.();
|
|
4382
|
+
state?.detachIdentityAppState?.();
|
|
3647
4383
|
state?.eventQueue?.dispose();
|
|
3648
4384
|
state = null;
|
|
3649
4385
|
authSubscribers.clear();
|
|
@@ -3677,35 +4413,18 @@ const Amba = {
|
|
|
3677
4413
|
},
|
|
3678
4414
|
events: {
|
|
3679
4415
|
track: async (event, properties, options) => {
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
properties: props ?? void 0,
|
|
3687
|
-
anonymous_id: s.identity.getAnonymousId(),
|
|
3688
|
-
timestamp: occurredAt
|
|
3689
|
-
};
|
|
3690
|
-
if (options?.telemetry) body["telemetry"] = true;
|
|
3691
|
-
const idempotencyKey = s.offlineEnabled ? newIdempotencyKey() : void 0;
|
|
3692
|
-
if (idempotencyKey && keyedLiveTrack(props, options?.telemetry)) body["idempotency_key"] = idempotencyKey;
|
|
3693
|
-
const outboxTicket = s.offlineEnabled ? getOfflineQueue(s).ticket() : void 0;
|
|
3694
|
-
try {
|
|
3695
|
-
await s.http.post(P.events, body);
|
|
3696
|
-
} catch (err) {
|
|
3697
|
-
if (s.offlineEnabled && isOfflineError(err)) {
|
|
3698
|
-
await getOfflineQueue(s).enqueue({
|
|
3699
|
-
kind: "track",
|
|
3700
|
-
event,
|
|
3701
|
-
properties: props,
|
|
3702
|
-
telemetry: options?.telemetry,
|
|
3703
|
-
occurredAt,
|
|
3704
|
-
idempotencyKey
|
|
3705
|
-
}, outboxTicket);
|
|
3706
|
-
return;
|
|
4416
|
+
let s = getState();
|
|
4417
|
+
for (let attempt = 0;; attempt += 1) {
|
|
4418
|
+
const hold = (s.heldEvents?.length ?? 0) > 0 || await s.identity.anonymousIdStillUnknown();
|
|
4419
|
+
if (s.disposed && attempt === 0) {
|
|
4420
|
+
s = await liveState();
|
|
4421
|
+
continue;
|
|
3707
4422
|
}
|
|
3708
|
-
|
|
4423
|
+
if (!hold) return sendEvent(s, event, properties, options);
|
|
4424
|
+
if (typeof event !== "string" || event.trim() === "") throw new AmbaApiError("VALIDATION_ERROR", "event name must be non-empty");
|
|
4425
|
+
holdEvent(s, event, properties, options);
|
|
4426
|
+
if (!s.identity.hasStandInAnonymousId()) releaseHeldEvents(s);
|
|
4427
|
+
return;
|
|
3709
4428
|
}
|
|
3710
4429
|
},
|
|
3711
4430
|
flush: async () => getState().eventQueue?.flush() ?? {
|
|
@@ -3727,18 +4446,24 @@ const Amba = {
|
|
|
3727
4446
|
},
|
|
3728
4447
|
auth: {
|
|
3729
4448
|
signInAnonymously: async () => {
|
|
3730
|
-
const s =
|
|
4449
|
+
const s = await liveState();
|
|
4450
|
+
await loadUnreadSession(s);
|
|
4451
|
+
if (await s.identity.anonymousIdStillUnknown()) throw secureStoreUnavailableError();
|
|
4452
|
+
const startedAt = s.identity.getGeneration();
|
|
3731
4453
|
try {
|
|
3732
4454
|
return await postAuth(s, P.authAnonymous, { anonymous_id: s.identity.getAnonymousId() });
|
|
3733
4455
|
} catch (err) {
|
|
3734
4456
|
if (!(err instanceof AmbaApiError) || err.code !== IDENTITY_SIGN_IN_REQUIRED) throw err;
|
|
3735
|
-
|
|
4457
|
+
const held = await continueHeldSession(s);
|
|
4458
|
+
if (held) return held;
|
|
4459
|
+
if (!await s.identity.clearSession(true, { unlessSignedInSince: startedAt })) throw sessionChangedError("a sign-in stored a newer session while this call was in flight");
|
|
4460
|
+
return postAuth(s, P.authAnonymous, { anonymous_id: s.identity.getAnonymousId() });
|
|
3736
4461
|
}
|
|
3737
4462
|
},
|
|
3738
4463
|
signInWithEmail: async (email, password) => {
|
|
3739
4464
|
requireEmail(email);
|
|
3740
4465
|
if (password === "") throw new AmbaApiError("VALIDATION_ERROR", "password must not be empty");
|
|
3741
|
-
return postAuth(
|
|
4466
|
+
return postAuth(await liveState(), P.authEmailLogin, {
|
|
3742
4467
|
email,
|
|
3743
4468
|
password
|
|
3744
4469
|
});
|
|
@@ -3746,14 +4471,14 @@ const Amba = {
|
|
|
3746
4471
|
signUpWithEmail: async (email, password) => {
|
|
3747
4472
|
requireEmail(email);
|
|
3748
4473
|
requirePassword(password);
|
|
3749
|
-
return postAuth(
|
|
4474
|
+
return postAuth(await liveState(), P.authEmailSignup, {
|
|
3750
4475
|
email,
|
|
3751
4476
|
password
|
|
3752
4477
|
});
|
|
3753
4478
|
},
|
|
3754
4479
|
signInWithApple: async (identityToken, options) => {
|
|
3755
4480
|
if (identityToken.trim() === "") throw new AmbaApiError("VALIDATION_ERROR", "identity_token must not be empty");
|
|
3756
|
-
return postAuth(
|
|
4481
|
+
return postAuth(await liveState(), P.authSocial, {
|
|
3757
4482
|
provider: "apple",
|
|
3758
4483
|
id_token: identityToken,
|
|
3759
4484
|
...nonceField(options)
|
|
@@ -3761,7 +4486,7 @@ const Amba = {
|
|
|
3761
4486
|
},
|
|
3762
4487
|
signInWithGoogle: async (idToken, options) => {
|
|
3763
4488
|
if (idToken.trim() === "") throw new AmbaApiError("VALIDATION_ERROR", "id_token must not be empty");
|
|
3764
|
-
return postAuth(
|
|
4489
|
+
return postAuth(await liveState(), P.authSocial, {
|
|
3765
4490
|
provider: "google",
|
|
3766
4491
|
id_token: idToken,
|
|
3767
4492
|
...nonceField(options)
|
|
@@ -3774,7 +4499,7 @@ const Amba = {
|
|
|
3774
4499
|
verifyEmailOtp: async (email, code) => {
|
|
3775
4500
|
requireEmail(email);
|
|
3776
4501
|
requireOtpCode(code);
|
|
3777
|
-
return postAuth(
|
|
4502
|
+
return postAuth(await liveState(), P.authEmailOtpVerify, {
|
|
3778
4503
|
email,
|
|
3779
4504
|
code
|
|
3780
4505
|
});
|
|
@@ -3786,7 +4511,7 @@ const Amba = {
|
|
|
3786
4511
|
verifySmsOtp: async (phone, code) => {
|
|
3787
4512
|
requireE164Phone(phone);
|
|
3788
4513
|
requireOtpCode(code);
|
|
3789
|
-
return postAuth(
|
|
4514
|
+
return postAuth(await liveState(), P.authSmsOtpVerify, {
|
|
3790
4515
|
phone,
|
|
3791
4516
|
code
|
|
3792
4517
|
});
|
|
@@ -3797,24 +4522,27 @@ const Amba = {
|
|
|
3797
4522
|
},
|
|
3798
4523
|
verifyMagicLink: async (token) => {
|
|
3799
4524
|
if (token.trim() === "") throw new AmbaApiError("VALIDATION_ERROR", "token must not be empty");
|
|
3800
|
-
return postAuth(
|
|
4525
|
+
return postAuth(await liveState(), P.authMagicLinkVerify, { token });
|
|
3801
4526
|
},
|
|
3802
4527
|
linkAccount: async (provider, credential, options) => {
|
|
3803
4528
|
const providerTrimmed = provider?.trim() ?? "";
|
|
3804
4529
|
if (providerTrimmed === "") throw new AmbaApiError("VALIDATION_ERROR", "provider must not be empty");
|
|
3805
4530
|
if (typeof credential !== "string" || credential.trim() === "") throw new AmbaApiError("VALIDATION_ERROR", "credential must not be empty");
|
|
3806
|
-
const s =
|
|
4531
|
+
const s = await liveState();
|
|
3807
4532
|
return withBodySession(s, (sessionToken) => trackLink(s, postAuth(s, P.authLink, {
|
|
3808
4533
|
provider: providerTrimmed,
|
|
3809
4534
|
token: credential,
|
|
3810
4535
|
session_token: sessionToken,
|
|
3811
4536
|
...nonceField(options)
|
|
4537
|
+
}, {
|
|
4538
|
+
ifGeneration: s.identity.getGeneration(),
|
|
4539
|
+
advance: false
|
|
3812
4540
|
})));
|
|
3813
4541
|
},
|
|
3814
4542
|
linkSmsOtp: async (phone, code) => {
|
|
3815
4543
|
requireE164Phone(phone);
|
|
3816
4544
|
requireOtpCode(code);
|
|
3817
|
-
return postLink(
|
|
4545
|
+
return postLink(await liveState(), {
|
|
3818
4546
|
provider: "sms_otp",
|
|
3819
4547
|
phone,
|
|
3820
4548
|
code
|
|
@@ -3823,7 +4551,7 @@ const Amba = {
|
|
|
3823
4551
|
linkEmailOtp: async (email, code) => {
|
|
3824
4552
|
requireEmail(email);
|
|
3825
4553
|
requireOtpCode(code);
|
|
3826
|
-
return postLink(
|
|
4554
|
+
return postLink(await liveState(), {
|
|
3827
4555
|
provider: "email_otp",
|
|
3828
4556
|
email,
|
|
3829
4557
|
code
|
|
@@ -3832,7 +4560,7 @@ const Amba = {
|
|
|
3832
4560
|
linkEmailPassword: async (email, password) => {
|
|
3833
4561
|
requireEmail(email);
|
|
3834
4562
|
requirePassword(password);
|
|
3835
|
-
return postLink(
|
|
4563
|
+
return postLink(await liveState(), {
|
|
3836
4564
|
provider: "email_password",
|
|
3837
4565
|
email,
|
|
3838
4566
|
password
|
|
@@ -3845,34 +4573,29 @@ const Amba = {
|
|
|
3845
4573
|
getCurrentIdentifiers: async () => {
|
|
3846
4574
|
const s = getState();
|
|
3847
4575
|
await settleSessionInFlight(s);
|
|
3848
|
-
|
|
4576
|
+
requireSession(s);
|
|
3849
4577
|
return (await s.http.get(P.authIdentifiers)).data.identifiers;
|
|
3850
4578
|
},
|
|
3851
4579
|
signOut: async (rotateAnonymousId = false) => {
|
|
3852
|
-
const s =
|
|
4580
|
+
const s = await liveState();
|
|
4581
|
+
await loadUnreadSession(s);
|
|
4582
|
+
const startedAt = s.identity.getGeneration();
|
|
4583
|
+
const revoked = s.identity.getRefreshToken();
|
|
3853
4584
|
if (s.identity.getSessionToken()) try {
|
|
3854
|
-
|
|
3855
|
-
await s.http.post(P.authLogout, refreshToken ? { refresh_token: refreshToken } : {});
|
|
4585
|
+
await s.http.post(P.authLogout, revoked ? { refresh_token: revoked } : {});
|
|
3856
4586
|
} catch (err) {
|
|
3857
4587
|
if (typeof console !== "undefined") console.warn("[amba] signOut server call failed", err);
|
|
3858
4588
|
}
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
try {
|
|
3864
|
-
await getOfflineQueue(s).purge();
|
|
3865
|
-
} catch (err) {
|
|
3866
|
-
if (typeof console !== "undefined") console.warn("[amba] signOut could not clear the offline outbox", err);
|
|
3867
|
-
}
|
|
3868
|
-
s.eventQueue?.flush();
|
|
3869
|
-
notifyAuthSubscribers(null);
|
|
4589
|
+
if (await endSignedOutSession(s, rotateAnonymousId, startedAt)) return;
|
|
4590
|
+
if (!s.disposed || revoked === null) return;
|
|
4591
|
+
const next = await liveState();
|
|
4592
|
+
if (next !== s && !next.disposed && sameIdentityTarget(next, s) && next.identity.continuesLaunchSession(revoked)) await endSignedOutSession(next, rotateAnonymousId, next.identity.getGeneration());
|
|
3870
4593
|
},
|
|
3871
|
-
refresh: async () => refreshSession(
|
|
4594
|
+
refresh: async () => refreshSession(await liveState()),
|
|
3872
4595
|
me: async () => {
|
|
3873
4596
|
const s = getState();
|
|
3874
4597
|
await settleSessionInFlight(s);
|
|
3875
|
-
|
|
4598
|
+
requireSession(s);
|
|
3876
4599
|
return normalizeUserAuthProviders((await s.http.get(P.authMe)).data);
|
|
3877
4600
|
},
|
|
3878
4601
|
getSession: async () => {
|
|
@@ -3887,7 +4610,9 @@ const Amba = {
|
|
|
3887
4610
|
}
|
|
3888
4611
|
},
|
|
3889
4612
|
getAnonymousId: async () => {
|
|
3890
|
-
|
|
4613
|
+
const s = getState();
|
|
4614
|
+
if (await s.identity.anonymousIdStillUnknown()) throw secureStoreUnavailableError();
|
|
4615
|
+
return s.identity.getAnonymousId();
|
|
3891
4616
|
},
|
|
3892
4617
|
onAuthStateChange: (cb) => {
|
|
3893
4618
|
authSubscribers.add(cb);
|
|
@@ -3899,17 +4624,19 @@ const Amba = {
|
|
|
3899
4624
|
users: {
|
|
3900
4625
|
get: async (userId) => {
|
|
3901
4626
|
const s = getState();
|
|
3902
|
-
|
|
4627
|
+
await settleSessionInFlight(s);
|
|
4628
|
+
requireSession(s);
|
|
3903
4629
|
const path = userId === void 0 || userId === null || userId === "" ? P.usersMe : `${P.users}/${encodePathSegment(userId)}`;
|
|
3904
4630
|
return normalizeUserAuthProviders((await s.http.get(path)).data);
|
|
3905
4631
|
},
|
|
3906
|
-
update: ((arg1, arg2) => {
|
|
4632
|
+
update: (async (arg1, arg2) => {
|
|
3907
4633
|
const s = getState();
|
|
3908
|
-
|
|
4634
|
+
await settleSessionInFlight(s);
|
|
4635
|
+
requireSession(s);
|
|
3909
4636
|
const isById = typeof arg1 === "string";
|
|
3910
4637
|
const patch = isById ? arg2 ?? {} : arg1;
|
|
3911
4638
|
const path = isById ? `${P.users}/${encodePathSegment(arg1)}` : `${P.users}/me`;
|
|
3912
|
-
return s.http.patch(path, patch)
|
|
4639
|
+
return normalizeUserAuthProviders((await s.http.patch(path, patch)).data);
|
|
3913
4640
|
})
|
|
3914
4641
|
},
|
|
3915
4642
|
sessions: {
|