@oxyhq/auth 2.0.4 → 2.0.6

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.
Files changed (58) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/WebOxyProvider.js +37 -0
  3. package/dist/cjs/hooks/mutations/index.js +5 -1
  4. package/dist/cjs/hooks/mutations/useAccountMutations.js +185 -43
  5. package/dist/cjs/hooks/mutations/useAppData.js +133 -0
  6. package/dist/cjs/hooks/queries/appDataQueryKeys.js +46 -0
  7. package/dist/cjs/hooks/queries/index.js +8 -1
  8. package/dist/cjs/hooks/queries/useAppData.js +87 -0
  9. package/dist/cjs/hooks/queryClient.js +136 -92
  10. package/dist/cjs/hooks/useFileDownloadUrl.js +12 -36
  11. package/dist/cjs/hooks/useSessionSocket.js +81 -94
  12. package/dist/cjs/index.js +8 -3
  13. package/dist/cjs/utils/sessionHelpers.js +3 -1
  14. package/dist/cjs/utils/storageHelpers.js +36 -10
  15. package/dist/esm/.tsbuildinfo +1 -1
  16. package/dist/esm/WebOxyProvider.js +38 -1
  17. package/dist/esm/hooks/mutations/index.js +2 -0
  18. package/dist/esm/hooks/mutations/useAccountMutations.js +186 -44
  19. package/dist/esm/hooks/mutations/useAppData.js +128 -0
  20. package/dist/esm/hooks/queries/appDataQueryKeys.js +42 -0
  21. package/dist/esm/hooks/queries/index.js +3 -0
  22. package/dist/esm/hooks/queries/useAppData.js +82 -0
  23. package/dist/esm/hooks/queryClient.js +132 -89
  24. package/dist/esm/hooks/useFileDownloadUrl.js +11 -34
  25. package/dist/esm/hooks/useSessionSocket.js +81 -94
  26. package/dist/esm/index.js +3 -3
  27. package/dist/esm/utils/sessionHelpers.js +3 -1
  28. package/dist/esm/utils/storageHelpers.js +36 -10
  29. package/dist/types/.tsbuildinfo +1 -1
  30. package/dist/types/WebOxyProvider.d.ts +1 -1
  31. package/dist/types/hooks/mutations/index.d.ts +1 -0
  32. package/dist/types/hooks/mutations/useAccountMutations.d.ts +153 -9
  33. package/dist/types/hooks/mutations/useAppData.d.ts +47 -0
  34. package/dist/types/hooks/queries/appDataQueryKeys.d.ts +24 -0
  35. package/dist/types/hooks/queries/index.d.ts +2 -0
  36. package/dist/types/hooks/queries/useAccountQueries.d.ts +11 -7
  37. package/dist/types/hooks/queries/useAppData.d.ts +46 -0
  38. package/dist/types/hooks/queries/useSecurityQueries.d.ts +2 -2
  39. package/dist/types/hooks/queries/useServicesQueries.d.ts +7 -5
  40. package/dist/types/hooks/queryClient.d.ts +24 -10
  41. package/dist/types/hooks/useAssets.d.ts +1 -1
  42. package/dist/types/hooks/useFileDownloadUrl.d.ts +2 -6
  43. package/dist/types/index.d.ts +3 -3
  44. package/dist/types/utils/sessionHelpers.d.ts +3 -1
  45. package/package.json +22 -3
  46. package/src/WebOxyProvider.tsx +39 -1
  47. package/src/hooks/mutations/index.ts +3 -0
  48. package/src/hooks/mutations/useAccountMutations.ts +230 -57
  49. package/src/hooks/mutations/useAppData.ts +167 -0
  50. package/src/hooks/queries/appDataQueryKeys.ts +53 -0
  51. package/src/hooks/queries/index.ts +4 -0
  52. package/src/hooks/queries/useAppData.ts +105 -0
  53. package/src/hooks/queryClient.ts +140 -83
  54. package/src/hooks/useFileDownloadUrl.ts +15 -39
  55. package/src/hooks/useSessionSocket.ts +123 -91
  56. package/src/index.ts +7 -1
  57. package/src/utils/sessionHelpers.ts +3 -1
  58. package/src/utils/storageHelpers.ts +49 -10
