@oxyhq/services 5.13.12 → 5.13.16

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