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