@@ -60,7 +60,8 @@ function readTokenFromStorage() {
60
60
  try {
61
61
  return window.localStorage.getItem(LS_ACCESS_TOKEN_KEY);
62
62
  }
63
- catch {
63
+ catch (err) {
64
+ console.warn('[oxy.session-socket] localStorage read failed:', err);
64
65
  return null;
65
66
  }
66
67
  }
@@ -73,12 +74,12 @@ async function getSocketIO() {
73
74
  return null;
74
75
  _ioLoadAttempted = true;
75
76
  try {
76
- // eslint-disable-next-line @typescript-eslint/no-require-imports
77
- const mod = await Promise.resolve().then(() => __importStar(require('socket.io-client')));
78
- _io = (mod.io ?? mod.default);
77
+ const mod = (await Promise.resolve().then(() => __importStar(require('socket.io-client'))));
78
+ _io = mod.io ?? mod.default ?? null;
79
79
  return _io;
80
80
  }
81
- catch {
81
+ catch (err) {
82
+ console.warn('[oxy.session-socket] socket.io-client import failed:', err);
82
83
  debug.warn('socket.io-client is not installed. useSessionSocket will be disabled. Install it with: bun add socket.io-client');
83
84
  return null;
84
85
  }
@@ -145,7 +146,7 @@ function useSessionSocket(options) {
145
146
  // If no token is available at all, we skip the initial connect and let
146
147
  // the storage listener or retry logic connect when a token appears.
147
148
  const token = resolveToken();
148
- socketRef.current = ioFn(baseURL, {
149
+ const socket = ioFn(baseURL, {
149
150
  transports: ['websocket'],
150
151
  autoConnect: !!token, // don't auto-connect when there is no token
151
152
  auth: (cb) => {
@@ -162,7 +163,7 @@ function useSessionSocket(options) {
162
163
  cb({ token: resolved });
163
164
  },
164
165
  });
165
- const socket = socketRef.current;
166
+ socketRef.current = socket;
166
167
  // Server auto-joins the user to `user:<userId>` room on connection
167
168
  const handleConnect = () => {
168
169
  debug.log('Socket connected:', socket.id);
@@ -198,110 +199,94 @@ function useSessionSocket(options) {
198
199
  (0, queryKeys_1.invalidateSessionQueries)(queryClientRef.current);
199
200
  return Promise.resolve();
200
201
  };
202
+ const triggerLocalSignOut = async (toastMessage, errorContext) => {
203
+ if (onRemoteSignOutRef.current) {
204
+ onRemoteSignOutRef.current();
205
+ }
206
+ else {
207
+ sonner_1.toast.info(toastMessage);
208
+ }
209
+ // Clear local state since the server has already removed the session.
210
+ // Await so storage cleanup completes before any subsequent navigation.
211
+ try {
212
+ await clearSessionStateRef.current();
213
+ }
214
+ catch (error) {
215
+ if (process.env.NODE_ENV !== 'production') {
216
+ core_1.logger.error(`Failed to clear session state after ${errorContext}`, error instanceof Error ? error : new Error(String(error)), { component: 'useSessionSocket' });
217
+ }
218
+ }
219
+ };
201
220
  const handleSessionUpdate = async (data) => {
202
221
  debug.log('Received session_update:', data);
203
222
  const currentActiveSessionId = activeSessionIdRef.current;
204
223
  const deviceId = currentDeviceIdRef.current;
205
- // Handle different event types
206
- if (data.type === 'session_removed') {
207
- // Track removed session
208
- if (data.sessionId && onSessionRemovedRef.current) {
209
- onSessionRemovedRef.current(data.sessionId);
210
- }
211
- // If the removed sessionId matches the current activeSessionId, immediately clear state
212
- if (data.sessionId === currentActiveSessionId) {
213
- if (onRemoteSignOutRef.current) {
214
- onRemoteSignOutRef.current();
215
- }
216
- else {
217
- sonner_1.toast.info('You have been signed out remotely.');
224
+ // Strict whitelist. Every event type that may sign the user out must
225
+ // appear in the switch. Anything unknown falls through to `default`,
226
+ // which only logs in dev. This guards against future server-side event
227
+ // additions (e.g. `session_created` after a successful sign-in)
228
+ // accidentally triggering sign-out via a fallback branch that compares
229
+ // `data.sessionId === currentActiveSessionId` — that branch would match
230
+ // the user's NEW session id and trigger an instant remote sign-out
231
+ // toast on every login.
232
+ switch (data.type) {
233
+ case 'session_removed': {
234
+ if (data.sessionId && onSessionRemovedRef.current) {
235
+ onSessionRemovedRef.current(data.sessionId);
218
236
  }
219
- try {
220
- await clearSessionStateRef.current();
237
+ if (data.sessionId && data.sessionId === currentActiveSessionId) {
238
+ await triggerLocalSignOut('You have been signed out remotely.', 'session_removed');
221
239
  }
222
- catch (error) {
223
- if (process.env.NODE_ENV !== 'production') {
224
- core_1.logger.error('Failed to clear session state after session_removed', error instanceof Error ? error : new Error(String(error)), { component: 'useSessionSocket' });
225
- }
240
+ else {
241
+ refreshSessions();
226
242
  }
243
+ break;
227
244
  }
228
- else {
229
- refreshSessions();
230
- }
231
- }
232
- else if (data.type === 'device_removed') {
233
- // Track all removed sessions from this device
234
- if (data.sessionIds && onSessionRemovedRef.current) {
235
- for (const sessionId of data.sessionIds) {
236
- onSessionRemovedRef.current(sessionId);
245
+ case 'device_removed': {
246
+ if (data.sessionIds && onSessionRemovedRef.current) {
247
+ for (const sessionId of data.sessionIds) {
248
+ onSessionRemovedRef.current(sessionId);
249
+ }
237
250
  }
238
- }
239
- // If the removed deviceId matches the current device, immediately clear state
240
- if (data.deviceId && data.deviceId === deviceId) {
241
- if (onRemoteSignOutRef.current) {
242
- onRemoteSignOutRef.current();
251
+ if (data.deviceId && deviceId && data.deviceId === deviceId) {
252
+ await triggerLocalSignOut('This device has been removed. You have been signed out.', 'device_removed');
243
253
  }
244
254
  else {
245
- sonner_1.toast.info('This device has been removed. You have been signed out.');
246
- }
247
- try {
248
- await clearSessionStateRef.current();
249
- }
250
- catch (error) {
251
- if (process.env.NODE_ENV !== 'production') {
252
- core_1.logger.error('Failed to clear session state after device_removed', error instanceof Error ? error : new Error(String(error)), { component: 'useSessionSocket' });
253
- }
255
+ refreshSessions();
254
256
  }
257
+ break;
255
258
  }
256
- else {
257
- refreshSessions();
258
- }
259
- }
260
- else if (data.type === 'sessions_removed') {
261
- // Track all removed sessions
262
- if (data.sessionIds && onSessionRemovedRef.current) {
263
- for (const sessionId of data.sessionIds) {
264
- onSessionRemovedRef.current(sessionId);
259
+ case 'sessions_removed': {
260
+ if (data.sessionIds && onSessionRemovedRef.current) {
261
+ for (const sessionId of data.sessionIds) {
262
+ onSessionRemovedRef.current(sessionId);
263
+ }
265
264
  }
266
- }
267
- // If the current activeSessionId is in the removed sessionIds list, immediately clear state
268
- if (data.sessionIds && currentActiveSessionId && data.sessionIds.includes(currentActiveSessionId)) {
269
- if (onRemoteSignOutRef.current) {
270
- onRemoteSignOutRef.current();
265
+ if (data.sessionIds &&
266
+ currentActiveSessionId &&
267
+ data.sessionIds.includes(currentActiveSessionId)) {
268
+ await triggerLocalSignOut('You have been signed out remotely.', 'sessions_removed');
271
269
  }
272
270
  else {
273
- sonner_1.toast.info('You have been signed out remotely.');
274
- }
275
- try {
276
- await clearSessionStateRef.current();
277
- }
278
- catch (error) {
279
- if (process.env.NODE_ENV !== 'production') {
280
- core_1.logger.error('Failed to clear session state after sessions_removed', error instanceof Error ? error : new Error(String(error)), { component: 'useSessionSocket' });
281
- }
271
+ refreshSessions();
282
272
  }
273
+ break;
283
274
  }
284
- else {
275
+ case 'session_created':
276
+ case 'session_update': {
277
+ // Lifecycle event for the current user. Just resync the sessions
278
+ // list — never sign out.
285
279
  refreshSessions();
280
+ break;
286
281
  }
287
- }
288
- else {
289
- // For other event types (e.g., session_created), refresh sessions
290
- refreshSessions();
291
- // If the current session was logged out (legacy behavior), handle it specially
292
- if (data.sessionId === currentActiveSessionId) {
293
- if (onRemoteSignOutRef.current) {
294
- onRemoteSignOutRef.current();
295
- }
296
- else {
297
- sonner_1.toast.info('You have been signed out remotely.');
298
- }
299
- try {
300
- await clearSessionStateRef.current();
301
- }
302
- catch (error) {
303
- debug.error('Failed to clear session state after session_update:', error);
282
+ default: {
283
+ if (process.env.NODE_ENV !== 'production') {
284
+ core_1.logger.warn('Unknown session socket event type', {
285
+ component: 'useSessionSocket',
286
+ type: data.type,
287
+ });
304
288
  }
289
+ break;
305
290
  }
306
291
  }
307
292
  };
@@ -329,12 +314,14 @@ function useSessionSocket(options) {
329
314
  if (authRetryTimer) {
330
315
  clearTimeout(authRetryTimer);
331
316
  }
332
- if (socketRef.current) {
317
+ const currentSocket = socketRef.current;
318
+ if (currentSocket) {
333
319
  // Remove cross-tab storage listener
334
- if (typeof window !== 'undefined' && socketRef.current.__oxyStorageHandler) {
335
- window.removeEventListener('storage', socketRef.current.__oxyStorageHandler);
320
+ const storageHandler = currentSocket.__oxyStorageHandler;
321
+ if (typeof window !== 'undefined' && storageHandler) {
322
+ window.removeEventListener('storage', storageHandler);
336
323
  }
337
- socketRef.current.disconnect();
324
+ currentSocket.disconnect();
338
325
  socketRef.current = null;
339
326
  }
340
327
  };
package/dist/cjs/index.js CHANGED
@@ -24,8 +24,8 @@
24
24
  * ```
25
25
  */
26
26
  Object.defineProperty(exports, "__esModule", { value: true });
27
- exports.useAssets = exports.useSessionSocket = exports.isWebBrowser = exports.useWebSSO = exports.createGenericMutation = exports.createProfileMutation = exports.useRemoveDevice = exports.useUpdateDeviceName = exports.useLogoutAll = exports.useLogoutSession = exports.useSwitchSession = exports.useUploadFile = exports.useUpdatePrivacySettings = exports.useUpdateAccountSettings = exports.useUploadAvatar = exports.useUpdateProfile = exports.useRecentSecurityActivity = exports.useSecurityActivity = exports.useSecurityInfo = exports.useUserDevices = exports.useDeviceSessions = exports.useSession = exports.useSessions = exports.usePrivacySettings = exports.useUsersBySessions = exports.useUserByUsername = exports.useUserById = exports.useCurrentUser = exports.useUserProfiles = exports.useUserProfile = exports.useFollowStore = exports.useAccountLoadingSession = exports.useAccountError = exports.useAccountLoading = exports.useAccounts = exports.useAccountStore = exports.useIsAssetLinked = exports.useAssetUsageCount = exports.useAssetsByEntity = exports.useAssetsByApp = exports.useAssetErrors = exports.useAssetLoading = exports.useUploadProgress = exports.useAsset = exports.useAssetsStore = exports.useAssetStore = exports.useAuthStore = exports.useAuth = exports.useWebOxy = exports.WebOxyProvider = void 0;
28
- exports.extractErrorMessage = exports.isTimeoutOrNetworkError = exports.isInvalidSessionError = exports.handleAuthError = exports.useFileFiltering = exports.useFollowerCounts = exports.useFollow = exports.setOxyFileUrlInstance = exports.useFileDownloadUrl = exports.setOxyAssetInstance = void 0;
27
+ exports.useDeleteAppData = exports.useSetAppData = exports.useRemoveDevice = exports.useUpdateDeviceName = exports.useLogoutAll = exports.useLogoutSession = exports.useSwitchSession = exports.useUploadFile = exports.useUpdatePrivacySettings = exports.useUpdateAccountSettings = exports.useUploadAvatar = exports.useUpdateProfile = exports.isMissingAppDataEndpointError = exports.appDataQueryKeys = exports.useAppDataNamespace = exports.useAppData = exports.useRecentSecurityActivity = exports.useSecurityActivity = exports.useSecurityInfo = exports.useUserDevices = exports.useDeviceSessions = exports.useSession = exports.useSessions = exports.usePrivacySettings = exports.useUsersBySessions = exports.useUserByUsername = exports.useUserById = exports.useCurrentUser = exports.useUserProfiles = exports.useUserProfile = exports.useFollowStore = exports.useAccountLoadingSession = exports.useAccountError = exports.useAccountLoading = exports.useAccounts = exports.useAccountStore = exports.useIsAssetLinked = exports.useAssetUsageCount = exports.useAssetsByEntity = exports.useAssetsByApp = exports.useAssetErrors = exports.useAssetLoading = exports.useUploadProgress = exports.useAsset = exports.useAssetsStore = exports.useAssetStore = exports.useAuthStore = exports.useAuth = exports.useWebOxy = exports.WebOxyProvider = void 0;
28
+ exports.extractErrorMessage = exports.isTimeoutOrNetworkError = exports.isInvalidSessionError = exports.handleAuthError = exports.useFileFiltering = exports.useFollowerCounts = exports.useFollow = exports.useFileDownloadUrl = exports.setOxyAssetInstance = exports.useAssets = exports.useSessionSocket = exports.isWebBrowser = exports.useWebSSO = exports.createGenericMutation = exports.createProfileMutation = void 0;
29
29
  // --- Provider & Hooks ---
30
30
  var WebOxyProvider_1 = require("./WebOxyProvider");
31
31
  Object.defineProperty(exports, "WebOxyProvider", { enumerable: true, get: function () { return WebOxyProvider_1.WebOxyProvider; } });
@@ -69,6 +69,10 @@ Object.defineProperty(exports, "useUserDevices", { enumerable: true, get: functi
69
69
  Object.defineProperty(exports, "useSecurityInfo", { enumerable: true, get: function () { return queries_1.useSecurityInfo; } });
70
70
  Object.defineProperty(exports, "useSecurityActivity", { enumerable: true, get: function () { return queries_1.useSecurityActivity; } });
71
71
  Object.defineProperty(exports, "useRecentSecurityActivity", { enumerable: true, get: function () { return queries_1.useRecentSecurityActivity; } });
72
+ Object.defineProperty(exports, "useAppData", { enumerable: true, get: function () { return queries_1.useAppData; } });
73
+ Object.defineProperty(exports, "useAppDataNamespace", { enumerable: true, get: function () { return queries_1.useAppDataNamespace; } });
74
+ Object.defineProperty(exports, "appDataQueryKeys", { enumerable: true, get: function () { return queries_1.appDataQueryKeys; } });
75
+ Object.defineProperty(exports, "isMissingAppDataEndpointError", { enumerable: true, get: function () { return queries_1.isMissingAppDataEndpointError; } });
72
76
  // --- Mutation Hooks ---
73
77
  var mutations_1 = require("./hooks/mutations");
74
78
  Object.defineProperty(exports, "useUpdateProfile", { enumerable: true, get: function () { return mutations_1.useUpdateProfile; } });
@@ -81,6 +85,8 @@ Object.defineProperty(exports, "useLogoutSession", { enumerable: true, get: func
81
85
  Object.defineProperty(exports, "useLogoutAll", { enumerable: true, get: function () { return mutations_1.useLogoutAll; } });
82
86
  Object.defineProperty(exports, "useUpdateDeviceName", { enumerable: true, get: function () { return mutations_1.useUpdateDeviceName; } });
83
87
  Object.defineProperty(exports, "useRemoveDevice", { enumerable: true, get: function () { return mutations_1.useRemoveDevice; } });
88
+ Object.defineProperty(exports, "useSetAppData", { enumerable: true, get: function () { return mutations_1.useSetAppData; } });
89
+ Object.defineProperty(exports, "useDeleteAppData", { enumerable: true, get: function () { return mutations_1.useDeleteAppData; } });
84
90
  var mutationFactory_1 = require("./hooks/mutations/mutationFactory");
85
91
  Object.defineProperty(exports, "createProfileMutation", { enumerable: true, get: function () { return mutationFactory_1.createProfileMutation; } });
86
92
  Object.defineProperty(exports, "createGenericMutation", { enumerable: true, get: function () { return mutationFactory_1.createGenericMutation; } });
@@ -95,7 +101,6 @@ Object.defineProperty(exports, "useAssets", { enumerable: true, get: function ()
95
101
  Object.defineProperty(exports, "setOxyAssetInstance", { enumerable: true, get: function () { return useAssets_1.setOxyAssetInstance; } });
96
102
  var useFileDownloadUrl_1 = require("./hooks/useFileDownloadUrl");
97
103
  Object.defineProperty(exports, "useFileDownloadUrl", { enumerable: true, get: function () { return useFileDownloadUrl_1.useFileDownloadUrl; } });
98
- Object.defineProperty(exports, "setOxyFileUrlInstance", { enumerable: true, get: function () { return useFileDownloadUrl_1.setOxyFileUrlInstance; } });
99
104
  var useFollow_1 = require("./hooks/useFollow");
100
105
  Object.defineProperty(exports, "useFollow", { enumerable: true, get: function () { return useFollow_1.useFollow; } });
101
106
  Object.defineProperty(exports, "useFollowerCounts", { enumerable: true, get: function () { return useFollow_1.useFollowerCounts; } });
@@ -25,7 +25,9 @@ const mapSessionsToClient = (sessions, fallbackDeviceId, fallbackUserId) => {
25
25
  };
26
26
  exports.mapSessionsToClient = mapSessionsToClient;
27
27
  /**
28
- * Fetch device sessions with fallback to the legacy session endpoint when needed.
28
+ * Fetch device sessions, falling back to the per-user session endpoint
29
+ * if the device endpoint is unavailable (older API versions or disabled
30
+ * device-grouping feature flag).
29
31
  *
30
32
  * @param oxyServices - Oxy service instance
31
33
  * @param sessionId - Session identifier to fetch
@@ -41,7 +41,8 @@ const MEMORY_STORAGE = () => {
41
41
  const store = new Map();
42
42
  return {
43
43
  async getItem(key) {
44
- return store.has(key) ? store.get(key) : null;
44
+ const value = store.get(key);
45
+ return value === undefined ? null : value;
45
46
  },
46
47
  async setItem(key, value) {
47
48
  store.set(key, value);
@@ -67,7 +68,8 @@ const createWebStorage = () => {
67
68
  try {
68
69
  return window.localStorage.getItem(key);
69
70
  }
70
- catch {
71
+ catch (err) {
72
+ console.warn('[oxy.storage] localStorage.getItem failed:', err);
71
73
  return null;
72
74
  }
73
75
  },
@@ -75,29 +77,44 @@ const createWebStorage = () => {
75
77
  try {
76
78
  window.localStorage.setItem(key, value);
77
79
  }
78
- catch {
79
- // Ignore quota or access issues for now.
80
+ catch (err) {
81
+ // Quota exceeded or storage disabled (e.g., Safari private mode).
82
+ // Surface to logs so it is debuggable, but do not throw so callers
83
+ // can keep functioning with degraded persistence.
84
+ console.warn('[oxy.storage] localStorage.setItem failed:', err);
80
85
  }
81
86
  },
82
87
  async removeItem(key) {
83
88
  try {
84
89
  window.localStorage.removeItem(key);
85
90
  }
86
- catch {
87
- // Ignore failures.
91
+ catch (err) {
92
+ console.warn('[oxy.storage] localStorage.removeItem failed:', err);
88
93
  }
89
94
  },
90
95
  async clear() {
91
96
  try {
92
97
  window.localStorage.clear();
93
98
  }
94
- catch {
95
- // Ignore failures.
99
+ catch (err) {
100
+ console.warn('[oxy.storage] localStorage.clear failed:', err);
96
101
  }
97
102
  },
98
103
  };
99
104
  };
100
105
  let asyncStorageInstance = null;
106
+ /**
107
+ * Type guard verifying that an imported value exposes the AsyncStorage API.
108
+ */
109
+ const isAsyncStorageLike = (value) => {
110
+ if (typeof value !== 'object' || value === null)
111
+ return false;
112
+ const candidate = value;
113
+ return (typeof candidate.getItem === 'function' &&
114
+ typeof candidate.setItem === 'function' &&
115
+ typeof candidate.removeItem === 'function' &&
116
+ typeof candidate.clear === 'function');
117
+ };
101
118
  /**
102
119
  * Lazily import React Native AsyncStorage implementation.
103
120
  */
@@ -108,8 +125,17 @@ const createNativeStorage = async () => {
108
125
  try {
109
126
  // Variable indirection prevents bundlers (Vite, webpack) from statically resolving this
110
127
  const moduleName = '@react-native-async-storage/async-storage';
111
- const asyncStorageModule = await Promise.resolve(`${moduleName}`).then(s => __importStar(require(s)));
112
- asyncStorageInstance = asyncStorageModule.default;
128
+ const asyncStorageModule = (await Promise.resolve(`${moduleName}`).then(s => __importStar(require(s))));
129
+ const candidate = asyncStorageModule.default;
130
+ if (!isAsyncStorageLike(candidate)) {
131
+ throw new Error('AsyncStorage default export does not match expected API');
132
+ }
133
+ asyncStorageInstance = {
134
+ getItem: (key) => candidate.getItem(key),
135
+ setItem: (key, value) => candidate.setItem(key, value),
136
+ removeItem: (key) => candidate.removeItem(key),
137
+ clear: () => candidate.clear(),
138
+ };
113
139
  return asyncStorageInstance;
114
140
  }
115
141
  catch (error) {