@oxyhq/services 5.13.15 → 5.13.17

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 (138) 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 -2168
  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 +424 -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 +388 -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/ui/context/OxyContext.js +27 -2
  39. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  40. package/lib/module/core/OxyServices.base.js +265 -0
  41. package/lib/module/core/OxyServices.base.js.map +1 -0
  42. package/lib/module/core/OxyServices.errors.js +20 -0
  43. package/lib/module/core/OxyServices.errors.js.map +1 -0
  44. package/lib/module/core/OxyServices.js +43 -2164
  45. package/lib/module/core/OxyServices.js.map +1 -1
  46. package/lib/module/core/mixins/OxyServices.analytics.js +56 -0
  47. package/lib/module/core/mixins/OxyServices.analytics.js.map +1 -0
  48. package/lib/module/core/mixins/OxyServices.assets.js +420 -0
  49. package/lib/module/core/mixins/OxyServices.assets.js.map +1 -0
  50. package/lib/module/core/mixins/OxyServices.auth.js +299 -0
  51. package/lib/module/core/mixins/OxyServices.auth.js.map +1 -0
  52. package/lib/module/core/mixins/OxyServices.developer.js +111 -0
  53. package/lib/module/core/mixins/OxyServices.developer.js.map +1 -0
  54. package/lib/module/core/mixins/OxyServices.devices.js +115 -0
  55. package/lib/module/core/mixins/OxyServices.devices.js.map +1 -0
  56. package/lib/module/core/mixins/OxyServices.karma.js +113 -0
  57. package/lib/module/core/mixins/OxyServices.karma.js.map +1 -0
  58. package/lib/module/core/mixins/OxyServices.language.js +120 -0
  59. package/lib/module/core/mixins/OxyServices.language.js.map +1 -0
  60. package/lib/module/core/mixins/OxyServices.location.js +51 -0
  61. package/lib/module/core/mixins/OxyServices.location.js.map +1 -0
  62. package/lib/module/core/mixins/OxyServices.payment.js +62 -0
  63. package/lib/module/core/mixins/OxyServices.payment.js.map +1 -0
  64. package/lib/module/core/mixins/OxyServices.privacy.js +170 -0
  65. package/lib/module/core/mixins/OxyServices.privacy.js.map +1 -0
  66. package/lib/module/core/mixins/OxyServices.totp.js +49 -0
  67. package/lib/module/core/mixins/OxyServices.totp.js.map +1 -0
  68. package/lib/module/core/mixins/OxyServices.user.js +384 -0
  69. package/lib/module/core/mixins/OxyServices.user.js.map +1 -0
  70. package/lib/module/core/mixins/OxyServices.utility.js +156 -0
  71. package/lib/module/core/mixins/OxyServices.utility.js.map +1 -0
  72. package/lib/module/core/mixins/index.js +36 -0
  73. package/lib/module/core/mixins/index.js.map +1 -0
  74. package/lib/module/core/mixins/mixinHelpers.js +56 -0
  75. package/lib/module/core/mixins/mixinHelpers.js.map +1 -0
  76. package/lib/module/ui/context/OxyContext.js +27 -2
  77. package/lib/module/ui/context/OxyContext.js.map +1 -1
  78. package/lib/typescript/core/OxyServices.base.d.ts +123 -0
  79. package/lib/typescript/core/OxyServices.base.d.ts.map +1 -0
  80. package/lib/typescript/core/OxyServices.d.ts +970 -746
  81. package/lib/typescript/core/OxyServices.d.ts.map +1 -1
  82. package/lib/typescript/core/OxyServices.errors.d.ts +12 -0
  83. package/lib/typescript/core/OxyServices.errors.d.ts.map +1 -0
  84. package/lib/typescript/core/mixins/OxyServices.analytics.d.ts +70 -0
  85. package/lib/typescript/core/mixins/OxyServices.analytics.d.ts.map +1 -0
  86. package/lib/typescript/core/mixins/OxyServices.assets.d.ts +166 -0
  87. package/lib/typescript/core/mixins/OxyServices.assets.d.ts.map +1 -0
  88. package/lib/typescript/core/mixins/OxyServices.auth.d.ts +168 -0
  89. package/lib/typescript/core/mixins/OxyServices.auth.d.ts.map +1 -0
  90. package/lib/typescript/core/mixins/OxyServices.developer.d.ts +103 -0
  91. package/lib/typescript/core/mixins/OxyServices.developer.d.ts.map +1 -0
  92. package/lib/typescript/core/mixins/OxyServices.devices.d.ts +93 -0
  93. package/lib/typescript/core/mixins/OxyServices.devices.d.ts.map +1 -0
  94. package/lib/typescript/core/mixins/OxyServices.karma.d.ts +89 -0
  95. package/lib/typescript/core/mixins/OxyServices.karma.d.ts.map +1 -0
  96. package/lib/typescript/core/mixins/OxyServices.language.d.ts +85 -0
  97. package/lib/typescript/core/mixins/OxyServices.language.d.ts.map +1 -0
  98. package/lib/typescript/core/mixins/OxyServices.location.d.ts +68 -0
  99. package/lib/typescript/core/mixins/OxyServices.location.d.ts.map +1 -0
  100. package/lib/typescript/core/mixins/OxyServices.payment.d.ts +74 -0
  101. package/lib/typescript/core/mixins/OxyServices.payment.d.ts.map +1 -0
  102. package/lib/typescript/core/mixins/OxyServices.privacy.d.ts +126 -0
  103. package/lib/typescript/core/mixins/OxyServices.privacy.d.ts.map +1 -0
  104. package/lib/typescript/core/mixins/OxyServices.totp.d.ts +69 -0
  105. package/lib/typescript/core/mixins/OxyServices.totp.d.ts.map +1 -0
  106. package/lib/typescript/core/mixins/OxyServices.user.d.ts +189 -0
  107. package/lib/typescript/core/mixins/OxyServices.user.d.ts.map +1 -0
  108. package/lib/typescript/core/mixins/OxyServices.utility.d.ts +97 -0
  109. package/lib/typescript/core/mixins/OxyServices.utility.d.ts.map +1 -0
  110. package/lib/typescript/core/mixins/index.d.ts +899 -0
  111. package/lib/typescript/core/mixins/index.d.ts.map +1 -0
  112. package/lib/typescript/core/mixins/mixinHelpers.d.ts +32 -0
  113. package/lib/typescript/core/mixins/mixinHelpers.d.ts.map +1 -0
  114. package/lib/typescript/models/interfaces.d.ts +10 -0
  115. package/lib/typescript/models/interfaces.d.ts.map +1 -1
  116. package/lib/typescript/ui/context/OxyContext.d.ts +2 -0
  117. package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
  118. package/package.json +1 -1
  119. package/src/core/OxyServices.base.ts +311 -0
  120. package/src/core/OxyServices.errors.ts +26 -0
  121. package/src/core/OxyServices.ts +43 -2199
  122. package/src/core/mixins/OxyServices.analytics.ts +53 -0
  123. package/src/core/mixins/OxyServices.assets.ts +410 -0
  124. package/src/core/mixins/OxyServices.auth.ts +275 -0
  125. package/src/core/mixins/OxyServices.developer.ts +114 -0
  126. package/src/core/mixins/OxyServices.devices.ts +103 -0
  127. package/src/core/mixins/OxyServices.karma.ts +111 -0
  128. package/src/core/mixins/OxyServices.language.ts +127 -0
  129. package/src/core/mixins/OxyServices.location.ts +46 -0
  130. package/src/core/mixins/OxyServices.payment.ts +59 -0
  131. package/src/core/mixins/OxyServices.privacy.ts +182 -0
  132. package/src/core/mixins/OxyServices.totp.ts +36 -0
  133. package/src/core/mixins/OxyServices.user.ts +384 -0
  134. package/src/core/mixins/OxyServices.utility.ts +187 -0
  135. package/src/core/mixins/index.ts +58 -0
  136. package/src/core/mixins/mixinHelpers.ts +69 -0
  137. package/src/models/interfaces.ts +12 -0
  138. package/src/ui/context/OxyContext.tsx +36 -0
