@explorins/pers-sdk-react-native 2.1.18 → 2.1.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -9588,7 +9588,113 @@ class AuthTokenManager {
9588
9588
  const token = await this.getRefreshToken();
9589
9589
  return !!token;
9590
9590
  }
9591
+ /**
9592
+ * Clears only the refresh token.
9593
+ * Used when DPoP keys are regenerated, invalidating the token binding.
9594
+ */
9595
+ async clearRefreshToken() {
9596
+ this.cache.refreshToken = null;
9597
+ await this.storage.remove(AUTH_STORAGE_KEYS.REFRESH_TOKEN);
9598
+ }
9599
+ }
9600
+
9601
+ class IndexedDBTokenStorage {
9602
+ constructor() {
9603
+ this.supportsObjects = true;
9604
+ this.dbPromise = null;
9605
+ if (typeof indexedDB === 'undefined') {
9606
+ console.warn('IndexedDB is not available in this environment');
9607
+ }
9608
+ }
9609
+ getDB() {
9610
+ if (this.dbPromise)
9611
+ return this.dbPromise;
9612
+ this.dbPromise = new Promise((resolve, reject) => {
9613
+ if (typeof indexedDB === 'undefined') {
9614
+ return reject(new Error('IndexedDB not supported'));
9615
+ }
9616
+ const request = indexedDB.open(IndexedDBTokenStorage.DB_NAME, IndexedDBTokenStorage.DB_VERSION);
9617
+ request.onupgradeneeded = (event) => {
9618
+ const db = event.target.result;
9619
+ if (!db.objectStoreNames.contains(IndexedDBTokenStorage.STORE_NAME)) {
9620
+ db.createObjectStore(IndexedDBTokenStorage.STORE_NAME);
9621
+ }
9622
+ };
9623
+ request.onsuccess = (event) => {
9624
+ resolve(event.target.result);
9625
+ };
9626
+ request.onerror = (event) => {
9627
+ reject(event.target.error);
9628
+ };
9629
+ });
9630
+ return this.dbPromise;
9631
+ }
9632
+ async get(key) {
9633
+ try {
9634
+ const db = await this.getDB();
9635
+ return new Promise((resolve, reject) => {
9636
+ const transaction = db.transaction(IndexedDBTokenStorage.STORE_NAME, 'readonly');
9637
+ const store = transaction.objectStore(IndexedDBTokenStorage.STORE_NAME);
9638
+ const request = store.get(key);
9639
+ request.onsuccess = () => resolve(request.result || null);
9640
+ request.onerror = () => reject(request.error);
9641
+ });
9642
+ }
9643
+ catch (e) {
9644
+ console.warn('[IndexedDBTokenStorage] Failed to get key', key, e);
9645
+ return null;
9646
+ }
9647
+ }
9648
+ async set(key, value) {
9649
+ try {
9650
+ const db = await this.getDB();
9651
+ return new Promise((resolve, reject) => {
9652
+ const transaction = db.transaction(IndexedDBTokenStorage.STORE_NAME, 'readwrite');
9653
+ const store = transaction.objectStore(IndexedDBTokenStorage.STORE_NAME);
9654
+ const request = store.put(value, key);
9655
+ request.onsuccess = () => resolve();
9656
+ request.onerror = () => reject(request.error);
9657
+ });
9658
+ }
9659
+ catch (e) {
9660
+ console.error('[IndexedDBTokenStorage] Failed to set key', key, e);
9661
+ throw e;
9662
+ }
9663
+ }
9664
+ async remove(key) {
9665
+ try {
9666
+ const db = await this.getDB();
9667
+ return new Promise((resolve, reject) => {
9668
+ const transaction = db.transaction(IndexedDBTokenStorage.STORE_NAME, 'readwrite');
9669
+ const store = transaction.objectStore(IndexedDBTokenStorage.STORE_NAME);
9670
+ const request = store.delete(key);
9671
+ request.onsuccess = () => resolve();
9672
+ request.onerror = () => reject(request.error);
9673
+ });
9674
+ }
9675
+ catch (e) {
9676
+ console.warn('[IndexedDBTokenStorage] Failed to remove key', key, e);
9677
+ }
9678
+ }
9679
+ async clear() {
9680
+ try {
9681
+ const db = await this.getDB();
9682
+ return new Promise((resolve, reject) => {
9683
+ const transaction = db.transaction(IndexedDBTokenStorage.STORE_NAME, 'readwrite');
9684
+ const store = transaction.objectStore(IndexedDBTokenStorage.STORE_NAME);
9685
+ const request = store.clear();
9686
+ request.onsuccess = () => resolve();
9687
+ request.onerror = () => reject(request.error);
9688
+ });
9689
+ }
9690
+ catch (e) {
9691
+ console.warn('[IndexedDBTokenStorage] Failed to clear storage', e);
9692
+ }
9693
+ }
9591
9694
  }
