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