@devicecloud.dev/dcd 4.1.6 → 4.1.9-beta.0

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.
package/dist/methods.js CHANGED
@@ -77,7 +77,8 @@ const verifyAppZip = async (zipPath) => {
77
77
  zip.close();
78
78
  };
79
79
  exports.verifyAppZip = verifyAppZip;
80
- const uploadBinary = async (filePath, apiUrl, apiKey, ignoreShaCheck = false, log = true, debug = false) => {
80
+ const uploadBinary = async (config) => {
81
+ const { filePath, apiUrl, apiKey, ignoreShaCheck = false, log = true, debug = false } = config;
81
82
  if (log) {
82
83
  core_1.ux.action.start(styling_1.colors.bold('Checking and uploading binary'), styling_1.colors.dim('Initializing'), {
83
84
  stdout: true,
@@ -107,7 +108,7 @@ const uploadBinary = async (filePath, apiUrl, apiKey, ignoreShaCheck = false, lo
107
108
  }
108
109
  }
109
110
  // Perform the upload
110
- const uploadId = await performUpload(filePath, apiUrl, apiKey, file, sha, debug, startTime);
111
+ const uploadId = await performUpload({ apiKey, apiUrl, debug, file, filePath, sha, startTime });
111
112
  if (log) {
112
113
  core_1.ux.action.stop(styling_1.colors.success('\n✓ Binary uploaded with ID: ') + (0, styling_1.formatId)(uploadId));
113
114
  }
@@ -232,68 +233,421 @@ async function checkExistingUpload(apiUrl, apiKey, sha, debug) {
232
233
  }
233
234
  }
234
235
  /**
235
- * Performs the actual file upload
236
- * @param filePath Path to the file being uploaded
237
- * @param apiUrl API base URL
238
- * @param apiKey API authentication key
239
- * @param file Prepared file to upload
240
- * @param sha SHA-256 hash of the file
241
- * @param debug Whether debug logging is enabled
242
- * @param startTime Timestamp when upload started
243
- * @returns Promise resolving to upload ID
236
+ * Uploads file to Supabase using resumable uploads
237
+ * @param env - Environment (dev or prod)
238
+ * @param tempPath - Temporary staging path for upload
239
+ * @param file - File to upload
240
+ * @param debug - Enable debug logging
241
+ * @returns Upload result with success status and any error
242
+ */
243
+ async function uploadToSupabase(env, tempPath, file, debug) {
244
+ if (debug) {
245
+ console.log(`[DEBUG] Uploading to Supabase storage (${env}) using resumable uploads...`);
246
+ console.log(`[DEBUG] Staging path: ${tempPath}`);
247
+ console.log(`[DEBUG] File size: ${(file.size / 1024 / 1024).toFixed(2)} MB`);
248
+ }
249
+ try {
250
+ const uploadStartTime = Date.now();
251
+ await supabase_gateway_1.SupabaseGateway.uploadResumable(env, tempPath, file, debug);
252
+ if (debug) {
253
+ const uploadDuration = Date.now() - uploadStartTime;
254
+ const uploadDurationSeconds = uploadDuration / 1000;
255
+ const uploadSpeed = (file.size / 1024 / 1024) / uploadDurationSeconds;
256
+ console.log(`[DEBUG] Supabase resumable upload completed in ${uploadDurationSeconds.toFixed(2)}s (${uploadDuration}ms)`);
257
+ console.log(`[DEBUG] Average upload speed: ${uploadSpeed.toFixed(2)} MB/s`);
258
+ }
259
+ return { error: null, success: true };
260
+ }
261
+ catch (error) {
262
+ const uploadError = error instanceof Error ? error : new Error(String(error));
263
+ if (debug) {
264
+ console.error(`[DEBUG] === SUPABASE RESUMABLE UPLOAD FAILED ===`);
265
+ console.error(`[DEBUG] Error message: ${uploadError.message}`);
266
+ console.error(`[DEBUG] Error name: ${uploadError.name}`);
267
+ if (uploadError.stack) {
268
+ console.error(`[DEBUG] Error stack:\n${uploadError.stack}`);
269
+ }
270
+ console.error(`[DEBUG] Staging path: ${tempPath}`);
271
+ console.error(`[DEBUG] File size: ${file.size} bytes`);
272
+ console.log('[DEBUG] Will attempt Backblaze fallback if available...');
273
+ }
274
+ return { error: uploadError, success: false };
275
+ }
276
+ }
277
+ /**
278
+ * Handles Backblaze upload with appropriate strategy
279
+ * @param config - Configuration object for Backblaze upload
280
+ * @returns Upload result with success status and any error
244
281
  */
245
- async function performUpload(filePath, apiUrl, apiKey, file, sha, debug, startTime) {
282
+ async function handleBackblazeUpload(config) {
283
+ const { b2, apiUrl, apiKey, finalPath, file, filePath, debug, supabaseSuccess } = config;
284
+ if (!b2) {
285
+ if (debug && !supabaseSuccess) {
286
+ console.log('[DEBUG] Backblaze not configured, cannot fallback');
287
+ }
288
+ return { error: null, success: false };
289
+ }
290
+ if (debug) {
291
+ console.log(supabaseSuccess ? '[DEBUG] Starting Backblaze backup upload...' : '[DEBUG] Starting Backblaze fallback upload...');
292
+ }
293
+ try {
294
+ const b2UploadStartTime = Date.now();
295
+ let backblazeSuccess = false;
296
+ if (b2.strategy === 'simple' && b2.simple) {
297
+ const simple = b2.simple;
298
+ backblazeSuccess = await uploadToBackblaze(simple.uploadUrl, simple.authorizationToken, `organizations/${finalPath}`, file, debug);
299
+ }
300
+ else if (b2.strategy === 'large' && b2.large) {
301
+ const large = b2.large;
302
+ backblazeSuccess = await uploadLargeFileToBackblaze({
303
+ apiKey,
304
+ apiUrl,
305
+ debug,
306
+ fileId: large.fileId,
307
+ fileName: `organizations/${finalPath}`,
308
+ fileObject: file,
309
+ filePath,
310
+ fileSize: file.size,
311
+ uploadPartUrls: large.uploadPartUrls,
312
+ });
313
+ }
314
+ if (debug) {
315
+ const duration = Date.now() - b2UploadStartTime;
316
+ const durationSeconds = duration / 1000;
317
+ console.log(backblazeSuccess
318
+ ? `[DEBUG] Backblaze upload completed successfully in ${durationSeconds.toFixed(2)}s (${duration}ms)`
319
+ : `[DEBUG] Backblaze upload failed after ${durationSeconds.toFixed(2)}s (${duration}ms)`);
320
+ }
321
+ return { error: null, success: backblazeSuccess };
322
+ }
323
+ catch (error) {
324
+ const b2Error = error instanceof Error ? error : new Error(String(error));
325
+ if (debug) {
326
+ console.error(`[DEBUG] === UNEXPECTED BACKBLAZE UPLOAD ERROR ===`);
327
+ console.error(`[DEBUG] Error message: ${b2Error.message}`);
328
+ console.error(`[DEBUG] Error name: ${b2Error.name}`);
329
+ if (b2Error.stack) {
330
+ console.error(`[DEBUG] Error stack:\n${b2Error.stack}`);
331
+ }
332
+ console.error(`[DEBUG] Upload strategy: ${b2.strategy}`);
333
+ }
334
+ return { error: b2Error, success: false };
335
+ }
336
+ }
337
+ /**
338
+ * Requests upload URL and paths from API
339
+ * @param apiUrl - API base URL
340
+ * @param apiKey - API authentication key
341
+ * @param filePath - Path to the file being uploaded
342
+ * @param fileSize - Size of the file in bytes
343
+ * @param debug - Enable debug logging
344
+ * @returns Promise resolving to upload paths and configuration
345
+ */
346
+ async function requestUploadPaths(apiUrl, apiKey, filePath, fileSize, debug) {
347
+ const platform = filePath?.endsWith('.apk') ? 'android' : 'ios';
246
348
  if (debug) {
247
349
  console.log('[DEBUG] Requesting upload URL...');
248
350
  console.log(`[DEBUG] Target endpoint: ${apiUrl}/uploads/getBinaryUploadUrl`);
249
- console.log(`[DEBUG] Platform: ${filePath?.endsWith('.apk') ? 'android' : 'ios'}`);
351
+ console.log(`[DEBUG] Platform: ${platform}`);
250
352
  }
251
353
  const urlRequestStartTime = Date.now();
252
- const { id, message, path, token } = await api_gateway_1.ApiGateway.getBinaryUploadUrl(apiUrl, apiKey, filePath?.endsWith('.apk') ? 'android' : 'ios');
354
+ const { id, tempPath, finalPath, b2 } = await api_gateway_1.ApiGateway.getBinaryUploadUrl(apiUrl, apiKey, platform, fileSize);
253
355
  if (debug) {
356
+ const hasStrategy = b2 && typeof b2 === 'object' && 'strategy' in b2;
254
357
  console.log(`[DEBUG] Upload URL request completed in ${Date.now() - urlRequestStartTime}ms`);
255
358
  console.log(`[DEBUG] Upload ID: ${id}`);
256
- console.log(`[DEBUG] Upload path: ${path}`);
359
+ console.log(`[DEBUG] Temp path (TUS upload): ${tempPath}`);
360
+ console.log(`[DEBUG] Final path (after finalize): ${finalPath}`);
361
+ console.log(`[DEBUG] Backblaze upload URL provided: ${Boolean(b2)}`);
362
+ if (hasStrategy)
363
+ console.log(`[DEBUG] Backblaze strategy: ${b2.strategy}`);
257
364
  }
258
- if (!path)
259
- throw new Error(message);
260
- // Extract app metadata using the service
261
- if (debug) {
365
+ if (!tempPath)
366
+ throw new Error('No upload path provided by API');
367
+ return { b2, finalPath, id, tempPath };
368
+ }
369
+ /**
370
+ * Extracts metadata from the binary file
371
+ * @param filePath - Path to the binary file
372
+ * @param debug - Enable debug logging
373
+ * @returns Promise resolving to extracted metadata containing appId and platform
374
+ */
375
+ async function extractBinaryMetadata(filePath, debug) {
376
+ if (debug)
262
377
  console.log('[DEBUG] Extracting app metadata...');
263
- }
264
378
  const metadataExtractor = new metadata_extractor_service_1.MetadataExtractorService();
265
379
  const metadata = await metadataExtractor.extract(filePath);
266
380
  if (!metadata) {
267
381
  throw new Error(`Failed to extract metadata from ${filePath}. Supported formats: .apk, .app, .zip`);
268
382
  }
269
- if (debug) {
383
+ if (debug)
270
384
  console.log(`[DEBUG] Metadata extracted: ${JSON.stringify(metadata)}`);
385
+ return metadata;
386
+ }
387
+ /**
388
+ * Validates upload results and throws if all uploads failed
389
+ * @param supabaseSuccess - Whether Supabase upload succeeded
390
+ * @param backblazeSuccess - Whether Backblaze upload succeeded
391
+ * @param lastError - Last error encountered during uploads
392
+ * @param b2 - Backblaze configuration
393
+ * @param debug - Enable debug logging
394
+ * @returns void - throws error if all uploads failed
395
+ */
396
+ function validateUploadResults(supabaseSuccess, backblazeSuccess, lastError, b2, debug) {
397
+ if (supabaseSuccess || backblazeSuccess) {
398
+ return;
271
399
  }
272
- const env = apiUrl === 'https://api.devicecloud.dev' ? 'prod' : 'dev';
273
400
  if (debug) {
274
- console.log(`[DEBUG] Uploading to Supabase storage (${env})...`);
275
- console.log(`[DEBUG] File size: ${(file.size / 1024 / 1024).toFixed(2)} MB`);
401
+ console.error(`[DEBUG] === ALL UPLOADS FAILED ===`);
402
+ console.error(`[DEBUG] Supabase upload: FAILED`);
403
+ console.error(`[DEBUG] Backblaze upload: ${b2 ? 'FAILED' : 'NOT CONFIGURED'}`);
404
+ if (lastError) {
405
+ console.error(`[DEBUG] Final error details:`);
406
+ console.error(`[DEBUG] - Message: ${lastError.message}`);
407
+ console.error(`[DEBUG] - Name: ${lastError.name}`);
408
+ console.error(`[DEBUG] - Stack: ${lastError.stack}`);
409
+ }
276
410
  }
277
- const uploadStartTime = Date.now();
278
- await supabase_gateway_1.SupabaseGateway.uploadToSignedUrl(env, path, token, file, debug);
279
- if (debug) {
280
- const uploadDuration = Date.now() - uploadStartTime;
281
- const uploadSpeed = (file.size / 1024 / 1024) / (uploadDuration / 1000);
282
- console.log(`[DEBUG] File upload completed in ${uploadDuration}ms`);
283
- console.log(`[DEBUG] Average upload speed: ${uploadSpeed.toFixed(2)} MB/s`);
411
+ throw new Error(`All uploads failed. ${lastError ? `Last error: ${JSON.stringify({ message: lastError.message, name: lastError.name, stack: lastError.stack })}` : 'No upload targets available.'}`);
412
+ }
413
+ /**
414
+ * Performs the actual file upload
415
+ * @param config Configuration object for the upload
416
+ * @returns Promise resolving to upload ID
417
+ */
418
+ async function performUpload(config) {
419
+ const { filePath, apiUrl, apiKey, file, sha, debug, startTime } = config;
420
+ // Request upload URL and paths
421
+ const { id, tempPath, finalPath, b2 } = await requestUploadPaths(apiUrl, apiKey, filePath, file.size, debug);
422
+ // Extract app metadata
423
+ const metadata = await extractBinaryMetadata(filePath, debug);
424
+ const env = apiUrl === 'https://api.devicecloud.dev' ? 'prod' : 'dev';
425
+ // Upload to Supabase
426
+ const supabaseResult = await uploadToSupabase(env, tempPath, file, debug);
427
+ let lastError = supabaseResult.error;
428
+ // Upload to Backblaze
429
+ const backblazeResult = await handleBackblazeUpload({
430
+ apiKey,
431
+ apiUrl,
432
+ b2: b2,
433
+ debug,
434
+ file,
435
+ filePath,
436
+ finalPath,
437
+ supabaseSuccess: supabaseResult.success,
438
+ });
439
+ // Update lastError if Supabase also failed
440
+ if (!supabaseResult.success && backblazeResult.error) {
441
+ lastError = backblazeResult.error;
284
442
  }
443
+ // Validate results
444
+ validateUploadResults(supabaseResult.success, backblazeResult.success, lastError, b2, debug);
445
+ // Log upload summary
285
446
  if (debug) {
447
+ const hasWarning = !supabaseResult.success && backblazeResult.success;
448
+ console.log(`[DEBUG] Upload summary - Supabase: ${supabaseResult.success ? '✓' : '✗'}, Backblaze: ${backblazeResult.success ? '✓' : '✗'}`);
286
449
  console.log('[DEBUG] Finalizing upload...');
287
450
  console.log(`[DEBUG] Target endpoint: ${apiUrl}/uploads/finaliseUpload`);
451
+ console.log(`[DEBUG] Moving from temp: ${tempPath}`);
452
+ console.log(`[DEBUG] Moving to final: ${finalPath}`);
453
+ console.log(`[DEBUG] Supabase upload status: ${supabaseResult.success ? 'SUCCESS' : 'FAILED'}`);
454
+ console.log(`[DEBUG] Backblaze upload status: ${backblazeResult.success ? 'SUCCESS' : 'FAILED'}`);
455
+ if (hasWarning)
456
+ console.log('[DEBUG] ⚠ Warning: File only exists in Backblaze (Supabase failed)');
288
457
  }
458
+ // Finalize upload
289
459
  const finalizeStartTime = Date.now();
290
- await api_gateway_1.ApiGateway.finaliseUpload(apiUrl, apiKey, id, metadata, path, sha);
460
+ await api_gateway_1.ApiGateway.finaliseUpload({
461
+ apiKey,
462
+ backblazeSuccess: backblazeResult.success,
463
+ baseUrl: apiUrl,
464
+ id,
465
+ metadata,
466
+ path: tempPath,
467
+ sha: sha,
468
+ supabaseSuccess: supabaseResult.success,
469
+ });
291
470
  if (debug) {
292
471
  console.log(`[DEBUG] Upload finalization completed in ${Date.now() - finalizeStartTime}ms`);
293
472
  console.log(`[DEBUG] Total upload time: ${Date.now() - startTime}ms`);
294
473
  }
295
474
  return id;
296
475
  }
476
+ /**
477
+ * Upload file to Backblaze using signed URL (simple upload for files < 100MB)
478
+ * @param uploadUrl - Backblaze upload URL
479
+ * @param authorizationToken - Authorization token for the upload
480
+ * @param fileName - Name/path of the file
481
+ * @param file - File to upload
482
+ * @param debug - Whether debug logging is enabled
483
+ * @returns Promise that resolves when upload completes or fails gracefully
484
+ */
485
+ async function uploadToBackblaze(uploadUrl, authorizationToken, fileName, file, debug) {
486
+ try {
487
+ const arrayBuffer = await file.arrayBuffer();
488
+ // Calculate SHA1 hash for Backblaze (B2 requires SHA1, not SHA256)
489
+ const sha1 = (0, node_crypto_1.createHash)('sha1');
490
+ sha1.update(Buffer.from(arrayBuffer));
491
+ const sha1Hex = sha1.digest('hex');
492
+ // Detect if this is an S3 pre-signed URL (authorization token is empty)
493
+ const isS3PreSignedUrl = !authorizationToken || authorizationToken === '';
494
+ if (debug) {
495
+ console.log(`[DEBUG] Uploading to Backblaze URL: ${uploadUrl}`);
496
+ console.log(`[DEBUG] Upload method: ${isS3PreSignedUrl ? 'S3 pre-signed URL (PUT)' : 'B2 native API (POST)'}`);
497
+ console.log(`[DEBUG] File name: ${fileName}`);
498
+ console.log(`[DEBUG] File SHA1: ${sha1Hex}`);
499
+ }
500
+ // Build headers based on upload method
501
+ const headers = {
502
+ 'Content-Length': file.size.toString(),
503
+ 'Content-Type': file.type || 'application/octet-stream',
504
+ 'X-Bz-Content-Sha1': sha1Hex,
505
+ };
506
+ // S3 pre-signed URLs have auth embedded in URL, native B2 uses Authorization header
507
+ if (!isS3PreSignedUrl) {
508
+ headers.Authorization = authorizationToken;
509
+ headers['X-Bz-File-Name'] = encodeURIComponent(fileName);
510
+ }
511
+ const response = await fetch(uploadUrl, {
512
+ body: arrayBuffer,
513
+ headers,
514
+ method: isS3PreSignedUrl ? 'PUT' : 'POST',
515
+ });
516
+ if (!response.ok) {
517
+ const errorText = await response.text();
518
+ if (debug) {
519
+ console.error(`[DEBUG] Backblaze upload failed with status ${response.status}: ${errorText}`);
520
+ }
521
+ // Don't throw - we don't want Backblaze failures to block the primary upload
522
+ console.warn(`Warning: Backblaze upload failed with status ${response.status}`);
523
+ return false;
524
+ }
525
+ if (debug) {
526
+ console.log('[DEBUG] Backblaze upload successful');
527
+ }
528
+ return true;
529
+ }
530
+ catch (error) {
531
+ if (debug) {
532
+ console.error('[DEBUG] Backblaze upload exception:', error);
533
+ }
534
+ // Don't throw - we don't want Backblaze failures to block the primary upload
535
+ console.warn(`Warning: Backblaze upload failed: ${error instanceof Error ? error.message : String(error)}`);
536
+ return false;
537
+ }
538
+ }
539
+ /**
540
+ * Helper function to read a chunk from a file stream
541
+ * @param filePath - Path to the file
542
+ * @param start - Start byte position
543
+ * @param end - End byte position (exclusive)
544
+ * @returns Promise resolving to Buffer containing the chunk
545
+ */
546
+ async function readFileChunk(filePath, start, end) {
547
+ return new Promise((resolve, reject) => {
548
+ const chunks = [];
549
+ const stream = (0, node_fs_1.createReadStream)(filePath, { start, end: end - 1 }); // end is inclusive in createReadStream
550
+ stream.on('data', (chunk) => {
551
+ chunks.push(chunk);
552
+ });
553
+ stream.on('end', () => {
554
+ resolve(Buffer.concat(chunks));
555
+ });
556
+ stream.on('error', (error) => {
557
+ reject(error);
558
+ });
559
+ });
560
+ }
561
+ /**
562
+ * Helper function to read a chunk from a File/Blob object
563
+ * @param file - File or Blob object
564
+ * @param start - Start byte position
565
+ * @param end - End byte position (exclusive)
566
+ * @returns Promise resolving to Buffer containing the chunk
567
+ */
568
+ async function readFileObjectChunk(file, start, end) {
569
+ const slice = file.slice(start, end);
570
+ const arrayBuffer = await slice.arrayBuffer();
571
+ return Buffer.from(arrayBuffer);
572
+ }
573
+ /**
574
+ * Upload large file to Backblaze using multi-part upload with streaming (for files >= 5MB)
575
+ * Uses file streaming to avoid loading entire file into memory, preventing OOM errors on large files
576
+ * @param config - Configuration object for the large file upload
577
+ * @returns Promise that resolves when upload completes or fails gracefully
578
+ */
579
+ async function uploadLargeFileToBackblaze(config) {
580
+ const { apiUrl, apiKey, fileId, uploadPartUrls, filePath, fileSize, debug, fileObject } = config;
581
+ try {
582
+ const partSha1Array = [];
583
+ // Calculate part size (divide file evenly across all parts)
584
+ const partSize = Math.ceil(fileSize / uploadPartUrls.length);
585
+ if (debug) {
586
+ console.log(`[DEBUG] Uploading large file in ${uploadPartUrls.length} parts (streaming mode)`);
587
+ console.log(`[DEBUG] Part size: ${(partSize / 1024 / 1024).toFixed(2)} MB`);
588
+ console.log(`[DEBUG] Reading from: ${fileObject ? 'in-memory File object' : filePath}`);
589
+ }
590
+ // Upload each part using streaming to avoid loading entire file into memory
591
+ for (let i = 0; i < uploadPartUrls.length; i++) {
592
+ const partNumber = i + 1;
593
+ const start = i * partSize;
594
+ const end = Math.min(start + partSize, fileSize);
595
+ const partLength = end - start;
596
+ if (debug) {
597
+ console.log(`[DEBUG] Reading part ${partNumber}/${uploadPartUrls.length} bytes ${start}-${end}`);
598
+ }
599
+ // Read part from File object (for .app bundles) or from disk
600
+ const partBuffer = fileObject
601
+ ? await readFileObjectChunk(fileObject, start, end)
602
+ : await readFileChunk(filePath, start, end);
603
+ // Calculate SHA1 for this part
604
+ const sha1 = (0, node_crypto_1.createHash)('sha1');
605
+ sha1.update(partBuffer);
606
+ const sha1Hex = sha1.digest('hex');
607
+ partSha1Array.push(sha1Hex);
608
+ if (debug) {
609
+ console.log(`[DEBUG] Uploading part ${partNumber}/${uploadPartUrls.length} (${(partLength / 1024 / 1024).toFixed(2)} MB, SHA1: ${sha1Hex})`);
610
+ }
611
+ const response = await fetch(uploadPartUrls[i].uploadUrl, {
612
+ body: new Uint8Array(partBuffer),
613
+ headers: {
614
+ Authorization: uploadPartUrls[i].authorizationToken,
615
+ 'Content-Length': partLength.toString(),
616
+ 'X-Bz-Content-Sha1': sha1Hex,
617
+ 'X-Bz-Part-Number': partNumber.toString(),
618
+ },
619
+ method: 'POST',
620
+ });
621
+ if (!response.ok) {
622
+ const errorText = await response.text();
623
+ if (debug) {
624
+ console.error(`[DEBUG] Part ${partNumber} upload failed with status ${response.status}: ${errorText}`);
625
+ }
626
+ throw new Error(`Part ${partNumber} upload failed with status ${response.status}`);
627
+ }
628
+ if (debug) {
629
+ console.log(`[DEBUG] Part ${partNumber}/${uploadPartUrls.length} uploaded successfully`);
630
+ }
631
+ }
632
+ // Finish the large file upload
633
+ if (debug) {
634
+ console.log('[DEBUG] Finishing large file upload...');
635
+ }
636
+ await api_gateway_1.ApiGateway.finishLargeFile(apiUrl, apiKey, fileId, partSha1Array);
637
+ if (debug) {
638
+ console.log('[DEBUG] Large file upload completed successfully');
639
+ }
640
+ return true;
641
+ }
642
+ catch (error) {
643
+ if (debug) {
644
+ console.error('[DEBUG] Large file upload exception:', error);
645
+ }
646
+ // Don't throw - we don't want Backblaze failures to block the primary upload
647
+ console.warn(`Warning: Backblaze large file upload failed: ${error instanceof Error ? error.message : String(error)}`);
648
+ return false;
649
+ }
650
+ }
297
651
  async function getFileHashFromFile(file) {
298
652
  return new Promise((resolve, reject) => {
299
653
  const hash = (0, node_crypto_1.createHash)('sha256');
@@ -34,10 +34,10 @@ class DeviceValidationService {
34
34
  throw new Error(`${androidDeviceID} ${googlePlay ? '(Play Store) ' : ''}only supports these Android API levels: ${supportedAndroidVersions.join(', ')}`);
35
35
  }
36
36
  if (debug && logger) {
37
- logger(`DEBUG: Android device: ${androidDeviceID}`);
38
- logger(`DEBUG: Android API level: ${version}`);
39
- logger(`DEBUG: Google Play enabled: ${googlePlay}`);
40
- logger(`DEBUG: Supported Android versions: ${supportedAndroidVersions.join(', ')}`);
37
+ logger(`[DEBUG] Android device: ${androidDeviceID}`);
38
+ logger(`[DEBUG] Android API level: ${version}`);
39
+ logger(`[DEBUG] Google Play enabled: ${googlePlay}`);
40
+ logger(`[DEBUG] Supported Android versions: ${supportedAndroidVersions.join(', ')}`);
41
41
  }
42
42
  }
43
43
  /**
@@ -65,9 +65,9 @@ class DeviceValidationService {
65
65
  throw new Error(`${iOSDeviceID} only supports these iOS versions: ${supportediOSVersions.join(', ')}`);
66
66
  }
67
67
  if (debug && logger) {
68
- logger(`DEBUG: iOS device: ${iOSDeviceID}`);
69
- logger(`DEBUG: iOS version: ${version}`);
70
- logger(`DEBUG: Supported iOS versions: ${supportediOSVersions.join(', ')}`);
68
+ logger(`[DEBUG] iOS device: ${iOSDeviceID}`);
69
+ logger(`[DEBUG] iOS version: ${version}`);
70
+ logger(`[DEBUG] Supported iOS versions: ${supportediOSVersions.join(', ')}`);
71
71
  }
72
72
  }
73
73
  }
@@ -18,8 +18,8 @@ class MoropoService {
18
18
  */
19
19
  async downloadAndExtract(options) {
20
20
  const { apiKey, branchName = 'main', debug = false, quiet = false, json = false, logger } = options;
21
- this.logDebug(debug, logger, 'DEBUG: Moropo v1 API key detected, downloading tests from Moropo API');
22
- this.logDebug(debug, logger, `DEBUG: Using branch name: ${branchName}`);
21
+ this.logDebug(debug, logger, '[DEBUG] Moropo v1 API key detected, downloading tests from Moropo API');
22
+ this.logDebug(debug, logger, `[DEBUG] Using branch name: ${branchName}`);
23
23
  try {
24
24
  if (!quiet && !json) {
25
25
  core_1.ux.action.start('Downloading Moropo tests', 'Initializing', {
@@ -37,7 +37,7 @@ class MoropoService {
37
37
  throw new Error(`Failed to download Moropo tests: ${response.statusText}`);
38
38
  }
39
39
  const moropoDir = path.join(os.tmpdir(), `moropo-tests-${Date.now()}`);
40
- this.logDebug(debug, logger, `DEBUG: Extracting Moropo tests to: ${moropoDir}`);
40
+ this.logDebug(debug, logger, `[DEBUG] Extracting Moropo tests to: ${moropoDir}`);
41
41
  // Create moropo directory if it doesn't exist
42
42
  if (!fs.existsSync(moropoDir)) {
43
43
  fs.mkdirSync(moropoDir, { recursive: true });
@@ -51,17 +51,17 @@ class MoropoService {
51
51
  if (!quiet && !json) {
52
52
  core_1.ux.action.stop('completed');
53
53
  }
54
- this.logDebug(debug, logger, 'DEBUG: Successfully extracted Moropo tests');
54
+ this.logDebug(debug, logger, '[DEBUG] Successfully extracted Moropo tests');
55
55
  // Create config.yaml file
56
56
  this.createConfigFile(moropoDir);
57
- this.logDebug(debug, logger, 'DEBUG: Created config.yaml file');
57
+ this.logDebug(debug, logger, '[DEBUG] Created config.yaml file');
58
58
  return moropoDir;
59
59
  }
60
60
  catch (error) {
61
61
  if (!quiet && !json) {
62
62
  core_1.ux.action.stop('failed');
63
63
  }
64
- this.logDebug(debug, logger, `DEBUG: Error downloading/extracting Moropo tests: ${error}`);
64
+ this.logDebug(debug, logger, `[DEBUG] Error downloading/extracting Moropo tests: ${error}`);
65
65
  throw new Error(`Failed to download/extract Moropo tests: ${error}`);
66
66
  }
67
67
  }
@@ -16,7 +16,7 @@ class ReportDownloadService {
16
16
  const { apiUrl, apiKey, uploadId, downloadType, artifactsPath = './artifacts.zip', debug = false, logger, warnLogger, } = options;
17
17
  try {
18
18
  if (debug && logger) {
19
- logger(`DEBUG: Downloading artifacts: ${downloadType}`);
19
+ logger(`[DEBUG] Downloading artifacts: ${downloadType}`);
20
20
  }
21
21
  await api_gateway_1.ApiGateway.downloadArtifactsZip(apiUrl, apiKey, uploadId, downloadType, artifactsPath);
22
22
  if (logger) {
@@ -26,7 +26,7 @@ class ReportDownloadService {
26
26
  }
27
27
  catch (error) {
28
28
  if (debug && logger) {
29
- logger(`DEBUG: Error downloading artifacts: ${error}`);
29
+ logger(`[DEBUG] Error downloading artifacts: ${error}`);
30
30
  }
31
31
  if (warnLogger) {
32
32
  warnLogger('Failed to download artifacts');
@@ -83,7 +83,7 @@ class ReportDownloadService {
83
83
  const { apiUrl, apiKey, uploadId, debug = false, logger, warnLogger } = options;
84
84
  try {
85
85
  if (debug && logger) {
86
- logger(`DEBUG: Downloading ${type.toUpperCase()} report`);
86
+ logger(`[DEBUG] Downloading ${type.toUpperCase()} report`);
87
87
  }
88
88
  await api_gateway_1.ApiGateway.downloadReportGeneric(apiUrl, apiKey, uploadId, type, filePath);
89
89
  if (logger) {
@@ -92,7 +92,7 @@ class ReportDownloadService {
92
92
  }
93
93
  catch (error) {
94
94
  if (debug && logger) {
95
- logger(`DEBUG: Error downloading ${type.toUpperCase()} report: ${error}`);
95
+ logger(`[DEBUG] Error downloading ${type.toUpperCase()} report: ${error}`);
96
96
  }
97
97
  const errorMessage = error instanceof Error ? error.message : String(error);
98
98
  if (warnLogger) {
@@ -37,7 +37,7 @@ class ResultsPollingService {
37
37
  let sequentialPollFailures = 0;
38
38
  let previousSummary = '';
39
39
  if (debug && logger) {
40
- logger(`DEBUG: Starting polling loop for results`);
40
+ logger(`[DEBUG] Starting polling loop for results`);
41
41
  }
42
42
  // Poll in a loop until all tests complete
43
43
  // eslint-disable-next-line no-constant-condition
@@ -186,16 +186,16 @@ class ResultsPollingService {
186
186
  */
187
187
  async fetchAndLogResults(apiUrl, apiKey, uploadId, debug, logger) {
188
188
  if (debug && logger) {
189
- logger(`DEBUG: Polling for results: ${uploadId}`);
189
+ logger(`[DEBUG] Polling for results: ${uploadId}`);
190
190
  }
191
191
  const { results: updatedResults } = await api_gateway_1.ApiGateway.getResultsForUpload(apiUrl, apiKey, uploadId);
192
192
  if (!updatedResults) {
193
193
  throw new Error('no results');
194
194
  }
195
195
  if (debug && logger) {
196
- logger(`DEBUG: Poll received ${updatedResults.length} results`);
196
+ logger(`[DEBUG] Poll received ${updatedResults.length} results`);
197
197
  for (const result of updatedResults) {
198
- logger(`DEBUG: Result status: ${result.test_file_name} - ${result.status}`);
198
+ logger(`[DEBUG] Result status: ${result.test_file_name} - ${result.status}`);
199
199
  }
200
200
  }
201
201
  return updatedResults;
@@ -216,39 +216,39 @@ class ResultsPollingService {
216
216
  async handleCompletedTests(updatedResults, options) {
217
217
  const { uploadId, consoleUrl, json, debug, logger } = options;
218
218
  if (debug && logger) {
219
- logger(`DEBUG: All tests completed, stopping poll`);
219
+ logger(`[DEBUG] All tests completed, stopping poll`);
220
220
  }
221
221
  this.displayFinalResults(updatedResults, consoleUrl, json, logger);
222
222
  const output = this.buildPollingResult(updatedResults, uploadId, consoleUrl);
223
223
  if (output.status === 'FAILED') {
224
224
  if (debug && logger) {
225
- logger(`DEBUG: Some tests failed, returning failed status`);
225
+ logger(`[DEBUG] Some tests failed, returning failed status`);
226
226
  }
227
227
  throw new RunFailedError(output);
228
228
  }
229
229
  if (debug && logger) {
230
- logger(`DEBUG: All tests passed, returning success status`);
230
+ logger(`[DEBUG] All tests passed, returning success status`);
231
231
  }
232
232
  return output;
233
233
  }
234
234
  async handlePollingError(error, sequentialPollFailures, debug, logger) {
235
235
  if (debug && logger) {
236
- logger(`DEBUG: Error polling for results: ${error}`);
237
- logger(`DEBUG: Sequential poll failures: ${sequentialPollFailures}`);
236
+ logger(`[DEBUG] Error polling for results: ${error}`);
237
+ logger(`[DEBUG] Sequential poll failures: ${sequentialPollFailures}`);
238
238
  }
239
239
  if (sequentialPollFailures > this.MAX_SEQUENTIAL_FAILURES) {
240
240
  if (debug && logger) {
241
- logger('DEBUG: Checking internet connectivity...');
241
+ logger('[DEBUG] Checking internet connectivity...');
242
242
  }
243
243
  const connectivityCheck = await (0, connectivity_1.checkInternetConnectivity)();
244
244
  if (debug && logger) {
245
- logger(`DEBUG: ${connectivityCheck.message}`);
245
+ logger(`[DEBUG] ${connectivityCheck.message}`);
246
246
  for (const result of connectivityCheck.endpointResults) {
247
247
  if (result.success) {
248
- logger(`DEBUG: ✓ ${result.endpoint} - ${result.statusCode} (${result.latencyMs}ms)`);
248
+ logger(`[DEBUG] ✓ ${result.endpoint} - ${result.statusCode} (${result.latencyMs}ms)`);
249
249
  }
250
250
  else {
251
- logger(`DEBUG: ✗ ${result.endpoint} - ${result.error} (${result.latencyMs}ms)`);
251
+ logger(`[DEBUG] ✗ ${result.endpoint} - ${result.error} (${result.latencyMs}ms)`);
252
252
  }
253
253
  }
254
254
  }
@@ -26,7 +26,6 @@ export interface TestSubmissionConfig {
26
26
  retry?: number;
27
27
  runnerType?: string;
28
28
  showCrosshairs?: boolean;
29
- skipChromeOnboarding?: boolean;
30
29
  }
31
30
  /**
32
31
  * Service for building test submission form data