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

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,61 @@ 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:
9952
+ // 1. Custom crypto provider is supplied (e.g., React Native), OR
9953
+ // 2. IndexedDB is available for browser CryptoKey persistence
9954
+ const hasCustomCryptoProvider = !!config.dpop?.cryptoProvider;
9955
+ const hasIndexedDB = this.canEnableDPoP();
9956
+ const canUseDPoP = hasCustomCryptoProvider || hasIndexedDB;
9957
+ this.dpopEnabled = !!(config.dpop?.enabled && canUseDPoP);
9958
+ // Debug logging for DPoP configuration
9959
+ console.log('[DefaultAuthProvider] DPoP config:', {
9960
+ requestedEnabled: config.dpop?.enabled ?? false,
9961
+ hasCustomCryptoProvider,
9962
+ hasIndexedDB,
9963
+ canUseDPoP,
9964
+ finalEnabled: this.dpopEnabled,
9965
+ cryptoProviderType: config.dpop?.cryptoProvider?.constructor?.name || 'none'
9966
+ });
9967
+ if (config.dpop?.enabled && !canUseDPoP) {
9968
+ console.warn('[DefaultAuthProvider] DPoP disabled: IndexedDB not available and no custom crypto provider supplied. CryptoKey persistence requires IndexedDB or a custom provider.');
9969
+ }
9817
9970
  const storage = config.storage || this.createStorage();
9818
9971
  this.tokenManager = new AuthTokenManager(storage);
9819
- if (config.dpop?.enabled) {
9820
- this.dpopManager = new DPoPManager(storage, config.dpop.cryptoProvider);
9972
+ if (this.dpopEnabled) {
9973
+ this.dpopManager = new DPoPManager(storage, config.dpop?.cryptoProvider, {
9974
+ // When DPoP keys are regenerated (corrupted/missing), the refresh token
9975
+ // bound to the old keys becomes invalid. Clear it to prevent
9976
+ // "DPoP proof does not match refresh token binding" errors.
9977
+ onKeysRegenerated: () => {
9978
+ console.info('[DefaultAuthProvider] DPoP keys regenerated, clearing invalid refresh token');
9979
+ this.tokenManager.clearRefreshToken().catch(err => {
9980
+ console.warn('[DefaultAuthProvider] Failed to clear refresh token:', err);
9981
+ });
9982
+ }
9983
+ });
9821
9984
  }
9822
9985
  }
9986
+ canEnableDPoP() {
9987
+ // DPoP requires IndexedDB for proper CryptoKey persistence
9988
+ // LocalStorage cannot serialize CryptoKey objects correctly
9989
+ return typeof window !== 'undefined' && typeof indexedDB !== 'undefined';
9990
+ }
9823
9991
  createStorage() {
9824
- return typeof window === 'undefined' || typeof localStorage === 'undefined'
9825
- ? new MemoryTokenStorage()
9826
- : new LocalStorageTokenStorage();
9992
+ // Server-side: use memory storage
9993
+ if (typeof window === 'undefined') {
9994
+ return new MemoryTokenStorage();
9995
+ }
9996
+ // Browser with DPoP enabled: use IndexedDB for CryptoKey persistence
9997
+ if (this.dpopEnabled) {
9998
+ return new IndexedDBTokenStorage();
9999
+ }
10000
+ // Browser without DPoP: use LocalStorage
10001
+ if (typeof localStorage !== 'undefined') {
10002
+ return new LocalStorageTokenStorage();
10003
+ }
10004
+ // Fallback
10005
+ return new MemoryTokenStorage();
9827
10006
  }
