@oxyhq/auth 1.0.0

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 (119) hide show
  1. package/README.md +56 -0
  2. package/dist/cjs/WebOxyProvider.js +287 -0
  3. package/dist/cjs/hooks/mutations/index.js +23 -0
  4. package/dist/cjs/hooks/mutations/mutationFactory.js +126 -0
  5. package/dist/cjs/hooks/mutations/useAccountMutations.js +275 -0
  6. package/dist/cjs/hooks/mutations/useServicesMutations.js +149 -0
  7. package/dist/cjs/hooks/queries/index.js +35 -0
  8. package/dist/cjs/hooks/queries/queryKeys.js +82 -0
  9. package/dist/cjs/hooks/queries/useAccountQueries.js +141 -0
  10. package/dist/cjs/hooks/queries/useSecurityQueries.js +45 -0
  11. package/dist/cjs/hooks/queries/useServicesQueries.js +113 -0
  12. package/dist/cjs/hooks/queryClient.js +110 -0
  13. package/dist/cjs/hooks/useAssets.js +225 -0
  14. package/dist/cjs/hooks/useFileDownloadUrl.js +91 -0
  15. package/dist/cjs/hooks/useFileFiltering.js +81 -0
  16. package/dist/cjs/hooks/useFollow.js +159 -0
  17. package/dist/cjs/hooks/useFollow.types.js +4 -0
  18. package/dist/cjs/hooks/useQueryClient.js +16 -0
  19. package/dist/cjs/hooks/useSessionSocket.js +215 -0
  20. package/dist/cjs/hooks/useWebSSO.js +146 -0
  21. package/dist/cjs/index.js +115 -0
  22. package/dist/cjs/stores/accountStore.js +226 -0
  23. package/dist/cjs/stores/assetStore.js +192 -0
  24. package/dist/cjs/stores/authStore.js +47 -0
  25. package/dist/cjs/stores/followStore.js +154 -0
  26. package/dist/cjs/utils/authHelpers.js +154 -0
  27. package/dist/cjs/utils/avatarUtils.js +77 -0
  28. package/dist/cjs/utils/errorHandlers.js +128 -0
  29. package/dist/cjs/utils/sessionHelpers.js +90 -0
  30. package/dist/cjs/utils/storageHelpers.js +147 -0
  31. package/dist/esm/WebOxyProvider.js +282 -0
  32. package/dist/esm/hooks/mutations/index.js +10 -0
  33. package/dist/esm/hooks/mutations/mutationFactory.js +122 -0
  34. package/dist/esm/hooks/mutations/useAccountMutations.js +267 -0
  35. package/dist/esm/hooks/mutations/useServicesMutations.js +141 -0
  36. package/dist/esm/hooks/queries/index.js +14 -0
  37. package/dist/esm/hooks/queries/queryKeys.js +76 -0
  38. package/dist/esm/hooks/queries/useAccountQueries.js +131 -0
  39. package/dist/esm/hooks/queries/useSecurityQueries.js +40 -0
  40. package/dist/esm/hooks/queries/useServicesQueries.js +105 -0
  41. package/dist/esm/hooks/queryClient.js +104 -0
  42. package/dist/esm/hooks/useAssets.js +220 -0
  43. package/dist/esm/hooks/useFileDownloadUrl.js +86 -0
  44. package/dist/esm/hooks/useFileFiltering.js +78 -0
  45. package/dist/esm/hooks/useFollow.js +154 -0
  46. package/dist/esm/hooks/useFollow.types.js +3 -0
  47. package/dist/esm/hooks/useQueryClient.js +12 -0
  48. package/dist/esm/hooks/useSessionSocket.js +209 -0
  49. package/dist/esm/hooks/useWebSSO.js +143 -0
  50. package/dist/esm/index.js +48 -0
  51. package/dist/esm/stores/accountStore.js +219 -0
  52. package/dist/esm/stores/assetStore.js +180 -0
  53. package/dist/esm/stores/authStore.js +44 -0
  54. package/dist/esm/stores/followStore.js +151 -0
  55. package/dist/esm/utils/authHelpers.js +145 -0
  56. package/dist/esm/utils/avatarUtils.js +72 -0
  57. package/dist/esm/utils/errorHandlers.js +121 -0
  58. package/dist/esm/utils/sessionHelpers.js +84 -0
  59. package/dist/esm/utils/storageHelpers.js +108 -0
  60. package/dist/types/WebOxyProvider.d.ts +97 -0
  61. package/dist/types/hooks/mutations/index.d.ts +8 -0
  62. package/dist/types/hooks/mutations/mutationFactory.d.ts +75 -0
  63. package/dist/types/hooks/mutations/useAccountMutations.d.ts +68 -0
  64. package/dist/types/hooks/mutations/useServicesMutations.d.ts +22 -0
  65. package/dist/types/hooks/queries/index.d.ts +10 -0
  66. package/dist/types/hooks/queries/queryKeys.d.ts +64 -0
  67. package/dist/types/hooks/queries/useAccountQueries.d.ts +42 -0
  68. package/dist/types/hooks/queries/useSecurityQueries.d.ts +14 -0
  69. package/dist/types/hooks/queries/useServicesQueries.d.ts +31 -0
  70. package/dist/types/hooks/queryClient.d.ts +18 -0
  71. package/dist/types/hooks/useAssets.d.ts +34 -0
  72. package/dist/types/hooks/useFileDownloadUrl.d.ts +18 -0
  73. package/dist/types/hooks/useFileFiltering.d.ts +28 -0
  74. package/dist/types/hooks/useFollow.d.ts +61 -0
  75. package/dist/types/hooks/useFollow.types.d.ts +32 -0
  76. package/dist/types/hooks/useQueryClient.d.ts +6 -0
  77. package/dist/types/hooks/useSessionSocket.d.ts +13 -0
  78. package/dist/types/hooks/useWebSSO.d.ts +57 -0
  79. package/dist/types/index.d.ts +46 -0
  80. package/dist/types/stores/accountStore.d.ts +33 -0
  81. package/dist/types/stores/assetStore.d.ts +53 -0
  82. package/dist/types/stores/authStore.d.ts +16 -0
  83. package/dist/types/stores/followStore.d.ts +24 -0
  84. package/dist/types/utils/authHelpers.d.ts +98 -0
  85. package/dist/types/utils/avatarUtils.d.ts +33 -0
  86. package/dist/types/utils/errorHandlers.d.ts +34 -0
  87. package/dist/types/utils/sessionHelpers.d.ts +63 -0
  88. package/dist/types/utils/storageHelpers.d.ts +27 -0
  89. package/package.json +71 -0
  90. package/src/WebOxyProvider.tsx +372 -0
  91. package/src/global.d.ts +1 -0
  92. package/src/hooks/mutations/index.ts +25 -0
  93. package/src/hooks/mutations/mutationFactory.ts +215 -0
  94. package/src/hooks/mutations/useAccountMutations.ts +344 -0
  95. package/src/hooks/mutations/useServicesMutations.ts +164 -0
  96. package/src/hooks/queries/index.ts +36 -0
  97. package/src/hooks/queries/queryKeys.ts +88 -0
  98. package/src/hooks/queries/useAccountQueries.ts +152 -0
  99. package/src/hooks/queries/useSecurityQueries.ts +64 -0
  100. package/src/hooks/queries/useServicesQueries.ts +126 -0
  101. package/src/hooks/queryClient.ts +112 -0
  102. package/src/hooks/useAssets.ts +291 -0
  103. package/src/hooks/useFileDownloadUrl.ts +118 -0
  104. package/src/hooks/useFileFiltering.ts +115 -0
  105. package/src/hooks/useFollow.ts +175 -0
  106. package/src/hooks/useFollow.types.ts +33 -0
  107. package/src/hooks/useQueryClient.ts +17 -0
  108. package/src/hooks/useSessionSocket.ts +233 -0
  109. package/src/hooks/useWebSSO.ts +187 -0
  110. package/src/index.ts +144 -0
  111. package/src/stores/accountStore.ts +296 -0
  112. package/src/stores/assetStore.ts +281 -0
  113. package/src/stores/authStore.ts +63 -0
  114. package/src/stores/followStore.ts +181 -0
  115. package/src/utils/authHelpers.ts +183 -0
  116. package/src/utils/avatarUtils.ts +103 -0
  117. package/src/utils/errorHandlers.ts +194 -0
  118. package/src/utils/sessionHelpers.ts +151 -0
  119. package/src/utils/storageHelpers.ts +130 -0
