@oxyhq/services 5.13.12 → 5.13.16

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 (209) hide show
  1. package/README.md +10 -0
  2. package/lib/commonjs/core/OxyServices.base.js +271 -0
  3. package/lib/commonjs/core/OxyServices.base.js.map +1 -0
  4. package/lib/commonjs/core/OxyServices.errors.js +26 -0
  5. package/lib/commonjs/core/OxyServices.errors.js.map +1 -0
  6. package/lib/commonjs/core/OxyServices.js +58 -2009
  7. package/lib/commonjs/core/OxyServices.js.map +1 -1
  8. package/lib/commonjs/core/mixins/OxyServices.analytics.js +60 -0
  9. package/lib/commonjs/core/mixins/OxyServices.analytics.js.map +1 -0
  10. package/lib/commonjs/core/mixins/OxyServices.assets.js +406 -0
  11. package/lib/commonjs/core/mixins/OxyServices.assets.js.map +1 -0
  12. package/lib/commonjs/core/mixins/OxyServices.auth.js +303 -0
  13. package/lib/commonjs/core/mixins/OxyServices.auth.js.map +1 -0
  14. package/lib/commonjs/core/mixins/OxyServices.developer.js +115 -0
  15. package/lib/commonjs/core/mixins/OxyServices.developer.js.map +1 -0
  16. package/lib/commonjs/core/mixins/OxyServices.devices.js +119 -0
  17. package/lib/commonjs/core/mixins/OxyServices.devices.js.map +1 -0
  18. package/lib/commonjs/core/mixins/OxyServices.karma.js +117 -0
  19. package/lib/commonjs/core/mixins/OxyServices.karma.js.map +1 -0
  20. package/lib/commonjs/core/mixins/OxyServices.language.js +124 -0
  21. package/lib/commonjs/core/mixins/OxyServices.language.js.map +1 -0
  22. package/lib/commonjs/core/mixins/OxyServices.location.js +55 -0
  23. package/lib/commonjs/core/mixins/OxyServices.location.js.map +1 -0
  24. package/lib/commonjs/core/mixins/OxyServices.payment.js +66 -0
  25. package/lib/commonjs/core/mixins/OxyServices.payment.js.map +1 -0
  26. package/lib/commonjs/core/mixins/OxyServices.privacy.js +174 -0
  27. package/lib/commonjs/core/mixins/OxyServices.privacy.js.map +1 -0
  28. package/lib/commonjs/core/mixins/OxyServices.totp.js +53 -0
  29. package/lib/commonjs/core/mixins/OxyServices.totp.js.map +1 -0
  30. package/lib/commonjs/core/mixins/OxyServices.user.js +389 -0
  31. package/lib/commonjs/core/mixins/OxyServices.user.js.map +1 -0
  32. package/lib/commonjs/core/mixins/OxyServices.utility.js +161 -0
  33. package/lib/commonjs/core/mixins/OxyServices.utility.js.map +1 -0
  34. package/lib/commonjs/core/mixins/index.js +39 -0
  35. package/lib/commonjs/core/mixins/index.js.map +1 -0
  36. package/lib/commonjs/core/mixins/mixinHelpers.js +62 -0
  37. package/lib/commonjs/core/mixins/mixinHelpers.js.map +1 -0
  38. package/lib/commonjs/index.js.map +1 -1
  39. package/lib/commonjs/ui/context/OxyContext.js +26 -47
  40. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  41. package/lib/commonjs/ui/screens/PrivacySettingsScreen.js +239 -1
  42. package/lib/commonjs/ui/screens/PrivacySettingsScreen.js.map +1 -1
  43. package/lib/commonjs/utils/apiUtils.js +0 -14
  44. package/lib/commonjs/utils/apiUtils.js.map +1 -1
  45. package/lib/commonjs/utils/asyncUtils.js +0 -20
  46. package/lib/commonjs/utils/asyncUtils.js.map +1 -1
  47. package/lib/module/core/OxyServices.base.js +265 -0
  48. package/lib/module/core/OxyServices.base.js.map +1 -0
  49. package/lib/module/core/OxyServices.errors.js +20 -0
  50. package/lib/module/core/OxyServices.errors.js.map +1 -0
  51. package/lib/module/core/OxyServices.js +43 -2005
  52. package/lib/module/core/OxyServices.js.map +1 -1
  53. package/lib/module/core/mixins/OxyServices.analytics.js +56 -0
  54. package/lib/module/core/mixins/OxyServices.analytics.js.map +1 -0
  55. package/lib/module/core/mixins/OxyServices.assets.js +402 -0
  56. package/lib/module/core/mixins/OxyServices.assets.js.map +1 -0
  57. package/lib/module/core/mixins/OxyServices.auth.js +299 -0
  58. package/lib/module/core/mixins/OxyServices.auth.js.map +1 -0
  59. package/lib/module/core/mixins/OxyServices.developer.js +111 -0
  60. package/lib/module/core/mixins/OxyServices.developer.js.map +1 -0
  61. package/lib/module/core/mixins/OxyServices.devices.js +115 -0
  62. package/lib/module/core/mixins/OxyServices.devices.js.map +1 -0
  63. package/lib/module/core/mixins/OxyServices.karma.js +113 -0
  64. package/lib/module/core/mixins/OxyServices.karma.js.map +1 -0
  65. package/lib/module/core/mixins/OxyServices.language.js +120 -0
  66. package/lib/module/core/mixins/OxyServices.language.js.map +1 -0
  67. package/lib/module/core/mixins/OxyServices.location.js +51 -0
  68. package/lib/module/core/mixins/OxyServices.location.js.map +1 -0
  69. package/lib/module/core/mixins/OxyServices.payment.js +62 -0
  70. package/lib/module/core/mixins/OxyServices.payment.js.map +1 -0
  71. package/lib/module/core/mixins/OxyServices.privacy.js +170 -0
  72. package/lib/module/core/mixins/OxyServices.privacy.js.map +1 -0
  73. package/lib/module/core/mixins/OxyServices.totp.js +49 -0
  74. package/lib/module/core/mixins/OxyServices.totp.js.map +1 -0
  75. package/lib/module/core/mixins/OxyServices.user.js +385 -0
  76. package/lib/module/core/mixins/OxyServices.user.js.map +1 -0
  77. package/lib/module/core/mixins/OxyServices.utility.js +156 -0
  78. package/lib/module/core/mixins/OxyServices.utility.js.map +1 -0
  79. package/lib/module/core/mixins/index.js +36 -0
  80. package/lib/module/core/mixins/index.js.map +1 -0
  81. package/lib/module/core/mixins/mixinHelpers.js +56 -0
  82. package/lib/module/core/mixins/mixinHelpers.js.map +1 -0
  83. package/lib/module/index.js.map +1 -1
  84. package/lib/module/ui/context/OxyContext.js +26 -47
  85. package/lib/module/ui/context/OxyContext.js.map +1 -1
  86. package/lib/module/ui/navigation/types.js.map +1 -1
  87. package/lib/module/ui/screens/PrivacySettingsScreen.js +241 -3
  88. package/lib/module/ui/screens/PrivacySettingsScreen.js.map +1 -1
  89. package/lib/module/utils/apiUtils.js +0 -13
  90. package/lib/module/utils/apiUtils.js.map +1 -1
  91. package/lib/module/utils/asyncUtils.js +0 -20
  92. package/lib/module/utils/asyncUtils.js.map +1 -1
  93. package/lib/typescript/core/OxyServices.base.d.ts +123 -0
  94. package/lib/typescript/core/OxyServices.base.d.ts.map +1 -0
  95. package/lib/typescript/core/OxyServices.d.ts +969 -682
  96. package/lib/typescript/core/OxyServices.d.ts.map +1 -1
  97. package/lib/typescript/core/OxyServices.errors.d.ts +12 -0
  98. package/lib/typescript/core/OxyServices.errors.d.ts.map +1 -0
  99. package/lib/typescript/core/mixins/OxyServices.analytics.d.ts +70 -0
  100. package/lib/typescript/core/mixins/OxyServices.analytics.d.ts.map +1 -0
  101. package/lib/typescript/core/mixins/OxyServices.assets.d.ts +159 -0
  102. package/lib/typescript/core/mixins/OxyServices.assets.d.ts.map +1 -0
  103. package/lib/typescript/core/mixins/OxyServices.auth.d.ts +168 -0
  104. package/lib/typescript/core/mixins/OxyServices.auth.d.ts.map +1 -0
  105. package/lib/typescript/core/mixins/OxyServices.developer.d.ts +103 -0
  106. package/lib/typescript/core/mixins/OxyServices.developer.d.ts.map +1 -0
  107. package/lib/typescript/core/mixins/OxyServices.devices.d.ts +93 -0
  108. package/lib/typescript/core/mixins/OxyServices.devices.d.ts.map +1 -0
  109. package/lib/typescript/core/mixins/OxyServices.karma.d.ts +89 -0
  110. package/lib/typescript/core/mixins/OxyServices.karma.d.ts.map +1 -0
  111. package/lib/typescript/core/mixins/OxyServices.language.d.ts +85 -0
  112. package/lib/typescript/core/mixins/OxyServices.language.d.ts.map +1 -0
  113. package/lib/typescript/core/mixins/OxyServices.location.d.ts +68 -0
  114. package/lib/typescript/core/mixins/OxyServices.location.d.ts.map +1 -0
  115. package/lib/typescript/core/mixins/OxyServices.payment.d.ts +74 -0
  116. package/lib/typescript/core/mixins/OxyServices.payment.d.ts.map +1 -0
  117. package/lib/typescript/core/mixins/OxyServices.privacy.d.ts +126 -0
  118. package/lib/typescript/core/mixins/OxyServices.privacy.d.ts.map +1 -0
  119. package/lib/typescript/core/mixins/OxyServices.totp.d.ts +69 -0
  120. package/lib/typescript/core/mixins/OxyServices.totp.d.ts.map +1 -0
  121. package/lib/typescript/core/mixins/OxyServices.user.d.ts +189 -0
  122. package/lib/typescript/core/mixins/OxyServices.user.d.ts.map +1 -0
  123. package/lib/typescript/core/mixins/OxyServices.utility.d.ts +97 -0
  124. package/lib/typescript/core/mixins/OxyServices.utility.d.ts.map +1 -0
  125. package/lib/typescript/core/mixins/index.d.ts +898 -0
  126. package/lib/typescript/core/mixins/index.d.ts.map +1 -0
  127. package/lib/typescript/core/mixins/mixinHelpers.d.ts +32 -0
  128. package/lib/typescript/core/mixins/mixinHelpers.d.ts.map +1 -0
  129. package/lib/typescript/index.d.ts +1 -1
  130. package/lib/typescript/index.d.ts.map +1 -1
  131. package/lib/typescript/models/interfaces.d.ts +36 -0
  132. package/lib/typescript/models/interfaces.d.ts.map +1 -1
  133. package/lib/typescript/ui/context/OxyContext.d.ts +2 -6
  134. package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
  135. package/lib/typescript/ui/navigation/types.d.ts +0 -1
  136. package/lib/typescript/ui/navigation/types.d.ts.map +1 -1
  137. package/lib/typescript/ui/screens/PrivacySettingsScreen.d.ts.map +1 -1
  138. package/lib/typescript/utils/apiUtils.d.ts +0 -7
  139. package/lib/typescript/utils/apiUtils.d.ts.map +1 -1
  140. package/lib/typescript/utils/asyncUtils.d.ts +0 -11
  141. package/lib/typescript/utils/asyncUtils.d.ts.map +1 -1
  142. package/package.json +1 -1
  143. package/src/core/OxyServices.base.ts +311 -0
  144. package/src/core/OxyServices.errors.ts +26 -0
  145. package/src/core/OxyServices.ts +43 -2026
  146. package/src/core/mixins/OxyServices.analytics.ts +53 -0
  147. package/src/core/mixins/OxyServices.assets.ts +390 -0
  148. package/src/core/mixins/OxyServices.auth.ts +275 -0
  149. package/src/core/mixins/OxyServices.developer.ts +114 -0
  150. package/src/core/mixins/OxyServices.devices.ts +103 -0
  151. package/src/core/mixins/OxyServices.karma.ts +111 -0
  152. package/src/core/mixins/OxyServices.language.ts +127 -0
  153. package/src/core/mixins/OxyServices.location.ts +46 -0
  154. package/src/core/mixins/OxyServices.payment.ts +59 -0
  155. package/src/core/mixins/OxyServices.privacy.ts +182 -0
  156. package/src/core/mixins/OxyServices.totp.ts +36 -0
  157. package/src/core/mixins/OxyServices.user.ts +380 -0
  158. package/src/core/mixins/OxyServices.utility.ts +187 -0
  159. package/src/core/mixins/index.ts +58 -0
  160. package/src/core/mixins/mixinHelpers.ts +69 -0
  161. package/src/index.ts +4 -0
  162. package/src/models/interfaces.ts +40 -0
  163. package/src/ui/context/OxyContext.tsx +35 -53
  164. package/src/ui/navigation/types.ts +0 -1
  165. package/src/ui/screens/PrivacySettingsScreen.tsx +240 -2
  166. package/src/utils/apiUtils.ts +0 -14
  167. package/src/utils/asyncUtils.ts +0 -20
  168. package/lib/commonjs/ui/screens/internal/SignInPasswordStep.js +0 -192
  169. package/lib/commonjs/ui/screens/internal/SignInPasswordStep.js.map +0 -1
  170. package/lib/commonjs/ui/screens/internal/SignInUsernameStep.js +0 -142
  171. package/lib/commonjs/ui/screens/internal/SignInUsernameStep.js.map +0 -1
  172. package/lib/commonjs/ui/screens/internal/SignUpIdentityStep.js +0 -113
  173. package/lib/commonjs/ui/screens/internal/SignUpIdentityStep.js.map +0 -1
  174. package/lib/commonjs/ui/screens/internal/SignUpSecurityStep.js +0 -132
  175. package/lib/commonjs/ui/screens/internal/SignUpSecurityStep.js.map +0 -1
  176. package/lib/commonjs/ui/screens/internal/SignUpSummaryStep.js +0 -83
  177. package/lib/commonjs/ui/screens/internal/SignUpSummaryStep.js.map +0 -1
  178. package/lib/commonjs/ui/screens/internal/SignUpWelcomeStep.js +0 -58
  179. package/lib/commonjs/ui/screens/internal/SignUpWelcomeStep.js.map +0 -1
  180. package/lib/module/ui/screens/internal/SignInPasswordStep.js +0 -186
  181. package/lib/module/ui/screens/internal/SignInPasswordStep.js.map +0 -1
  182. package/lib/module/ui/screens/internal/SignInUsernameStep.js +0 -136
  183. package/lib/module/ui/screens/internal/SignInUsernameStep.js.map +0 -1
  184. package/lib/module/ui/screens/internal/SignUpIdentityStep.js +0 -108
  185. package/lib/module/ui/screens/internal/SignUpIdentityStep.js.map +0 -1
  186. package/lib/module/ui/screens/internal/SignUpSecurityStep.js +0 -127
  187. package/lib/module/ui/screens/internal/SignUpSecurityStep.js.map +0 -1
  188. package/lib/module/ui/screens/internal/SignUpSummaryStep.js +0 -78
  189. package/lib/module/ui/screens/internal/SignUpSummaryStep.js.map +0 -1
  190. package/lib/module/ui/screens/internal/SignUpWelcomeStep.js +0 -53
  191. package/lib/module/ui/screens/internal/SignUpWelcomeStep.js.map +0 -1
  192. package/lib/typescript/ui/screens/internal/SignInPasswordStep.d.ts +0 -28
  193. package/lib/typescript/ui/screens/internal/SignInPasswordStep.d.ts.map +0 -1
  194. package/lib/typescript/ui/screens/internal/SignInUsernameStep.d.ts +0 -25
  195. package/lib/typescript/ui/screens/internal/SignInUsernameStep.d.ts.map +0 -1
  196. package/lib/typescript/ui/screens/internal/SignUpIdentityStep.d.ts +0 -20
  197. package/lib/typescript/ui/screens/internal/SignUpIdentityStep.d.ts.map +0 -1
  198. package/lib/typescript/ui/screens/internal/SignUpSecurityStep.d.ts +0 -24
  199. package/lib/typescript/ui/screens/internal/SignUpSecurityStep.d.ts.map +0 -1
  200. package/lib/typescript/ui/screens/internal/SignUpSummaryStep.d.ts +0 -15
  201. package/lib/typescript/ui/screens/internal/SignUpSummaryStep.d.ts.map +0 -1
  202. package/lib/typescript/ui/screens/internal/SignUpWelcomeStep.d.ts +0 -13
  203. package/lib/typescript/ui/screens/internal/SignUpWelcomeStep.d.ts.map +0 -1
  204. package/src/ui/screens/internal/SignInPasswordStep.tsx +0 -184
  205. package/src/ui/screens/internal/SignInUsernameStep.tsx +0 -145
  206. package/src/ui/screens/internal/SignUpIdentityStep.tsx +0 -112
  207. package/src/ui/screens/internal/SignUpSecurityStep.tsx +0 -132
  208. package/src/ui/screens/internal/SignUpSummaryStep.tsx +0 -66
  209. package/src/ui/screens/internal/SignUpWelcomeStep.tsx +0 -52