9828
10007
  getToken() {
9829
10008
  return this.tokenManager.getAccessToken();
@@ -9905,10 +10084,9 @@ class DefaultAuthProvider {
9905
10084
  async hasValidToken() {
9906
10085
  return this.tokenManager.hasAccessToken();
9907
10086
  }
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
10087
+ async setAuthType(_authType) {
10088
+ // No-op: authType is derived from JWT payload via getAuthType()
10089
+ // @see getAuthType() - reads accountType directly from JWT
9912
10090
  }
9913
10091
  async getAuthType() {
9914
10092
  // Read from JWT instead of separate storage for single source of truth
@@ -9949,12 +10127,14 @@ class DefaultAuthProvider {
9949
10127
  * Exported for consumers who need version info.
9950
10128
  * Used internally for X-SDK-Version header.
9951
10129
  *
10130
+ * Version is injected at build time via @rollup/plugin-replace
10131
+ *
9952
10132
  * @module @explorins/pers-sdk/core
9953
10133
  */
9954
10134
  /** 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';
10135
+ const SDK_NAME = "@explorins/pers-sdk";
10136
+ /** SDK version - injected from package.json at build time */
10137
+ const SDK_VERSION = "2.1.27";
9958
10138
  /** Full SDK identifier for headers */
9959
10139
  const SDK_USER_AGENT = `${SDK_NAME}/${SDK_VERSION}`;
9960
10140
 
@@ -10884,7 +11064,14 @@ class AuthManager {
10884
11064
  */
10885
11065
  async loginWithRawData(rawUserData) {
10886
11066
  const authService = this.apiClient.getAuthService();
10887
- return authService.loginUserWithRawData(rawUserData);
11067
+ const result = await authService.loginUserWithRawData(rawUserData);
11068
+ this.events?.emitSuccess({
11069
+ domain: 'authentication',
11070
+ type: 'login_success',
11071
+ userMessage: 'Successfully logged in',
11072
+ details: { user: result.user }
11073
+ });
11074
+ return result;
10888
11075
  }
10889
11076
  /**
10890
11077
  * Get current authenticated user
@@ -18859,104 +19046,6 @@ function createPersSDK(httpClient, config) {
18859
19046
  return new PersSDK(httpClient, config);
18860
19047
  }
18861
19048
 
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
19049
  /**
18961
19050
  * Environment Detection Utility
18962
19051
  * Detects runtime environment for platform-specific behaviors
@@ -19282,11 +19371,19 @@ class ReactNativeSecureStorage {
19282
19371
  * React Native Unified Auth Provider
19283
19372
  *
19284
19373
  * Creates a platform-specific auth provider that automatically selects the appropriate storage:
19285
- * - Web: Uses LocalStorageTokenStorage from core SDK
19374
+ * - Web: Uses IndexedDBTokenStorage for DPoP (CryptoKey persistence) or LocalStorage
19286
19375
  * - Mobile: Uses AsyncStorage-based storage
19287
19376
  */
19288
19377
  // Use React Native's built-in platform detection
19289
19378
  const isWebPlatform = reactNative.Platform.OS === 'web';
19379
+ /**
19380
+ * Check if IndexedDB is available (web environments only)
19381
+ * Uses globalThis to avoid TypeScript errors in React Native context
19382
+ */
19383
+ function hasIndexedDB() {
19384
+ return typeof globalThis !== 'undefined' &&
19385
+ typeof globalThis.indexedDB !== 'undefined';
19386
+ }
19290
19387
  /**
19291
19388
  * Create a React Native auth provider with platform-appropriate storage
19292
19389
  *
@@ -19304,18 +19401,43 @@ function createReactNativeAuthProvider(projectKey, config = {}) {
19304
19401
  }
19305
19402
  const { keyPrefix = `pers_${projectKey.slice(0, 8)}_`, debug = false, customStorage, authType = exports.AccountOwnerType.USER, dpop } = config;
19306
19403
  // Platform-specific storage selection
19307
- const tokenStorage = customStorage || (isWebPlatform
19308
- ? new LocalStorageTokenStorage()
19309
- : new ReactNativeSecureStorage(keyPrefix));
19404
+ // Web with DPoP: Use IndexedDB for CryptoKey persistence
19405
+ // Web without DPoP or no IndexedDB: Use LocalStorage
19406
+ // Mobile: Use ReactNativeSecureStorage
19407
+ let tokenStorage;
19408
+ if (customStorage) {
19409
+ tokenStorage = customStorage;
19410
+ }
19411
+ else if (isWebPlatform) {
19412
+ // On web, prefer IndexedDB when DPoP is enabled for proper CryptoKey persistence
19413
+ const dpopEnabled = dpop?.enabled ?? true;
19414
+ if (dpopEnabled && hasIndexedDB()) {
19415
+ tokenStorage = new IndexedDBTokenStorage();
19416
+ }
19417
+ else {
19418
+ if (dpopEnabled && !hasIndexedDB()) {
19419
+ console.warn('[ReactNativeAuthProvider] DPoP requested but IndexedDB not available. Falling back to LocalStorage (DPoP will be disabled by DefaultAuthProvider).');
19420
+ }
19421
+ tokenStorage = new LocalStorageTokenStorage();
19422
+ }
19423
+ }
19424
+ else {
19425
+ tokenStorage = new ReactNativeSecureStorage(keyPrefix);
19426
+ }
19310
19427
  // Prepare DPoP config ensuring enabled is boolean (default true)
19311
19428
  const dpopConfig = dpop ? {
19312
19429
  enabled: dpop.enabled ?? true,
19313
19430
  cryptoProvider: dpop.cryptoProvider
19314
19431
  } : undefined;
19315
- // DEBUG LOGGING
19316
- if (debug) {
19317
- console.log('[ReactNativeAuthProvider] Creating provider with DPoP config:', JSON.stringify(dpopConfig));
19318
- }
19432
+ // Always log DPoP config for debugging (critical for mobile auth)
19433
+ console.log('[ReactNativeAuthProvider] Config:', {
19434
+ platform: reactNative.Platform.OS,
19435
+ isWebPlatform,
19436
+ dpopEnabled: dpopConfig?.enabled ?? false,
19437
+ hasCryptoProvider: !!dpopConfig?.cryptoProvider,
19438
+ cryptoProviderType: dpopConfig?.cryptoProvider?.constructor?.name || 'none',
19439
+ storageType: tokenStorage.constructor.name
19440
+ });
19319
19441
  // Return DefaultAuthProvider configured with platform-appropriate storage
19320
19442
  return new DefaultAuthProvider({
19321
19443
  authType,
@@ -19642,6 +19764,13 @@ const PersSDKProvider = ({ children, config }) => {
19642
19764
  if (reactNative.Platform.OS !== 'web' && isDpopEnabled && !dpopSettings.cryptoProvider) {
19643
19765
  dpopSettings.cryptoProvider = new ReactNativeDPoPProvider();
19644
19766
  }
19767
+ // Log DPoP settings before creating auth provider
19768
+ console.log('[PersSDKProvider] DPoP settings:', {
19769
+ platform: reactNative.Platform.OS,
19770
+ isDpopEnabled,
19771
+ hasCryptoProvider: !!dpopSettings.cryptoProvider,
19772
+ cryptoProviderType: dpopSettings.cryptoProvider?.constructor?.name || 'none'
19773
+ });
19645
19774
  // Create platform-aware auth provider if not provided
19646
19775
  let authProvider;
19647
19776
  if (config.authProvider) {