@oxyhq/auth 2.0.3 → 2.0.5

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 (42) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/WebOxyProvider.js +37 -0
  3. package/dist/cjs/hooks/mutations/useAccountMutations.js +185 -43
  4. package/dist/cjs/hooks/queryClient.js +136 -92
  5. package/dist/cjs/hooks/useFileDownloadUrl.js +12 -36
  6. package/dist/cjs/hooks/useSessionSocket.js +250 -115
  7. package/dist/cjs/index.js +13 -3
  8. package/dist/cjs/stores/accountStore.js +2 -2
  9. package/dist/cjs/utils/sessionHelpers.js +4 -2
  10. package/dist/cjs/utils/storageHelpers.js +37 -11
  11. package/dist/esm/.tsbuildinfo +1 -1
  12. package/dist/esm/WebOxyProvider.js +38 -1
  13. package/dist/esm/hooks/mutations/useAccountMutations.js +186 -44
  14. package/dist/esm/hooks/queryClient.js +132 -89
  15. package/dist/esm/hooks/useFileDownloadUrl.js +11 -34
  16. package/dist/esm/hooks/useSessionSocket.js +217 -112
  17. package/dist/esm/index.js +4 -1
  18. package/dist/esm/stores/accountStore.js +2 -2
  19. package/dist/esm/utils/sessionHelpers.js +4 -2
  20. package/dist/esm/utils/storageHelpers.js +37 -11
  21. package/dist/types/.tsbuildinfo +1 -1
  22. package/dist/types/WebOxyProvider.d.ts +1 -1
  23. package/dist/types/hooks/mutations/useAccountMutations.d.ts +153 -9
  24. package/dist/types/hooks/queries/useAccountQueries.d.ts +11 -7
  25. package/dist/types/hooks/queries/useSecurityQueries.d.ts +2 -2
  26. package/dist/types/hooks/queries/useServicesQueries.d.ts +7 -5
  27. package/dist/types/hooks/queryClient.d.ts +24 -10
  28. package/dist/types/hooks/useAssets.d.ts +1 -1
  29. package/dist/types/hooks/useFileDownloadUrl.d.ts +2 -6
  30. package/dist/types/index.d.ts +5 -1
  31. package/dist/types/utils/sessionHelpers.d.ts +3 -1
  32. package/package.json +29 -5
  33. package/src/WebOxyProvider.tsx +39 -1
  34. package/src/hooks/mutations/useAccountMutations.ts +230 -57
  35. package/src/hooks/queryClient.ts +140 -83
  36. package/src/hooks/useFileDownloadUrl.ts +15 -39
  37. package/src/hooks/useSessionSocket.ts +273 -112
  38. package/src/index.ts +13 -1
  39. package/src/stores/accountStore.ts +2 -2
  40. package/src/utils/sessionHelpers.ts +4 -2
  41. package/src/utils/storageHelpers.ts +50 -11
  42. package/src/global.d.ts +0 -1
@@ -59,7 +59,7 @@ export interface WebOxyProviderProps {
59
59
  * }