@@ -22,7 +22,7 @@
22
22
  * // Upload a file (browser File API)
23
23
  * const fileInput = document.querySelector('input[type=file]');
24
24
  * const file = fileInput.files[0];
25
- * await oxy.uploadFile(file);
25
+ * await oxy.uploadRawFile(file);
26
26
  *
27
27
  * // Get a file stream URL for <img src>
28
28
  * const url = oxy.getFileStreamUrl('fileId');
@@ -58,2022 +58,60 @@
58
58
  *
59
59
  * See method JSDoc for more details and options.
60
60
  */
61
- import { jwtDecode } from 'jwt-decode';
62
- import { normalizeLanguageCode, getLanguageMetadata, getLanguageName, getNativeLanguageName } from '../utils/languageUtils';
63
61
 
64
- /**
65
- * OxyConfig - Configuration for OxyServices
66
- * @property baseURL - The Oxy API base URL (e.g., https://api.oxy.so)
67
- * @property cloudURL - The Oxy Cloud (file storage/CDN) URL (e.g., https://cloud.oxy.so)
68
- */
62
+ import { OxyAuthenticationError, OxyAuthenticationTimeoutError } from './OxyServices.errors';
69
63
 
70
- import { handleHttpError } from '../utils/errorUtils';
71
- import { buildSearchParams, buildPaginationParams } from '../utils/apiUtils';
72
- import { HttpClient } from './HttpClient';
73
- import { RequestManager } from './RequestManager';
74
- /**
75
- * Custom error types for better error handling
76
- */
77
- export class OxyAuthenticationError extends Error {
78
- constructor(message, code = 'AUTH_ERROR', status = 401) {
79
- super(message);
80
- this.name = 'OxyAuthenticationError';
81
- this.code = code;
82
- this.status = status;
83
- }
84
- }
85
- export class OxyAuthenticationTimeoutError extends OxyAuthenticationError {
86
- constructor(operationName, timeoutMs) {
87
- super(`Authentication timeout (${timeoutMs}ms): ${operationName} requires user authentication. Please ensure the user is logged in before calling this method.`, 'AUTH_TIMEOUT', 408);
88
- this.name = 'OxyAuthenticationTimeoutError';
89
- }
90
- }
64
+ // Import mixin composition helper
65
+ import { composeOxyServices } from './mixins';
91
66
 
92
67
  /**
93
68
  * OxyServices - Unified client library for interacting with the Oxy API
94
69
  *
95
70
  * This class provides all API functionality in one simple, easy-to-use interface.
96
- * Architecture:
97
- * - HttpClient: Handles HTTP communication and authentication
98
- * - RequestManager: Handles caching, deduplication, queuing, and retry
99
- * - OxyServices: Provides high-level API methods
71
+ *
72
+ * ## Architecture
73
+ * - **HttpClient**: Handles HTTP communication and authentication
74
+ * - **RequestManager**: Handles caching, deduplication, queuing, and retry
75
+ * - **OxyServices**: Provides high-level API methods
76
+ *
77
+ * ## Mixin Composition
78
+ * The class is composed using TypeScript mixins for better code organization:
79
+ * - **Base**: Core infrastructure (HTTP client, request management, error handling)
80
+ * - **Auth**: Authentication and session management
81
+ * - **User**: User profiles, follow, notifications
82
+ * - **TOTP**: Two-factor authentication enrollment
83
+ * - **Privacy**: Blocked and restricted users
84
+ * - **Language**: Language detection and metadata
85
+ * - **Payment**: Payment processing
86
+ * - **Karma**: Karma system
87
+ * - **Assets**: File upload and asset management
88
+ * - **Developer**: Developer API management
89
+ * - **Location**: Location-based features
90
+ * - **Analytics**: Analytics tracking
91
+ * - **Devices**: Device management
92
+ * - **Utility**: Utility methods and Express middleware
93
+ *
94
+ * @example
95
+ * ```typescript
96
+ * const oxy = new OxyServices({
97
+ * baseURL: 'https://api.oxy.so',
98
+ * cloudURL: 'https://cloud.oxy.so'
99
+ * });
100
+ * ```
100
101
  */