@@ -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,2181 +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
- // BLOCKED USERS METHODS
815
- // ============================================================================
816
-
817
- /**
818
- * Get list of blocked users
819
- * @returns Array of blocked users
820
- */
821
- async getBlockedUsers() {
822
- try {
823
- return await this.makeRequest('GET', '/api/privacy/blocked', undefined, {
824
- cache: true,
825
- cacheTTL: 1 * 60 * 1000 // 1 minute cache
826
- });
827
- } catch (error) {
828
- throw this.handleError(error);
829
- }
830
- }
831
-
832
- /**
833
- * Block a user
834
- * @param userId - The user ID to block
835
- * @returns Success message
836
- */
837
- async blockUser(userId) {
838
- try {
839
- if (!userId) {
840
- throw new Error('User ID is required');
841
- }
842
- return await this.makeRequest('POST', `/api/privacy/blocked/${userId}`, undefined, {
843
- cache: false
844
- });
845
- } catch (error) {
846
- throw this.handleError(error);
847
- }
848
- }
849
-
850
- /**
851
- * Unblock a user
852
- * @param userId - The user ID to unblock
853
- * @returns Success message
854
- */
855
- async unblockUser(userId) {
856
- try {
857
- if (!userId) {
858
- throw new Error('User ID is required');
859
- }
860
- return await this.makeRequest('DELETE', `/api/privacy/blocked/${userId}`, undefined, {
861
- cache: false
862
- });
863
- } catch (error) {
864
- throw this.handleError(error);
865
- }
866
- }
867
-
868
- /**
869
- * Extract user ID from blocked/restricted user object
870
- * @private
871
- */
872
- extractUserId(userIdField) {
873
- return typeof userIdField === 'string' ? userIdField : userIdField._id;
874
- }
875
-
876
- /**
877
- * Check if a user is in a list (blocked or restricted)
878
- * @private
879
- */
880
- async isUserInList(userId, getUserList, getIdField) {
881
- try {
882
- if (!userId) {
883
- return false;
884
- }
885
- const users = await getUserList();
886
- return users.some(item => {
887
- const itemId = this.extractUserId(getIdField(item));
888
- return itemId === userId;
889
- });
890
- } catch (error) {
891
- // If there's an error, assume not in list to avoid breaking functionality
892
- if (__DEV__) {
893
- console.warn('Error checking user list:', error);
894
- }
895
- return false;
896
- }
897
- }
898
-
899
- /**
900
- * Check if a user is blocked
901
- * @param userId - The user ID to check
902
- * @returns True if the user is blocked, false otherwise
903
- */
904
- async isUserBlocked(userId) {
905
- return this.isUserInList(userId, () => this.getBlockedUsers(), block => block.blockedId);
906
- }
907
-
908
- // ============================================================================
909
- // RESTRICTED USERS METHODS
910
- // ============================================================================
911
-
912
- /**
913
- * Get list of restricted users
914
- * @returns Array of restricted users
915
- */
916
- async getRestrictedUsers() {
917
- try {
918
- return await this.makeRequest('GET', '/api/privacy/restricted', undefined, {
919
- cache: true,
920
- cacheTTL: 1 * 60 * 1000 // 1 minute cache
921
- });
922
- } catch (error) {
923
- throw this.handleError(error);
924
- }
925
- }
926
-
927
- /**
928
- * Restrict a user (limit their interactions without fully blocking)
929
- * @param userId - The user ID to restrict
930
- * @returns Success message
931
- */
932
- async restrictUser(userId) {
933
- try {
934
- if (!userId) {
935
- throw new Error('User ID is required');
936
- }
937
- return await this.makeRequest('POST', `/api/privacy/restricted/${userId}`, undefined, {
938
- cache: false
939
- });
940
- } catch (error) {
941
- throw this.handleError(error);
942
- }
943
- }
944
-
945
- /**
946
- * Unrestrict a user
947
- * @param userId - The user ID to unrestrict
948
- * @returns Success message
949
- */
950
- async unrestrictUser(userId) {
951
- try {
952
- if (!userId) {
953
- throw new Error('User ID is required');
954
- }
955
- return await this.makeRequest('DELETE', `/api/privacy/restricted/${userId}`, undefined, {
956
- cache: false
957
- });
958
- } catch (error) {
959
- throw this.handleError(error);
960
- }
961
- }
962
-
963
- /**
964
- * Check if a user is restricted
965
- * @param userId - The user ID to check
966
- * @returns True if the user is restricted, false otherwise
967
- */
968
- async isUserRestricted(userId) {
969
- return this.isUserInList(userId, () => this.getRestrictedUsers(), restrict => restrict.restrictedId);
970
- }
971
-
972
- /**
973
- * Request account verification
974
- */
975
- async requestAccountVerification(reason, evidence) {
976
- try {
977
- return await this.makeRequest('POST', '/api/users/verify/request', {
978
- reason,
979
- evidence
980
- }, {
981
- cache: false
982
- });
983
- } catch (error) {
984
- throw this.handleError(error);
985
- }
986
- }
987
-
988
- /**
989
- * Download account data export
990
- */
991
- async downloadAccountData(format = 'json') {
992
- try {
993
- // Use axios instance directly for blob responses since RequestManager doesn't handle blobs
994
- const axiosInstance = this.httpClient.getAxiosInstance();
995
- const response = await axiosInstance.get(`/api/users/me/data?format=${format}`, {
996
- responseType: 'blob'
997
- });
998
- return response.data;
999
- } catch (error) {
1000
- throw this.handleError(error);
1001
- }
1002
- }
1003
-
1004
- /**
1005
- * Delete account permanently
1006
- * @param password - User password for confirmation
1007
- * @param confirmText - Confirmation text (usually username)
1008
- */
1009
- async deleteAccount(password, confirmText) {
1010
- try {
1011
- return await this.makeRequest('DELETE', '/api/users/me', {
1012
- password,
1013
- confirmText
1014
- }, {
1015
- cache: false
1016
- });
1017
- } catch (error) {
1018
- throw this.handleError(error);
1019
- }
1020
- }
1021
-
1022
- // ============================================================================
1023
- // LANGUAGE METHODS
1024
- // ============================================================================
102
+ // Compose all mixins into the final OxyServices class
103
+ const OxyServicesComposed = composeOxyServices();
1025
104
 
