@onairos/react-native 3.1.8 → 3.1.11

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.
@@ -89,7 +89,15 @@ export const initializeApiKey = async (config: OnairosConfig): Promise<void> =>
89
89
  const validation = await validateApiKey(config.apiKey);
90
90
 
91
91
  if (!validation.isValid) {
92
- throw new Error(`API key validation failed: ${validation.error}`);
92
+ // If it's a network error or JSON parse error, warn but don't fail initialization
93
+ if (validation.error?.includes('Network error') ||
94
+ validation.error?.includes('JSON Parse error') ||
95
+ validation.error?.includes('API validation endpoint returned')) {
96
+ console.warn('⚠️ API key validation failed due to network/server issues, continuing in offline mode:', validation.error);
97
+ console.warn('📝 SDK will function with limited validation. Ensure your API key is valid for production use.');
98
+ } else {
99
+ throw new Error(`API key validation failed: ${validation.error}`);
100
+ }
93
101
  }
94
102
 
95
103
  // Try to load existing JWT token
@@ -195,82 +203,192 @@ export const validateApiKey = async (apiKey: string): Promise<ApiKeyValidationRe
195
203
  const environment = globalConfig?.environment || 'production';
196
204
  const baseUrl = API_ENDPOINTS[environment];
197
205
  const timeout = globalConfig?.timeout || 30000;
206
+ const maxRetries = globalConfig?.retryAttempts || 3;
198
207
 