101
- export class OxyServices {
102
- /**
103
- * Creates a new instance of the OxyServices client
104
- * @param config - Configuration for the client
105
- * config.baseURL: Oxy API URL (e.g., https://api.oxy.so)
106
- * config.cloudURL: Oxy Cloud URL (e.g., https://cloud.oxy.so)
107
- */
108
- constructor(config) {
109
- this.config = config;
110
- this.cloudURL = config.cloudURL || OXY_CLOUD_URL;
111
-
112
- // Initialize HTTP client (handles authentication and interceptors)
113
- this.httpClient = new HttpClient(config);
114
-
115
- // Initialize request manager (handles caching, deduplication, queuing, retry)
116
- this.requestManager = new RequestManager(this.httpClient, config);
117
- }
118
-
119
- // Test-only utility to reset global tokens between jest tests
120
- static __resetTokensForTests() {
121
- HttpClient.__resetTokensForTests();
122
- }
123
-
124
- /**
125
- * Make a request with all performance optimizations
126
- * This is the main method for all API calls - ensures authentication and performance features
127
- */
128
- async makeRequest(method, url, data, options = {}) {
129
- return this.requestManager.request(method, url, data, options);
130
- }
131
-
132
- // ============================================================================
133
- // CORE METHODS (HTTP Client, Token Management, Error Handling)
134
- // ============================================================================
135
-
136
- /**
137
- * Get the configured Oxy API base URL
138
- */
139
- getBaseURL() {
140
- return this.httpClient.getBaseURL();
141
- }
142
-
143
- /**
144
- * Get the HTTP client instance
145
- * Useful for advanced use cases where direct access to the HTTP client is needed
146
- */
147
- getClient() {
148
- return this.httpClient;
149
- }
150
-
151
- /**
152
- * Get performance metrics
153
- */
154
- getMetrics() {
155
- return this.requestManager.getMetrics();
156
- }
157
-
158
- /**
159
- * Clear request cache
160
- */
161
- clearCache() {
162
- this.requestManager.clearCache();
163
- }
164
-
165
- /**
166
- * Clear specific cache entry
167
- */
168
- clearCacheEntry(key) {
169
- this.requestManager.clearCacheEntry(key);
170
- }
171
-
172
- /**
173
- * Get cache statistics
174
- */
175
- getCacheStats() {
176
- return this.requestManager.getCacheStats();
177
- }
178
-
179
- /**
180
- * Get the configured Oxy Cloud (file storage/CDN) URL
181
- */
182
- getCloudURL() {
183
- return this.cloudURL;
184
- }
185
-
186
- /**
187
- * Set authentication tokens
188
- */
189
- setTokens(accessToken, refreshToken = '') {
190
- this.httpClient.setTokens(accessToken, refreshToken);
191
- }
192
-
193
- /**
194
- * Clear stored authentication tokens
195
- */
196
- clearTokens() {
197
- this.httpClient.clearTokens();
198
- }
199
-
200
- /**
201
- * Get the current user ID from the access token
202
- */
203
- getCurrentUserId() {
204
- const accessToken = this.httpClient.getAccessToken();
205
- if (!accessToken) {
206
- return null;
207
- }
208
- try {
209
- const decoded = jwtDecode(accessToken);
210
- return decoded.userId || decoded.id || null;
211
- } catch (error) {
212
- return null;
213
- }
214
- }
215
-
216
- /**
217
- * Check if the client has a valid access token
218
- */
219
- hasAccessToken() {
220
- return this.httpClient.hasAccessToken();
221
- }
222
-
223
- /**
224
- * Check if the client has a valid access token (public method)
225
- */
226
- hasValidToken() {
227
- return this.httpClient.hasAccessToken();
228
- }
229
-
230
- /**
231
- * Get the raw access token (for constructing anchor URLs when needed)
232
- */
233
- getAccessToken() {
234
- return this.httpClient.getAccessToken();
235
- }
236
-
237
- /**
238
- * Wait for authentication to be ready (public method)
239
- * Useful for apps that want to ensure authentication is complete before proceeding
240
- */
241
- async waitForAuth(timeoutMs = 5000) {
242
- return this.waitForAuthentication(timeoutMs);
243
- }
244
-
245
- /**
246
- * Wait for authentication to be ready with timeout
247
- */
248
- async waitForAuthentication(timeoutMs = 5000) {
249
- const startTime = Date.now();
250
- const checkInterval = 100; // Check every 100ms
251
-
252
- while (Date.now() - startTime < timeoutMs) {
253
- if (this.httpClient.hasAccessToken()) {
254
- return true;
255
- }
256
- await new Promise(resolve => setTimeout(resolve, checkInterval));
257
- }
258
- return false;
259
- }
260
-
261
- /**
262
- * Execute a function with automatic authentication retry logic
263
- * This handles the common case where API calls are made before authentication completes
264
- */
265
- async withAuthRetry(operation, operationName, options = {}) {
266
- const {
267
- maxRetries = 2,
268
- retryDelay = 1000,
269
- authTimeoutMs = 5000
270
- } = options;
271
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
272
- try {
273
- // First attempt: check if we have a token
274
- if (!this.httpClient.hasAccessToken()) {
275
- if (attempt === 0) {
276
- // On first attempt, wait briefly for authentication to complete
277
- const authReady = await this.waitForAuthentication(authTimeoutMs);
278
- if (!authReady) {
279
- throw new OxyAuthenticationTimeoutError(operationName, authTimeoutMs);
280
- }
281
- } else {
282
- // On retry attempts, fail immediately if no token
283
- throw new OxyAuthenticationError(`Authentication required: ${operationName} requires a valid access token.`, 'AUTH_REQUIRED');
284
- }
285
- }
286
-
287
- // Execute the operation
288
- return await operation();
289
- } catch (error) {
290
- const isLastAttempt = attempt === maxRetries;
291
- const isAuthError = error?.response?.status === 401 || error?.code === 'MISSING_TOKEN' || error?.message?.includes('Authentication') || error instanceof OxyAuthenticationError;
292
- if (isAuthError && !isLastAttempt && !(error instanceof OxyAuthenticationTimeoutError)) {
293
- await new Promise(resolve => setTimeout(resolve, retryDelay));
294
- continue;
295
- }
296
-
297
- // If it's not an auth error, or it's the last attempt, throw the error
298
- if (error instanceof OxyAuthenticationError) {
299
- throw error;
300
- }
301
- throw this.handleError(error);
302
- }
303
- }
304
-
305
- // This should never be reached, but TypeScript requires it
306
- throw new OxyAuthenticationError(`${operationName} failed after ${maxRetries + 1} attempts`);
307
- }
308
-
309
- /**
310
- * Validate the current access token with the server
311
- */
312
- async validate() {
313
- if (!this.hasAccessToken()) {
314
- return false;
315
- }
316
- try {
317
- const res = await this.makeRequest('GET', '/api/auth/validate', undefined, {
318
- cache: false,
319
- retry: false
320
- });
321
- return res.valid === true;
322
- } catch (error) {
323
- return false;
324
- }
325
- }
326
-
327
- /**
328
- * Centralized error handling
329
- */
330
- handleError(error) {
331
- const api = handleHttpError(error);
332
- const err = new Error(api.message);
333
- err.code = api.code;
334
- err.status = api.status;
335
- err.details = api.details;
336
- return err;
337
- }
338
-
339
- /**
340
- * Health check endpoint
341
- */
342
- async healthCheck() {
343
- try {
344
- return await this.makeRequest('GET', '/health', undefined, {
345
- cache: false
346
- });
347
- } catch (error) {
348
- throw this.handleError(error);
349
- }
350
- }
351
-
352
- // ============================================================================
353
- // AUTHENTICATION METHODS
354
- // ============================================================================
355
-
356
- /**
357
- * Sign up a new user
358
- */
359
- async signUp(username, email, password) {
360
- try {
361
- const res = await this.makeRequest('POST', '/api/auth/signup', {
362
- username,
363
- email,
364
- password
365
- }, {
366
- cache: false
367
- });
368
- if (!res || typeof res === 'object' && Object.keys(res).length === 0) {
369
- throw new OxyAuthenticationError('Sign up failed', 'SIGNUP_FAILED', 400);
370
- }
371
- return res;
372
- } catch (error) {
373
- throw this.handleError(error);
374
- }
375
- }
376
-
377
- /**
378
- * Request account recovery (send verification code)
379
- */
380
- async requestRecovery(identifier) {
381
- try {
382
- return await this.makeRequest('POST', '/api/auth/recover/request', {
383
- identifier
384
- }, {
385
- cache: false
386
- });
387
- } catch (error) {
388
- throw this.handleError(error);
389
- }
390
- }
391
-
392
- /**
393
- * Verify recovery code
394
- */
395
- async verifyRecoveryCode(identifier, code) {
396
- try {
397
- return await this.makeRequest('POST', '/api/auth/recover/verify', {
398
- identifier,
399
- code
400
- }, {
401
- cache: false
402
- });
403
- } catch (error) {
404
- throw this.handleError(error);
405
- }
406
- }
407
-
408
- /**
409
- * Reset password using verified code
410
- */
411
- async resetPassword(identifier, code, newPassword) {
412
- try {
413
- return await this.makeRequest('POST', '/api/auth/recover/reset', {
414
- identifier,
415
- code,
416
- newPassword
417
- }, {
418
- cache: false
419
- });
420
- } catch (error) {
421
- throw this.handleError(error);
422
- }
423
- }
424
-
425
- /**
426
- * Reset password using TOTP code (recommended recovery)
427
- */
428
- async resetPasswordWithTotp(identifier, code, newPassword) {
429
- try {
430
- return await this.makeRequest('POST', '/api/auth/recover/totp/reset', {
431
- identifier,
432
- code,
433
- newPassword
434
- }, {
435
- cache: false
436
- });
437
- } catch (error) {
438
- throw this.handleError(error);
439
- }
440
- }
441
- async resetPasswordWithBackupCode(identifier, backupCode, newPassword) {
442
- try {
443
- return await this.makeRequest('POST', '/api/auth/recover/backup/reset', {
444
- identifier,
445
- backupCode,
446
- newPassword
447
- }, {
448
- cache: false
449
- });
450
- } catch (error) {
451
- throw this.handleError(error);
452
- }
453
- }
454
- async resetPasswordWithRecoveryKey(identifier, recoveryKey, newPassword) {
455
- try {
456
- return await this.makeRequest('POST', '/api/auth/recover/recovery-key/reset', {
457
- identifier,
458
- recoveryKey,
459
- newPassword
460
- }, {
461
- cache: false
462
- });
463
- } catch (error) {
464
- throw this.handleError(error);
465
- }
466
- }
467
-
468
- /**
469
- * Sign in with device management
470
- */
471
- async signIn(username, password, deviceName, deviceFingerprint) {
472
- try {
473
- return await this.makeRequest('POST', '/api/auth/login', {
474
- username,
475
- password,
476
- deviceName,
477
- deviceFingerprint
478
- }, {
479
- cache: false
480
- });
481
- } catch (error) {
482
- throw this.handleError(error);
483
- }
484
- }
485
-
486
- /**
487
- * Complete login by verifying TOTP with MFA token
488
- */
489
- async verifyTotpLogin(mfaToken, code) {
490
- try {
491
- return await this.makeRequest('POST', '/api/auth/totp/verify-login', {
492
- mfaToken,
493
- code
494
- }, {
495
- cache: false
496
- });
497
- } catch (error) {
498
- throw this.handleError(error);
499
- }
500
- }
501
-
502
- /**
503
- * Get user by session ID
504
- */
505
- async getUserBySession(sessionId) {
506
- try {
507
- return await this.makeRequest('GET', `/api/session/user/${sessionId}`, undefined, {
508
- cache: true,
509
- cacheTTL: 2 * 60 * 1000 // 2 minutes cache for user data
510
- });
511
- } catch (error) {
512
- throw this.handleError(error);
513
- }
514
- }
515
-
516
- /**
517
- * Batch get multiple user profiles by session IDs (optimized for account switching)
518
- * Returns array of { sessionId, user } objects
519
- */
520
- async getUsersBySessions(sessionIds) {
521
- try {
522
- if (!Array.isArray(sessionIds) || sessionIds.length === 0) {
523
- return [];
524
- }
525
-
526
- // Deduplicate and sort sessionIds for consistent cache keys
527
- const uniqueSessionIds = Array.from(new Set(sessionIds)).sort();
528
- return await this.makeRequest('POST', '/api/session/users/batch', {
529
- sessionIds: uniqueSessionIds
530
- }, {
531
- cache: true,
532
- cacheTTL: 2 * 60 * 1000,
533
- // 2 minutes cache
534
- deduplicate: true // Important for batch requests
535
- });
536
- } catch (error) {
537
- throw this.handleError(error);
538
- }
539
- }
540
-
541
- /**
542
- * Get access token by session ID and set it in the token store
543
- */
544
- async getTokenBySession(sessionId) {
545
- try {
546
- const res = await this.makeRequest('GET', `/api/session/token/${sessionId}`, undefined, {
547
- cache: false,
548
- retry: false
549
- });
550
-
551
- // Set the token in the centralized token store
552
- this.setTokens(res.accessToken);
553
- return res;
554
- } catch (error) {
555
- throw this.handleError(error);
556
- }
557
- }
558
-
559
- /**
560
- * Get sessions by session ID
561
- */
562
- async getSessionsBySessionId(sessionId) {
563
- try {
564
- return await this.makeRequest('GET', `/api/session/sessions/${sessionId}`, undefined, {
565
- cache: false
566
- });
567
- } catch (error) {
568
- throw this.handleError(error);
569
- }
570
- }
571
-
572
- /**
573
- * Logout from a specific session
574
- */
575
- async logoutSession(sessionId, targetSessionId) {
576
- try {
577
- const url = targetSessionId ? `/api/session/logout/${sessionId}/${targetSessionId}` : `/api/session/logout/${sessionId}`;
578
- await this.makeRequest('POST', url, undefined, {
579
- cache: false
580
- });
581
- } catch (error) {
582
- throw this.handleError(error);
583
- }
584
- }
585
-
586
- /**
587
- * Logout from all sessions
588
- */
589
- async logoutAllSessions(sessionId) {
590
- try {
591
- await this.makeRequest('POST', `/api/session/logout-all/${sessionId}`, undefined, {
592
- cache: false
593
- });
594
- } catch (error) {
595
- throw this.handleError(error);
596
- }
597
- }
598
-
599
- /**
600
- * Validate session
601
- */
602
- async validateSession(sessionId, options = {}) {
603
- try {
604
- const params = new URLSearchParams();
605
- if (options.deviceFingerprint) {
606
- params.append('deviceFingerprint', options.deviceFingerprint);
607
- }
608
- if (options.useHeaderValidation) {
609
- params.append('useHeaderValidation', 'true');
610
- }
611
- const url = `/api/session/validate/${sessionId}`;
612
- const urlParams = {};
613
- if (options.deviceFingerprint) urlParams.deviceFingerprint = options.deviceFingerprint;
614
- if (options.useHeaderValidation) urlParams.useHeaderValidation = 'true';
615
- return await this.makeRequest('GET', url, urlParams, {
616
- cache: false
617
- });
618
- } catch (error) {
619
- throw this.handleError(error);
620
- }
621
- }
622
-
623
- /**
624
- * Check username availability
625
- */
626
- async checkUsernameAvailability(username) {
627
- try {
628
- return await this.makeRequest('GET', `/api/auth/check-username/${username}`, undefined, {
629
- cache: false
630
- });
631
- } catch (error) {
632
- throw this.handleError(error);
633
- }
634
- }
635
-
636
- /**
637
- * Check email availability
638
- */
639
- async checkEmailAvailability(email) {
640
- try {
641
- return await this.makeRequest('GET', `/api/auth/check-email/${email}`, undefined, {
642
- cache: false
643
- });
644
- } catch (error) {
645
- throw this.handleError(error);
646
- }
647
- }
648
-
649
- // ============================================================================
650
- // USER METHODS
651
- // ============================================================================
652
-
653
- /**
654
- * Get profile by username
655
- */
656
- async getProfileByUsername(username) {
657
- try {
658
- return await this.makeRequest('GET', `/api/profiles/username/${username}`, undefined, {
659
- cache: true,
660
- cacheTTL: 5 * 60 * 1000 // 5 minutes cache for profiles
661
- });
662
- } catch (error) {
663
- throw this.handleError(error);
664
- }
665
- }
666
-
667
- // ============================================================================
668
- // TOTP ENROLLMENT
669
- // ============================================================================
670
-
671
- async startTotpEnrollment(sessionId) {
672
- try {
673
- // Note: x-session-id header is handled by HttpClient interceptors if needed
674
- return await this.makeRequest('POST', '/api/auth/totp/enroll/start', {
675
- sessionId
676
- }, {
677
- cache: false
678
- });
679
- } catch (error) {
680
- throw this.handleError(error);
681
- }
682
- }
683
- async verifyTotpEnrollment(sessionId, code) {
684
- try {
685
- return await this.makeRequest('POST', '/api/auth/totp/enroll/verify', {
686
- sessionId,
687
- code
688
- }, {
689
- cache: false
690
- });
691
- } catch (error) {
692
- throw this.handleError(error);
693
- }
694
- }
695
- async disableTotp(sessionId, code) {
696
- try {
697
- return await this.makeRequest('POST', '/api/auth/totp/disable', {
698
- sessionId,
699
- code
700
- }, {
701
- cache: false
702
- });
703
- } catch (error) {
704
- throw this.handleError(error);
705
- }
706
- }
707
-
708
- /**
709
- * Search user profiles
710
- */
711
- async searchProfiles(query, pagination) {
712
- try {
713
- const params = {
714
- query,
715
- ...pagination
716
- };
717
- const searchParams = buildSearchParams(params);
718
- const paramsObj = {};
719
- searchParams.forEach((value, key) => {
720
- paramsObj[key] = value;
721
- });
722
- return await this.makeRequest('GET', '/api/profiles/search', paramsObj, {
723
- cache: true,
724
- cacheTTL: 2 * 60 * 1000 // 2 minutes cache
725
- });
726
- } catch (error) {
727
- throw this.handleError(error);
728
- }
729
- }
730
-
731
- /**
732
- * Get profile recommendations
733
- */
734
- async getProfileRecommendations() {
735
- return this.withAuthRetry(async () => {
736
- return await this.makeRequest('GET', '/api/profiles/recommendations', undefined, {
737
- cache: true
738
- });
739
- }, 'getProfileRecommendations');
740
- }
741
-
742
- /**
743
- * Get user by ID
744
- */
745
- async getUserById(userId) {
746
- try {
747
- return await this.makeRequest('GET', `/api/users/${userId}`, undefined, {
748
- cache: true,
749
- cacheTTL: 5 * 60 * 1000 // 5 minutes cache
750
- });
751
- } catch (error) {
752
- throw this.handleError(error);
753
- }
754
- }
755
-
756
- /**
757
- * Get current user
758
- */
759
- async getCurrentUser() {
760
- return this.withAuthRetry(async () => {
761
- return await this.makeRequest('GET', '/api/users/me', undefined, {
762
- cache: true,
763
- cacheTTL: 1 * 60 * 1000 // 1 minute cache for current user
764
- });
765
- }, 'getCurrentUser');
766
- }
767
-
768
- /**
769
- * Update user profile
770
- */
771
- async updateProfile(updates) {
772
- try {
773
- return await this.makeRequest('PUT', '/api/users/me', updates, {
774
- cache: false
775
- });
776
- } catch (error) {
777
- throw this.handleError(error);
778
- }
779
- }
780
-
781
- /**
782
- * Get privacy settings for a user
783
- * @param userId - The user ID (defaults to current user)
784
- */
785
- async getPrivacySettings(userId) {
786
- try {
787
- const id = userId || (await this.getCurrentUser()).id;
788
- return await this.makeRequest('GET', `/api/privacy/${id}/privacy`, undefined, {
789
- cache: true,
790
- cacheTTL: 2 * 60 * 1000 // 2 minutes cache
791
- });
792
- } catch (error) {
793
- throw this.handleError(error);
794
- }
795
- }
796
-
797
- /**
798
- * Update privacy settings
799
- * @param settings - Partial privacy settings object
800
- * @param userId - The user ID (defaults to current user)
801
- */
802
- async updatePrivacySettings(settings, userId) {
803
- try {
804
- const id = userId || (await this.getCurrentUser()).id;
805
- return await this.makeRequest('PATCH', `/api/privacy/${id}/privacy`, settings, {
806
- cache: false
807
- });
808
- } catch (error) {
809
- throw this.handleError(error);
810
- }
811
- }
812
-
813
- /**
814
- * Request account verification
815
- */
816
- async requestAccountVerification(reason, evidence) {
817
- try {
818
- return await this.makeRequest('POST', '/api/users/verify/request', {
819
- reason,
820
- evidence
821
- }, {
822
- cache: false
823
- });
824
- } catch (error) {
825
- throw this.handleError(error);
826
- }
827
- }
828
-
829
- /**
830
- * Download account data export
831
- */
832
- async downloadAccountData(format = 'json') {
833
- try {
834
- // Use axios instance directly for blob responses since RequestManager doesn't handle blobs
835
- const axiosInstance = this.httpClient.getAxiosInstance();
836
- const response = await axiosInstance.get(`/api/users/me/data?format=${format}`, {
837
- responseType: 'blob'
838
- });
839
- return response.data;
840
- } catch (error) {
841
- throw this.handleError(error);
842
- }
843
- }
844
-
845
- /**
846
- * Delete account permanently
847
- * @param password - User password for confirmation
848
- * @param confirmText - Confirmation text (usually username)
849
- */
850
- async deleteAccount(password, confirmText) {
851
- try {
852
- return await this.makeRequest('DELETE', '/api/users/me', {
853
- password,
854
- confirmText
855
- }, {
856
- cache: false
857
- });
858
- } catch (error) {
859
- throw this.handleError(error);
860
- }
861
- }
862
-
863
- // ============================================================================
864
- // LANGUAGE METHODS
865
- // ============================================================================
866
-
867
- /**
868
- * Get the current language from storage or user profile
869
- * @param storageKeyPrefix - Optional prefix for storage key (default: 'oxy_session')
870
- * @returns The current language code (e.g., 'en-US') or null if not set
871
- */
872
- async getCurrentLanguage(storageKeyPrefix = 'oxy_session') {
873
- try {
874
- // First try to get from user profile if authenticated
875
- try {
876
- const user = await this.getCurrentUser();
877
- const userLanguage = user?.language;
878
- if (userLanguage) {
879
- return normalizeLanguageCode(userLanguage) || userLanguage;
880
- }
881
- } catch (e) {
882
- // User not authenticated or error, continue to storage
883
- }
884
-
885
- // Fall back to storage
886
- const storage = await this.getStorage();
887
- const storageKey = `${storageKeyPrefix}_language`;
888
- const storedLanguage = await storage.getItem(storageKey);
889
- if (storedLanguage) {
890
- return normalizeLanguageCode(storedLanguage) || storedLanguage;
891
- }
892
- return null;
893
- } catch (error) {
894
- if (__DEV__) {
895
- console.warn('Failed to get current language:', error);
896
- }
897
- return null;
898
- }
899
- }
900
-
901
- /**
902
- * Get the current language with metadata (name, nativeName, etc.)
903
- * @param storageKeyPrefix - Optional prefix for storage key (default: 'oxy_session')
904
- * @returns Language metadata object or null if not set
905
- */
906
- async getCurrentLanguageMetadata(storageKeyPrefix = 'oxy_session') {
907
- const languageCode = await this.getCurrentLanguage(storageKeyPrefix);
908
- return getLanguageMetadata(languageCode);
909
- }
910
-
911
- /**
912
- * Get the current language name (e.g., 'English')
913
- * @param storageKeyPrefix - Optional prefix for storage key (default: 'oxy_session')
914
- * @returns Language name or null if not set
915
- */
916
- async getCurrentLanguageName(storageKeyPrefix = 'oxy_session') {
917
- const languageCode = await this.getCurrentLanguage(storageKeyPrefix);
918
- if (!languageCode) return null;
919
- return getLanguageName(languageCode);
920
- }
921
-
922
- /**
923
- * Get the current native language name (e.g., 'Español')
924
- * @param storageKeyPrefix - Optional prefix for storage key (default: 'oxy_session')
925
- * @returns Native language name or null if not set
926
- */
927
- async getCurrentNativeLanguageName(storageKeyPrefix = 'oxy_session') {
928
- const languageCode = await this.getCurrentLanguage(storageKeyPrefix);
929
- if (!languageCode) return null;
930
- return getNativeLanguageName(languageCode);
931
- }
932
-
933
- /**
934
- * Get appropriate storage for the platform (similar to DeviceManager)
935
- * @private
936
- */
937
- async getStorage() {
938
- const isReactNative = typeof navigator !== 'undefined' && navigator.product === 'ReactNative';
939
- if (isReactNative) {
940
- try {
941
- const asyncStorageModule = await import('@react-native-async-storage/async-storage');
942
- const storage = asyncStorageModule.default;
943
- return {
944
- getItem: storage.getItem.bind(storage),
945
- setItem: storage.setItem.bind(storage),
946
- removeItem: storage.removeItem.bind(storage)
947
- };
948
- } catch (error) {
949
- console.error('AsyncStorage not available in React Native:', error);
950
- throw new Error('AsyncStorage is required in React Native environment');
951
- }
952
- } else {
953
- // Use localStorage for web
954
- return {
955
- getItem: async key => {
956
- if (typeof window !== 'undefined' && window.localStorage) {
957
- return localStorage.getItem(key);
958
- }
959
- return null;
960
- },
961
- setItem: async (key, value) => {
962
- if (typeof window !== 'undefined' && window.localStorage) {
963
- localStorage.setItem(key, value);
964
- }
965
- },
966
- removeItem: async key => {
967
- if (typeof window !== 'undefined' && window.localStorage) {
968
- localStorage.removeItem(key);
969
- }
970
- }
971
- };
972
- }
973
- }
102
+ // Compose all mixins into the final OxyServices class
103
+ const OxyServicesComposed = composeOxyServices();
974
104
 
