@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.
@@ -55,7 +55,6 @@ export default class Cloud extends Command {
55
55
  'ios-version': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
56
56
  orientation: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
57
57
  'show-crosshairs': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
58
- 'skip-chrome-onboarding': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
59
58
  'app-binary-id': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
60
59
  'app-file': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
61
60
  'ignore-sha-check': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
@@ -100,10 +100,10 @@ class Cloud extends core_1.Command {
100
100
  debugFlag = debug === true;
101
101
  jsonFile = flags['json-file'] === true;
102
102
  if (debug) {
103
- this.log('DEBUG: Starting command execution with debug logging enabled');
104
- this.log(`DEBUG: CLI Version: ${this.config.version}`);
105
- this.log(`DEBUG: Node version: ${process.versions.node}`);
106
- this.log(`DEBUG: OS: ${process.platform} ${process.arch}`);
103
+ this.log('[DEBUG] Starting command execution with debug logging enabled');
104
+ this.log(`[DEBUG] CLI Version: ${this.config.version}`);
105
+ this.log(`[DEBUG] Node version: ${process.versions.node}`);
106
+ this.log(`[DEBUG] OS: ${process.platform} ${process.arch}`);
107
107
  }
108
108
  if (flags['json-file']) {
109
109
  quiet = true;
@@ -146,18 +146,18 @@ class Cloud extends core_1.Command {
146
146
  try {
147
147
  compatibilityData = await (0, compatibility_1.fetchCompatibilityData)(apiUrl, apiKey);
148
148
  if (debug) {
149
- this.log('DEBUG: Successfully fetched compatibility data from API');
149
+ this.log('[DEBUG] Successfully fetched compatibility data from API');
150
150
  }
151
151
  }
152
152
  catch (error) {
153
153
  const errorMessage = error instanceof Error ? error.message : String(error);
154
154
  if (debug) {
155
- this.log(`DEBUG: Failed to fetch compatibility data from API: ${errorMessage}`);
155
+ this.log(`[DEBUG] Failed to fetch compatibility data from API: ${errorMessage}`);
156
156
  }
157
157
  throw new Error(`Failed to fetch device compatibility data: ${errorMessage}. Please check your API key and connection.`);
158
158
  }
159
159
  if (debug) {
160
- this.log(`DEBUG: API URL: ${apiUrl}`);
160
+ this.log(`[DEBUG] API URL: ${apiUrl}`);
161
161
  }
162
162
  // Resolve and validate Maestro version using API data
163
163
  const resolvedMaestroVersion = this.versionService.resolveMaestroVersion(maestroVersion, compatibilityData, {
@@ -176,18 +176,18 @@ class Cloud extends core_1.Command {
176
176
  this.log(styling_1.colors.info('ℹ') + ' ' + styling_1.colors.dim('Note: runnerType m1 is experimental and currently supports Android (Pixel 7, API Level 34) only.'));
177
177
  }
178
178
  if (runnerType === 'gpu1') {
179
- this.log(styling_1.colors.info('ℹ') + ' ' + styling_1.colors.dim('Note: runnerType gpu1 is Android-only and requires contacting support to enable. Without support enablement, your runner type will revert to default.'));
179
+ this.log(styling_1.colors.info('ℹ') + ' ' + styling_1.colors.dim('Note: runnerType gpu1 is Android-only (Pixel 7, API Level 34 or 35), available to all users.'));
180
180
  }
181
181
  const { firstFile, secondFile } = args;
182
182
  let finalBinaryId = appBinaryId;
183
183
  const finalAppFile = appFile ?? firstFile;
184
184
  let flowFile = flows ?? secondFile;
185
185
  if (debug) {
186
- this.log(`DEBUG: First file argument: ${firstFile || 'not provided'}`);
187
- this.log(`DEBUG: Second file argument: ${secondFile || 'not provided'}`);
188
- this.log(`DEBUG: App binary ID: ${appBinaryId || 'not provided'}`);
189
- this.log(`DEBUG: App file: ${finalAppFile || 'not provided'}`);
190
- this.log(`DEBUG: Flow file: ${flowFile || 'not provided'}`);
186
+ this.log(`[DEBUG] First file argument: ${firstFile || 'not provided'}`);
187
+ this.log(`[DEBUG] Second file argument: ${secondFile || 'not provided'}`);
188
+ this.log(`[DEBUG] App binary ID: ${appBinaryId || 'not provided'}`);
189
+ this.log(`[DEBUG] App file: ${finalAppFile || 'not provided'}`);
190
+ this.log(`[DEBUG] Flow file: ${flowFile || 'not provided'}`);
191
191
  }
192
192
  if (appBinaryId) {
193
193
  if (secondFile) {
@@ -209,12 +209,12 @@ class Cloud extends core_1.Command {
209
209
  flowFile += '/';
210
210
  }
211
211
  if (debug) {
212
- this.log(`DEBUG: Resolved flow file path: ${flowFile}`);
212
+ this.log(`[DEBUG] Resolved flow file path: ${flowFile}`);
213
213
  }
214
214
  let executionPlan;
215
215
  try {
216
216
  if (debug) {
217
- this.log('DEBUG: Generating execution plan...');
217
+ this.log('[DEBUG] Generating execution plan...');
218
218
  }
219
219
  executionPlan = await (0, execution_plan_service_1.plan)({
220
220
  input: flowFile,
@@ -225,24 +225,24 @@ class Cloud extends core_1.Command {
225
225
  debug,
226
226
  });
227
227
  if (debug) {
228
- this.log(`DEBUG: Execution plan generated`);
229
- this.log(`DEBUG: Total flow files: ${executionPlan.totalFlowFiles}`);
230
- this.log(`DEBUG: Flows to run: ${executionPlan.flowsToRun.length}`);
231
- this.log(`DEBUG: Referenced files: ${executionPlan.referencedFiles.length}`);
232
- this.log(`DEBUG: Sequential flows: ${executionPlan.sequence?.flows.length || 0}`);
228
+ this.log(`[DEBUG] Execution plan generated`);
229
+ this.log(`[DEBUG] Total flow files: ${executionPlan.totalFlowFiles}`);
230
+ this.log(`[DEBUG] Flows to run: ${executionPlan.flowsToRun.length}`);
231
+ this.log(`[DEBUG] Referenced files: ${executionPlan.referencedFiles.length}`);
232
+ this.log(`[DEBUG] Sequential flows: ${executionPlan.sequence?.flows.length || 0}`);
233
233
  }
234
234
  }
235
235
  catch (error) {
236
236
  if (debug) {
237
- this.log(`DEBUG: Error generating execution plan: ${error}`);
237
+ this.log(`[DEBUG] Error generating execution plan: ${error}`);
238
238
  }
239
239
  throw error;
240
240
  }
241
241
  const { allExcludeTags, allIncludeTags, flowOverrides, flowsToRun: testFileNames, referencedFiles, sequence, } = executionPlan;
242
242
  if (debug) {
243
- this.log(`DEBUG: All include tags: ${allIncludeTags?.join(', ') || 'none'}`);
244
- this.log(`DEBUG: All exclude tags: ${allExcludeTags?.join(', ') || 'none'}`);
245
- this.log(`DEBUG: Test file names: ${testFileNames.join(', ')}`);
243
+ this.log(`[DEBUG] All include tags: ${allIncludeTags?.join(', ') || 'none'}`);
244
+ this.log(`[DEBUG] All exclude tags: ${allExcludeTags?.join(', ') || 'none'}`);
245
+ this.log(`[DEBUG] Test file names: ${testFileNames.join(', ')}`);
246
246
  }
247
247
  const pathsShortestToLongest = [
248
248
  ...testFileNames,
@@ -257,12 +257,12 @@ class Cloud extends core_1.Command {
257
257
  commonRoot = folderPath;
258
258
  }
259
259
  if (debug) {
260
- this.log(`DEBUG: Common root directory: ${commonRoot}`);
260
+ this.log(`[DEBUG] Common root directory: ${commonRoot}`);
261
261
  }
262
262
  const { continueOnFailure = true, flows: sequentialFlows = [] } = sequence ?? {};
263
263
  if (debug && sequentialFlows.length > 0) {
264
- this.log(`DEBUG: Sequential flows: ${sequentialFlows.join(', ')}`);
265
- this.log(`DEBUG: Continue on failure: ${continueOnFailure}`);
264
+ this.log(`[DEBUG] Sequential flows: ${sequentialFlows.join(', ')}`);
265
+ this.log(`[DEBUG] Continue on failure: ${continueOnFailure}`);
266
266
  }
267
267
  if (!appBinaryId) {
268
268
  if (!(flowFile && finalAppFile)) {
@@ -273,7 +273,7 @@ class Cloud extends core_1.Command {
273
273
  }
274
274
  if (finalAppFile.endsWith('.zip')) {
275
275
  if (debug) {
276
- this.log(`DEBUG: Verifying iOS app zip file: ${finalAppFile}`);
276
+ this.log(`[DEBUG] Verifying iOS app zip file: ${finalAppFile}`);
277
277
  }
278
278
  await (0, methods_1.verifyAppZip)(finalAppFile);
279
279
  }
@@ -337,12 +337,19 @@ class Cloud extends core_1.Command {
337
337
  if (!finalAppFile)
338
338
  throw new Error('You must provide either an app binary id or an app file');
339
339
  if (debug) {
340
- this.log(`DEBUG: Uploading binary file: ${finalAppFile}`);
340
+ this.log(`[DEBUG] Uploading binary file: ${finalAppFile}`);
341
341
  }
342
- const binaryId = await (0, methods_1.uploadBinary)(finalAppFile, apiUrl, apiKey, ignoreShaCheck, !json, debug);
342
+ const binaryId = await (0, methods_1.uploadBinary)({
343
+ apiKey,
344
+ apiUrl,
345
+ debug,
346
+ filePath: finalAppFile,
347
+ ignoreShaCheck,
348
+ log: !json,
349
+ });
343
350
  finalBinaryId = binaryId;
344
351
  if (debug) {
345
- this.log(`DEBUG: Binary uploaded with ID: ${binaryId}`);
352
+ this.log(`[DEBUG] Binary uploaded with ID: ${binaryId}`);
346
353
  }
347
354
  }
348
355
  // finalBinaryId should always be defined after validation - fail fast if not
@@ -376,16 +383,15 @@ class Cloud extends core_1.Command {
376
383
  retry,
377
384
  runnerType,
378
385
  showCrosshairs: flags['show-crosshairs'],
379
- skipChromeOnboarding: flags['skip-chrome-onboarding'],
380
386
  });
381
387
  if (debug) {
382
- this.log(`DEBUG: Submitting flow upload request to ${apiUrl}/uploads/flow`);
388
+ this.log(`[DEBUG] Submitting flow upload request to ${apiUrl}/uploads/flow`);
383
389
  }
384
390
  const { message, results } = await api_gateway_1.ApiGateway.uploadFlow(apiUrl, apiKey, testFormData);
385
391
  if (debug) {
386
- this.log(`DEBUG: Flow upload response received`);
387
- this.log(`DEBUG: Message: ${message}`);
388
- this.log(`DEBUG: Results count: ${results?.length || 0}`);
392
+ this.log(`[DEBUG] Flow upload response received`);
393
+ this.log(`[DEBUG] Message: ${message}`);
394
+ this.log(`[DEBUG] Results count: ${results?.length || 0}`);
389
395
  }
390
396
  if (!results?.length)
391
397
  (0, errors_1.error)('No tests created: ' + message);
@@ -403,7 +409,7 @@ class Cloud extends core_1.Command {
403
409
  this.log(styling_1.colors.dim('Poll upload status using: ') + styling_1.colors.info(`dcd status --api-key ... --upload-id ${results[0].test_upload_id}`));
404
410
  if (async) {
405
411
  if (debug) {
406
- this.log(`DEBUG: Async flag is set, not waiting for results`);
412
+ this.log(`[DEBUG] Async flag is set, not waiting for results`);
407
413
  }
408
414
  const jsonOutput = {
409
415
  consoleUrl: url,
@@ -517,8 +523,8 @@ class Cloud extends core_1.Command {
517
523
  }
518
524
  catch (error) {
519
525
  if (debugFlag && error instanceof Error) {
520
- this.log(`DEBUG: Error in command execution: ${error.message}`);
521
- this.log(`DEBUG: Error stack: ${error.stack}`);
526
+ this.log(`[DEBUG] Error in command execution: ${error.message}`);
527
+ this.log(`[DEBUG] Error stack: ${error.stack}`);
522
528
  }
523
529
  if (error instanceof Error && error.message === 'RUN_FAILED') {
524
530
  if (jsonFile) {
@@ -42,7 +42,14 @@ class Upload extends core_1.Command {
42
42
  this.log((0, styling_1.sectionHeader)('Uploading app binary'));
43
43
  this.log(` ${styling_1.colors.dim('→ File:')} ${styling_1.colors.highlight(appFile)}`);
44
44
  this.log('');
45
- const appBinaryId = await (0, methods_1.uploadBinary)(appFile, apiUrl, apiKey, ignoreShaCheck, !json, debug);
45
+ const appBinaryId = await (0, methods_1.uploadBinary)({
46
+ apiKey,
47
+ apiUrl,
48
+ debug,
49
+ filePath: appFile,
50
+ ignoreShaCheck,
51
+ log: !json,
52
+ });
46
53
  if (json) {
47
54
  return { appBinaryId };
48
55
  }
@@ -10,5 +10,4 @@ export declare const deviceFlags: {
10
10
  'ios-version': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
11
11
  orientation: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
12
12
  'show-crosshairs': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
13
- 'skip-chrome-onboarding': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
14
13
  };
@@ -39,8 +39,4 @@ exports.deviceFlags = {
39
39
  default: false,
40
40
  description: '[Android only] Display crosshairs for screen interactions during test execution',
41
41
  }),
42
- 'skip-chrome-onboarding': core_1.Flags.boolean({
43
- default: false,
44
- description: '[Android only] Skip Chrome browser onboarding screens when running tests',
45
- }),
46
42
  };
@@ -42,7 +42,6 @@ export declare const flags: {
42
42
  'ios-version': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
43
43
  orientation: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
44
44
  'show-crosshairs': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
45
- 'skip-chrome-onboarding': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
46
45
  'app-binary-id': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
47
46
  'app-file': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
48
47
  'ignore-sha-check': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
@@ -12,13 +12,24 @@ export declare const ApiGateway: {
12
12
  exists: boolean;
13
13
  }>;
14
14
  downloadArtifactsZip(baseUrl: string, apiKey: string, uploadId: string, results: "ALL" | "FAILED", artifactsPath?: string): Promise<void>;
15
- finaliseUpload(baseUrl: string, apiKey: string, id: string, metadata: TAppMetadata, path: string, sha: string): Promise<Record<string, never>>;
16
- getBinaryUploadUrl(baseUrl: string, apiKey: string, platform: "android" | "ios"): Promise<{
17
- message: string;
15
+ finaliseUpload(config: {
16
+ apiKey: string;
17
+ backblazeSuccess: boolean;
18
+ baseUrl: string;
19
+ id: string;
20
+ metadata: TAppMetadata;
21
+ path: string;
22
+ sha: string;
23
+ supabaseSuccess: boolean;
24
+ }): Promise<Record<string, never>>;
25
+ getBinaryUploadUrl(baseUrl: string, apiKey: string, platform: "android" | "ios", fileSize: number): Promise<{
18
26
  path: string;
19
- token: string;
27
+ tempPath: string;
28
+ finalPath: string;
20
29
  id: string;
30
+ b2?: import("../types/generated/schema.types").components["schemas"]["B2UploadStrategy"];
21
31
  }>;
32
+ finishLargeFile(baseUrl: string, apiKey: string, fileId: string, partSha1Array: string[]): Promise<any>;
22
33
  getResultsForUpload(baseUrl: string, apiKey: string, uploadId: string): Promise<{
23
34
  statusCode?: number;
24
35
  results?: import("../types/generated/schema.types").components["schemas"]["TResultResponse"][];
@@ -29,6 +29,15 @@ exports.ApiGateway = {
29
29
  throw new Error(`Authentication failed. Please check your API key.`);
30
30
  }
31
31
  case 403: {
32
+ // For 403, use the server's error message directly as it's now detailed
33
+ // If the message suggests an API key issue, provide additional guidance
34
+ if (userMessage.toLowerCase().includes('api key')) {
35
+ throw new Error(`${userMessage}\n\nTroubleshooting steps:\n` +
36
+ ` 1. Verify DEVICE_CLOUD_API_KEY environment variable is set\n` +
37
+ ` 2. Check you're using the correct API key for this environment\n` +
38
+ ` 3. Ensure the API key hasn't been deleted or revoked\n` +
39
+ ` 4. Confirm you're connecting to the correct API URL`);
40
+ }
32
41
  throw new Error(`Access denied. ${userMessage}`);
33
42
  }
34
43
  case 404: {
@@ -98,10 +107,17 @@ exports.ApiGateway = {
98
107
  const fileStream = createWriteStream(artifactsPath, { flags: 'w' });
99
108
  await finished(Readable.fromWeb(res.body).pipe(fileStream));
100
109
  },
101
- // eslint-disable-next-line max-params
102
- async finaliseUpload(baseUrl, apiKey, id, metadata, path, sha) {
110
+ async finaliseUpload(config) {
111
+ const { baseUrl, apiKey, id, metadata, path, sha, supabaseSuccess, backblazeSuccess } = config;
103
112
  const res = await fetch(`${baseUrl}/uploads/finaliseUpload`, {
104
- body: JSON.stringify({ id, metadata, path, sha }),
113
+ body: JSON.stringify({
114
+ backblazeSuccess,
115
+ id,
116
+ metadata,
117
+ path, // This is tempPath for TUS uploads
118
+ sha,
119
+ supabaseSuccess,
120
+ }),
105
121
  headers: {
106
122
  'content-type': 'application/json',
107
123
  'x-app-api-key': apiKey,
@@ -113,9 +129,9 @@ exports.ApiGateway = {
113
129
  }
114
130
  return res.json();
115
131
  },
116
- async getBinaryUploadUrl(baseUrl, apiKey, platform) {
132
+ async getBinaryUploadUrl(baseUrl, apiKey, platform, fileSize) {
117
133
  const res = await fetch(`${baseUrl}/uploads/getBinaryUploadUrl`, {
118
- body: JSON.stringify({ platform }),
134
+ body: JSON.stringify({ platform, fileSize }),
119
135
  headers: {
120
136
  'content-type': 'application/json',
121
137
  'x-app-api-key': apiKey,
@@ -127,6 +143,20 @@ exports.ApiGateway = {
127
143
  }
128
144
  return res.json();
129
145
  },
146
+ async finishLargeFile(baseUrl, apiKey, fileId, partSha1Array) {
147
+ const res = await fetch(`${baseUrl}/uploads/finishLargeFile`, {
148
+ body: JSON.stringify({ fileId, partSha1Array }),
149
+ headers: {
150
+ 'content-type': 'application/json',
151
+ 'x-app-api-key': apiKey,
152
+ },
153
+ method: 'POST',
154
+ });
155
+ if (!res.ok) {
156
+ await this.handleApiError(res, 'Failed to finish large file');
157
+ }
158
+ return res.json();
159
+ },
130
160
  async getResultsForUpload(baseUrl, apiKey, uploadId) {
131
161
  // TODO: merge with getUploadStatus
132
162
  const res = await fetch(`${baseUrl}/results/${uploadId}`, {
@@ -7,5 +7,33 @@ export declare class SupabaseGateway {
7
7
  SUPABASE_PUBLIC_KEY: string;
8
8
  SUPABASE_URL: string;
9
9
  };
10
+ /**
11
+ * Upload to Supabase using resumable uploads (TUS protocol)
12
+ * Uploads to staging location (uploads/{id}/) using anon key
13
+ * File is later moved to final location by API after finalization
14
+ * @param env - Environment (dev or prod)
15
+ * @param path - Staging storage path (uploads/{id}/file.ext)
16
+ * @param file - File to upload
17
+ * @param debug - Enable debug logging
18
+ * @param onProgress - Optional callback for upload progress (bytesUploaded, bytesTotal)
19
+ * @returns Promise that resolves when upload completes
20
+ */
21
+ static uploadResumable(env: 'dev' | 'prod', path: string, file: File, debug?: boolean, onProgress?: (bytesUploaded: number, bytesTotal: number) => void): Promise<void>;
10
22
  static uploadToSignedUrl(env: 'dev' | 'prod', path: string, token: string, file: File, debug?: boolean): Promise<void>;
23
+ /**
24
+ * Logs network error details for debugging
25
+ * @param error - Error object to analyze for network-related issues
26
+ * @returns void
27
+ */
28
+ private static logNetworkError;
29
+ /**
30
+ * Logs upload exception details for debugging
31
+ * @param error - Exception that occurred during upload
32
+ * @param env - Environment (dev or prod)
33
+ * @param supabaseUrl - Supabase URL being used
34
+ * @param path - Upload path
35
+ * @param file - File being uploaded
36
+ * @returns void
37
+ */
38
+ private static logUploadException;
11
39
  }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SupabaseGateway = void 0;
4
4
  const supabase_js_1 = require("@supabase/supabase-js");
5
+ const tus = require("tus-js-client");
5
6
  class SupabaseGateway {
6
7
  static SB = {
7
8
  dev: {
@@ -16,6 +17,108 @@ class SupabaseGateway {
16
17
  static getSupabaseKeys(env) {
17
18
  return this.SB[env];
18
19
  }
20
+ /**
21
+ * Upload to Supabase using resumable uploads (TUS protocol)
22
+ * Uploads to staging location (uploads/{id}/) using anon key
23
+ * File is later moved to final location by API after finalization
24
+ * @param env - Environment (dev or prod)
25
+ * @param path - Staging storage path (uploads/{id}/file.ext)
26
+ * @param file - File to upload
27
+ * @param debug - Enable debug logging
28
+ * @param onProgress - Optional callback for upload progress (bytesUploaded, bytesTotal)
29
+ * @returns Promise that resolves when upload completes
30
+ */
31
+ static async uploadResumable(env, path, file, debug = false, onProgress) {
32
+ const { SUPABASE_PUBLIC_KEY, SUPABASE_URL } = this.getSupabaseKeys(env);
33
+ // Extract project ID from Supabase URL for the storage endpoint
34
+ // Format: https://project-id.supabase.co or https://cloud.devicecloud.dev (custom domain)
35
+ let projectId;
36
+ if (SUPABASE_URL.includes('.supabase.co')) {
37
+ projectId = SUPABASE_URL.replace('https://', '').split('.')[0];
38
+ }
39
+ else {
40
+ // For custom domains like cloud.devicecloud.dev, we need the project ref
41
+ // This is the dev environment project ref
42
+ projectId = env === 'dev' ? 'lbmsowehtjwnqlurpemb' : 'pgydnphbimetinsgfkbo';
43
+ }
44
+ const storageUrl = `https://${projectId}.storage.supabase.co`;
45
+ if (debug) {
46
+ console.log(`[DEBUG] Resumable upload starting...`);
47
+ console.log(`[DEBUG] Storage URL: ${storageUrl}`);
48
+ console.log(`[DEBUG] Upload path: ${path}`);
49
+ console.log(`[DEBUG] File name: ${file.name}`);
50
+ console.log(`[DEBUG] File size: ${(file.size / 1024 / 1024).toFixed(2)} MB`);
51
+ }
52
+ // Convert File to Buffer for Node.js tus-js-client
53
+ // In Node.js environment, tus-js-client expects Buffer or Readable stream, not File
54
+ const fileBuffer = Buffer.from(await file.arrayBuffer());
55
+ if (debug) {
56
+ console.log(`[DEBUG] Converted File to Buffer (${fileBuffer.length} bytes)`);
57
+ }
58
+ return new Promise((resolve, reject) => {
59
+ const upload = new tus.Upload(fileBuffer, {
60
+ // TUS endpoint for Supabase Storage
61
+ endpoint: `${storageUrl}/storage/v1/upload/resumable`,
62
+ // Retry configuration - will retry 5 times with increasing delays
63
+ retryDelays: [0, 3000, 5000, 10_000, 20_000],
64
+ // Authentication and headers
65
+ // Use anon key for staging uploads to uploads/* folder
66
+ headers: {
67
+ authorization: `Bearer ${SUPABASE_PUBLIC_KEY}`,
68
+ 'x-upsert': 'true', // Allow overwriting existing files
69
+ },
70
+ // Upload metadata
71
+ metadata: {
72
+ bucketName: 'organizations',
73
+ objectName: path,
74
+ contentType: file.type || 'application/octet-stream',
75
+ cacheControl: '3600',
76
+ },
77
+ // Chunk size must be 6MB for Supabase
78
+ chunkSize: 6 * 1024 * 1024,
79
+ // Remove fingerprint on success to allow re-uploading identical files
80
+ removeFingerprintOnSuccess: true,
81
+ // Upload data during creation for better performance
82
+ uploadDataDuringCreation: true,
83
+ // Progress callback
84
+ onProgress(bytesUploaded, bytesTotal) {
85
+ if (debug) {
86
+ const percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(2);
87
+ console.log(`[DEBUG] Upload progress: ${(bytesUploaded / 1024 / 1024).toFixed(2)} MB / ${(bytesTotal / 1024 / 1024).toFixed(2)} MB (${percentage}%)`);
88
+ }
89
+ // Call user-provided progress callback if available
90
+ if (onProgress) {
91
+ onProgress(bytesUploaded, bytesTotal);
92
+ }
93
+ },
94
+ // Error handler
95
+ onError(error) {
96
+ if (debug) {
97
+ console.error(`[DEBUG] === RESUMABLE UPLOAD ERROR ===`);
98
+ console.error(`[DEBUG] Error:`, error);
99
+ console.error(`[DEBUG] Error message: ${error.message}`);
100
+ if (error.stack) {
101
+ console.error(`[DEBUG] Stack trace:\n${error.stack}`);
102
+ }
103
+ }
104
+ reject(new Error(`Resumable upload failed: ${error.message}`));
105
+ },
106
+ // Success handler
107
+ onSuccess() {
108
+ if (debug) {
109
+ console.log(`[DEBUG] Resumable upload completed successfully`);
110
+ console.log(`[DEBUG] Upload path: ${path}`);
111
+ }
112
+ resolve();
113
+ },
114
+ });
115
+ // Start the upload
116
+ if (debug) {
117
+ console.log(`[DEBUG] Starting TUS upload...`);
118
+ }
119
+ upload.start();
120
+ });
121
+ }
19
122
  static async uploadToSignedUrl(env, path, token, file, debug = false) {
20
123
  const { SUPABASE_PUBLIC_KEY, SUPABASE_URL } = this.getSupabaseKeys(env);
21
124
  if (debug) {
@@ -48,26 +151,63 @@ class SupabaseGateway {
48
151
  }
49
152
  catch (error) {
50
153
  if (debug) {
51
- console.error(`[DEBUG] Supabase upload exception:`, error);
52
- console.error(`[DEBUG] Error type: ${error instanceof Error ? error.name : typeof error}`);
53
- console.error(`[DEBUG] Error message: ${error instanceof Error ? error.message : String(error)}`);
54
- // Check for common network errors
55
- if (error instanceof Error) {
56
- if (error.message.includes('ECONNREFUSED')) {
57
- console.error(`[DEBUG] Network error: Connection refused - check internet connectivity`);
58
- }
59
- else if (error.message.includes('ETIMEDOUT')) {
60
- console.error(`[DEBUG] Network error: Connection timeout - check internet connectivity or try again`);
61
- }
62
- else if (error.message.includes('ENOTFOUND')) {
63
- console.error(`[DEBUG] Network error: DNS lookup failed - check internet connectivity`);
64
- }
65
- else if (error.message.includes('ECONNRESET')) {
66
- console.error(`[DEBUG] Network error: Connection reset - network unstable or interrupted`);
67
- }
68
- }
154
+ this.logUploadException(error, env, SUPABASE_URL, path, file);
69
155
  }
70
- throw error;
156
+ // Re-throw with additional context
157
+ const errorMsg = error instanceof Error ? error.message : String(error);
158
+ throw new Error(`Supabase upload error: ${errorMsg}`);
159
+ }
160
+ }
161
+ /**
162
+ * Logs network error details for debugging
163
+ * @param error - Error object to analyze for network-related issues
164
+ * @returns void
165
+ */
166
+ static logNetworkError(error) {
167
+ const errorMsg = error.message.toLowerCase();
168
+ if (errorMsg.includes('econnrefused') || errorMsg.includes('connection refused')) {
169
+ console.error(`[DEBUG] Network error: Connection refused - check internet connectivity`);
170
+ }
171
+ else if (errorMsg.includes('etimedout') || errorMsg.includes('timeout')) {
172
+ console.error(`[DEBUG] Network error: Connection timeout - check internet connectivity or try again`);
173
+ }
174
+ else if (errorMsg.includes('enotfound') || errorMsg.includes('not found')) {
175
+ console.error(`[DEBUG] Network error: DNS lookup failed - check internet connectivity`);
176
+ }
177
+ else if (errorMsg.includes('econnreset') || errorMsg.includes('connection reset') || errorMsg.includes('connection lost')) {
178
+ console.error(`[DEBUG] Network error: Connection reset/lost - network unstable or interrupted`);
179
+ }
180
+ else if (errorMsg.includes('network')) {
181
+ console.error(`[DEBUG] Network-related error detected - check internet connectivity`);
182
+ }
183
+ }
184
+ /**
185
+ * Logs upload exception details for debugging
186
+ * @param error - Exception that occurred during upload
187
+ * @param env - Environment (dev or prod)
188
+ * @param supabaseUrl - Supabase URL being used
189
+ * @param path - Upload path
190
+ * @param file - File being uploaded
191
+ * @returns void
192
+ */
193
+ static logUploadException(error, env, supabaseUrl, path, file) {
194
+ console.error(`[DEBUG] === SUPABASE UPLOAD EXCEPTION ===`);
195
+ console.error(`[DEBUG] Exception caught:`, error);
196
+ console.error(`[DEBUG] Error type: ${error instanceof Error ? error.name : typeof error}`);
197
+ console.error(`[DEBUG] Error message: ${error instanceof Error ? error.message : String(error)}`);
198
+ if (error instanceof Error && error.stack) {
199
+ console.error(`[DEBUG] Error stack:\n${error.stack}`);
200
+ }
201
+ // Log additional context
202
+ console.error(`[DEBUG] Upload context:`);
203
+ console.error(`[DEBUG] - Environment: ${env}`);
204
+ console.error(`[DEBUG] - Supabase URL: ${supabaseUrl}`);
205
+ console.error(`[DEBUG] - Upload path: ${path}`);
206
+ console.error(`[DEBUG] - File name: ${file.name}`);
207
+ console.error(`[DEBUG] - File size: ${file.size} bytes`);
208
+ // Check for common network errors
209
+ if (error instanceof Error) {
210
+ this.logNetworkError(error);
71
211
  }
72
212
  }
73
213
  }
package/dist/methods.d.ts CHANGED
@@ -3,7 +3,15 @@ export declare const toBuffer: (archive: archiver.Archiver) => Promise<Buffer<Ar
3
3
  export declare const compressFolderToBlob: (sourceDir: string) => Promise<Blob>;
4
4
  export declare const compressFilesFromRelativePath: (basePath: string, files: string[], commonRoot: string) => Promise<Buffer<ArrayBuffer>>;
5
5
  export declare const verifyAppZip: (zipPath: string) => Promise<void>;
6
- export declare const uploadBinary: (filePath: string, apiUrl: string, apiKey: string, ignoreShaCheck?: boolean, log?: boolean, debug?: boolean) => Promise<string>;
6
+ interface UploadBinaryConfig {
7
+ apiKey: string;
8
+ apiUrl: string;
9
+ debug?: boolean;
10
+ filePath: string;
11
+ ignoreShaCheck?: boolean;
12
+ log?: boolean;
13
+ }
14
+ export declare const uploadBinary: (config: UploadBinaryConfig) => Promise<string>;
7
15
  /**
8
16
  * Writes JSON data to a file with error handling
9
17
  * @param filePath - Path to the output JSON file
@@ -21,3 +29,4 @@ export declare const writeJSONFile: (filePath: string, data: unknown, logger: {
21
29
  * @returns Formatted duration string (e.g. "2m 30s" or "45s")
22
30
  */
23
31
  export declare const formatDurationSeconds: (durationSeconds: number) => string;
32
+ export {};