@@ -0,0 +1,296 @@
1
+ import { create } from 'zustand';
2
+ import { shallow } from 'zustand/shallow';
3
+ import type { OxyServices } from '@oxyhq/core';
4
+
5
+ export interface QuickAccount {
6
+ sessionId: string;
7
+ userId?: string; // User ID for deduplication
8
+ username: string;
9
+ displayName: string;
10
+ avatar?: string;
11
+ avatarUrl?: string; // Cached avatar URL to prevent recalculation
12
+ }
13
+
14
+ interface AccountState {
15
+ // Account data
16
+ accounts: Record<string, QuickAccount>;
17
+ accountOrder: string[]; // Maintain order for display
18
+ accountsArray: QuickAccount[]; // Cached array to prevent infinite loops
19
+
20
+ // Loading states
21
+ loading: boolean;
22
+ loadingSessionIds: Set<string>;
23
+
24
+ // Error state
25
+ error: string | null;
26
+
27
+ // Actions
28
+ setAccounts: (accounts: QuickAccount[]) => void;
29
+ addAccount: (account: QuickAccount) => void;
30
+ updateAccount: (sessionId: string, updates: Partial<QuickAccount>) => void;
31
+ removeAccount: (sessionId: string) => void;
32
+ moveAccountToTop: (sessionId: string) => void;
33
+
34
+ // Loading actions
35
+ setLoading: (loading: boolean) => void;
36
+ setLoadingSession: (sessionId: string, loading: boolean) => void;
37
+
38
+ // Error actions
39
+ setError: (error: string | null) => void;
40
+
41
+ // Load accounts from API
42
+ loadAccounts: (sessionIds: string[], oxyServices: OxyServices, existingAccounts?: QuickAccount[], preserveOrder?: boolean) => Promise<void>;
43
+
44
+ // Reset store
45
+ reset: () => void;
46
+ }
47
+
48
+ const initialState = {
49
+ accounts: {} as Record<string, QuickAccount>,
50
+ accountOrder: [] as string[],
51
+ accountsArray: [] as QuickAccount[],
52
+ loading: false,
53
+ loadingSessionIds: new Set<string>(),
54
+ error: null,
55
+ };
56
+
57
+ // Helper: Build accounts array from accounts map and order
58
+ const buildAccountsArray = (accounts: Record<string, QuickAccount>, order: string[]): QuickAccount[] => {
59
+ const result: QuickAccount[] = [];
60
+ for (const id of order) {
61
+ const account = accounts[id];
62
+ if (account) result.push(account);
63
+ }
64
+ return result;
65
+ };
66
+
67
+ // Helper: Create QuickAccount from user data
68
+ const createQuickAccount = (sessionId: string, userData: any, existingAccount?: QuickAccount, oxyServices?: OxyServices): QuickAccount => {
69
+ const displayName = userData.name?.full || userData.name?.first || userData.username || 'Account';
70
+ const userId = userData.id || userData._id?.toString();
71
+
72
+ // Preserve existing avatarUrl if avatar hasn't changed (prevents image reload)
73
+ let avatarUrl: string | undefined;
74
+ if (existingAccount && existingAccount.avatar === userData.avatar && existingAccount.avatarUrl) {
75
+ avatarUrl = existingAccount.avatarUrl; // Reuse existing URL
76
+ } else if (userData.avatar && oxyServices) {
77
+ avatarUrl = oxyServices.getFileDownloadUrl(userData.avatar, 'thumb');
78
+ }
79
+
80
+ return {
81
+ sessionId,
82
+ userId,
83
+ username: userData.username || '',
84
+ displayName,
85
+ avatar: userData.avatar,
86
+ avatarUrl,
87
+ };
88
+ };
89
+
90
+ export const useAccountStore = create<AccountState>((set, get) => ({
91
+ ...initialState,
92
+
93
+ setAccounts: (accounts) => set((state) => {
94
+ const accountMap: Record<string, QuickAccount> = {};
95
+ const order: string[] = [];
96
+ const seenSessionIds = new Set<string>();
97
+
98
+ for (const account of accounts) {
99
+ if (seenSessionIds.has(account.sessionId)) continue;
100
+ seenSessionIds.add(account.sessionId);
101
+ accountMap[account.sessionId] = account;
102
+ order.push(account.sessionId);
103
+ }
104
+
105
+ const accountsArray = buildAccountsArray(accountMap, order);
106
+ const sameOrder = order.length === state.accountOrder.length &&
107
+ order.every((id, i) => id === state.accountOrder[i]);
108
+ const sameAccounts = sameOrder &&
109
+ order.every(id => {
110
+ const existing = state.accounts[id];
111
+ const newAccount = accountMap[id];
112
+ return existing &&
113
+ existing.sessionId === newAccount.sessionId &&
114
+ existing.userId === newAccount.userId &&
115
+ existing.avatar === newAccount.avatar &&
116
+ existing.avatarUrl === newAccount.avatarUrl;
117
+ });
118
+
119
+ if (sameAccounts) return {} as any;
120
+
121
+ return { accounts: accountMap, accountOrder: order, accountsArray };
122
+ }),
123
+
124
+ addAccount: (account) => set((state) => {
125
+ // Check if account with same sessionId exists
126
+ if (state.accounts[account.sessionId]) {
127
+ // Update existing
128
+ const existing = state.accounts[account.sessionId];
129
+ if (existing.avatar === account.avatar && existing.avatarUrl === account.avatarUrl) {
130
+ return {} as any; // No change
131
+ }
132
+ const newAccounts = { ...state.accounts, [account.sessionId]: account };
133
+ return {
134
+ accounts: newAccounts,
135
+ accountsArray: buildAccountsArray(newAccounts, state.accountOrder),
136
+ };
137
+ }
138
+
139
+ const newAccounts = { ...state.accounts, [account.sessionId]: account };
140
+ const newOrder = [account.sessionId, ...state.accountOrder];
141
+ return {
142
+ accounts: newAccounts,
143
+ accountOrder: newOrder,
144
+ accountsArray: buildAccountsArray(newAccounts, newOrder),
145
+ };
146
+ }),
147
+
148
+ updateAccount: (sessionId, updates) => set((state) => {
149
+ const existing = state.accounts[sessionId];
150
+ if (!existing) return {} as any;
151
+
152
+ const updated = { ...existing, ...updates };
153
+ if (existing.avatar === updated.avatar && existing.avatarUrl === updated.avatarUrl) {
154
+ return {} as any; // No change
155
+ }
156
+
157
+ const newAccounts = { ...state.accounts, [sessionId]: updated };
158
+ return {
159
+ accounts: newAccounts,
160
+ accountsArray: buildAccountsArray(newAccounts, state.accountOrder),
161
+ };
162
+ }),
163
+
164
+ removeAccount: (sessionId) => set((state) => {
165
+ if (!state.accounts[sessionId]) return {} as any;
166
+
167
+ const { [sessionId]: _removed, ...rest } = state.accounts;
168
+ const newOrder = state.accountOrder.filter(id => id !== sessionId);
169
+
170
+ return {
171
+ accounts: rest,
172
+ accountOrder: newOrder,
173
+ accountsArray: buildAccountsArray(rest, newOrder),
174
+ };
175
+ }),
176
+
177
+ moveAccountToTop: (sessionId) => set((state) => {
178
+ if (!state.accounts[sessionId]) return {} as any;
179
+
180
+ const filtered = state.accountOrder.filter(id => id !== sessionId);
181
+ const newOrder = [sessionId, ...filtered];
182
+
183
+ return {
184
+ accountOrder: newOrder,
185
+ accountsArray: buildAccountsArray(state.accounts, newOrder),
186
+ };
187
+ }),
188
+
189
+ setLoading: (loading) => set({ loading }),
190
+
191
+ setLoadingSession: (sessionId, loading) => set((state) => {
192
+ const newSet = new Set(state.loadingSessionIds);
193
+ if (loading) {
194
+ newSet.add(sessionId);
195
+ } else {
196
+ newSet.delete(sessionId);
197
+ }
198
+ return { loadingSessionIds: newSet };
199
+ }),
200
+
201
+ setError: (error) => set({ error }),
202
+
203
+ loadAccounts: async (sessionIds, oxyServices, existingAccounts = [], preserveOrder = true) => {
204
+ const state = get();
205
+
206
+ const uniqueSessionIds = Array.from(new Set(sessionIds));
207
+ if (uniqueSessionIds.length === 0) {
208
+ get().setAccounts([]);
209
+ return;
210
+ }
211
+
212
+ // Try to get data from TanStack Query cache first
213
+ try {
214
+ // This will be called from a component, so we need to access queryClient differently
215
+ // For now, we'll keep the API call but optimize it
216
+ const existingMap = new Map(existingAccounts.map(a => [a.sessionId, a]));
217
+ for (const account of Object.values(state.accounts)) {
218
+ existingMap.set(account.sessionId, account);
219
+ }
220
+
221
+ const missingSessionIds = uniqueSessionIds.filter(id => !existingMap.has(id));
222
+
223
+ if (missingSessionIds.length === 0) {
224
+ const ordered = uniqueSessionIds
225
+ .map(id => existingMap.get(id))
226
+ .filter((acc): acc is QuickAccount => acc !== undefined);
227
+ get().setAccounts(ordered);
228
+ return;
229
+ }
230
+
231
+ if (state.loading) {
232
+ return;
233
+ }
234
+
235
+ set({ loading: true, error: null });
236
+
237
+ try {
238
+ const batchResults = await oxyServices.getUsersBySessions(missingSessionIds);
239
+
240
+ const accountMap = new Map<string, QuickAccount>();
241
+
242
+ for (const { sessionId, user: userData } of batchResults) {
243
+ if (userData && !accountMap.has(sessionId)) {
244
+ const existing = existingMap.get(sessionId);
245
+ accountMap.set(sessionId, createQuickAccount(sessionId, userData, existing, oxyServices));
246
+ }
247
+ }
248
+
249
+ for (const [sessionId, account] of accountMap) {
250
+ existingMap.set(sessionId, account);
251
+ }
252
+
253
+ const orderToUse = preserveOrder ? uniqueSessionIds : [...uniqueSessionIds, ...state.accountOrder];
254
+ const seen = new Set<string>();
255
+ const ordered: QuickAccount[] = [];
256
+
257
+ for (const sessionId of orderToUse) {
258
+ if (seen.has(sessionId)) continue;
259
+ seen.add(sessionId);
260
+
261
+ const account = existingMap.get(sessionId);
262
+ if (account) ordered.push(account);
263
+ }
264
+
265
+ get().setAccounts(ordered);
266
+ } catch (error) {
267
+ const errorMessage = error instanceof Error ? error.message : 'Failed to load accounts';
268
+ if (__DEV__) {
269
+ console.error('AccountStore: Failed to load accounts:', error);
270
+ }
271
+ set({ error: errorMessage });
272
+ } finally {
273
+ set({ loading: false });
274
+ }
275
+ } catch (error) {
276
+ const errorMessage = error instanceof Error ? error.message : 'Failed to load accounts';
277
+ if (__DEV__) {
278
+ console.error('AccountStore: Failed to load accounts:', error);
279
+ }
280
+ set({ error: errorMessage, loading: false });
281
+ }
282
+ },
283
+
284
+ reset: () => set(initialState),
285
+ }));
286
+
287
+ // Selectors for performance - return cached array to prevent infinite loops
288
+ export const useAccounts = (): QuickAccount[] => {
289
+ return useAccountStore(state => state.accountsArray);
290
+ };
291
+
292
+ export const useAccountLoading = () => useAccountStore(s => s.loading);
293
+ export const useAccountError = () => useAccountStore(s => s.error);
294
+ export const useAccountLoadingSession = (sessionId: string) =>
295
+ useAccountStore(s => s.loadingSessionIds.has(sessionId));
296
+
@@ -0,0 +1,281 @@
1
+ import { create } from 'zustand';
2
+ import { Asset, AssetUploadProgress, AssetLink } from '@oxyhq/core';
3
+
4
+ interface AssetState {
5
+ // Asset data
6
+ assets: Record<string, Asset>;
7
+ uploadProgress: Record<string, AssetUploadProgress>;
8
+
9
+ // Loading states
10
+ loading: {
11
+ uploading: boolean;
12
+ linking: boolean;
13
+ deleting: boolean;
14
+ };
15
+
16
+ // Error states
17
+ errors: {
18
+ upload?: string;
19
+ link?: string;
20
+ delete?: string;
21
+ };
22
+
23
+ // Actions
24
+ setAsset: (asset: Asset) => void;
25
+ setAssets: (assets: Asset[]) => void;
26
+ removeAsset: (assetId: string) => void;
27
+
28
+ // Upload progress actions
29
+ setUploadProgress: (fileId: string, progress: AssetUploadProgress) => void;
30
+ removeUploadProgress: (fileId: string) => void;
31
+
32
+ // Link management
33
+ addLink: (assetId: string, link: AssetLink) => void;
34
+ removeLink: (assetId: string, app: string, entityType: string, entityId: string) => void;
35
+
36
+ // Loading states
37
+ setUploading: (uploading: boolean) => void;
38
+ setLinking: (linking: boolean) => void;
39
+ setDeleting: (deleting: boolean) => void;
40
+
41
+ // Error management
42
+ setUploadError: (error?: string) => void;
43
+ setLinkError: (error?: string) => void;
44
+ setDeleteError: (error?: string) => void;
45
+ clearErrors: () => void;
46
+
47
+ // Utility methods
48
+ getAssetsByApp: (app: string) => Asset[];
49
+ getAssetsByEntity: (app: string, entityType: string, entityId: string) => Asset[];
50
+ getAssetUsageCount: (assetId: string) => number;
51
+ isAssetLinked: (assetId: string, app: string, entityType: string, entityId: string) => boolean;
52
+
53
+ // Reset store
54
+ reset: () => void;
55
+ }
56
+
57
+ const initialState = {
58
+ assets: {},
59
+ uploadProgress: {},
60
+ loading: {
61
+ uploading: false,
62
+ linking: false,
63
+ deleting: false,
64
+ },
65
+ errors: {},
66
+ };
67
+
68
+ export const useAssetStore = create<AssetState>((set, get) => ({
69
+ ...initialState,
70
+
71
+ // Asset management
72
+ setAsset: (asset: Asset) => {
73
+ set((state) => ({
74
+ assets: {
75
+ ...state.assets,
76
+ [asset.id]: asset,
77
+ },
78
+ }));
79
+ },
80
+
81
+ setAssets: (assets: Asset[]) => {
82
+ set((state) => {
83
+ const assetMap = assets.reduce((acc, asset) => {
84
+ acc[asset.id] = asset;
85
+ return acc;
86
+ }, {} as Record<string, Asset>);
87
+
88
+ return {
89
+ assets: {
90
+ ...state.assets,
91
+ ...assetMap,
92
+ },
93
+ };
94
+ });
95
+ },
96
+
97
+ removeAsset: (assetId: string) => {
98
+ set((state) => {
99
+ const { [assetId]: removed, ...rest } = state.assets;
100
+ return { assets: rest };
101
+ });
102
+ },
103
+
104
+ // Upload progress
105
+ setUploadProgress: (fileId: string, progress: AssetUploadProgress) => {
106
+ set((state) => ({
107
+ uploadProgress: {
108
+ ...state.uploadProgress,
109
+ [fileId]: progress,
110
+ },
111
+ }));
112
+ },
113
+
114
+ removeUploadProgress: (fileId: string) => {
115
+ set((state) => {
116
+ const { [fileId]: removed, ...rest } = state.uploadProgress;
117
+ return { uploadProgress: rest };
118
+ });
119
+ },
120
+
121
+ // Link management
122
+ addLink: (assetId: string, link: AssetLink) => {
123
+ set((state) => {
124
+ const asset = state.assets[assetId];
125
+ if (!asset) return state;
126
+
127
+ // Check if link already exists
128
+ const existingLink = asset.links.find(
129
+ (l: AssetLink) => l.app === link.app &&
130
+ l.entityType === link.entityType &&
131
+ l.entityId === link.entityId
132
+ );
133
+
134
+ if (existingLink) return state;
135
+
136
+ const updatedAsset = {
137
+ ...asset,
138
+ links: [...asset.links, link],
139
+ usageCount: asset.links.length + 1,
140
+ };
141
+
142
+ return {
143
+ assets: {
144
+ ...state.assets,
145
+ [assetId]: updatedAsset,
146
+ },
147
+ };
148
+ });
149
+ },
150
+
151
+ removeLink: (assetId: string, app: string, entityType: string, entityId: string) => {
152
+ set((state) => {
153
+ const asset = state.assets[assetId];
154
+ if (!asset) return state;
155
+
156
+ const filteredLinks = asset.links.filter(
157
+ (link: AssetLink) => !(link.app === app &&
158
+ link.entityType === entityType &&
159
+ link.entityId === entityId)
160
+ );
161
+
162
+ const updatedAsset = {
163
+ ...asset,
164
+ links: filteredLinks,
165
+ usageCount: filteredLinks.length,
166
+ status: filteredLinks.length === 0 ? 'trash' as const : asset.status,
167
+ };
168
+
169
+ return {
170
+ assets: {
171
+ ...state.assets,
172
+ [assetId]: updatedAsset,
173
+ },
174
+ };
175
+ });
176
+ },
177
+
178
+ // Loading states
179
+ setUploading: (uploading: boolean) => {
180
+ set((state) => ({
181
+ loading: { ...state.loading, uploading },
182
+ }));
183
+ },
184
+
185
+ setLinking: (linking: boolean) => {
186
+ set((state) => ({
187
+ loading: { ...state.loading, linking },
188
+ }));
189
+ },
190
+
191
+ setDeleting: (deleting: boolean) => {
192
+ set((state) => ({
193
+ loading: { ...state.loading, deleting },
194
+ }));
195
+ },
196
+
197
+ // Error management
198
+ setUploadError: (error?: string) => {
199
+ set((state) => ({
200
+ errors: { ...state.errors, upload: error },
201
+ }));
202
+ },
203
+
204
+ setLinkError: (error?: string) => {
205
+ set((state) => ({
206
+ errors: { ...state.errors, link: error },
207
+ }));
208
+ },
209
+
210
+ setDeleteError: (error?: string) => {
211
+ set((state) => ({
212
+ errors: { ...state.errors, delete: error },
213
+ }));
214
+ },
215
+
216
+ clearErrors: () => {
217
+ set({ errors: {} });
218
+ },
219
+
220
+ // Utility methods
221
+ getAssetsByApp: (app: string) => {
222
+ const { assets } = get();
223
+ return Object.values(assets).filter((asset) =>
224
+ asset.links.some((link: AssetLink) => link.app === app)
225
+ );
226
+ },
227
+
228
+ getAssetsByEntity: (app: string, entityType: string, entityId: string) => {
229
+ const { assets } = get();
230
+ return Object.values(assets).filter((asset) =>
231
+ asset.links.some(
232
+ (link: AssetLink) => link.app === app &&
233
+ link.entityType === entityType &&
234
+ link.entityId === entityId
235
+ )
236
+ );
237
+ },
238
+
239
+ getAssetUsageCount: (assetId: string) => {
240
+ const { assets } = get();
241
+ const asset = assets[assetId];
242
+ return asset ? asset.usageCount : 0;
243
+ },
244
+
245
+ isAssetLinked: (assetId: string, app: string, entityType: string, entityId: string) => {
246
+ const { assets } = get();
247
+ const asset = assets[assetId];
248
+ if (!asset) return false;
249
+
250
+ return asset.links.some(
251
+ (link: AssetLink) => link.app === app &&
252
+ link.entityType === entityType &&
253
+ link.entityId === entityId
254
+ );
255
+ },
256
+
257
+ // Reset store
258
+ reset: () => {
259
+ set(initialState);
260
+ },
261
+ }));
262
+
263
+ // Selector hooks for convenience
264
+ export const useAssets = () => useAssetStore((state) => Object.values(state.assets));
265
+ export const useAsset = (assetId: string) => useAssetStore((state) => state.assets[assetId]);
266
+ export const useUploadProgress = () => useAssetStore((state) => state.uploadProgress);
267
+ export const useAssetLoading = () => useAssetStore((state) => state.loading);
268
+ export const useAssetErrors = () => useAssetStore((state) => state.errors);
269
+
270
+ // Typed selectors for specific use cases
271
+ export const useAssetsByApp = (app: string) =>
272
+ useAssetStore((state) => state.getAssetsByApp(app));
273
+
274
+ export const useAssetsByEntity = (app: string, entityType: string, entityId: string) =>
275
+ useAssetStore((state) => state.getAssetsByEntity(app, entityType, entityId));
276
+
277
+ export const useAssetUsageCount = (assetId: string) =>
278
+ useAssetStore((state) => state.getAssetUsageCount(assetId));
279
+
280
+ export const useIsAssetLinked = (assetId: string, app: string, entityType: string, entityId: string) =>
281
+ useAssetStore((state) => state.isAssetLinked(assetId, app, entityType, entityId));
@@ -0,0 +1,63 @@
1
+ import { create } from 'zustand';
2
+ import type { User } from '@oxyhq/core';
3
+ import { createDebugLogger } from '@oxyhq/core';
4
+
5
+ const debug = createDebugLogger('AuthStore');
6
+
7
+ export interface AuthState {
8
+ user: User | null;
9
+ isAuthenticated: boolean;
10
+ isLoading: boolean;
11
+ error: string | null;
12
+ lastUserFetch: number | null; // Timestamp of last user fetch for caching
13
+
14
+ loginSuccess: (user: User) => void;
15
+ loginFailure: (error: string) => void;
16
+ logout: () => void;
17
+ fetchUser: (oxyServices: { getCurrentUser: () => Promise<User> }, forceRefresh?: boolean) => Promise<void>;
18
+ setUser: (user: User) => void; // Direct user setter for caching
19
+ }
20
+
21
+ export const useAuthStore = create<AuthState>((set: (state: Partial<AuthState>) => void, get: () => AuthState) => ({
22
+ user: null,
23
+ isAuthenticated: false,
24
+ isLoading: false,
25
+ error: null,
26
+ lastUserFetch: null,
27
+
28
+ loginSuccess: (user: User) => set({
29
+ isLoading: false,
30
+ isAuthenticated: true,
31
+ user,
32
+ lastUserFetch: Date.now(),
33
+ }),
34
+ loginFailure: (error: string) => set({ isLoading: false, error }),
35
+ logout: () => set({
36
+ user: null,
37
+ isAuthenticated: false,
38
+ lastUserFetch: null,
39
+ }),
40
+ setUser: (user: User) => set({ user, lastUserFetch: Date.now() }),
41
+ fetchUser: async (oxyServices, forceRefresh = false) => {
42
+ const state = get();
43
+ const now = Date.now();
44
+ const cacheAge = state.lastUserFetch ? now - state.lastUserFetch : Number.POSITIVE_INFINITY;
45
+ const cacheValid = cacheAge < 5 * 60 * 1000; // 5 minutes cache
46
+
47
+ // Use cached data if available and not forcing refresh
48
+ if (!forceRefresh && state.user && cacheValid) {
49
+ debug.log('Using cached user data (age:', cacheAge, 'ms)');
50
+ return;
51
+ }
52
+
53
+ set({ isLoading: true, error: null });
54
+ try {
55
+ const user = await oxyServices.getCurrentUser();
56
+ set({ user, isLoading: false, isAuthenticated: true, lastUserFetch: now });
57
+ } catch (error) {
58
+ const errorMessage = error instanceof Error ? error.message : 'Failed to fetch user';
59
+ debug.error('Error fetching user:', error);
60
+ set({ error: errorMessage, isLoading: false });
61
+ }
62
+ },
63
+ }));