9695
+ IndexedDBTokenStorage.DB_NAME = 'pers-sdk-storage';
9696
+ IndexedDBTokenStorage.STORE_NAME = 'auth-tokens';
9697
+ IndexedDBTokenStorage.DB_VERSION = 1;
9592
9698
 
9593
9699
  class WebDPoPCryptoProvider {
9594
9700
  get crypto() {
@@ -9724,13 +9830,30 @@ const DPOP_STORAGE_KEYS = {
9724
9830
  PRIVATE: 'pers_dpop_private_key'
9725
9831
  };
9726
9832
  class DPoPManager {
9727
- constructor(storage, cryptoProvider) {
9833
+ constructor(storage, cryptoProvider, callbacks) {
9728
9834
  this.storage = storage;
9729
9835
  this.memoryKeyPair = null;
9730
9836
  this.cryptoProvider = cryptoProvider || new WebDPoPCryptoProvider();
9837
+ this.callbacks = callbacks || {};
9838
+ }
9839
+ /**
9840
+ * Checks if DPoP keys exist in storage without generating new ones.
9841
+ * Useful for detecting if a key regeneration would be needed.
9842
+ */
9843
+ async hasStoredKeys() {
9844
+ if (this.memoryKeyPair)
9845
+ return true;
9846
+ const storedPublic = await this.storage.get(DPOP_STORAGE_KEYS.PUBLIC);
9847
+ const storedPrivate = await this.storage.get(DPOP_STORAGE_KEYS.PRIVATE);
9848
+ return !!(storedPublic && storedPrivate);
9731
9849
  }
9732
9850
  /**
9733
9851
  * Ensures a DPoP key pair exists, loading from storage or generating new one.
9852
+ *
9853
+ * IMPORTANT: If keys are regenerated (not loaded from storage), this will
9854
+ * invalidate any refresh tokens bound to the old keys. The onKeysRegenerated
9855
+ * callback should clear the refresh token to prevent "DPoP proof does not
9856
+ * match refresh token binding" errors.
9734
9857
  */
9735
9858
  async ensureKeyPair() {
9736
9859
  if (this.memoryKeyPair)
@@ -9746,10 +9869,12 @@ class DPoPManager {
9746
9869
  return this.memoryKeyPair;
9747
9870
  }
9748
9871
  catch (e) {
9749
- console.warn('Corrupted DPoP keys in storage, regenerating.');
9872
+ console.warn('[DPoPManager] Corrupted DPoP keys in storage, regenerating.');
9750
9873
  }
9751
9874
  }
9752
9875
  // Generate new key pair
9876
+ // IMPORTANT: This invalidates any existing refresh tokens bound to the old keys
9877
+ console.info('[DPoPManager] Generating new DPoP key pair');
9753
9878
  // Adaptation: If storage supports raw objects (like IndexedDB or Native Keychain),
9754
9879
  // we can generate Non-Extractable keys for maximum security.
9755
9880
  // If storage is text-only (LocalStorage), we must use Extractable keys to serialize them.
@@ -9761,6 +9886,15 @@ class DPoPManager {
9761
9886
  await this.storage.set(DPOP_STORAGE_KEYS.PUBLIC, keyPair.publicKey);
9762
9887
  await this.storage.set(DPOP_STORAGE_KEYS.PRIVATE, keyPair.privateKey);
9763
9888
  this.memoryKeyPair = keyPair;
9889
+ // Notify that keys were regenerated - refresh tokens bound to old keys are now invalid
9890
+ if (this.callbacks.onKeysRegenerated) {
9891
+ try {
9892
+ this.callbacks.onKeysRegenerated();
9893
+ }
9894
+ catch (e) {
9895
+ console.warn('[DPoPManager] onKeysRegenerated callback failed:', e);
9896
+ }
9897
+ }
9764
9898
  return keyPair;
9765
9899
  }
9766
9900
  /**
@@ -9814,16 +9948,48 @@ class DefaultAuthProvider {
9814
9948
  this.config = config;
9815
9949
  this.authTypeCache = null;
9816
9950
  this.authType = config.authType || exports.AccountOwnerType.USER;
9951
+ // Determine if DPoP can be enabled (requires IndexedDB for key persistence)
9952
+ const canUseDPoP = this.canEnableDPoP();
9953
+ this.dpopEnabled = !!(config.dpop?.enabled && canUseDPoP);
9954
+ if (config.dpop?.enabled && !canUseDPoP) {
9955
+ console.warn('[DefaultAuthProvider] DPoP disabled: IndexedDB not available. CryptoKey persistence requires IndexedDB.');
9956
+ }
9817
9957
  const storage = config.storage || this.createStorage();
9818
9958
  this.tokenManager = new AuthTokenManager(storage);
9819
- if (config.dpop?.enabled) {
9820
- this.dpopManager = new DPoPManager(storage, config.dpop.cryptoProvider);
9959
+ if (this.dpopEnabled) {
9960
+ this.dpopManager = new DPoPManager(storage, config.dpop?.cryptoProvider, {
9961
+ // When DPoP keys are regenerated (corrupted/missing), the refresh token
9962
+ // bound to the old keys becomes invalid. Clear it to prevent
9963
+ // "DPoP proof does not match refresh token binding" errors.
9964
+ onKeysRegenerated: () => {
9965
+ console.info('[DefaultAuthProvider] DPoP keys regenerated, clearing invalid refresh token');
9966
+ this.tokenManager.clearRefreshToken().catch(err => {
9967
+ console.warn('[DefaultAuthProvider] Failed to clear refresh token:', err);
9968
+ });
9969
+ }
9970
+ });
9821
9971
  }
9822
9972
  }
9973
+ canEnableDPoP() {
9974
+ // DPoP requires IndexedDB for proper CryptoKey persistence
9975
+ // LocalStorage cannot serialize CryptoKey objects correctly
9976
+ return typeof window !== 'undefined' && typeof indexedDB !== 'undefined';
9977
+ }
9823
9978
  createStorage() {
9824
- return typeof window === 'undefined' || typeof localStorage === 'undefined'
9825
- ? new MemoryTokenStorage()
9826
- : new LocalStorageTokenStorage();
9979
+ // Server-side: use memory storage
9980
+ if (typeof window === 'undefined') {
9981
+ return new MemoryTokenStorage();
9982
+ }
9983
+ // Browser with DPoP enabled: use IndexedDB for CryptoKey persistence
9984
+ if (this.dpopEnabled) {
9985
+ return new IndexedDBTokenStorage();
9986
+ }
9987
+ // Browser without DPoP: use LocalStorage
9988
+ if (typeof localStorage !== 'undefined') {
9989
+ return new LocalStorageTokenStorage();
9990
+ }
9991
+ // Fallback
9992
+ return new MemoryTokenStorage();
9827
9993
  }
9828
9994
  getToken() {
9829
9995
  return this.tokenManager.getAccessToken();
@@ -9905,10 +10071,9 @@ class DefaultAuthProvider {
9905
10071
  async hasValidToken() {
9906
10072
  return this.tokenManager.hasAccessToken();
9907
10073
  }
9908
- async setAuthType(authType) {
9909
- // Auth type is derived from JWT, so this is a no-op
9910
- // The authType in JWT is the source of truth
9911
- // This method exists to satisfy the ExtendedTokenStorageProvider interface
10074
+ async setAuthType(_authType) {
10075
+ // No-op: authType is derived from JWT payload via getAuthType()
10076
+ // @see getAuthType() - reads accountType directly from JWT
9912
10077
  }
9913
10078
  async getAuthType() {
9914
10079
  // Read from JWT instead of separate storage for single source of truth
@@ -9949,12 +10114,14 @@ class DefaultAuthProvider {
9949
10114
  * Exported for consumers who need version info.
9950
10115
  * Used internally for X-SDK-Version header.
9951
10116
  *
10117
+ * Version is injected at build time via @rollup/plugin-replace
10118
+ *
9952
10119
  * @module @explorins/pers-sdk/core
9953
10120
  */
9954
10121
  /** SDK package name */
9955
- const SDK_NAME = '@explorins/pers-sdk';
9956
- /** SDK version - TODO: Load dynamically from package.json */
9957
- const SDK_VERSION = '2.1.1';
10122
+ const SDK_NAME = "@explorins/pers-sdk";
10123
+ /** SDK version - injected from package.json at build time */
10124
+ const SDK_VERSION = "2.1.27";
9958
10125
  /** Full SDK identifier for headers */
9959
10126
  const SDK_USER_AGENT = `${SDK_NAME}/${SDK_VERSION}`;
9960
10127
 
@@ -10884,7 +11051,14 @@ class AuthManager {
10884
11051
  */
10885
11052
  async loginWithRawData(rawUserData) {
10886
11053
  const authService = this.apiClient.getAuthService();
10887
- return authService.loginUserWithRawData(rawUserData);
11054
+ const result = await authService.loginUserWithRawData(rawUserData);
11055
+ this.events?.emitSuccess({
11056
+ domain: 'authentication',
11057
+ type: 'login_success',
11058
+ userMessage: 'Successfully logged in',
11059
+ details: { user: result.user }
11060
+ });
11061
+ return result;
10888
11062
  }
10889
11063
  /**
10890
11064
  * Get current authenticated user
@@ -18859,104 +19033,6 @@ function createPersSDK(httpClient, config) {
18859
19033
  return new PersSDK(httpClient, config);
18860
19034
  }
18861
19035
 
18862
- class IndexedDBTokenStorage {
18863
- constructor() {
18864
- this.supportsObjects = true;
18865
- this.dbPromise = null;
18866
- if (typeof indexedDB === 'undefined') {
18867
- console.warn('IndexedDB is not available in this environment');
18868
- }
18869
- }
18870
- getDB() {
18871
- if (this.dbPromise)
18872
- return this.dbPromise;
18873
- this.dbPromise = new Promise((resolve, reject) => {
18874
- if (typeof indexedDB === 'undefined') {
18875
- return reject(new Error('IndexedDB not supported'));
18876
- }
18877
- const request = indexedDB.open(IndexedDBTokenStorage.DB_NAME, IndexedDBTokenStorage.DB_VERSION);
18878
- request.onupgradeneeded = (event) => {
18879
- const db = event.target.result;
18880
- if (!db.objectStoreNames.contains(IndexedDBTokenStorage.STORE_NAME)) {
18881
- db.createObjectStore(IndexedDBTokenStorage.STORE_NAME);
18882
- }
18883
- };
18884
- request.onsuccess = (event) => {
18885
- resolve(event.target.result);
18886
- };
18887
- request.onerror = (event) => {
18888
- reject(event.target.error);
18889
- };
18890
- });
18891
- return this.dbPromise;
18892
- }
18893
- async get(key) {
18894
- try {
18895
- const db = await this.getDB();
18896
- return new Promise((resolve, reject) => {
18897
- const transaction = db.transaction(IndexedDBTokenStorage.STORE_NAME, 'readonly');
18898
- const store = transaction.objectStore(IndexedDBTokenStorage.STORE_NAME);
18899
- const request = store.get(key);
18900
- request.onsuccess = () => resolve(request.result || null);
18901
- request.onerror = () => reject(request.error);
18902
- });
18903
- }
18904
- catch (e) {
18905
- console.warn('[IndexedDBTokenStorage] Failed to get key', key, e);
18906
- return null;
18907
- }
18908
- }
18909
- async set(key, value) {
18910
- try {
18911
- const db = await this.getDB();
18912
- return new Promise((resolve, reject) => {
18913
- const transaction = db.transaction(IndexedDBTokenStorage.STORE_NAME, 'readwrite');
18914
- const store = transaction.objectStore(IndexedDBTokenStorage.STORE_NAME);
18915
- const request = store.put(value, key);
18916
- request.onsuccess = () => resolve();
18917
- request.onerror = () => reject(request.error);
18918
- });
18919
- }
18920
- catch (e) {
18921
- console.error('[IndexedDBTokenStorage] Failed to set key', key, e);
18922
- throw e;
18923
- }
18924
- }
18925
- async remove(key) {
18926
- try {
18927
- const db = await this.getDB();
18928
- return new Promise((resolve, reject) => {
18929
- const transaction = db.transaction(IndexedDBTokenStorage.STORE_NAME, 'readwrite');
18930
- const store = transaction.objectStore(IndexedDBTokenStorage.STORE_NAME);
18931
- const request = store.delete(key);
18932
- request.onsuccess = () => resolve();
18933
- request.onerror = () => reject(request.error);
18934
- });
18935
- }
18936
- catch (e) {
18937
- console.warn('[IndexedDBTokenStorage] Failed to remove key', key, e);
18938
- }
18939
- }
18940
- async clear() {
18941
- try {
18942
- const db = await this.getDB();
18943
- return new Promise((resolve, reject) => {
18944
- const transaction = db.transaction(IndexedDBTokenStorage.STORE_NAME, 'readwrite');
18945
- const store = transaction.objectStore(IndexedDBTokenStorage.STORE_NAME);
18946
- const request = store.clear();
18947
- request.onsuccess = () => resolve();
18948
- request.onerror = () => reject(request.error);
18949
- });
18950
- }
18951
- catch (e) {
18952
- console.warn('[IndexedDBTokenStorage] Failed to clear storage', e);
18953
- }
18954
- }
18955
- }
18956
- IndexedDBTokenStorage.DB_NAME = 'pers-sdk-storage';
18957
- IndexedDBTokenStorage.STORE_NAME = 'auth-tokens';
18958
- IndexedDBTokenStorage.DB_VERSION = 1;
18959
-
18960
19036
  /**
18961
19037
  * Environment Detection Utility
18962
19038
  * Detects runtime environment for platform-specific behaviors
@@ -19282,11 +19358,19 @@ class ReactNativeSecureStorage {
19282
19358
  * React Native Unified Auth Provider
19283
19359
  *
19284
19360
  * Creates a platform-specific auth provider that automatically selects the appropriate storage:
19285
- * - Web: Uses LocalStorageTokenStorage from core SDK
19361
+ * - Web: Uses IndexedDBTokenStorage for DPoP (CryptoKey persistence) or LocalStorage
19286
19362
  * - Mobile: Uses AsyncStorage-based storage
19287
19363
  */
19288
19364
  // Use React Native's built-in platform detection
19289
19365
  const isWebPlatform = reactNative.Platform.OS === 'web';
19366
+ /**
19367
+ * Check if IndexedDB is available (web environments only)
19368
+ * Uses globalThis to avoid TypeScript errors in React Native context
19369
+ */
19370
+ function hasIndexedDB() {
19371
+ return typeof globalThis !== 'undefined' &&
19372
+ typeof globalThis.indexedDB !== 'undefined';
19373
+ }
19290
19374
  /**
19291
19375
  * Create a React Native auth provider with platform-appropriate storage
19292
19376
  *
@@ -19304,9 +19388,29 @@ function createReactNativeAuthProvider(projectKey, config = {}) {
19304
19388
  }
19305
19389
  const { keyPrefix = `pers_${projectKey.slice(0, 8)}_`, debug = false, customStorage, authType = exports.AccountOwnerType.USER, dpop } = config;
19306
19390
  // Platform-specific storage selection
19307
- const tokenStorage = customStorage || (isWebPlatform
19308
- ? new LocalStorageTokenStorage()
19309
- : new ReactNativeSecureStorage(keyPrefix));
19391
+ // Web with DPoP: Use IndexedDB for CryptoKey persistence
19392
+ // Web without DPoP or no IndexedDB: Use LocalStorage
19393
+ // Mobile: Use ReactNativeSecureStorage
19394
+ let tokenStorage;
19395
+ if (customStorage) {
19396
+ tokenStorage = customStorage;
19397
+ }
19398
+ else if (isWebPlatform) {
19399
+ // On web, prefer IndexedDB when DPoP is enabled for proper CryptoKey persistence
19400
+ const dpopEnabled = dpop?.enabled ?? true;
19401
+ if (dpopEnabled && hasIndexedDB()) {
19402
+ tokenStorage = new IndexedDBTokenStorage();
19403
+ }
19404
+ else {
19405
+ if (dpopEnabled && !hasIndexedDB()) {
19406
+ console.warn('[ReactNativeAuthProvider] DPoP requested but IndexedDB not available. Falling back to LocalStorage (DPoP will be disabled by DefaultAuthProvider).');
19407
+ }
19408
+ tokenStorage = new LocalStorageTokenStorage();
19409
+ }
19410
+ }
19411
+ else {
19412
+ tokenStorage = new ReactNativeSecureStorage(keyPrefix);
19413
+ }
19310
19414
  // Prepare DPoP config ensuring enabled is boolean (default true)
19311
19415
  const dpopConfig = dpop ? {
19312
19416
  enabled: dpop.enabled ?? true,