199
- // Create abort controller for timeout
200
- const controller = new AbortController();
201
- const timeoutId = setTimeout(() => controller.abort(), timeout);
202
-
203
- try {
204
- const response = await fetch(`${baseUrl}/auth/validate-key`, {
205
- method: 'POST',
206
- headers: {
207
- 'Content-Type': 'application/json',
208
- 'Authorization': `Bearer ${apiKey}`,
209
- 'User-Agent': 'OnairosReactNative/1.0',
210
- 'X-API-Key-Type': keyType,
211
- },
212
- body: JSON.stringify({
213
- environment,
214
- sdk_version: '3.0.72',
215
- platform: 'react-native',
216
- keyType,
217
- timestamp: new Date().toISOString(),
218
- }),
219
- signal: controller.signal,
220
- });
208
+ // Retry logic for network failures
209
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
210
+ // Create abort controller for timeout
211
+ const controller = new AbortController();
212
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
221
213
 
222
- clearTimeout(timeoutId);
214
+ try {
215
+ if (globalConfig?.enableLogging && attempt > 1) {
216
+ console.log(`🔄 Retry attempt ${attempt}/${maxRetries} for API key validation`);
217
+ }
223
218
 
224
- const data = await response.json();
219
+ const response = await fetch(`${baseUrl}/auth/validate-key`, {
220
+ method: 'POST',
221
+ headers: {
222
+ 'Content-Type': 'application/json',
223
+ 'Authorization': `Bearer ${apiKey}`,
224
+ 'User-Agent': 'OnairosReactNative/3.1.10',
225
+ 'X-API-Key-Type': keyType,
226
+ 'X-SDK-Platform': 'react-native',
227
+ 'X-Retry-Attempt': attempt.toString(),
228
+ },
229
+ body: JSON.stringify({
230
+ environment,
231
+ sdk_version: '3.1.10',
232
+ platform: 'react-native',
233
+ keyType,
234
+ timestamp: new Date().toISOString(),
235
+ attempt,
236
+ }),
237
+ signal: controller.signal,
238
+ });
225
239
 
226
- if (response.ok && data.success) {
227
- const result: ApiKeyValidationResult = {
228
- isValid: true,
229
- permissions: data.permissions || [],
230
- rateLimits: data.rateLimits || null,
231
- keyType: keyType,
232
- };
240
+ clearTimeout(timeoutId);
233
241
 
234
- // Cache the successful result
235
- validationCache.set(apiKey, {
236
- result,
237
- timestamp: Date.now(),
238
- });
242
+ // First check if we got a valid response
243
+ if (!response) {
244
+ throw new Error('No response received from server');
245
+ }
239
246
 
240
- if (globalConfig?.enableLogging) {
241
- console.log('✅ API key validation successful');
247
+ // Check if response is actually JSON before trying to parse
248
+ const contentType = response.headers.get('content-type');
249
+ const isJsonResponse = contentType && contentType.includes('application/json');
250
+
251
+ if (!isJsonResponse) {
252
+ const textContent = await response.text();
253
+ const previewText = textContent.substring(0, 200);
254
+
255
+ console.error('❌ API endpoint returned non-JSON response:', {
256
+ status: response.status,
257
+ statusText: response.statusText,
258
+ contentType: contentType || 'unknown',
259
+ preview: previewText,
260
+ url: `${baseUrl}/auth/validate-key`,
261
+ attempt: attempt
262
+ });
263
+
264
+ // Handle specific error cases
265
+ if (response.status === 404) {
266
+ throw new Error(`API validation endpoint not found (404). The endpoint ${baseUrl}/auth/validate-key may not exist or be configured correctly.`);
267
+ } else if (response.status === 500) {
268
+ throw new Error(`Server error (500). The Onairos backend is experiencing issues.`);
269
+ } else if (response.status === 502 || response.status === 503) {
270
+ throw new Error(`Service unavailable (${response.status}). The Onairos backend may be temporarily down.`);
271
+ } else if (textContent.includes('<html') || textContent.includes('<!DOCTYPE')) {
272
+ throw new Error(`Server returned HTML page instead of JSON API response. This often indicates a routing issue or server misconfiguration.`);
273
+ } else {
274
+ throw new Error(`API validation endpoint returned ${response.status} - ${response.statusText}. Expected JSON but got ${contentType || 'unknown content type'}.`);
275
+ }
242
276
  }
243
277
 
244
- return result;
245
- } else {
246
- const errorMessage = data.error || `HTTP ${response.status}: ${response.statusText}`;
278
+ // Parse JSON response
279
+ let data;
280
+ try {
281
+ data = await response.json();
282
+ } catch (jsonError) {
283
+ console.error('❌ Failed to parse JSON response:', {
284
+ error: jsonError.message,
285
+ status: response.status,
286
+ contentType,
287
+ attempt: attempt
288
+ });
289
+ throw new Error(`Failed to parse server response as JSON: ${jsonError.message}`);
290
+ }
291
+
292
+ // Handle successful response
293
+ if (response.ok && data.success) {
294
+ const result: ApiKeyValidationResult = {
295
+ isValid: true,
296
+ permissions: data.permissions || [],
297
+ rateLimits: data.rateLimits || null,
298
+ keyType: keyType,
299
+ };
300
+
301
+ // Cache the successful result
302
+ validationCache.set(apiKey, {
303
+ result,
304
+ timestamp: Date.now(),
305
+ });
306
+
307
+ if (globalConfig?.enableLogging) {
308
+ console.log('✅ API key validation successful');
309
+ }
310
+
311
+ return result;
312
+ } else {
313
+ // Handle API errors (invalid key, etc.)
314
+ const errorMessage = data.error || data.message || `HTTP ${response.status}: ${response.statusText}`;
315
+
316
+ const result: ApiKeyValidationResult = {
317
+ isValid: false,
318
+ error: errorMessage,
319
+ keyType: keyType,
320
+ };
321
+
322
+ // For client errors (4xx), don't retry
323
+ if (response.status >= 400 && response.status < 500) {
324
+ if (globalConfig?.enableLogging) {
325
+ console.error('❌ API key validation failed (client error):', errorMessage);
326
+ }
327
+ return result;
328
+ }
329
+
330
+ // For server errors (5xx), retry
331
+ throw new Error(errorMessage);
332
+ }
333
+
334
+ } catch (fetchError: any) {
335
+ clearTimeout(timeoutId);
247
336
 
248
- const result: ApiKeyValidationResult = {
249
- isValid: false,
250
- error: errorMessage,
251
- keyType: keyType,
252
- };
253
-
254
- // Don't cache failed results
255
- if (globalConfig?.enableLogging) {
256
- console.error('❌ API key validation failed:', errorMessage);
337
+ if (fetchError.name === 'AbortError') {
338
+ const errorMessage = `API key validation timeout (${timeout}ms)`;
339
+ console.error('⏱️ API key validation timeout');
340
+
341
+ if (attempt === maxRetries) {
342
+ return { isValid: false, error: errorMessage, keyType: keyType };
343
+ }
344
+ continue; // Retry timeout errors
257
345
  }
346
+
347
+ // Enhanced error message based on error type
348
+ let errorMessage = `Network error during API key validation: ${fetchError.message}`;
349
+
350
+ // Add specific guidance for common errors
351
+ if (fetchError.message.includes('JSON Parse error') || fetchError.message.includes('Unexpected character')) {
352
+ errorMessage = `Server returned invalid JSON response. This usually indicates the API endpoint returned HTML instead of JSON (often a 404 or server error page). ${fetchError.message}`;
353
+ } else if (fetchError.message.includes('Network request failed') || fetchError.message.includes('fetch')) {
354
+ errorMessage = `Network connectivity issue. Please check internet connection and verify the Onairos API is accessible. ${fetchError.message}`;
355
+ } else if (fetchError.message.includes('DNS') || fetchError.message.includes('ENOTFOUND')) {
356
+ errorMessage = `DNS resolution failed for ${baseUrl}. Please check network settings and domain accessibility. ${fetchError.message}`;
357
+ }
358
+
359
+ console.error('🌐 Network error during API key validation:', {
360
+ error: fetchError,
361
+ endpoint: `${baseUrl}/auth/validate-key`,
362
+ attempt: attempt,
363
+ maxRetries: maxRetries,
364
+ retryable: attempt < maxRetries
365
+ });
258
366
 
259
- return result;
260
- }
261
- } catch (fetchError: any) {
262
- clearTimeout(timeoutId);
263
-
264
- if (fetchError.name === 'AbortError') {
265
- const errorMessage = 'API key validation timeout';
266
- console.error('⏱️ API key validation timeout');
267
- return { isValid: false, error: errorMessage, keyType: keyType };
367
+ // If this is the last attempt, return the error
368
+ if (attempt === maxRetries) {
369
+ return {
370
+ isValid: false,
371
+ error: errorMessage,
372
+ keyType: keyType
373
+ };
374
+ }
375
+
376
+ // Wait before retrying (exponential backoff)
377
+ const backoffDelay = Math.min(1000 * Math.pow(2, attempt - 1), 5000);
378
+ if (globalConfig?.enableLogging) {
379
+ console.log(`⏳ Waiting ${backoffDelay}ms before retry...`);
380
+ }
381
+ await new Promise<void>(resolve => setTimeout(() => resolve(), backoffDelay));
268
382
  }
269
-
270
- const errorMessage = `Network error during API key validation: ${fetchError.message}`;
271
- console.error('🌐 Network error during API key validation:', fetchError);
272
- return { isValid: false, error: errorMessage, keyType: keyType };
273
383
  }
384
+
385
+ // This should never be reached, but just in case
386
+ return {
387
+ isValid: false,
388
+ error: 'All retry attempts exhausted',
389
+ keyType: keyType
390
+ };
391
+
274
392
  } catch (error: any) {
275
393
  const errorMessage = `API key validation error: ${error.message}`;
276
394
  console.error('❌ API key validation error:', error);
@@ -0,0 +1,275 @@
1
+ /**
2
+ * 🔄 Retry Helper Utility
3
+ *
4
+ * Provides robust retry logic with exponential backoff for network operations.
5
+ * Used throughout the Onairos SDK for handling transient failures gracefully.
6
+ */
7
+
8
+ export interface RetryOptions {
9
+ /** Maximum number of retry attempts */
10
+ maxRetries: number;
11
+ /** Base delay between retries in milliseconds */
12
+ baseDelay: number;
13
+ /** Maximum delay between retries in milliseconds */
14
+ maxDelay: number;
15
+ /** Whether to use exponential backoff */
16
+ exponentialBackoff: boolean;
17
+ /** Function to determine if an error should trigger a retry */
18
+ shouldRetry?: (error: any, attempt: number) => boolean;
19
+ /** Function called before each retry attempt */
20
+ onRetry?: (error: any, attempt: number, nextDelay: number) => void;
21
+ /** Enable logging of retry attempts */
22
+ enableLogging: boolean;
23
+ }
24
+
25
+ export interface RetryResult<T> {
26
+ success: boolean;
27
+ data?: T;
28
+ error?: Error;
29
+ attempts: number;
30
+ totalDuration: number;
31
+ }
32
+
33
+ /**
34
+ * Default retry options for the Onairos SDK
35
+ */
36
+ export const DEFAULT_RETRY_OPTIONS: RetryOptions = {
37
+ maxRetries: 3,
38
+ baseDelay: 1000,
39
+ maxDelay: 5000,
40
+ exponentialBackoff: true,
41
+ enableLogging: false,
42
+ shouldRetry: (error: any, attempt: number) => {
43
+ // Don't retry client errors (4xx) except for 408 (timeout) and 429 (rate limit)
44
+ if (error.status >= 400 && error.status < 500 && error.status !== 408 && error.status !== 429) {
45
+ return false;
46
+ }
47
+
48
+ // Retry network errors, timeouts, and server errors (5xx)
49
+ if (error.name === 'AbortError' ||
50
+ error.message.includes('Network request failed') ||
51
+ error.message.includes('fetch') ||
52
+ error.message.includes('ENOTFOUND') ||
53
+ error.message.includes('timeout') ||
54
+ (error.status >= 500)) {
55
+ return true;
56
+ }
57
+
58
+ // Retry JSON parse errors (likely server issues)
59
+ if (error.message.includes('JSON Parse error') || error.message.includes('Unexpected character')) {
60
+ return true;
61
+ }
62
+
63
+ return false;
64
+ }
65
+ };
66
+
67
+ /**
68
+ * Execute a function with retry logic and exponential backoff
69
+ * @param fn Function to execute (should return a Promise)
70
+ * @param options Retry configuration options
71
+ * @returns Promise with retry result
72
+ */
73
+ export async function withRetry<T>(
74
+ fn: () => Promise<T>,
75
+ options: Partial<RetryOptions> = {}
76
+ ): Promise<RetryResult<T>> {
77
+ const config = { ...DEFAULT_RETRY_OPTIONS, ...options };
78
+ const startTime = Date.now();
79
+ let lastError: Error | null = null;
80
+
81
+ for (let attempt = 1; attempt <= config.maxRetries + 1; attempt++) {
82
+ try {
83
+ if (config.enableLogging && attempt > 1) {
84
+ console.log(`🔄 Retry attempt ${attempt}/${config.maxRetries + 1}`);
85
+ }
86
+
87
+ const result = await fn();
88
+
89
+ return {
90
+ success: true,
91
+ data: result,
92
+ attempts: attempt,
93
+ totalDuration: Date.now() - startTime
94
+ };
95
+
96
+ } catch (error: any) {
97
+ lastError = error;
98
+
99
+ // Check if we should retry this error
100
+ const shouldRetryError = config.shouldRetry ? config.shouldRetry(error, attempt) : true;
101
+
102
+ // If this is the last attempt or we shouldn't retry, throw the error
103
+ if (attempt > config.maxRetries || !shouldRetryError) {
104
+ if (config.enableLogging) {
105
+ console.error(`❌ All retry attempts exhausted or error not retryable: ${error.message}`);
106
+ }
107
+ break;
108
+ }
109
+
110
+ // Calculate delay for next attempt
111
+ let delay = config.baseDelay;
112
+ if (config.exponentialBackoff) {
113
+ delay = Math.min(config.baseDelay * Math.pow(2, attempt - 1), config.maxDelay);
114
+ }
115
+
116
+ // Add some jitter to prevent thundering herd
117
+ const jitter = Math.random() * 0.1 * delay;
118
+ delay = Math.floor(delay + jitter);
119
+
120
+ if (config.onRetry) {
121
+ config.onRetry(error, attempt, delay);
122
+ }
123
+
124
+ if (config.enableLogging) {
125
+ console.log(`⏳ Waiting ${delay}ms before retry (attempt ${attempt}/${config.maxRetries + 1})`);
126
+ }
127
+
128
+ // Wait before next attempt
129
+ await new Promise<void>(resolve => setTimeout(() => resolve(), delay));
130
+ }
131
+ }
132
+
133
+ return {
134
+ success: false,
135
+ error: lastError || new Error('Unknown error'),
136
+ attempts: config.maxRetries + 1,
137
+ totalDuration: Date.now() - startTime
138
+ };
139
+ }
140
+
141
+ /**
142
+ * Retry configuration for API calls
143
+ */
144
+ export const API_RETRY_OPTIONS: Partial<RetryOptions> = {
145
+ maxRetries: 3,
146
+ baseDelay: 1000,
147
+ maxDelay: 5000,
148
+ exponentialBackoff: true,
149
+ shouldRetry: (error: any, attempt: number) => {
150
+ // Enhanced retry logic for API calls
151
+
152
+ // Never retry authentication errors (401) or permission errors (403)
153
+ if (error.status === 401 || error.status === 403) {
154
+ return false;
155
+ }
156
+
157
+ // Never retry bad request errors (400) or not found (404) unless it's a specific case
158
+ if (error.status === 400 || (error.status === 404 && !error.message.includes('validation endpoint'))) {
159
+ return false;
160
+ }
161
+
162
+ // Retry rate limiting (429) with longer delays
163
+ if (error.status === 429) {
164
+ return attempt <= 2; // Limit retries for rate limiting
165
+ }
166
+
167
+ // Retry server errors (5xx)
168
+ if (error.status >= 500) {
169
+ return true;
170
+ }
171
+
172
+ // Retry timeout and network errors
173
+ if (error.name === 'AbortError' ||
174
+ error.message.includes('timeout') ||
175
+ error.message.includes('Network request failed') ||
176
+ error.message.includes('fetch') ||
177
+ error.message.includes('ENOTFOUND')) {
178
+ return true;
179
+ }
180
+
181
+ // Retry JSON parse errors (server returning HTML instead of JSON)
182
+ if (error.message.includes('JSON Parse error') ||
183
+ error.message.includes('Unexpected character') ||
184
+ error.message.includes('HTML page instead of JSON')) {
185
+ return true;
186
+ }
187
+
188
+ return false;
189
+ },
190
+ onRetry: (error: any, attempt: number, delay: number) => {
191
+ console.warn(`⚠️ API call failed (attempt ${attempt}), retrying in ${delay}ms: ${error.message}`);
192
+ }
193
+ };
194
+
195
+ /**
196
+ * Specialized retry for network/connectivity issues
197
+ */
198
+ export const NETWORK_RETRY_OPTIONS: Partial<RetryOptions> = {
199
+ maxRetries: 2,
200
+ baseDelay: 2000,
201
+ maxDelay: 8000,
202
+ exponentialBackoff: true,
203
+ shouldRetry: (error: any, attempt: number) => {
204
+ // Only retry actual network/connectivity issues
205
+ return error.message.includes('Network request failed') ||
206
+ error.message.includes('ENOTFOUND') ||
207
+ error.message.includes('DNS') ||
208
+ error.name === 'AbortError';
209
+ }
210
+ };
211
+
212
+ /**
213
+ * Create a retry wrapper for fetch requests
214
+ * @param url Request URL
215
+ * @param options Fetch options
216
+ * @param retryOptions Retry configuration
217
+ * @returns Promise with fetch response
218
+ */
219
+ export async function fetchWithRetry(
220
+ url: string,
221
+ options: RequestInit = {},
222
+ retryOptions: Partial<RetryOptions> = API_RETRY_OPTIONS
223
+ ): Promise<Response> {
224
+ const result = await withRetry(
225
+ () => fetch(url, options),
226
+ retryOptions
227
+ );
228
+
229
+ if (!result.success) {
230
+ throw result.error;
231
+ }
232
+
233
+ return result.data!;
234
+ }
235
+
236
+ /**
237
+ * Health check function with retry for testing connectivity
238
+ * @param url URL to check
239
+ * @param timeout Timeout in milliseconds
240
+ * @returns Promise indicating if the service is reachable
241
+ */
242
+ export async function healthCheck(
243
+ url: string,
244
+ timeout: number = 5000
245
+ ): Promise<{ reachable: boolean; status?: number; error?: string; duration: number }> {
246
+ const startTime = Date.now();
247
+
248
+ try {
249
+ const controller = new AbortController();
250
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
251
+
252
+ const response = await fetch(url, {
253
+ method: 'GET',
254
+ signal: controller.signal,
255
+ headers: {
256
+ 'User-Agent': 'OnairosReactNative/HealthCheck'
257
+ }
258
+ });
259
+
260
+ clearTimeout(timeoutId);
261
+
262
+ return {
263
+ reachable: true,
264
+ status: response.status,
265
+ duration: Date.now() - startTime
266
+ };
267
+
268
+ } catch (error: any) {
269
+ return {
270
+ reachable: false,
271
+ error: error.message,
272
+ duration: Date.now() - startTime
273
+ };
274
+ }
275
+ }