60
60
  * ```
61
61
  */
62
- export declare function WebOxyProvider({ children, baseURL, authWebUrl, onAuthStateChange, onError, preferredAuthMethod, skipAutoCheck, }: WebOxyProviderProps): import("react/jsx-runtime").JSX.Element;
62
+ export declare function WebOxyProvider({ children, baseURL, authWebUrl, onAuthStateChange, onError, preferredAuthMethod, skipAutoCheck, }: WebOxyProviderProps): import("react/jsx-runtime").JSX.Element | null;
63
63
  /**
64
64
  * Hook to access the full Web Oxy context.
65
65
  */
@@ -1,4 +1,4 @@
1
- import type { User } from '@oxyhq/core';
1
+ import type { PrivacySettings, User } from '@oxyhq/core';
2
2
  /**
3
3
  * Update user profile with optimistic updates and offline queue support
4
4
  */
@@ -12,19 +12,163 @@ export declare const useUploadAvatar: () => import("@tanstack/react-query").UseM
12
12
  previousUser: User | undefined;
13
13
  }>;
14
14
  /**
15
- * Update account settings
15
+ * Variables accepted by the `useUpdateAccountSettings` mutation.
16
+ *
17
+ * `currentUser` is captured at dispatch time so the rebuilt user object the
18
+ * mutation returns is computed against a stable snapshot — NOT the cache
19
+ * value at the moment the API call settles. Reading from the cache inside
20
+ * `mutationFn` would race with sibling optimistic updates: a concurrent
21
+ * write could already have overwritten the cache by the time the privacy
22
+ * update returns, causing the rebuilt user to clobber the sibling's
23
+ * optimistic value.
16
24
  */
17
- export declare const useUpdateAccountSettings: () => import("@tanstack/react-query").UseMutationResult<any, Error, Record<string, any>, {
18
- previousUser: User | undefined;
19
- }>;
25
+ interface UpdateAccountSettingsVariables {
26
+ updates: Partial<PrivacySettings>;
27
+ currentUser: User;
28
+ }
29
+ /**
30
+ * Update account settings (privacy preferences).
31
+ *
32
+ * Privacy settings are not part of the `PUT /users/me` allow-list; the API
33
+ * would silently drop them. Route through `updatePrivacySettings` so the
34
+ * dedicated `PATCH /privacy/:id/privacy` endpoint performs a dot-path merge
35
+ * and returns the updated `privacySettings` object.
36
+ *
37
+ * The returned object exposes the standard mutation surface PLUS a
38
+ * convenience `mutate(updates)` / `mutateAsync(updates)` that snapshots
39
+ * the current user from `useWebOxy()` at dispatch time.
40
+ */
41
+ export declare const useUpdateAccountSettings: () => {
42
+ mutate: (updates: Partial<PrivacySettings>) => void;
43
+ mutateAsync: (updates: Partial<PrivacySettings>) => Promise<User>;
44
+ data: undefined;
45
+ variables: undefined;
46
+ error: null;
47
+ isError: false;
48
+ isIdle: true;
49
+ isPending: false;
50
+ isSuccess: false;
51
+ status: "idle";
52
+ reset: () => void;
53
+ context: {
54
+ previousUser: User | undefined;
55
+ } | undefined;
56
+ failureCount: number;
57
+ failureReason: Error | null;
58
+ isPaused: boolean;
59
+ submittedAt: number;
60
+ } | {
61
+ mutate: (updates: Partial<PrivacySettings>) => void;
62
+ mutateAsync: (updates: Partial<PrivacySettings>) => Promise<User>;
63
+ data: undefined;
64
+ variables: UpdateAccountSettingsVariables;
65
+ error: null;
66
+ isError: false;
67
+ isIdle: false;
68
+ isPending: true;
69
+ isSuccess: false;
70
+ status: "pending";
71
+ reset: () => void;
72
+ context: {
73
+ previousUser: User | undefined;
74
+ } | undefined;
75
+ failureCount: number;
76
+ failureReason: Error | null;
77
+ isPaused: boolean;
78
+ submittedAt: number;
79
+ } | {
80
+ mutate: (updates: Partial<PrivacySettings>) => void;
81
+ mutateAsync: (updates: Partial<PrivacySettings>) => Promise<User>;
82
+ data: undefined;
83
+ error: Error;
84
+ variables: UpdateAccountSettingsVariables;
85
+ isError: true;
86
+ isIdle: false;
87
+ isPending: false;
88
+ isSuccess: false;
89
+ status: "error";
90
+ reset: () => void;
91
+ context: {
92
+ previousUser: User | undefined;
93
+ } | undefined;
94
+ failureCount: number;
95
+ failureReason: Error | null;
96
+ isPaused: boolean;
97
+ submittedAt: number;
98
+ } | {
99
+ mutate: (updates: Partial<PrivacySettings>) => void;
100
+ mutateAsync: (updates: Partial<PrivacySettings>) => Promise<User>;
101
+ data: {
102
+ privacySettings: PrivacySettings;
103
+ id: string;
104
+ publicKey: string;
105
+ username: string;
106
+ email?: string;
107
+ avatar?: string;
108
+ color?: string;
109
+ name?: {
110
+ first?: string;
111
+ last?: string;
112
+ full?: string;
113
+ [key: string]: unknown;
114
+ };
115
+ bio?: string;
116
+ karma?: number;
117
+ location?: string;
118
+ website?: string;
119
+ createdAt?: string;
120
+ updatedAt?: string;
121
+ links?: Array<{
122
+ title?: string;
123
+ description?: string;
124
+ image?: string;
125
+ link: string;
126
+ }>;
127
+ _count?: {
128
+ followers?: number;
129
+ following?: number;
130
+ };
131
+ accountExpiresAfterInactivityDays?: number | null;
132
+ type?: "local" | "federated" | "agent" | "automated";
133
+ isFederated?: boolean;
134
+ isAgent?: boolean;
135
+ isAutomated?: boolean;
136
+ instance?: string;
137
+ federation?: {
138
+ actorUri?: string;
139
+ domain?: string;
140
+ actorId?: string;
141
+ };
142
+ automation?: {
143
+ ownerId?: string;
144
+ };
145
+ isManagedAccount?: boolean;
146
+ managedBy?: string;
147
+ };
148
+ error: null;
149
+ variables: UpdateAccountSettingsVariables;
150
+ isError: false;
151
+ isIdle: false;
152
+ isPending: false;
153
+ isSuccess: true;
154
+ status: "success";
155
+ reset: () => void;
156
+ context: {
157
+ previousUser: User | undefined;
158
+ } | undefined;
159
+ failureCount: number;
160
+ failureReason: Error | null;
161
+ isPaused: boolean;
162
+ submittedAt: number;
163
+ };
20
164
  /**
21
165
  * Update privacy settings with optimistic updates and authentication handling
22
166
  */
23
- export declare const useUpdatePrivacySettings: () => import("@tanstack/react-query").UseMutationResult<Record<string, unknown>, Error, {
24
- settings: Record<string, any>;
167
+ export declare const useUpdatePrivacySettings: () => import("@tanstack/react-query").UseMutationResult<PrivacySettings, Error, {
168
+ settings: Partial<PrivacySettings>;
25
169
  userId?: string;
26
170
  }, {
27
- previousPrivacySettings: unknown;
171
+ previousPrivacySettings: PrivacySettings | undefined;
28
172
  previousUser: User | undefined;
29
173
  } | undefined>;
30
174
  /** Uploaded file data structure from API */
@@ -57,7 +201,7 @@ interface UploadResult {
57
201
  export declare const useUploadFile: () => import("@tanstack/react-query").UseMutationResult<UploadResult, Error, {
58
202
  file: File;
59
203
  visibility?: "private" | "public" | "unlisted";
60
- metadata?: Record<string, any>;
204
+ metadata?: Record<string, unknown>;
61
205
  onProgress?: (progress: number) => void;
62
206
  }, unknown>;
63
207
  export {};
@@ -1,42 +1,46 @@
1
+ import type { User } from '@oxyhq/core';
1
2
  /**
2
3
  * Get user profile by session ID
3
4
  */
4
5
  export declare const useUserProfile: (sessionId: string | null, options?: {
5
6
  enabled?: boolean;
6
- }) => import("@tanstack/react-query").UseQueryResult<any, Error>;
7
+ }) => import("@tanstack/react-query").UseQueryResult<NoInfer<User>, Error>;
7
8
  /**
8
9
  * Get multiple user profiles by session IDs (batch query)
9
10
  */
10
11
  export declare const useUserProfiles: (sessionIds: string[], options?: {
11
12
  enabled?: boolean;
12
- }) => import("@tanstack/react-query").UseQueryResult<any, Error>[];
13
+ }) => import("@tanstack/react-query").UseQueryResult<User | null, Error>[];
13
14
  /**
14
15
  * Get current authenticated user
15
16
  */
16
17
  export declare const useCurrentUser: (options?: {
17
18
  enabled?: boolean;
18
- }) => import("@tanstack/react-query").UseQueryResult<any, Error>;
19
+ }) => import("@tanstack/react-query").UseQueryResult<NoInfer<User>, Error>;
19
20
  /**
20
21
  * Get user by ID
21
22
  */
22
23
  export declare const useUserById: (userId: string | null, options?: {
23
24
  enabled?: boolean;
24
- }) => import("@tanstack/react-query").UseQueryResult<any, Error>;
25
+ }) => import("@tanstack/react-query").UseQueryResult<NoInfer<User>, Error>;
25
26
  /**
26
27
  * Get user profile by username
27
28
  */
28
29
  export declare const useUserByUsername: (username: string | null, options?: {
29
30
  enabled?: boolean;
30
- }) => import("@tanstack/react-query").UseQueryResult<any, Error>;
31
+ }) => import("@tanstack/react-query").UseQueryResult<NoInfer<User>, Error>;
31
32
  /**
32
33
  * Batch get users by session IDs (optimized single API call)
33
34
  */
34
35
  export declare const useUsersBySessions: (sessionIds: string[], options?: {
35
36
  enabled?: boolean;
36
- }) => import("@tanstack/react-query").UseQueryResult<any, Error>;
37
+ }) => import("@tanstack/react-query").UseQueryResult<NoInfer<{
38
+ sessionId: string;
39
+ user: User | null;
40
+ }[]>, Error>;
37
41
  /**
38
42
  * Get privacy settings for a user
39
43
  */
40
44
  export declare const usePrivacySettings: (userId?: string, options?: {
41
45
  enabled?: boolean;
42
- }) => import("@tanstack/react-query").UseQueryResult<unknown, Error>;
46
+ }) => import("@tanstack/react-query").UseQueryResult<NoInfer<import("@oxyhq/core").PrivacySettings>, Error>;
@@ -7,8 +7,8 @@ export declare const useSecurityActivity: (options?: {
7
7
  offset?: number;
8
8
  eventType?: SecurityEventType;
9
9
  enabled?: boolean;
10
- }) => import("@tanstack/react-query").UseQueryResult<any, Error>;
10
+ }) => import("@tanstack/react-query").UseQueryResult<NoInfer<import("@oxyhq/core").SecurityActivityResponse>, Error>;
11
11
  /**
12
12
  * Get recent security activity (convenience hook)
13
13
  */
14
- export declare const useRecentSecurityActivity: (limit?: number) => import("@tanstack/react-query").UseQueryResult<SecurityActivity[], Error>;
14
+ export declare const useRecentSecurityActivity: (limit?: number) => import("@tanstack/react-query").UseQueryResult<NoInfer<SecurityActivity[]>, Error>;
@@ -4,28 +4,30 @@ import type { ClientSession } from '@oxyhq/core';
4
4
  */
5
5
  export declare const useSessions: (userId?: string, options?: {
6
6
  enabled?: boolean;
7
- }) => import("@tanstack/react-query").UseQueryResult<ClientSession[], Error>;
7
+ }) => import("@tanstack/react-query").UseQueryResult<NoInfer<ClientSession[]>, Error>;
8
8
  /**
9
9
  * Get specific session by ID
10
10
  */
11
11
  export declare const useSession: (sessionId: string | null, options?: {
12
12
  enabled?: boolean;
13
- }) => import("@tanstack/react-query").UseQueryResult<ClientSession, Error>;
13
+ }) => import("@tanstack/react-query").UseQueryResult<NoInfer<ClientSession>, Error>;
14
14
  /**
15
15
  * Get device sessions for the current active session
16
16
  */
17
17
  export declare const useDeviceSessions: (options?: {
18
18
  enabled?: boolean;
19
- }) => import("@tanstack/react-query").UseQueryResult<any, Error>;
19
+ }) => import("@tanstack/react-query").UseQueryResult<NoInfer<any[]>, Error>;
20
20
  /**
21
21
  * Get user devices
22
22
  */
23
23
  export declare const useUserDevices: (options?: {
24
24
  enabled?: boolean;
25
- }) => import("@tanstack/react-query").UseQueryResult<unknown, Error>;
25
+ }) => import("@tanstack/react-query").UseQueryResult<NoInfer<any[]>, Error>;
26
26
  /**
27
27
  * Get security information
28
28
  */
29
29
  export declare const useSecurityInfo: (options?: {
30
30
  enabled?: boolean;
31
- }) => import("@tanstack/react-query").UseQueryResult<any, Error>;
31
+ }) => import("@tanstack/react-query").UseQueryResult<NoInfer<{
32
+ recoveryEmail: string | null;
33
+ }>, Error>;
@@ -1,18 +1,32 @@
1
- import { QueryClient } from '@tanstack/react-query';
2
- import type { StorageInterface } from '../utils/storageHelpers';
3
1
  /**
4
- * Custom persistence adapter for TanStack Query using our StorageInterface
2
+ * Web QueryClient with offline-first defaults + localStorage persistence.
3
+ *
4
+ * Mirrors the persistence behaviour in `@oxyhq/services/queryClient` so
5
+ * web auth apps (FedCM, popup, redirect flows) survive a page reload with
6
+ * cached identity + paused mutations intact.
7
+ *
8
+ * Persistence is opt-in via `attachQueryPersistence(...)` so SSR callers
9
+ * (Next.js getServerSideProps, Vite SSR, tests) can create a stateless
10
+ * client without touching `window`.
5
11
  */
12
+ import { QueryClient } from '@tanstack/react-query';
13
+ import { type PersistedClient } from '@tanstack/react-query-persist-client';
14
+ import type { StorageInterface } from '../utils/storageHelpers';
6
15
  export declare const createPersistenceAdapter: (storage: StorageInterface) => {
7
- persistClient: (client: any) => Promise<void>;
8
- restoreClient: () => Promise<any>;
16
+ persistClient: (client: unknown) => Promise<void>;
17
+ restoreClient: () => Promise<unknown>;
9
18
  removeClient: () => Promise<void>;
10
19
  };
20
+ export declare const createQueryClient: () => QueryClient;
21
+ export interface AttachPersistenceResult {
22
+ restored: Promise<void>;
23
+ unsubscribe: () => void;
24
+ }
11
25
  /**
12
- * Create a QueryClient with offline-first configuration
13
- */
14
- export declare const createQueryClient: (storage?: StorageInterface | null) => QueryClient;
15
- /**
16
- * Clear persisted query cache
26
+ * Wire `persistQueryClient` to browser `localStorage` (or a no-op when not
27
+ * in a browser). Returns the restore promise so consumers can `await` it
28
+ * before exposing the client to <Suspense> boundaries.
17
29
  */
30
+ export declare const attachQueryPersistence: (queryClient: QueryClient) => AttachPersistenceResult;
18
31
  export declare const clearQueryCache: (storage: StorageInterface) => Promise<void>;
32
+ export type { PersistedClient };
@@ -24,7 +24,7 @@ export declare const useAssets: () => {
24
24
  getAsset: (assetId: string) => Promise<Asset>;
25
25
  deleteAsset: (assetId: string, force?: boolean) => Promise<void>;
26
26
  restore: (assetId: string) => Promise<void>;
27
- getVariants: (assetId: string) => Promise<any>;
27
+ getVariants: (assetId: string) => Promise<import("@oxyhq/core").AssetVariant[]>;
28
28
  getAssetsByApp: (app: string) => Asset[];
29
29
  getAssetsByEntity: (app: string, entityType: string, entityId: string) => Asset[];
30
30
  getAssetUsageCount: (assetId: string) => number;
@@ -1,5 +1,4 @@
1
- import { OxyServices } from '@oxyhq/core';
2
- export declare const setOxyFileUrlInstance: (instance: OxyServices) => void;
1
+ import type { OxyServices } from '@oxyhq/core';
3
2
  export interface UseFileDownloadUrlOptions {
4
3
  variant?: string;
5
4
  expiresIn?: number;
@@ -12,10 +11,7 @@ export interface UseFileDownloadUrlResult {
12
11
  /**
13
12
  * Hook to resolve a file's download URL asynchronously.
14
13
  *
15
- * Prefers the provided `oxyServices` instance, falls back to the module-level
16
- * singleton set via `setOxyFileUrlInstance`.
17
- *
18
14
  * Uses `getFileDownloadUrlAsync` first, falling back to the synchronous
19
15
  * `getFileDownloadUrl` if the async call fails.
20
16
  */
21
- export declare const useFileDownloadUrl: (fileIdOrServices?: string | OxyServices | null, fileIdOrOptions?: string | UseFileDownloadUrlOptions | null, maybeOptions?: UseFileDownloadUrlOptions) => UseFileDownloadUrlResult;
17
+ export declare const useFileDownloadUrl: (oxyServices: OxyServices | null | undefined, fileId: string | null | undefined, options?: UseFileDownloadUrlOptions) => UseFileDownloadUrlResult;
@@ -26,14 +26,18 @@ export { WebOxyProvider, useWebOxy, useAuth } from './WebOxyProvider';
26
26
  export type { WebOxyProviderProps, WebAuthState, WebAuthActions, WebOxyContextValue, } from './WebOxyProvider';
27
27
  export { useAuthStore } from './stores/authStore';
28
28
  export { useAssetStore, useAssets as useAssetsStore, useAsset, useUploadProgress, useAssetLoading, useAssetErrors, useAssetsByApp, useAssetsByEntity, useAssetUsageCount, useIsAssetLinked, } from './stores/assetStore';
29
+ export { useAccountStore, useAccounts, useAccountLoading, useAccountError, useAccountLoadingSession, } from './stores/accountStore';
30
+ export type { QuickAccount } from './stores/accountStore';
31
+ export { useFollowStore, } from './stores/followStore';
29
32
  export { useUserProfile, useUserProfiles, useCurrentUser, useUserById, useUserByUsername, useUsersBySessions, usePrivacySettings, useSessions, useSession, useDeviceSessions, useUserDevices, useSecurityInfo, useSecurityActivity, useRecentSecurityActivity, } from './hooks/queries';
30
33
  export { useUpdateProfile, useUploadAvatar, useUpdateAccountSettings, useUpdatePrivacySettings, useUploadFile, useSwitchSession, useLogoutSession, useLogoutAll, useUpdateDeviceName, useRemoveDevice, } from './hooks/mutations';
31
34
  export { createProfileMutation, createGenericMutation, } from './hooks/mutations/mutationFactory';
32
35
  export type { ProfileMutationConfig, GenericMutationConfig, } from './hooks/mutations/mutationFactory';
36
+ export { useWebSSO, isWebBrowser } from './hooks/useWebSSO';
33
37
  export { useSessionSocket } from './hooks/useSessionSocket';
34
38
  export type { UseSessionSocketOptions } from './hooks/useSessionSocket';
35
39
  export { useAssets, setOxyAssetInstance } from './hooks/useAssets';
36
- export { useFileDownloadUrl, setOxyFileUrlInstance } from './hooks/useFileDownloadUrl';
40
+ export { useFileDownloadUrl } from './hooks/useFileDownloadUrl';
37
41
  export { useFollow, useFollowerCounts } from './hooks/useFollow';
38
42
  export { useFileFiltering } from './hooks/useFileFiltering';
39
43
  export type { ViewMode, SortBy, SortOrder } from './hooks/useFileFiltering';
@@ -45,7 +45,9 @@ export interface SessionValidationResult {
45
45
  */
46
46
  export declare const mapSessionsToClient: (sessions: DeviceSession[], fallbackDeviceId?: string, fallbackUserId?: string) => ClientSession[];
47
47
  /**
48
- * Fetch device sessions with fallback to the legacy session endpoint when needed.
48
+ * Fetch device sessions, falling back to the per-user session endpoint
49
+ * if the device endpoint is unavailable (older API versions or disabled
50
+ * device-grouping feature flag).
49
51
  *
50
52
  * @param oxyServices - Oxy service instance
51
53
  * @param sessionId - Session identifier to fetch
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/auth",
3
- "version": "2.0.3",
3
+ "version": "2.0.5",
4
4
  "description": "OxyHQ Web Authentication SDK — headless auth with React hooks for Next.js, Vite, and web apps",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -50,22 +50,46 @@
50
50
  "build:types": "tsc -p tsconfig.types.json",
51
51
  "clean": "rm -rf dist",
52
52
  "typescript": "tsc --noEmit",
53
- "lint": "biome lint --error-on-warnings ./src"
53
+ "lint": "biome lint --error-on-warnings ./src",
54
+ "test": "jest --passWithNoTests",
55
+ "release": "rm -rf dist && bun run build && release-it"
56
+ },
57
+ "release-it": {
58
+ "git": {
59
+ "tagName": "@oxyhq/auth@${version}",
60
+ "tagAnnotation": "Release @oxyhq/auth@${version}",
61
+ "commitMessage": "chore(auth-sdk): release @oxyhq/auth@${version}"
62
+ },
63
+ "github": {
64
+ "release": true,
65
+ "releaseName": "@oxyhq/auth@${version}"
66
+ },
67
+ "npm": {
68
+ "publish": true
69
+ }
54
70
  },
55
71
  "dependencies": {
56
- "@tanstack/react-query": "^5.90.20",
57
- "socket.io-client": "^4.8.1",
72
+ "@tanstack/query-sync-storage-persister": "^5.100",
73
+ "@tanstack/react-query": "^5.100",
74
+ "@tanstack/react-query-persist-client": "^5.100",
58
75
  "sonner": "^2.0.4",
59
76
  "zustand": "^5.0.9"
60
77
  },
61
78
  "peerDependencies": {
62
79
  "@oxyhq/core": "*",
63
- "react": ">=18.0.0"
80
+ "react": ">=18.0.0",
81
+ "socket.io-client": "^4.8.1"
82
+ },
83
+ "peerDependenciesMeta": {
84
+ "socket.io-client": {
85
+ "optional": true
86
+ }
64
87
  },
65
88
  "devDependencies": {
66
89
  "@biomejs/biome": "^1.9.4",
67
90
  "@types/react": "^19.2.0",
68
91
  "@types/node": "^20.19.9",
92
+ "release-it": "^19.0.6",
69
93
  "typescript": "^5.9.2"
70
94
  }
71
95
  }
@@ -28,7 +28,7 @@ import type {
28
28
  ClientSession,
29
29
  } from '@oxyhq/core';
30
30
  import { QueryClientProvider } from '@tanstack/react-query';
31
- import { createQueryClient } from './hooks/queryClient';
31
+ import { attachQueryPersistence, createQueryClient } from './hooks/queryClient';
32
32
 
33
33
  export interface WebAuthState {
34
34
  user: User | null;
@@ -101,6 +101,29 @@ export function WebOxyProvider({
101
101
  const [authManager] = useState(() => createAuthManager(oxyServices, { autoRefresh: true }));
102
102
  const [queryClient] = useState(() => createQueryClient());
103
103
 
104
+ // Block first render until the persisted localStorage cache has been
105
+ // restored — mirrors the RN OxyProvider pattern. Without this gate the
106
+ // first paint observes an empty cache and any consumer reading
107
+ // `getQueryData(...)` synchronously (or using `placeholderData: 'previous'`
108
+ // gating) misses the persisted blob.
109
+ //
110
+ // Persistence is attached inside the same effect so we can hold a
111
+ // reference to the `restored` promise and only flip `isRestoring` to
112
+ // false once it settles (success OR failure). Detach on unmount so HMR
113
+ // doesn't leak subscriptions.
114
+ const [isRestoring, setIsRestoring] = useState(true);
115
+ useEffect(() => {
116
+ let mounted = true;
117
+ const { restored, unsubscribe } = attachQueryPersistence(queryClient);
118
+ restored.finally(() => {
119
+ if (mounted) setIsRestoring(false);
120
+ });
121
+ return () => {
122
+ mounted = false;
123
+ unsubscribe();
124
+ };
125
+ }, [queryClient]);
126
+
104
127
  // Auth state
105
128
  const [user, setUser] = useState<User | null>(null);
106
129
  const [isLoading, setIsLoading] = useState(!skipAutoCheck);
@@ -334,6 +357,21 @@ export function WebOxyProvider({
334
357
  signOut, isFedCMSupported, switchSession, clearSessionState,
335
358
  ]);
336
359
 
360
+ // Mirror the RN OxyProvider pattern: don't expose the QueryClient (or
361
+ // mount children) until the persisted cache has been restored. On the
362
+ // web this prevents the first paint from observing an empty
363
+ // localStorage-backed cache, which would otherwise force every
364
+ // identity/session/auth query to refetch from the network even when a
365
+ // fresh blob was available on disk.
366
+ //
367
+ // The restored promise is wired with `.finally(...)` upstream, so this
368
+ // unblocks on both success and failure within typically <50ms (sync
369
+ // localStorage read + JSON.parse). A safety net is unnecessary: the
370
+ // restore promise always settles synchronously after one microtask.
371
+ if (isRestoring) {
372
+ return null;
373
+ }
374
+
337
375
  return (
338
376
  <QueryClientProvider client={queryClient}>
339
377
  <WebOxyContext.Provider value={contextValue}>