1026
- /**
1027
- * Get the current language from storage or user profile
1028
- * @param storageKeyPrefix - Optional prefix for storage key (default: 'oxy_session')
1029
- * @returns The current language code (e.g., 'en-US') or null if not set
1030
- */
1031
- async getCurrentLanguage(storageKeyPrefix = 'oxy_session') {
1032
- try {
1033
- // First try to get from user profile if authenticated
1034
- try {
1035
- const user = await this.getCurrentUser();
1036
- const userLanguage = user?.language;
1037
- if (userLanguage) {
1038
- return normalizeLanguageCode(userLanguage) || userLanguage;
1039
- }
1040
- } catch (e) {
1041
- // User not authenticated or error, continue to storage
1042
- }
1043
-
1044
- // Fall back to storage
1045
- const storage = await this.getStorage();
1046
- const storageKey = `${storageKeyPrefix}_language`;
1047
- const storedLanguage = await storage.getItem(storageKey);
1048
- if (storedLanguage) {
1049
- return normalizeLanguageCode(storedLanguage) || storedLanguage;
1050
- }
1051
- return null;
1052
- } catch (error) {
1053
- if (__DEV__) {
1054
- console.warn('Failed to get current language:', error);
1055
- }
1056
- return null;
1057
- }
1058
- }
1059
-
1060
- /**
1061
- * Get the current language with metadata (name, nativeName, etc.)
1062
- * @param storageKeyPrefix - Optional prefix for storage key (default: 'oxy_session')
1063
- * @returns Language metadata object or null if not set
1064
- */
1065
- async getCurrentLanguageMetadata(storageKeyPrefix = 'oxy_session') {
1066
- const languageCode = await this.getCurrentLanguage(storageKeyPrefix);
1067
- return getLanguageMetadata(languageCode);
1068
- }
1069
-
1070
- /**
1071
- * Get the current language name (e.g., 'English')
1072
- * @param storageKeyPrefix - Optional prefix for storage key (default: 'oxy_session')
1073
- * @returns Language name or null if not set
1074
- */
1075
- async getCurrentLanguageName(storageKeyPrefix = 'oxy_session') {
1076
- const languageCode = await this.getCurrentLanguage(storageKeyPrefix);
1077
- if (!languageCode) return null;
1078
- return getLanguageName(languageCode);
1079
- }
1080
-
1081
- /**
1082
- * Get the current native language name (e.g., 'Español')
1083
- * @param storageKeyPrefix - Optional prefix for storage key (default: 'oxy_session')
1084
- * @returns Native language name or null if not set
1085
- */
1086
- async getCurrentNativeLanguageName(storageKeyPrefix = 'oxy_session') {
1087
- const languageCode = await this.getCurrentLanguage(storageKeyPrefix);
1088
- if (!languageCode) return null;
1089
- return getNativeLanguageName(languageCode);
1090
- }
1091
-
1092
- /**
1093
- * Get appropriate storage for the platform (similar to DeviceManager)
1094
- * @private
1095
- */
1096
- async getStorage() {
1097
- const isReactNative = typeof navigator !== 'undefined' && navigator.product === 'ReactNative';
1098
- if (isReactNative) {
1099
- try {
1100
- const asyncStorageModule = await import('@react-native-async-storage/async-storage');
1101
- const storage = asyncStorageModule.default;
1102
- return {
1103
- getItem: storage.getItem.bind(storage),
1104
- setItem: storage.setItem.bind(storage),
1105
- removeItem: storage.removeItem.bind(storage)
1106
- };
1107
- } catch (error) {
1108
- console.error('AsyncStorage not available in React Native:', error);
1109
- throw new Error('AsyncStorage is required in React Native environment');
1110
- }
1111
- } else {
1112
- // Use localStorage for web
1113
- return {
1114
- getItem: async key => {
1115
- if (typeof window !== 'undefined' && window.localStorage) {
1116
- return localStorage.getItem(key);
1117
- }
1118
- return null;
1119
- },
1120
- setItem: async (key, value) => {
1121
- if (typeof window !== 'undefined' && window.localStorage) {
1122
- localStorage.setItem(key, value);
1123
- }
1124
- },
1125
- removeItem: async key => {
1126
- if (typeof window !== 'undefined' && window.localStorage) {
1127
- localStorage.removeItem(key);
1128
- }
1129
- }
1130
- };
1131
- }
1132
- }
1133
-
1134
- /**
1135
- * Update user by ID (admin function)
1136
- */
1137
- async updateUser(userId, updates) {
1138
- try {
1139
- return await this.makeRequest('PUT', `/api/users/${userId}`, updates, {
1140
- cache: false
1141
- });
1142
- } catch (error) {
1143
- throw this.handleError(error);
1144
- }
1145
- }
1146
-
1147
- /**
1148
- * Follow a user
1149
- */
1150
- async followUser(userId) {
1151
- try {
1152
- return await this.makeRequest('POST', `/api/users/${userId}/follow`, undefined, {
1153
- cache: false
1154
- });
1155
- } catch (error) {
1156
- throw this.handleError(error);
1157
- }
1158
- }
1159
-
1160
- /**
1161
- * Unfollow a user
1162
- */
1163
- async unfollowUser(userId) {
1164
- try {
1165
- return await this.makeRequest('DELETE', `/api/users/${userId}/follow`, undefined, {
1166
- cache: false
1167
- });
1168
- } catch (error) {
1169
- throw this.handleError(error);
1170
- }
1171
- }
1172
-
1173
- /**
1174
- * Get follow status
1175
- */
1176
- async getFollowStatus(userId) {
1177
- try {
1178
- return await this.makeRequest('GET', `/api/users/${userId}/follow-status`, undefined, {
1179
- cache: true,
1180
- cacheTTL: 1 * 60 * 1000 // 1 minute cache
1181
- });
1182
- } catch (error) {
1183
- throw this.handleError(error);
1184
- }
1185
- }
1186
-
1187
- /**
1188
- * Get user followers
1189
- */
1190
- async getUserFollowers(userId, pagination) {
1191
- try {
1192
- const params = buildPaginationParams(pagination || {});
1193
- const response = await this.makeRequest('GET', `/api/users/${userId}/followers`, params, {
1194
- cache: true,
1195
- cacheTTL: 2 * 60 * 1000 // 2 minutes cache
1196
- });
1197
- return {
1198
- followers: response.data || [],
1199
- total: response.pagination.total,
1200
- hasMore: response.pagination.hasMore
1201
- };
1202
- } catch (error) {
1203
- throw this.handleError(error);
1204
- }
1205
- }
1206
-
1207
- /**
1208
- * Get user following
1209
- */
1210
- async getUserFollowing(userId, pagination) {
1211
- try {
1212
- const params = buildPaginationParams(pagination || {});
1213
- const response = await this.makeRequest('GET', `/api/users/${userId}/following`, params, {
1214
- cache: true,
1215
- cacheTTL: 2 * 60 * 1000 // 2 minutes cache
1216
- });
1217
- return {
1218
- following: response.data || [],
1219
- total: response.pagination.total,
1220
- hasMore: response.pagination.hasMore
1221
- };
1222
- } catch (error) {
1223
- throw this.handleError(error);
1224
- }
1225
- }
1226
-
1227
- /**
1228
- * Get notifications
1229
- */
1230
- async getNotifications() {
1231
- return this.withAuthRetry(async () => {
1232
- return await this.makeRequest('GET', '/api/notifications', undefined, {
1233
- cache: false // Don't cache notifications - always get fresh data
1234
- });
1235
- }, 'getNotifications');
1236
- }
1237
-
1238
- /**
1239
- * Get unread notification count
1240
- */
1241
- async getUnreadCount() {
1242
- try {
1243
- const res = await this.makeRequest('GET', '/api/notifications/unread-count', undefined, {
1244
- cache: false // Don't cache unread count - always get fresh data
1245
- });
1246
- return res.count;
1247
- } catch (error) {
1248
- throw this.handleError(error);
1249
- }
1250
- }
1251
-
1252
- /**
1253
- * Create notification
1254
- */
1255
- async createNotification(data) {
1256
- try {
1257
- return await this.makeRequest('POST', '/api/notifications', data, {
1258
- cache: false
1259
- });
1260
- } catch (error) {
1261
- throw this.handleError(error);
1262
- }
1263
- }
1264
-
1265
- /**
1266
- * Mark notification as read
1267
- */
1268
- async markNotificationAsRead(notificationId) {
1269
- try {
1270
- await this.makeRequest('PUT', `/api/notifications/${notificationId}/read`, undefined, {
1271
- cache: false
1272
- });
1273
- } catch (error) {
1274
- throw this.handleError(error);
1275
- }
1276
- }
1277
-
1278
- /**
1279
- * Mark all notifications as read
1280
- */
1281
- async markAllNotificationsAsRead() {
1282
- try {
1283
- await this.makeRequest('PUT', '/api/notifications/read-all', undefined, {
1284
- cache: false
1285
- });
1286
- } catch (error) {
1287
- throw this.handleError(error);
1288
- }
1289
- }
1290
-
1291
- /**
1292
- * Delete notification
1293
- */
1294
- async deleteNotification(notificationId) {
1295
- try {
1296
- await this.makeRequest('DELETE', `/api/notifications/${notificationId}`, undefined, {
1297
- cache: false
1298
- });
1299
- } catch (error) {
1300
- throw this.handleError(error);
1301
- }
1302
- }
1303
-
1304
- // ============================================================================
1305
- // PAYMENT METHODS
1306
- // ============================================================================
1307
-
1308
- /**
1309
- * Create a payment
1310
- */
1311
- async createPayment(data) {
1312
- try {
1313
- return await this.makeRequest('POST', '/api/payments', data, {
1314
- cache: false
1315
- });
1316
- } catch (error) {
1317
- throw this.handleError(error);
1318
- }
1319
- }
1320
-
1321
- /**
1322
- * Get payment by ID
1323
- */
1324
- async getPayment(paymentId) {
1325
- try {
1326
- return await this.makeRequest('GET', `/api/payments/${paymentId}`, undefined, {
1327
- cache: true,
1328
- cacheTTL: 5 * 60 * 1000 // 5 minutes cache
1329
- });
1330
- } catch (error) {
1331
- throw this.handleError(error);
1332
- }
1333
- }
1334
-
1335
- /**
1336
- * Get user payments
1337
- */
1338
- async getUserPayments() {
1339
- try {
1340
- return await this.makeRequest('GET', '/api/payments/user', undefined, {
1341
- cache: false // Don't cache user payments - always get fresh data
1342
- });
1343
- } catch (error) {
1344
- throw this.handleError(error);
1345
- }
1346
- }
1347
-
1348
- // ============================================================================
1349
- // KARMA METHODS
1350
- // ============================================================================
1351
-
1352
- /**
1353
- * Get user karma
1354
- */
1355
- async getUserKarma(userId) {
1356
- try {
1357
- return await this.makeRequest('GET', `/api/karma/${userId}`, undefined, {
1358
- cache: true,
1359
- cacheTTL: 2 * 60 * 1000 // 2 minutes cache
1360
- });
1361
- } catch (error) {
1362
- throw this.handleError(error);
1363
- }
1364
- }
1365
-
1366
- /**
1367
- * Give karma to user
1368
- */
1369
- async giveKarma(userId, amount, reason) {
1370
- try {
1371
- return await this.makeRequest('POST', `/api/karma/${userId}/give`, {
1372
- amount,
1373
- reason
1374
- }, {
1375
- cache: false
1376
- });
1377
- } catch (error) {
1378
- throw this.handleError(error);
1379
- }
1380
- }
1381
-
1382
- /**
1383
- * Get user karma total
1384
- */
1385
- async getUserKarmaTotal(userId) {
1386
- try {
1387
- return await this.makeRequest('GET', `/api/karma/${userId}/total`, undefined, {
1388
- cache: true,
1389
- cacheTTL: 2 * 60 * 1000 // 2 minutes cache
1390
- });
1391
- } catch (error) {
1392
- throw this.handleError(error);
1393
- }
1394
- }
1395
-
1396
- /**
1397
- * Get user karma history
1398
- */
1399
- async getUserKarmaHistory(userId, limit, offset) {
1400
- try {
1401
- const params = {};
1402
- if (limit) params.limit = limit;
1403
- if (offset) params.offset = offset;
1404
- return await this.makeRequest('GET', `/api/karma/${userId}/history`, params, {
1405
- cache: true,
1406
- cacheTTL: 2 * 60 * 1000 // 2 minutes cache
1407
- });
1408
- } catch (error) {
1409
- throw this.handleError(error);
1410
- }
1411
- }
1412
-
1413
- /**
1414
- * Get karma leaderboard
1415
- */
1416
- async getKarmaLeaderboard() {
1417
- try {
1418
- return await this.makeRequest('GET', '/api/karma/leaderboard', undefined, {
1419
- cache: true,
1420
- cacheTTL: 5 * 60 * 1000 // 5 minutes cache
1421
- });
1422
- } catch (error) {
1423
- throw this.handleError(error);
1424
- }
1425
- }
1426
-
1427
- /**
1428
- * Get karma rules
1429
- */
1430
- async getKarmaRules() {
1431
- try {
1432
- return await this.makeRequest('GET', '/api/karma/rules', undefined, {
1433
- cache: true,
1434
- cacheTTL: 30 * 60 * 1000 // 30 minutes cache (rules don't change often)
1435
- });
1436
- } catch (error) {
1437
- throw this.handleError(error);
1438
- }
1439
- }
1440
-
1441
- // ============================================================================
1442
- // FILE METHODS (LEGACY - Using Asset Service)
1443
- // ============================================================================
1444
-
1445
- /**
1446
- * Delete file
1447
- */
1448
- async deleteFile(fileId) {
1449
- try {
1450
- // Central Asset Service delete with force=true behavior controlled by caller via assetDelete
1451
- return await this.makeRequest('DELETE', `/api/assets/${encodeURIComponent(fileId)}`, undefined, {
1452
- cache: false
1453
- });
1454
- } catch (error) {
1455
- throw this.handleError(error);
1456
- }
1457
- }
1458
-
1459
- /**
1460
- * Get file download URL (API streaming proxy, attaches token for <img src>)
1461
- */
1462
- getFileDownloadUrl(fileId, variant, expiresIn) {
1463
- const base = this.getBaseURL();
1464
- const params = new URLSearchParams();
1465
- if (variant) params.set('variant', variant);
1466
- if (expiresIn) params.set('expiresIn', String(expiresIn));
1467
- params.set('fallback', 'placeholderVisible');
1468
- const token = this.httpClient.getAccessToken();
1469
- if (token) params.set('token', token);
1470
-
1471
- // Use params.toString() to detect whether there are query params.
1472
- // URLSearchParams.size is not a standard property across all JS runtimes
1473
- // (some environments like React Native may not implement it), which
1474
- // caused the query string to be omitted on native. Checking the
1475
- // serialized string is reliable everywhere.
1476
- const qs = params.toString();
1477
- return `${base}/api/assets/${encodeURIComponent(fileId)}/stream${qs ? `?${qs}` : ''}`;
1478
- }
1479
-
1480
- /**
1481
- * Get file stream URL (direct Oxy Cloud/CDN URL, no token)
1482
- */
1483
- getFileStreamUrl(fileId) {
1484
- return `${this.getCloudURL()}/files/${fileId}/stream`;
1485
- }
1486
-
1487
- // ...existing code...
1488
-
1489
- /**
1490
- * List user files
1491
- */
1492
- async listUserFiles(limit, offset) {
1493
- try {
1494
- const paramsObj = {};
1495
- if (limit) paramsObj.limit = limit;
1496
- if (offset) paramsObj.offset = offset;
1497
- return await this.makeRequest('GET', '/api/assets', paramsObj, {
1498
- cache: false // Don't cache file lists - always get fresh data
1499
- });
1500
- } catch (error) {
1501
- throw this.handleError(error);
1502
- }
1503
- }
1504
-
1505
- // (removed legacy downloadFileContent; use getFileContentAsBlob/Text which resolve CAS URL first)
1506
-
1507
- /**
1508
- * Get file content as text
1509
- */
1510
- async getFileContentAsText(fileId, variant) {
1511
- try {
1512
- const params = variant ? {
1513
- variant
1514
- } : undefined;
1515
- const urlRes = await this.makeRequest('GET', `/api/assets/${encodeURIComponent(fileId)}/url`, params, {
1516
- cache: true,
1517
- cacheTTL: 10 * 60 * 1000 // 10 minutes cache for URLs
1518
- });
1519
- const downloadUrl = urlRes?.url;
1520
- const response = await fetch(downloadUrl);
1521
- return await response.text();
1522
- } catch (error) {
1523
- throw this.handleError(error);
1524
- }
1525
- }
1526
-
1527
- /**
1528
- * Get file content as blob
1529
- */
1530
- async getFileContentAsBlob(fileId, variant) {
1531
- try {
1532
- const params = variant ? {
1533
- variant
1534
- } : undefined;
1535
- const urlRes = await this.makeRequest('GET', `/api/assets/${encodeURIComponent(fileId)}/url`, params, {
1536
- cache: true,
1537
- cacheTTL: 10 * 60 * 1000 // 10 minutes cache for URLs
1538
- });
1539
- const downloadUrl = urlRes?.url;
1540
- const response = await fetch(downloadUrl);
1541
- return await response.blob();
1542
- } catch (error) {
1543
- throw this.handleError(error);
1544
- }
1545
- }
1546
-
1547
- /**
1548
- * Upload raw file data
1549
- */
1550
- async uploadRawFile(file, visibility, metadata) {
1551
- // Switch to Central Asset Service upload flow
1552
- return this.assetUpload(file, visibility, metadata);
1553
- }
1554
-
1555
- // ============================================================================
1556
- // CENTRAL ASSET SERVICE METHODS
1557
- // ============================================================================
1558
-
1559
- /**
1560
- * Calculate SHA256 hash of file content
1561
- */
1562
- async calculateSHA256(file) {
1563
- const buffer = await file.arrayBuffer();
1564
- const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
1565
- const hashArray = Array.from(new Uint8Array(hashBuffer));
1566
- return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
1567
- }
1568
-
1569
- /**
1570
- * Initialize asset upload - returns pre-signed URL and file ID
1571
- */
1572
- async assetInit(sha256, size, mime) {
1573
- try {
1574
- return await this.makeRequest('POST', '/api/assets/init', {
1575
- sha256,
1576
- size,
1577
- mime
1578
- }, {
1579
- cache: false
1580
- });
1581
- } catch (error) {
1582
- throw this.handleError(error);
1583
- }
1584
- }
1585
-
1586
- /**
1587
- * Complete asset upload - commit metadata and trigger variant generation
1588
- */
1589
- async assetComplete(fileId, originalName, size, mime, visibility, metadata) {
1590
- try {
1591
- return await this.makeRequest('POST', '/api/assets/complete', {
1592
- fileId,
1593
- originalName,
1594
- size,
1595
- mime,
1596
- visibility,
1597
- metadata
1598
- }, {
1599
- cache: false
1600
- });
1601
- } catch (error) {
1602
- throw this.handleError(error);
1603
- }
1604
- }
1605
-
1606
- /**
1607
- * Upload file using Central Asset Service
1608
- */
1609
- async assetUpload(file, visibility, metadata, onProgress) {
1610
- try {
1611
- // Calculate SHA256
1612
- const sha256 = await this.calculateSHA256(file);
1613
-
1614
- // Initialize upload
1615
- const initResponse = await this.assetInit(sha256, file.size, file.type);
1616
-
1617
- // Try presigned URL first
1618
- try {
1619
- await this.uploadToPresignedUrl(initResponse.uploadUrl, file, onProgress);
1620
- } catch (e) {
1621
- // Fallback: direct upload via API to avoid CORS issues
1622
- const fd = new FormData();
1623
- fd.append('file', file);
1624
- // Use httpClient directly for FormData uploads (bypasses RequestManager for special handling)
1625
- await this.httpClient.request({
1626
- method: 'POST',
1627
- url: `/api/assets/${encodeURIComponent(initResponse.fileId)}/upload-direct`,
1628
- data: fd
1629
- });
1630
- }
1631
-
1632
- // Complete upload
1633
- return await this.assetComplete(initResponse.fileId, file.name, file.size, file.type, visibility, metadata);
1634
- } catch (error) {
1635
- throw this.handleError(error);
1636
- }
1637
- }
1638
-
1639
- /**
1640
- * Upload file to pre-signed URL
1641
- */
1642
- async uploadToPresignedUrl(url, file, onProgress) {
1643
- return new Promise((resolve, reject) => {
1644
- const xhr = new XMLHttpRequest();
1645
- xhr.upload.addEventListener('progress', event => {
1646
- if (event.lengthComputable && onProgress) {
1647
- const progress = event.loaded / event.total * 100;
1648
- onProgress(progress);
1649
- }
1650
- });
1651
- xhr.addEventListener('load', () => {
1652
- if (xhr.status >= 200 && xhr.status < 300) {
1653
- resolve();
1654
- } else {
1655
- reject(new Error(`Upload failed with status ${xhr.status}`));
1656
- }
1657
- });
1658
- xhr.addEventListener('error', () => {
1659
- reject(new Error('Upload failed'));
1660
- });
1661
- xhr.open('PUT', url);
1662
- xhr.setRequestHeader('Content-Type', file.type);
1663
- xhr.send(file);
1664
- });
1665
- }
1666
-
1667
- /**
1668
- * Link asset to an entity
1669
- */
1670
- async assetLink(fileId, app, entityType, entityId, visibility, webhookUrl) {
1671
- try {
1672
- const body = {
1673
- app,
1674
- entityType,
1675
- entityId
1676
- };
1677
- if (visibility) body.visibility = visibility;
1678
- if (webhookUrl) body.webhookUrl = webhookUrl;
1679
- return await this.makeRequest('POST', `/api/assets/${fileId}/links`, body, {
1680
- cache: false
1681
- });
1682
- } catch (error) {
1683
- throw this.handleError(error);
1684
- }
1685
- }
1686
-
1687
- /**
1688
- * Unlink asset from an entity
1689
- */
1690
- async assetUnlink(fileId, app, entityType, entityId) {
1691
- try {
1692
- return await this.makeRequest('DELETE', `/api/assets/${fileId}/links`, {
1693
- app,
1694
- entityType,
1695
- entityId
1696
- }, {
1697
- cache: false
1698
- });
1699
- } catch (error) {
1700
- throw this.handleError(error);
1701
- }
1702
- }
1703
-
1704
- /**
1705
- * Get asset metadata
1706
- */
1707
- async assetGet(fileId) {
1708
- try {
1709
- return await this.makeRequest('GET', `/api/assets/${fileId}`, undefined, {
1710
- cache: true,
1711
- cacheTTL: 5 * 60 * 1000 // 5 minutes cache
1712
- });
1713
- } catch (error) {
1714
- throw this.handleError(error);
1715
- }
1716
- }
1717
-
1718
- /**
1719
- * Get asset URL (CDN or signed URL)
1720
- */
1721
- async assetGetUrl(fileId, variant, expiresIn) {
1722
- try {
1723
- const params = {};
1724
- if (variant) params.variant = variant;
1725
- if (expiresIn) params.expiresIn = expiresIn;
1726
- return await this.makeRequest('GET', `/api/assets/${fileId}/url`, params, {
1727
- cache: true,
1728
- cacheTTL: 10 * 60 * 1000 // 10 minutes cache for URLs
1729
- });
1730
- } catch (error) {
1731
- throw this.handleError(error);
1732
- }
1733
- }
1734
-
1735
- /**
1736
- * Restore asset from trash
1737
- */
1738
- async assetRestore(fileId) {
1739
- try {
1740
- return await this.makeRequest('POST', `/api/assets/${fileId}/restore`, undefined, {
1741
- cache: false
1742
- });
1743
- } catch (error) {
1744
- throw this.handleError(error);
1745
- }
1746
- }
1747
-
1748
- /**
1749
- * Delete asset with optional force
1750
- */
1751
- async assetDelete(fileId, force = false) {
1752
- try {
1753
- const params = force ? {
1754
- force: 'true'
1755
- } : undefined;
1756
- return await this.makeRequest('DELETE', `/api/assets/${fileId}`, params, {
1757
- cache: false
1758
- });
1759
- } catch (error) {
1760
- throw this.handleError(error);
1761
- }
1762
- }
1763
-
1764
- /**
1765
- * Get list of available variants for an asset
1766
- */
1767
- async assetGetVariants(fileId) {
1768
- try {
1769
- const assetData = await this.assetGet(fileId);
1770
- return assetData.file?.variants || [];
1771
- } catch (error) {
1772
- throw this.handleError(error);
1773
- }
1774
- }
1775
-
1776
- /**
1777
- * Update asset visibility
1778
- * @param fileId - The file ID
1779
- * @param visibility - New visibility level ('private', 'public', or 'unlisted')
1780
- * @returns Updated asset information
1781
- */
1782
- async assetUpdateVisibility(fileId, visibility) {
1783
- try {
1784
- return await this.makeRequest('PATCH', `/api/assets/${fileId}/visibility`, {
1785
- visibility
1786
- }, {
1787
- cache: false
1788
- });
1789
- } catch (error) {
1790
- throw this.handleError(error);
1791
- }
1792
- }
1793
-
1794
- /**
1795
- * Helper: Upload and link avatar with automatic public visibility
1796
- * @param file - The avatar file
1797
- * @param userId - User ID to link to
1798
- * @param app - App name (defaults to 'profiles')
1799
- * @returns The uploaded and linked asset
1800
- */
1801
- async uploadAvatar(file, userId, app = 'profiles') {
1802
- try {
1803
- // Upload as public
1804
- const asset = await this.assetUpload(file, 'public');
1805
-
1806
- // Link to user profile as avatar
1807
- await this.assetLink(asset.file.id, app, 'avatar', userId, 'public');
1808
- return asset;
1809
- } catch (error) {
1810
- throw this.handleError(error);
1811
- }
1812
- }
1813
-
1814
- /**
1815
- * Helper: Upload and link profile banner with automatic public visibility
1816
- * @param file - The banner file
1817
- * @param userId - User ID to link to
1818
- * @param app - App name (defaults to 'profiles')
1819
- * @returns The uploaded and linked asset
1820
- */
1821
- async uploadProfileBanner(file, userId, app = 'profiles') {
1822
- try {
1823
- // Upload as public
1824
- const asset = await this.assetUpload(file, 'public');
1825
-
1826
- // Link to user profile as banner
1827
- await this.assetLink(asset.file.id, app, 'profile-banner', userId, 'public');
1828
- return asset;
1829
- } catch (error) {
1830
- throw this.handleError(error);
1831
- }
1832
- }
1833
-
1834
- // ============================================================================
1835
- // DEVELOPER API METHODS
1836
- // ============================================================================
1837
-
1838
- /**
1839
- * Get developer apps for the current user
1840
- */
1841
- async getDeveloperApps() {
1842
- try {
1843
- const res = await this.makeRequest('GET', '/api/developer/apps', undefined, {
1844
- cache: true,
1845
- cacheTTL: 2 * 60 * 1000 // 2 minutes cache
1846
- });
1847
- return res.apps || [];
1848
- } catch (error) {
1849
- throw this.handleError(error);
1850
- }
1851
- }
1852
-
1853
- /**
1854
- * Create a new developer app
1855
- */
1856
- async createDeveloperApp(data) {
1857
- try {
1858
- const res = await this.makeRequest('POST', '/api/developer/apps', data, {
1859
- cache: false
1860
- });
1861
- return res.app;
1862
- } catch (error) {
1863
- throw this.handleError(error);
1864
- }
1865
- }
1866
-
1867
- /**
1868
- * Get a specific developer app
1869
- */
1870
- async getDeveloperApp(appId) {
1871
- try {
1872
- const res = await this.makeRequest('GET', `/api/developer/apps/${appId}`, undefined, {
1873
- cache: true,
1874
- cacheTTL: 5 * 60 * 1000 // 5 minutes cache
1875
- });
1876
- return res.app;
1877
- } catch (error) {
1878
- throw this.handleError(error);
1879
- }
1880
- }
1881
-
1882
- /**
1883
- * Update a developer app
1884
- */
1885
- async updateDeveloperApp(appId, data) {
1886
- try {
1887
- const res = await this.makeRequest('PATCH', `/api/developer/apps/${appId}`, data, {
1888
- cache: false
1889
- });
1890
- return res.app;
1891
- } catch (error) {
1892
- throw this.handleError(error);
1893
- }
1894
- }
1895
-
1896
- /**
1897
- * Regenerate API secret for a developer app
1898
- */
1899
- async regenerateDeveloperAppSecret(appId) {
1900
- try {
1901
- return await this.makeRequest('POST', `/api/developer/apps/${appId}/regenerate-secret`, undefined, {
1902
- cache: false
1903
- });
1904
- } catch (error) {
1905
- throw this.handleError(error);
1906
- }
1907
- }
1908
-
1909
- /**
1910
- * Delete a developer app
1911
- */
1912
- async deleteDeveloperApp(appId) {
1913
- try {
1914
- return await this.makeRequest('DELETE', `/api/developer/apps/${appId}`, undefined, {
1915
- cache: false
1916
- });
1917
- } catch (error) {
1918
- throw this.handleError(error);
1919
- }
1920
- }
1921
-
1922
- // ============================================================================
1923
- // LOCATION METHODS
1924
- // ============================================================================
1925
-
1926
- /**
1927
- * Update user location
1928
- */
1929
- async updateLocation(latitude, longitude) {
1930
- try {
1931
- return await this.makeRequest('POST', '/api/location', {
1932
- latitude,
1933
- longitude
1934
- }, {
1935
- cache: false
1936
- });
1937
- } catch (error) {
1938
- throw this.handleError(error);
1939
- }
1940
- }
1941
-
1942
- /**
1943
- * Get nearby users
1944
- */
1945
- async getNearbyUsers(radius) {
1946
- try {
1947
- const params = radius ? {
1948
- radius
1949
- } : undefined;
1950
- return await this.makeRequest('GET', '/api/location/nearby', params, {
1951
- cache: false // Don't cache location data - always get fresh data
1952
- });
1953
- } catch (error) {
1954
- throw this.handleError(error);
1955
- }
1956
- }
1957
-
1958
- // ============================================================================
1959
- // ANALYTICS METHODS
1960
- // ============================================================================
1961
-
1962
- /**
1963
- * Track event
1964
- */
1965
- async trackEvent(eventName, properties) {
1966
- try {
1967
- await this.makeRequest('POST', '/api/analytics/events', {
1968
- event: eventName,
1969
- properties
1970
- }, {
1971
- cache: false,
1972
- retry: false
1973
- }); // Don't retry analytics events
1974
- } catch (error) {
1975
- throw this.handleError(error);
1976
- }
1977
- }
1978
-
1979
- /**
1980
- * Get analytics data
1981
- */
1982
- async getAnalytics(startDate, endDate) {
1983
- try {
1984
- const params = {};
1985
- if (startDate) params.startDate = startDate;
1986
- if (endDate) params.endDate = endDate;
1987
- return await this.makeRequest('GET', '/api/analytics', params, {
1988
- cache: true,
1989
- cacheTTL: 5 * 60 * 1000 // 5 minutes cache
1990
- });
1991
- } catch (error) {
1992
- throw this.handleError(error);
1993
- }
1994
- }
1995
-
1996
- // ============================================================================
1997
- // DEVICE METHODS
1998
- // ============================================================================
1999
-
2000
- /**
2001
- * Register device
2002
- */
2003
- async registerDevice(deviceData) {
2004
- try {
2005
- return await this.makeRequest('POST', '/api/devices', deviceData, {
2006
- cache: false
2007
- });
2008
- } catch (error) {
2009
- throw this.handleError(error);
2010
- }
2011
- }
2012
-
2013
- /**
2014
- * Get user devices
2015
- */
2016
- async getUserDevices() {
2017
- try {
2018
- return await this.makeRequest('GET', '/api/devices', undefined, {
2019
- cache: false // Don't cache device list - always get fresh data
2020
- });
2021
- } catch (error) {
2022
- throw this.handleError(error);
2023
- }
2024
- }
2025
-
2026
- /**
2027
- * Remove device
2028
- */
2029
- async removeDevice(deviceId) {
2030
- try {
2031
- await this.makeRequest('DELETE', `/api/devices/${deviceId}`, undefined, {
2032
- cache: false
2033
- });
2034
- } catch (error) {
2035
- throw this.handleError(error);
2036
- }
2037
- }
2038
-
2039
- /**
2040
- * Get device sessions
2041
- * Note: Not cached by default to ensure fresh data, but can be cached via makeRequest if needed
2042
- */
2043
- async getDeviceSessions(sessionId) {
2044
- try {
2045
- // Use makeRequest for consistent error handling and optional caching
2046
- // Cache disabled by default to ensure fresh session data
2047
- return await this.makeRequest('GET', `/api/session/device/sessions/${sessionId}`, undefined, {
2048
- cache: false,
2049
- // Don't cache sessions - always get fresh data
2050
- deduplicate: true // Deduplicate concurrent requests for same sessionId
2051
- });
2052
- } catch (error) {
2053
- throw this.handleError(error);
2054
- }
2055
- }
2056
-
2057
- /**
2058
- * Logout all device sessions
2059
- */
2060
- async logoutAllDeviceSessions(sessionId, deviceId, excludeCurrent) {
2061
- try {
2062
- const params = new URLSearchParams();
2063
- if (deviceId) params.append('deviceId', deviceId);
2064
- if (excludeCurrent) params.append('excludeCurrent', 'true');
2065
- const urlParams = {};
2066
- params.forEach((value, key) => {
2067
- urlParams[key] = value;
2068
- });
2069
- return await this.makeRequest('POST', `/api/session/device/logout-all/${sessionId}`, urlParams, {
2070
- cache: false
2071
- });
2072
- } catch (error) {
2073
- throw this.handleError(error);
2074
- }
2075
- }
2076
-
2077
- /**
2078
- * Update device name
2079
- */
2080
- async updateDeviceName(sessionId, deviceName) {
2081
- try {
2082
- return await this.makeRequest('PUT', `/api/session/device/name/${sessionId}`, {
2083
- deviceName
2084
- }, {
2085
- cache: false
2086
- });
2087
- } catch (error) {
2088
- throw this.handleError(error);
2089
- }
2090
- }
2091
-
2092
- // ============================================================================
2093
- // UTILITY METHODS
2094
- // ============================================================================
2095
-
2096
- /**
2097
- * Fetch link metadata
2098
- */
2099
- async fetchLinkMetadata(url) {
2100
- try {
2101
- return await this.makeRequest('GET', '/api/link-metadata', {
2102
- url
2103
- }, {
2104
- cache: true,
2105
- cacheTTL: 30 * 60 * 1000 // 30 minutes cache for link metadata
2106
- });
2107
- } catch (error) {
2108
- throw this.handleError(error);
2109
- }
2110
- }
2111
-
2112
- /**
2113
- * Simple Express.js authentication middleware
2114
- *
2115
- * Built-in authentication middleware that validates JWT tokens and adds user data to requests.
2116
- *
2117
- * @example
2118
- * ```typescript
2119
- * // Basic usage - just add it to your routes
2120
- * app.use('/api/protected', oxyServices.auth());
2121
- *
2122
- * // With debug logging
2123
- * app.use('/api/protected', oxyServices.auth({ debug: true }));
2124
- *
2125
- * // With custom error handling
2126
- * app.use('/api/protected', oxyServices.auth({
2127
- * onError: (error) => console.error('Auth failed:', error)
2128
- * }));
2129
- *
2130
- * // Load full user data
2131
- * app.use('/api/protected', oxyServices.auth({ loadUser: true }));
2132
- * ```
2133
- *
2134
- * @param options Optional configuration
2135
- * @param options.debug Enable debug logging (default: false)
2136
- * @param options.onError Custom error handler
2137
- * @param options.loadUser Load full user data (default: false for performance)
2138
- * @param options.session Use session-based validation (default: false)
2139
- * @returns Express middleware function
2140
- */
2141
- auth(options = {}) {
2142
- const {
2143
- debug = false,
2144
- onError,
2145
- loadUser = false,
2146
- session = false
2147
- } = options;
2148
-
2149
- // Return a synchronous middleware function
2150
- return (req, res, next) => {
2151
- try {
2152
- // Extract token from Authorization header
2153
- const authHeader = req.headers['authorization'];
2154
- const token = authHeader?.startsWith('Bearer ') ? authHeader.substring(7) : null;
2155
- if (debug) {
2156
- console.log(`🔐 Auth: Processing ${req.method} ${req.path}`);
2157
- console.log(`🔐 Auth: Token present: ${!!token}`);
2158
- }
2159
- if (!token) {
2160
- const error = {
2161
- message: 'Access token required',
2162
- code: 'MISSING_TOKEN',
2163
- status: 401
2164
- };
2165
- if (debug) console.log(`❌ Auth: Missing token`);
2166
- if (onError) return onError(error);
2167
- return res.status(401).json(error);
2168
- }
2169
-
2170
- // Decode and validate token
2171
- let decoded;
2172
- try {
2173
- decoded = jwtDecode(token);
2174
- if (debug) {
2175
- console.log(`🔐 Auth: Token decoded, User ID: ${decoded.userId || decoded.id}`);
2176
- }
2177
- } catch (decodeError) {
2178
- const error = {
2179
- message: 'Invalid token format',
2180
- code: 'INVALID_TOKEN_FORMAT',
2181
- status: 403
2182
- };
2183
- if (debug) console.log(`❌ Auth: Token decode failed`);
2184
- if (onError) return onError(error);
2185
- return res.status(403).json(error);
2186
- }
2187
- const userId = decoded.userId || decoded.id;
2188
- if (!userId) {
2189
- const error = {
2190
- message: 'Token missing user ID',
2191
- code: 'INVALID_TOKEN_PAYLOAD',
2192
- status: 403
2193
- };
2194
- if (debug) console.log(`❌ Auth: Token missing user ID`);
2195
- if (onError) return onError(error);
2196
- return res.status(403).json(error);
2197
- }
2198
-
2199
- // Check token expiration
2200
- if (decoded.exp && decoded.exp < Math.floor(Date.now() / 1000)) {
2201
- const error = {
2202
- message: 'Token expired',
2203
- code: 'TOKEN_EXPIRED',
2204
- status: 403
2205
- };
2206
- if (debug) console.log(`❌ Auth: Token expired`);
2207
- if (onError) return onError(error);
2208
- return res.status(403).json(error);
2209
- }
2210
-
2211
- // For now, skip session validation to keep it simple
2212
- // Session validation can be added later if needed
2213
-
2214
- // Set request properties immediately
2215
- req.userId = userId;
2216
- req.accessToken = token;
2217
- req.user = {
2218
- id: userId
2219
- };
2220
- if (debug) {
2221
- console.log(`✅ Auth: Authentication successful for user ${userId}`);
2222
- }
2223
- next();
2224
- } catch (error) {
2225
- const apiError = this.handleError(error);
2226
- if (debug) {
2227
- console.log(`❌ Auth: Unexpected error:`, apiError);
2228
- }
2229
- if (onError) return onError(apiError);
2230
- return res.status(apiError && apiError.status || 500).json(apiError);
2231
- }
2232
- };
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);
2233
109
  }
2234
110
  }
2235
111
 
112
+ // Re-export error classes for convenience
113
+ export { OxyAuthenticationError, OxyAuthenticationTimeoutError };
114
+
2236
115
  /**
2237
116
  * Export the default Oxy Cloud URL (for backward compatibility)
2238
117
  */