975
- /**
976
- * Update user by ID (admin function)
977
- */
978
- async updateUser(userId, updates) {
979
- try {
980
- return await this.makeRequest('PUT', `/api/users/${userId}`, updates, {
981
- cache: false
982
- });
983
- } catch (error) {
984
- throw this.handleError(error);
985
- }
986
- }
987
-
988
- /**
989
- * Follow a user
990
- */
991
- async followUser(userId) {
992
- try {
993
- return await this.makeRequest('POST', `/api/users/${userId}/follow`, undefined, {
994
- cache: false
995
- });
996
- } catch (error) {
997
- throw this.handleError(error);
998
- }
999
- }
1000
-
1001
- /**
1002
- * Unfollow a user
1003
- */
1004
- async unfollowUser(userId) {
1005
- try {
1006
- return await this.makeRequest('DELETE', `/api/users/${userId}/follow`, undefined, {
1007
- cache: false
1008
- });
1009
- } catch (error) {
1010
- throw this.handleError(error);
1011
- }
1012
- }
1013
-
1014
- /**
1015
- * Get follow status
1016
- */
1017
- async getFollowStatus(userId) {
1018
- try {
1019
- return await this.makeRequest('GET', `/api/users/${userId}/follow-status`, undefined, {
1020
- cache: true,
1021
- cacheTTL: 1 * 60 * 1000 // 1 minute cache
1022
- });
1023
- } catch (error) {
1024
- throw this.handleError(error);
1025
- }
1026
- }
1027
-
1028
- /**
1029
- * Get user followers
1030
- */
1031
- async getUserFollowers(userId, pagination) {
1032
- try {
1033
- const params = buildPaginationParams(pagination || {});
1034
- const response = await this.makeRequest('GET', `/api/users/${userId}/followers`, params, {
1035
- cache: true,
1036
- cacheTTL: 2 * 60 * 1000 // 2 minutes cache
1037
- });
1038
- return {
1039
- followers: response.data || [],
1040
- total: response.pagination.total,
1041
- hasMore: response.pagination.hasMore
1042
- };
1043
- } catch (error) {
1044
- throw this.handleError(error);
1045
- }
1046
- }
1047
-
1048
- /**
1049
- * Get user following
1050
- */
1051
- async getUserFollowing(userId, pagination) {
1052
- try {
1053
- const params = buildPaginationParams(pagination || {});
1054
- const response = await this.makeRequest('GET', `/api/users/${userId}/following`, params, {
1055
- cache: true,
1056
- cacheTTL: 2 * 60 * 1000 // 2 minutes cache
1057
- });
1058
- return {
1059
- following: response.data || [],
1060
- total: response.pagination.total,
1061
- hasMore: response.pagination.hasMore
1062
- };
1063
- } catch (error) {
1064
- throw this.handleError(error);
1065
- }
1066
- }
1067
-
1068
- /**
1069
- * Get notifications
1070
- */
1071
- async getNotifications() {
1072
- return this.withAuthRetry(async () => {
1073
- return await this.makeRequest('GET', '/api/notifications', undefined, {
1074
- cache: false // Don't cache notifications - always get fresh data
1075
- });
1076
- }, 'getNotifications');
1077
- }
1078
-
1079
- /**
1080
- * Get unread notification count
1081
- */
1082
- async getUnreadCount() {
1083
- try {
1084
- const res = await this.makeRequest('GET', '/api/notifications/unread-count', undefined, {
1085
- cache: false // Don't cache unread count - always get fresh data
1086
- });
1087
- return res.count;
1088
- } catch (error) {
1089
- throw this.handleError(error);
1090
- }
1091
- }
1092
-
1093
- /**
1094
- * Create notification
1095
- */
1096
- async createNotification(data) {
1097
- try {
1098
- return await this.makeRequest('POST', '/api/notifications', data, {
1099
- cache: false
1100
- });
1101
- } catch (error) {
1102
- throw this.handleError(error);
1103
- }
1104
- }
1105
-
1106
- /**
1107
- * Mark notification as read
1108
- */
1109
- async markNotificationAsRead(notificationId) {
1110
- try {
1111
- await this.makeRequest('PUT', `/api/notifications/${notificationId}/read`, undefined, {
1112
- cache: false
1113
- });
1114
- } catch (error) {
1115
- throw this.handleError(error);
1116
- }
1117
- }
1118
-
1119
- /**
1120
- * Mark all notifications as read
1121
- */
1122
- async markAllNotificationsAsRead() {
1123
- try {
1124
- await this.makeRequest('PUT', '/api/notifications/read-all', undefined, {
1125
- cache: false
1126
- });
1127
- } catch (error) {
1128
- throw this.handleError(error);
1129
- }
1130
- }
1131
-
1132
- /**
1133
- * Delete notification
1134
- */
1135
- async deleteNotification(notificationId) {
1136
- try {
1137
- await this.makeRequest('DELETE', `/api/notifications/${notificationId}`, undefined, {
1138
- cache: false
1139
- });
1140
- } catch (error) {
1141
- throw this.handleError(error);
1142
- }
1143
- }
1144
-
1145
- // ============================================================================
1146
- // PAYMENT METHODS
1147
- // ============================================================================
1148
-
1149
- /**
1150
- * Create a payment
1151
- */
1152
- async createPayment(data) {
1153
- try {
1154
- return await this.makeRequest('POST', '/api/payments', data, {
1155
- cache: false
1156
- });
1157
- } catch (error) {
1158
- throw this.handleError(error);
1159
- }
1160
- }
1161
-
1162
- /**
1163
- * Get payment by ID
1164
- */
1165
- async getPayment(paymentId) {
1166
- try {
1167
- return await this.makeRequest('GET', `/api/payments/${paymentId}`, undefined, {
1168
- cache: true,
1169
- cacheTTL: 5 * 60 * 1000 // 5 minutes cache
1170
- });
1171
- } catch (error) {
1172
- throw this.handleError(error);
1173
- }
1174
- }
1175
-
1176
- /**
1177
- * Get user payments
1178
- */
1179
- async getUserPayments() {
1180
- try {
1181
- return await this.makeRequest('GET', '/api/payments/user', undefined, {
1182
- cache: false // Don't cache user payments - always get fresh data
1183
- });
1184
- } catch (error) {
1185
- throw this.handleError(error);
1186
- }
1187
- }
1188
-
1189
- // ============================================================================
1190
- // KARMA METHODS
1191
- // ============================================================================
1192
-
1193
- /**
1194
- * Get user karma
1195
- */
1196
- async getUserKarma(userId) {
1197
- try {
1198
- return await this.makeRequest('GET', `/api/karma/${userId}`, undefined, {
1199
- cache: true,
1200
- cacheTTL: 2 * 60 * 1000 // 2 minutes cache
1201
- });
1202
- } catch (error) {
1203
- throw this.handleError(error);
1204
- }
1205
- }
1206
-
1207
- /**
1208
- * Give karma to user
1209
- */
1210
- async giveKarma(userId, amount, reason) {
1211
- try {
1212
- return await this.makeRequest('POST', `/api/karma/${userId}/give`, {
1213
- amount,
1214
- reason
1215
- }, {
1216
- cache: false
1217
- });
1218
- } catch (error) {
1219
- throw this.handleError(error);
1220
- }
1221
- }
1222
-
1223
- /**
1224
- * Get user karma total
1225
- */
1226
- async getUserKarmaTotal(userId) {
1227
- try {
1228
- return await this.makeRequest('GET', `/api/karma/${userId}/total`, undefined, {
1229
- cache: true,
1230
- cacheTTL: 2 * 60 * 1000 // 2 minutes cache
1231
- });
1232
- } catch (error) {
1233
- throw this.handleError(error);
1234
- }
1235
- }
1236
-
1237
- /**
1238
- * Get user karma history
1239
- */
1240
- async getUserKarmaHistory(userId, limit, offset) {
1241
- try {
1242
- const params = {};
1243
- if (limit) params.limit = limit;
1244
- if (offset) params.offset = offset;
1245
- return await this.makeRequest('GET', `/api/karma/${userId}/history`, params, {
1246
- cache: true,
1247
- cacheTTL: 2 * 60 * 1000 // 2 minutes cache
1248
- });
1249
- } catch (error) {
1250
- throw this.handleError(error);
1251
- }
1252
- }
1253
-
1254
- /**
1255
- * Get karma leaderboard
1256
- */
1257
- async getKarmaLeaderboard() {
1258
- try {
1259
- return await this.makeRequest('GET', '/api/karma/leaderboard', undefined, {
1260
- cache: true,
1261
- cacheTTL: 5 * 60 * 1000 // 5 minutes cache
1262
- });
1263
- } catch (error) {
1264
- throw this.handleError(error);
1265
- }
1266
- }
1267
-
1268
- /**
1269
- * Get karma rules
1270
- */
1271
- async getKarmaRules() {
1272
- try {
1273
- return await this.makeRequest('GET', '/api/karma/rules', undefined, {
1274
- cache: true,
1275
- cacheTTL: 30 * 60 * 1000 // 30 minutes cache (rules don't change often)
1276
- });
1277
- } catch (error) {
1278
- throw this.handleError(error);
1279
- }
1280
- }
1281
-
1282
- // ============================================================================
1283
- // FILE METHODS (LEGACY - Using Asset Service)
1284
- // ============================================================================
1285
-
1286
- /**
1287
- * Delete file
1288
- */
1289
- async deleteFile(fileId) {
1290
- try {
1291
- // Central Asset Service delete with force=true behavior controlled by caller via assetDelete
1292
- return await this.makeRequest('DELETE', `/api/assets/${encodeURIComponent(fileId)}`, undefined, {
1293
- cache: false
1294
- });
1295
- } catch (error) {
1296
- throw this.handleError(error);
1297
- }
1298
- }
1299
-
1300
- /**
1301
- * Get file download URL (API streaming proxy, attaches token for <img src>)
1302
- */
1303
- getFileDownloadUrl(fileId, variant, expiresIn) {
1304
- const base = this.getBaseURL();
1305
- const params = new URLSearchParams();
1306
- if (variant) params.set('variant', variant);
1307
- if (expiresIn) params.set('expiresIn', String(expiresIn));
1308
- params.set('fallback', 'placeholderVisible');
1309
- const token = this.httpClient.getAccessToken();
1310
- if (token) params.set('token', token);
1311
-
1312
- // Use params.toString() to detect whether there are query params.
1313
- // URLSearchParams.size is not a standard property across all JS runtimes
1314
- // (some environments like React Native may not implement it), which
1315
- // caused the query string to be omitted on native. Checking the
1316
- // serialized string is reliable everywhere.
1317
- const qs = params.toString();
1318
- return `${base}/api/assets/${encodeURIComponent(fileId)}/stream${qs ? `?${qs}` : ''}`;
1319
- }
1320
-
1321
- /**
1322
- * Get file stream URL (direct Oxy Cloud/CDN URL, no token)
1323
- */
1324
- getFileStreamUrl(fileId) {
1325
- return `${this.getCloudURL()}/files/${fileId}/stream`;
1326
- }
1327
-
1328
- // ...existing code...
1329
-
1330
- /**
1331
- * List user files
1332
- */
1333
- async listUserFiles(limit, offset) {
1334
- try {
1335
- const paramsObj = {};
1336
- if (limit) paramsObj.limit = limit;
1337
- if (offset) paramsObj.offset = offset;
1338
- return await this.makeRequest('GET', '/api/assets', paramsObj, {
1339
- cache: false // Don't cache file lists - always get fresh data
1340
- });
1341
- } catch (error) {
1342
- throw this.handleError(error);
1343
- }
1344
- }
1345
-
1346
- // (removed legacy downloadFileContent; use getFileContentAsBlob/Text which resolve CAS URL first)
1347
-
1348
- /**
1349
- * Get file content as text
1350
- */
1351
- async getFileContentAsText(fileId, variant) {
1352
- try {
1353
- const params = variant ? {
1354
- variant
1355
- } : undefined;
1356
- const urlRes = await this.makeRequest('GET', `/api/assets/${encodeURIComponent(fileId)}/url`, params, {
1357
- cache: true,
1358
- cacheTTL: 10 * 60 * 1000 // 10 minutes cache for URLs
1359
- });
1360
- const downloadUrl = urlRes?.url;
1361
- const response = await fetch(downloadUrl);
1362
- return await response.text();
1363
- } catch (error) {
1364
- throw this.handleError(error);
1365
- }
1366
- }
1367
-
1368
- /**
1369
- * Get file content as blob
1370
- */
1371
- async getFileContentAsBlob(fileId, variant) {
1372
- try {
1373
- const params = variant ? {
1374
- variant
1375
- } : undefined;
1376
- const urlRes = await this.makeRequest('GET', `/api/assets/${encodeURIComponent(fileId)}/url`, params, {
1377
- cache: true,
1378
- cacheTTL: 10 * 60 * 1000 // 10 minutes cache for URLs
1379
- });
1380
- const downloadUrl = urlRes?.url;
1381
- const response = await fetch(downloadUrl);
1382
- return await response.blob();
1383
- } catch (error) {
1384
- throw this.handleError(error);
1385
- }
1386
- }
1387
-
1388
- /**
1389
- * Upload raw file data
1390
- */
1391
- async uploadRawFile(file, visibility, metadata) {
1392
- // Switch to Central Asset Service upload flow
1393
- return this.assetUpload(file, visibility, metadata);
1394
- }
1395
-
1396
- // ============================================================================
1397
- // CENTRAL ASSET SERVICE METHODS
1398
- // ============================================================================
1399
-
1400
- /**
1401
- * Calculate SHA256 hash of file content
1402
- */
1403
- async calculateSHA256(file) {
1404
- const buffer = await file.arrayBuffer();
1405
- const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
1406
- const hashArray = Array.from(new Uint8Array(hashBuffer));
1407
- return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
1408
- }
1409
-
1410
- /**
1411
- * Initialize asset upload - returns pre-signed URL and file ID
1412
- */
1413
- async assetInit(sha256, size, mime) {
1414
- try {
1415
- return await this.makeRequest('POST', '/api/assets/init', {
1416
- sha256,
1417
- size,
1418
- mime
1419
- }, {
1420
- cache: false
1421
- });
1422
- } catch (error) {
1423
- throw this.handleError(error);
1424
- }
1425
- }
1426
-
1427
- /**
1428
- * Complete asset upload - commit metadata and trigger variant generation
1429
- */
1430
- async assetComplete(fileId, originalName, size, mime, visibility, metadata) {
1431
- try {
1432
- return await this.makeRequest('POST', '/api/assets/complete', {
1433
- fileId,
1434
- originalName,
1435
- size,
1436
- mime,
1437
- visibility,
1438
- metadata
1439
- }, {
1440
- cache: false
1441
- });
1442
- } catch (error) {
1443
- throw this.handleError(error);
1444
- }
1445
- }
1446
-
1447
- /**
1448
- * Upload file using Central Asset Service
1449
- */
1450
- async assetUpload(file, visibility, metadata, onProgress) {
1451
- try {
1452
- // Calculate SHA256
1453
- const sha256 = await this.calculateSHA256(file);
1454
-
1455
- // Initialize upload
1456
- const initResponse = await this.assetInit(sha256, file.size, file.type);
1457
-
1458
- // Try presigned URL first
1459
- try {
1460
- await this.uploadToPresignedUrl(initResponse.uploadUrl, file, onProgress);
1461
- } catch (e) {
1462
- // Fallback: direct upload via API to avoid CORS issues
1463
- const fd = new FormData();
1464
- fd.append('file', file);
1465
- // Use httpClient directly for FormData uploads (bypasses RequestManager for special handling)
1466
- await this.httpClient.request({
1467
- method: 'POST',
1468
- url: `/api/assets/${encodeURIComponent(initResponse.fileId)}/upload-direct`,
1469
- data: fd
1470
- });
1471
- }
1472
-
1473
- // Complete upload
1474
- return await this.assetComplete(initResponse.fileId, file.name, file.size, file.type, visibility, metadata);
1475
- } catch (error) {
1476
- throw this.handleError(error);
1477
- }
1478
- }
1479
-
1480
- /**
1481
- * Upload file to pre-signed URL
1482
- */
1483
- async uploadToPresignedUrl(url, file, onProgress) {
1484
- return new Promise((resolve, reject) => {
1485
- const xhr = new XMLHttpRequest();
1486
- xhr.upload.addEventListener('progress', event => {
1487
- if (event.lengthComputable && onProgress) {
1488
- const progress = event.loaded / event.total * 100;
1489
- onProgress(progress);
1490
- }
1491
- });
1492
- xhr.addEventListener('load', () => {
1493
- if (xhr.status >= 200 && xhr.status < 300) {
1494
- resolve();
1495
- } else {
1496
- reject(new Error(`Upload failed with status ${xhr.status}`));
1497
- }
1498
- });
1499
- xhr.addEventListener('error', () => {
1500
- reject(new Error('Upload failed'));
1501
- });
1502
- xhr.open('PUT', url);
1503
- xhr.setRequestHeader('Content-Type', file.type);
1504
- xhr.send(file);
1505
- });
1506
- }
1507
-
1508
- /**
1509
- * Link asset to an entity
1510
- */
1511
- async assetLink(fileId, app, entityType, entityId, visibility, webhookUrl) {
1512
- try {
1513
- const body = {
1514
- app,
1515
- entityType,
1516
- entityId
1517
- };
1518
- if (visibility) body.visibility = visibility;
1519
- if (webhookUrl) body.webhookUrl = webhookUrl;
1520
- return await this.makeRequest('POST', `/api/assets/${fileId}/links`, body, {
1521
- cache: false
1522
- });
1523
- } catch (error) {
1524
- throw this.handleError(error);
1525
- }
1526
- }
1527
-
1528
- /**
1529
- * Unlink asset from an entity
1530
- */
1531
- async assetUnlink(fileId, app, entityType, entityId) {
1532
- try {
1533
- return await this.makeRequest('DELETE', `/api/assets/${fileId}/links`, {
1534
- app,
1535
- entityType,
1536
- entityId
1537
- }, {
1538
- cache: false
1539
- });
1540
- } catch (error) {
1541
- throw this.handleError(error);
1542
- }
1543
- }
1544
-
1545
- /**
1546
- * Get asset metadata
1547
- */
1548
- async assetGet(fileId) {
1549
- try {
1550
- return await this.makeRequest('GET', `/api/assets/${fileId}`, undefined, {
1551
- cache: true,
1552
- cacheTTL: 5 * 60 * 1000 // 5 minutes cache
1553
- });
1554
- } catch (error) {
1555
- throw this.handleError(error);
1556
- }
1557
- }
1558
-
1559
- /**
1560
- * Get asset URL (CDN or signed URL)
1561
- */
1562
- async assetGetUrl(fileId, variant, expiresIn) {
1563
- try {
1564
- const params = {};
1565
- if (variant) params.variant = variant;
1566
- if (expiresIn) params.expiresIn = expiresIn;
1567
- return await this.makeRequest('GET', `/api/assets/${fileId}/url`, params, {
1568
- cache: true,
1569
- cacheTTL: 10 * 60 * 1000 // 10 minutes cache for URLs
1570
- });
1571
- } catch (error) {
1572
- throw this.handleError(error);
1573
- }
1574
- }
1575
-
1576
- /**
1577
- * Restore asset from trash
1578
- */
1579
- async assetRestore(fileId) {
1580
- try {
1581
- return await this.makeRequest('POST', `/api/assets/${fileId}/restore`, undefined, {
1582
- cache: false
1583
- });
1584
- } catch (error) {
1585
- throw this.handleError(error);
1586
- }
1587
- }
1588
-
1589
- /**
1590
- * Delete asset with optional force
1591
- */
1592
- async assetDelete(fileId, force = false) {
1593
- try {
1594
- const params = force ? {
1595
- force: 'true'
1596
- } : undefined;
1597
- return await this.makeRequest('DELETE', `/api/assets/${fileId}`, params, {
1598
- cache: false
1599
- });
1600
- } catch (error) {
1601
- throw this.handleError(error);
1602
- }
1603
- }
1604
-
1605
- /**
1606
- * Get list of available variants for an asset
1607
- */
1608
- async assetGetVariants(fileId) {
1609
- try {
1610
- const assetData = await this.assetGet(fileId);
1611
- return assetData.file?.variants || [];
1612
- } catch (error) {
1613
- throw this.handleError(error);
1614
- }
1615
- }
1616
-
1617
- /**
1618
- * Update asset visibility
1619
- * @param fileId - The file ID
1620
- * @param visibility - New visibility level ('private', 'public', or 'unlisted')
1621
- * @returns Updated asset information
1622
- */
1623
- async assetUpdateVisibility(fileId, visibility) {
1624
- try {
1625
- return await this.makeRequest('PATCH', `/api/assets/${fileId}/visibility`, {
1626
- visibility
1627
- }, {
1628
- cache: false
1629
- });
1630
- } catch (error) {
1631
- throw this.handleError(error);
1632
- }
1633
- }
1634
-
1635
- /**
1636
- * Helper: Upload and link avatar with automatic public visibility
1637
- * @param file - The avatar file
1638
- * @param userId - User ID to link to
1639
- * @param app - App name (defaults to 'profiles')
1640
- * @returns The uploaded and linked asset
1641
- */
1642
- async uploadAvatar(file, userId, app = 'profiles') {
1643
- try {
1644
- // Upload as public
1645
- const asset = await this.assetUpload(file, 'public');
1646
-
1647
- // Link to user profile as avatar
1648
- await this.assetLink(asset.file.id, app, 'avatar', userId, 'public');
1649
- return asset;
1650
- } catch (error) {
1651
- throw this.handleError(error);
1652
- }
1653
- }
1654
-
1655
- /**
1656
- * Helper: Upload and link profile banner with automatic public visibility
1657
- * @param file - The banner file
1658
- * @param userId - User ID to link to
1659
- * @param app - App name (defaults to 'profiles')
1660
- * @returns The uploaded and linked asset
1661
- */
1662
- async uploadProfileBanner(file, userId, app = 'profiles') {
1663
- try {
1664
- // Upload as public
1665
- const asset = await this.assetUpload(file, 'public');
1666
-
1667
- // Link to user profile as banner
1668
- await this.assetLink(asset.file.id, app, 'profile-banner', userId, 'public');
1669
- return asset;
1670
- } catch (error) {
1671
- throw this.handleError(error);
1672
- }
1673
- }
1674
-
1675
- // ============================================================================
1676
- // DEVELOPER API METHODS
1677
- // ============================================================================
1678
-
1679
- /**
1680
- * Get developer apps for the current user
1681
- */
1682
- async getDeveloperApps() {
1683
- try {
1684
- const res = await this.makeRequest('GET', '/api/developer/apps', undefined, {
1685
- cache: true,
1686
- cacheTTL: 2 * 60 * 1000 // 2 minutes cache
1687
- });
1688
- return res.apps || [];
1689
- } catch (error) {
1690
- throw this.handleError(error);
1691
- }
1692
- }
1693
-
1694
- /**
1695
- * Create a new developer app
1696
- */
1697
- async createDeveloperApp(data) {
1698
- try {
1699
- const res = await this.makeRequest('POST', '/api/developer/apps', data, {
1700
- cache: false
1701
- });
1702
- return res.app;
1703
- } catch (error) {
1704
- throw this.handleError(error);
1705
- }
1706
- }
1707
-
1708
- /**
1709
- * Get a specific developer app
1710
- */
1711
- async getDeveloperApp(appId) {
1712
- try {
1713
- const res = await this.makeRequest('GET', `/api/developer/apps/${appId}`, undefined, {
1714
- cache: true,
1715
- cacheTTL: 5 * 60 * 1000 // 5 minutes cache
1716
- });
1717
- return res.app;
1718
- } catch (error) {
1719
- throw this.handleError(error);
1720
- }
1721
- }
1722
-
1723
- /**
1724
- * Update a developer app
1725
- */
1726
- async updateDeveloperApp(appId, data) {
1727
- try {
1728
- const res = await this.makeRequest('PATCH', `/api/developer/apps/${appId}`, data, {
1729
- cache: false
1730
- });
1731
- return res.app;
1732
- } catch (error) {
1733
- throw this.handleError(error);
1734
- }
1735
- }
1736
-
1737
- /**
1738
- * Regenerate API secret for a developer app
1739
- */
1740
- async regenerateDeveloperAppSecret(appId) {
1741
- try {
1742
- return await this.makeRequest('POST', `/api/developer/apps/${appId}/regenerate-secret`, undefined, {
1743
- cache: false
1744
- });
1745
- } catch (error) {
1746
- throw this.handleError(error);
1747
- }
1748
- }
1749
-
1750
- /**
1751
- * Delete a developer app
1752
- */
1753
- async deleteDeveloperApp(appId) {
1754
- try {
1755
- return await this.makeRequest('DELETE', `/api/developer/apps/${appId}`, undefined, {
1756
- cache: false
1757
- });
1758
- } catch (error) {
1759
- throw this.handleError(error);
1760
- }
1761
- }
1762
-
1763
- // ============================================================================
1764
- // LOCATION METHODS
1765
- // ============================================================================
1766
-
1767
- /**
1768
- * Update user location
1769
- */
1770
- async updateLocation(latitude, longitude) {
1771
- try {
1772
- return await this.makeRequest('POST', '/api/location', {
1773
- latitude,
1774
- longitude
1775
- }, {
1776
- cache: false
1777
- });
1778
- } catch (error) {
1779
- throw this.handleError(error);
1780
- }
1781
- }
1782
-
1783
- /**
1784
- * Get nearby users
1785
- */
1786
- async getNearbyUsers(radius) {
1787
- try {
1788
- const params = radius ? {
1789
- radius
1790
- } : undefined;
1791
- return await this.makeRequest('GET', '/api/location/nearby', params, {
1792
- cache: false // Don't cache location data - always get fresh data
1793
- });
1794
- } catch (error) {
1795
- throw this.handleError(error);
1796
- }
1797
- }
1798
-
1799
- // ============================================================================
1800
- // ANALYTICS METHODS
1801
- // ============================================================================
1802
-
1803
- /**
1804
- * Track event
1805
- */
1806
- async trackEvent(eventName, properties) {
1807
- try {
1808
- await this.makeRequest('POST', '/api/analytics/events', {
1809
- event: eventName,
1810
- properties
1811
- }, {
1812
- cache: false,
1813
- retry: false
1814
- }); // Don't retry analytics events
1815
- } catch (error) {
1816
- throw this.handleError(error);
1817
- }
1818
- }
1819
-
1820
- /**
1821
- * Get analytics data
1822
- */
1823
- async getAnalytics(startDate, endDate) {
1824
- try {
1825
- const params = {};
1826
- if (startDate) params.startDate = startDate;
1827
- if (endDate) params.endDate = endDate;
1828
- return await this.makeRequest('GET', '/api/analytics', params, {
1829
- cache: true,
1830
- cacheTTL: 5 * 60 * 1000 // 5 minutes cache
1831
- });
1832
- } catch (error) {
1833
- throw this.handleError(error);
1834
- }
1835
- }
1836
-
1837
- // ============================================================================
1838
- // DEVICE METHODS
1839
- // ============================================================================
1840
-
1841
- /**
1842
- * Register device
1843
- */
1844
- async registerDevice(deviceData) {
1845
- try {
1846
- return await this.makeRequest('POST', '/api/devices', deviceData, {
1847
- cache: false
1848
- });
1849
- } catch (error) {
1850
- throw this.handleError(error);
1851
- }
1852
- }
1853
-
1854
- /**
1855
- * Get user devices
1856
- */
1857
- async getUserDevices() {
1858
- try {
1859
- return await this.makeRequest('GET', '/api/devices', undefined, {
1860
- cache: false // Don't cache device list - always get fresh data
1861
- });
1862
- } catch (error) {
1863
- throw this.handleError(error);
1864
- }
1865
- }
1866
-
1867
- /**
1868
- * Remove device
1869
- */
1870
- async removeDevice(deviceId) {
1871
- try {
1872
- await this.makeRequest('DELETE', `/api/devices/${deviceId}`, undefined, {
1873
- cache: false
1874
- });
1875
- } catch (error) {
1876
- throw this.handleError(error);
1877
- }
1878
- }
1879
-
1880
- /**
1881
- * Get device sessions
1882
- * Note: Not cached by default to ensure fresh data, but can be cached via makeRequest if needed
1883
- */
1884
- async getDeviceSessions(sessionId) {
1885
- try {
1886
- // Use makeRequest for consistent error handling and optional caching
1887
- // Cache disabled by default to ensure fresh session data
1888
- return await this.makeRequest('GET', `/api/session/device/sessions/${sessionId}`, undefined, {
1889
- cache: false,
1890
- // Don't cache sessions - always get fresh data
1891
- deduplicate: true // Deduplicate concurrent requests for same sessionId
1892
- });
1893
- } catch (error) {
1894
- throw this.handleError(error);
1895
- }
1896
- }
1897
-
1898
- /**
1899
- * Logout all device sessions
1900
- */
1901
- async logoutAllDeviceSessions(sessionId, deviceId, excludeCurrent) {
1902
- try {
1903
- const params = new URLSearchParams();
1904
- if (deviceId) params.append('deviceId', deviceId);
1905
- if (excludeCurrent) params.append('excludeCurrent', 'true');
1906
- const urlParams = {};
1907
- params.forEach((value, key) => {
1908
- urlParams[key] = value;
1909
- });
1910
- return await this.makeRequest('POST', `/api/session/device/logout-all/${sessionId}`, urlParams, {
1911
- cache: false
1912
- });
1913
- } catch (error) {
1914
- throw this.handleError(error);
1915
- }
1916
- }
1917
-
1918
- /**
1919
- * Update device name
1920
- */
1921
- async updateDeviceName(sessionId, deviceName) {
1922
- try {
1923
- return await this.makeRequest('PUT', `/api/session/device/name/${sessionId}`, {
1924
- deviceName
1925
- }, {
1926
- cache: false
1927
- });
1928
- } catch (error) {
1929
- throw this.handleError(error);
1930
- }
1931
- }
1932
-
1933
- // ============================================================================
1934
- // UTILITY METHODS
1935
- // ============================================================================
1936
-
1937
- /**
1938
- * Fetch link metadata
1939
- */
1940
- async fetchLinkMetadata(url) {
1941
- try {
1942
- return await this.makeRequest('GET', '/api/link-metadata', {
1943
- url
1944
- }, {
1945
- cache: true,
1946
- cacheTTL: 30 * 60 * 1000 // 30 minutes cache for link metadata
1947
- });
1948
- } catch (error) {
1949
- throw this.handleError(error);
1950
- }
1951
- }
1952
-
1953
- /**
1954
- * Simple Express.js authentication middleware
1955
- *
1956
- * Built-in authentication middleware that validates JWT tokens and adds user data to requests.
1957
- *
1958
- * @example
1959
- * ```typescript
1960
- * // Basic usage - just add it to your routes
1961
- * app.use('/api/protected', oxyServices.auth());
1962
- *
1963
- * // With debug logging
1964
- * app.use('/api/protected', oxyServices.auth({ debug: true }));
1965
- *
1966
- * // With custom error handling
1967
- * app.use('/api/protected', oxyServices.auth({
1968
- * onError: (error) => console.error('Auth failed:', error)
1969
- * }));
1970
- *
1971
- * // Load full user data
1972
- * app.use('/api/protected', oxyServices.auth({ loadUser: true }));
1973
- * ```
1974
- *
1975
- * @param options Optional configuration
1976
- * @param options.debug Enable debug logging (default: false)
1977
- * @param options.onError Custom error handler
1978
- * @param options.loadUser Load full user data (default: false for performance)
1979
- * @param options.session Use session-based validation (default: false)
1980
- * @returns Express middleware function
1981
- */
1982
- auth(options = {}) {
1983
- const {
1984
- debug = false,
1985
- onError,
1986
- loadUser = false,
1987
- session = false
1988
- } = options;
1989
-
1990
- // Return a synchronous middleware function
1991
- return (req, res, next) => {
1992
- try {
1993
- // Extract token from Authorization header
1994
- const authHeader = req.headers['authorization'];
1995
- const token = authHeader?.startsWith('Bearer ') ? authHeader.substring(7) : null;
1996
- if (debug) {
1997
- console.log(`🔐 Auth: Processing ${req.method} ${req.path}`);
1998
- console.log(`🔐 Auth: Token present: ${!!token}`);
1999
- }
2000
- if (!token) {
2001
- const error = {
2002
- message: 'Access token required',
2003
- code: 'MISSING_TOKEN',
2004
- status: 401
2005
- };
2006
- if (debug) console.log(`❌ Auth: Missing token`);
2007
- if (onError) return onError(error);
2008
- return res.status(401).json(error);
2009
- }
2010
-
2011
- // Decode and validate token
2012
- let decoded;
2013
- try {
2014
- decoded = jwtDecode(token);
2015
- if (debug) {
2016
- console.log(`🔐 Auth: Token decoded, User ID: ${decoded.userId || decoded.id}`);
2017
- }
2018
- } catch (decodeError) {
2019
- const error = {
2020
- message: 'Invalid token format',
2021
- code: 'INVALID_TOKEN_FORMAT',
2022
- status: 403
2023
- };
2024
- if (debug) console.log(`❌ Auth: Token decode failed`);
2025
- if (onError) return onError(error);
2026
- return res.status(403).json(error);
2027
- }
2028
- const userId = decoded.userId || decoded.id;
2029
- if (!userId) {
2030
- const error = {
2031
- message: 'Token missing user ID',
2032
- code: 'INVALID_TOKEN_PAYLOAD',
2033
- status: 403
2034
- };
2035
- if (debug) console.log(`❌ Auth: Token missing user ID`);
2036
- if (onError) return onError(error);
2037
- return res.status(403).json(error);
2038
- }
2039
-
2040
- // Check token expiration
2041
- if (decoded.exp && decoded.exp < Math.floor(Date.now() / 1000)) {
2042
- const error = {
2043
- message: 'Token expired',
2044
- code: 'TOKEN_EXPIRED',
2045
- status: 403
2046
- };
2047
- if (debug) console.log(`❌ Auth: Token expired`);
2048
- if (onError) return onError(error);
2049
- return res.status(403).json(error);
2050
- }
2051
-
2052
- // For now, skip session validation to keep it simple
2053
- // Session validation can be added later if needed
2054
-
2055
- // Set request properties immediately
2056
- req.userId = userId;
2057
- req.accessToken = token;
2058
- req.user = {
2059
- id: userId
2060
- };
2061
- if (debug) {
2062
- console.log(`✅ Auth: Authentication successful for user ${userId}`);
2063
- }
2064
- next();
2065
- } catch (error) {
2066
- const apiError = this.handleError(error);
2067
- if (debug) {
2068
- console.log(`❌ Auth: Unexpected error:`, apiError);
2069
- }
2070
- if (onError) return onError(apiError);
2071
- return res.status(apiError && apiError.status || 500).json(apiError);
2072
- }
2073
- };
105
+ // Export as a named class to avoid TypeScript issues with anonymous class types
106
+ export class OxyServices extends OxyServicesComposed {
107
+ constructor(config) {
108
+ super(config);
2074
109
  }
2075
110
  }
2076
111
 
112
+ // Re-export error classes for convenience
113
+ export { OxyAuthenticationError, OxyAuthenticationTimeoutError };
114
+
2077
115
  /**
2078
116
  * Export the default Oxy Cloud URL (for backward compatibility)
2079
117
  */