@devicecloud.dev/dcd 4.4.9 → 5.0.0-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.
Files changed (97) hide show
  1. package/README.md +40 -2
  2. package/dist/commands/artifacts.d.ts +47 -18
  3. package/dist/commands/artifacts.js +68 -60
  4. package/dist/commands/cloud.d.ts +228 -88
  5. package/dist/commands/cloud.js +389 -288
  6. package/dist/commands/list.d.ts +39 -38
  7. package/dist/commands/list.js +122 -127
  8. package/dist/commands/live.d.ts +2 -0
  9. package/dist/commands/live.js +513 -0
  10. package/dist/commands/login.d.ts +17 -0
  11. package/dist/commands/login.js +250 -0
  12. package/dist/commands/logout.d.ts +2 -0
  13. package/dist/commands/logout.js +32 -0
  14. package/dist/commands/status.d.ts +23 -42
  15. package/dist/commands/status.js +162 -173
  16. package/dist/commands/switch-org.d.ts +12 -0
  17. package/dist/commands/switch-org.js +78 -0
  18. package/dist/commands/upgrade.d.ts +2 -0
  19. package/dist/commands/upgrade.js +122 -0
  20. package/dist/commands/upload.d.ts +33 -18
  21. package/dist/commands/upload.js +62 -67
  22. package/dist/commands/whoami.d.ts +2 -0
  23. package/dist/commands/whoami.js +34 -0
  24. package/dist/config/environments.d.ts +31 -0
  25. package/dist/config/environments.js +58 -0
  26. package/dist/config/flags/api.flags.d.ts +10 -2
  27. package/dist/config/flags/api.flags.js +12 -10
  28. package/dist/config/flags/binary.flags.d.ts +17 -4
  29. package/dist/config/flags/binary.flags.js +13 -14
  30. package/dist/config/flags/device.flags.d.ts +49 -11
  31. package/dist/config/flags/device.flags.js +41 -33
  32. package/dist/config/flags/environment.flags.d.ts +27 -6
  33. package/dist/config/flags/environment.flags.js +23 -25
  34. package/dist/config/flags/execution.flags.d.ts +35 -8
  35. package/dist/config/flags/execution.flags.js +30 -37
  36. package/dist/config/flags/github.flags.d.ts +23 -5
  37. package/dist/config/flags/github.flags.js +18 -11
  38. package/dist/config/flags/output.flags.d.ts +57 -13
  39. package/dist/config/flags/output.flags.js +47 -43
  40. package/dist/constants.d.ts +218 -51
  41. package/dist/constants.js +2 -2
  42. package/dist/gateways/api-gateway.d.ts +43 -12
  43. package/dist/gateways/api-gateway.js +240 -100
  44. package/dist/gateways/cli-auth-gateway.d.ts +13 -0
  45. package/dist/gateways/cli-auth-gateway.js +57 -0
  46. package/dist/gateways/supabase-gateway.d.ts +11 -11
  47. package/dist/gateways/supabase-gateway.js +15 -39
  48. package/dist/index.d.ts +2 -1
  49. package/dist/index.js +93 -2
  50. package/dist/methods.d.ts +3 -5
  51. package/dist/methods.js +170 -178
  52. package/dist/services/device-validation.service.d.ts +8 -0
  53. package/dist/services/device-validation.service.js +55 -35
  54. package/dist/services/execution-plan.service.js +27 -15
  55. package/dist/services/execution-plan.utils.d.ts +3 -0
  56. package/dist/services/execution-plan.utils.js +10 -32
  57. package/dist/services/metadata-extractor.service.d.ts +0 -2
  58. package/dist/services/metadata-extractor.service.js +57 -57
  59. package/dist/services/moropo.service.js +25 -24
  60. package/dist/services/report-download.service.d.ts +12 -1
  61. package/dist/services/report-download.service.js +31 -20
  62. package/dist/services/results-polling.service.d.ts +6 -7
  63. package/dist/services/results-polling.service.js +80 -33
  64. package/dist/services/telemetry.service.d.ts +40 -0
  65. package/dist/services/telemetry.service.js +230 -0
  66. package/dist/services/test-submission.service.js +2 -1
  67. package/dist/services/version.service.d.ts +3 -2
  68. package/dist/services/version.service.js +27 -11
  69. package/dist/types/domain/auth.types.d.ts +12 -0
  70. package/dist/types/{schema.types.js → domain/auth.types.js} +0 -1
  71. package/dist/types/domain/live.types.d.ts +76 -0
  72. package/dist/types/domain/live.types.js +4 -0
  73. package/dist/utils/auth.d.ts +13 -0
  74. package/dist/utils/auth.js +142 -0
  75. package/dist/utils/cli.d.ts +35 -0
  76. package/dist/utils/cli.js +127 -0
  77. package/dist/utils/compatibility.d.ts +2 -1
  78. package/dist/utils/compatibility.js +2 -2
  79. package/dist/utils/config-store.d.ts +35 -0
  80. package/dist/utils/config-store.js +125 -0
  81. package/dist/utils/connectivity.js +7 -3
  82. package/dist/utils/expo.js +14 -3
  83. package/dist/utils/orgs.d.ts +11 -0
  84. package/dist/utils/orgs.js +40 -0
  85. package/dist/utils/paths.d.ts +11 -0
  86. package/dist/utils/paths.js +24 -0
  87. package/dist/utils/progress.d.ts +13 -0
  88. package/dist/utils/progress.js +50 -0
  89. package/dist/utils/styling.d.ts +13 -5
  90. package/dist/utils/styling.js +37 -7
  91. package/package.json +26 -38
  92. package/bin/dev.cmd +0 -3
  93. package/bin/dev.js +0 -6
  94. package/bin/run.cmd +0 -3
  95. package/bin/run.js +0 -7
  96. package/dist/types/schema.types.d.ts +0 -2702
  97. package/oclif.manifest.json +0 -884
package/dist/methods.js CHANGED
@@ -1,14 +1,16 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.formatDurationSeconds = exports.writeJSONFile = exports.uploadBinary = exports.verifyAppZip = exports.compressFilesFromRelativePath = exports.compressFolderToBlob = exports.toBuffer = void 0;
4
- const core_1 = require("@oclif/core");
5
- const archiver = require("archiver");
3
+ exports.formatDurationSeconds = exports.writeJSONFile = exports.uploadBinary = exports.verifyAppZip = exports.compressFilesFromRelativePath = void 0;
4
+ const progress_1 = require("./utils/progress");
6
5
  const node_crypto_1 = require("node:crypto");
7
6
  const node_fs_1 = require("node:fs");
8
7
  const promises_1 = require("node:fs/promises");
8
+ const os = require("node:os");
9
9
  const path = require("node:path");
10
- const node_stream_1 = require("node:stream");
10
+ const promises_2 = require("node:stream/promises");
11
11
  const StreamZip = require("node-stream-zip");
12
+ const yazl = require("yazl");
13
+ const environments_1 = require("./config/environments");
12
14
  const api_gateway_1 = require("./gateways/api-gateway");
13
15
  const supabase_gateway_1 = require("./gateways/supabase-gateway");
14
16
  const metadata_extractor_service_1 = require("./services/metadata-extractor.service");
@@ -18,48 +20,55 @@ const mimeTypeLookupByExtension = {
18
20
  yaml: 'application/x-yaml',
19
21
  zip: 'application/zip',
20
22
  };
21
- const toBuffer = async (archive) => {
22
- const chunks = [];
23
- const writable = new node_stream_1.Writable();
24
- writable._write = (chunk, _, callback) => {
25
- // save to array to concatenate later
26
- chunks.push(chunk);
27
- callback();
28
- };
29
- // pipe to writable
30
- archive.pipe(writable);
31
- await archive.finalize();
32
- // once done, concatenate chunks
33
- return Buffer.concat(chunks);
34
- };
35
- exports.toBuffer = toBuffer;
36
- const compressFolderToBlob = async (sourceDir) => {
37
- const archive = archiver('zip', {
38
- zlib: { level: 9 },
23
+ async function zipToBuffer(zipfile) {
24
+ return new Promise((resolve, reject) => {
25
+ // Typed as Uint8Array[] so Buffer.concat infers Buffer<ArrayBuffer> — matches
26
+ // the `BlobPart` shape that callers pass to `new Blob([...])`.
27
+ const chunks = [];
28
+ zipfile.outputStream.on('data', (chunk) => chunks.push(chunk));
29
+ zipfile.outputStream.on('end', () => resolve(Buffer.concat(chunks)));
30
+ zipfile.outputStream.on('error', reject);
31
+ zipfile.end();
39
32
  });
40
- archive.on('error', (err) => {
41
- throw err;
33
+ }
34
+ /** Normalize a filesystem path to a POSIX-style zip entry name. */
35
+ function toZipEntryName(relativePath) {
36
+ return relativePath.split(path.sep).join('/').replace(/^\/+/, '');
37
+ }
38
+ /**
39
+ * Zip a .app directory to a temp file on disk, streaming entries through
40
+ * yazl so the archive is never held in memory (iOS bundles run to GBs).
41
+ * Returns the temp directory holding the zip — callers remove it after the
42
+ * upload completes.
43
+ */
44
+ async function compressFolderToTempZip(sourceDir) {
45
+ const zipfile = new yazl.ZipFile();
46
+ const rootName = path.basename(sourceDir);
47
+ const entries = (0, node_fs_1.readdirSync)(sourceDir, {
48
+ recursive: true,
49
+ withFileTypes: true,
42
50
  });
43
- archive.directory(sourceDir, sourceDir.split('/').pop());
44
- const buffer = await (0, exports.toBuffer)(archive);
45
- return new Blob([buffer], { type: 'application/zip' });
46
- };
47
- exports.compressFolderToBlob = compressFolderToBlob;
51
+ for (const entry of entries) {
52
+ if (!entry.isFile())
53
+ continue;
54
+ const absolutePath = path.join(entry.parentPath, entry.name);
55
+ const relativePath = path.relative(sourceDir, absolutePath);
56
+ zipfile.addFile(absolutePath, toZipEntryName(path.join(rootName, relativePath)));
57
+ }
58
+ const tempDir = await (0, promises_1.mkdtemp)(path.join(os.tmpdir(), 'dcd-app-zip-'));
59
+ const zipPath = path.join(tempDir, `${rootName}.zip`);
60
+ zipfile.end();
61
+ await (0, promises_2.pipeline)(zipfile.outputStream, (0, node_fs_1.createWriteStream)(zipPath));
62
+ return { tempDir, zipPath };
63
+ }
48
64
  const compressFilesFromRelativePath = async (basePath, files, commonRoot) => {
49
- const archive = archiver('zip', {
50
- zlib: { level: 9 },
51
- });
52
- archive.on('error', (err) => {
53
- throw err;
54
- });
65
+ const zipfile = new yazl.ZipFile();
55
66
  for (const file of files) {
56
- archive.file(path.resolve(basePath, file), {
57
- name: file.replace(commonRoot, ''),
58
- });
67
+ // Anchored prefix strip — replace() would remove the first occurrence
68
+ // of commonRoot anywhere in the path, not just at the start.
69
+ zipfile.addFile(path.resolve(basePath, file), toZipEntryName(file.startsWith(commonRoot) ? file.slice(commonRoot.length) : file));
59
70
  }
60
- const buffer = await (0, exports.toBuffer)(archive);
61
- // await writeFile('./my-zip.zip', buffer);
62
- return buffer;
71
+ return zipToBuffer(zipfile);
63
72
  };
64
73
  exports.compressFilesFromRelativePath = compressFilesFromRelativePath;
65
74
  const verifyAppZip = async (zipPath) => {
@@ -68,19 +77,30 @@ const verifyAppZip = async (zipPath) => {
68
77
  file: zipPath,
69
78
  storeEntries: true,
70
79
  });
71
- const entries = await zip.entries();
72
- const topLevelEntries = Object.values(entries).filter((entry) => !entry.name.split('/')[1]);
73
- if (topLevelEntries.length !== 1 ||
74
- !topLevelEntries[0].name.endsWith('.app/')) {
75
- throw new Error('Zip file must contain exactly one entry which is a .app, check the contents of the zip file');
80
+ try {
81
+ const entries = await zip.entries();
82
+ // Derive top-level names from all entries rather than requiring an
83
+ // explicit directory entry — zips created without directory entries
84
+ // (e.g. Python's zipfile) are valid but have no ".app/" entry itself.
85
+ // macOS metadata (__MACOSX resource forks, .DS_Store) doesn't count
86
+ // toward the "exactly one .app" rule.
87
+ const topLevelNames = new Set(Object.values(entries)
88
+ .filter((entry) => !entry.name.startsWith('__MACOSX/') &&
89
+ entry.name.split('/').pop() !== '.DS_Store')
90
+ .map((entry) => entry.name.split('/')[0]));
91
+ if (topLevelNames.size !== 1 || ![...topLevelNames][0].endsWith('.app')) {
92
+ throw new Error('Zip file must contain exactly one entry which is a .app, check the contents of the zip file');
93
+ }
94
+ }
95
+ finally {
96
+ zip.close();
76
97
  }
77
- zip.close();
78
98
  };
79
99
  exports.verifyAppZip = verifyAppZip;
80
100
  const uploadBinary = async (config) => {
81
- const { filePath, apiUrl, apiKey, ignoreShaCheck = false, log = true, debug = false } = config;
101
+ const { filePath, apiUrl, auth, ignoreShaCheck = false, log = true, debug = false } = config;
82
102
  if (log) {
83
- core_1.ux.action.start(styling_1.colors.bold('Checking and uploading binary'), styling_1.colors.dim('Initializing'), {
103
+ progress_1.ux.action.start(styling_1.colors.bold('Checking and uploading binary'), styling_1.colors.dim('Initializing'), {
84
104
  stdout: true,
85
105
  });
86
106
  }
@@ -91,32 +111,33 @@ const uploadBinary = async (config) => {
91
111
  console.log(`[DEBUG] Ignore SHA check: ${ignoreShaCheck}`);
92
112
  }
93
113
  const startTime = Date.now();
114
+ let source;
94
115
  try {
95
116
  // Prepare file for upload
96
- const file = await prepareFileForUpload(filePath, debug, startTime);
117
+ source = await prepareFileForUpload(filePath, debug, startTime);
97
118
  // Calculate SHA hash
98
- const sha = await calculateFileHash(file, debug, log);
119
+ const sha = await calculateFileHash(source, debug, log);
99
120
  // Check for existing upload with same SHA
100
121
  if (!ignoreShaCheck && sha) {
101
- const { exists, binaryId } = await checkExistingUpload(apiUrl, apiKey, sha, debug);
122
+ const { exists, binaryId } = await checkExistingUpload(apiUrl, auth, sha, debug);
102
123
  if (exists && binaryId) {
103
124
  if (log) {
104
- core_1.ux.info(styling_1.colors.dim('SHA hash matches existing binary with ID: ') + (0, styling_1.formatId)(binaryId) + styling_1.colors.dim(', skipping upload. Force upload with --ignore-sha-check'));
105
- core_1.ux.action.stop(styling_1.colors.info('Skipping upload'));
125
+ progress_1.ux.info(styling_1.colors.dim('SHA hash matches existing binary with ID: ') + (0, styling_1.formatId)(binaryId) + styling_1.colors.dim(', skipping upload. Force upload with --ignore-sha-check'));
126
+ progress_1.ux.action.stop(styling_1.colors.info('Skipping upload'));
106
127
  }
107
128
  return binaryId;
108
129
  }
109
130
  }
110
131
  // Perform the upload
111
- const uploadId = await performUpload({ apiKey, apiUrl, debug, file, filePath, sha, startTime });
132
+ const uploadId = await performUpload({ auth, apiUrl, debug, filePath, sha, source, startTime });
112
133
  if (log) {
113
- core_1.ux.action.stop(styling_1.colors.success('\n✓ Binary uploaded with ID: ') + (0, styling_1.formatId)(uploadId));
134
+ progress_1.ux.action.stop(styling_1.colors.success('\n✓ Binary uploaded with ID: ') + (0, styling_1.formatId)(uploadId));
114
135
  }
115
136
  return uploadId;
116
137
  }
117
138
  catch (error) {
118
139
  if (log) {
119
- core_1.ux.action.stop(styling_1.colors.error('✗ Failed'));
140
+ progress_1.ux.action.stop(styling_1.colors.error('✗ Failed'));
120
141
  }
121
142
  if (debug) {
122
143
  console.error('[DEBUG] === BINARY UPLOAD FAILED ===');
@@ -128,36 +149,34 @@ const uploadBinary = async (config) => {
128
149
  }
129
150
  console.error(`[DEBUG] Failed after ${Date.now() - startTime}ms`);
130
151
  }
131
- // Add helpful context for common errors
132
- if (error instanceof Error) {
133
- if (error.name === 'NetworkError') {
134
- throw error; // NetworkError already has detailed troubleshooting info
135
- }
136
- // Re-throw with original message
137
- throw error;
138
- }
139
152
  throw error;
140
153
  }
154
+ finally {
155
+ if (source?.cleanupDir) {
156
+ await (0, promises_1.rm)(source.cleanupDir, { recursive: true, force: true }).catch(() => { });
157
+ }
158
+ }
141
159
  };
142
160
  exports.uploadBinary = uploadBinary;
143
161
  /**
144
- * Prepares a file for upload by reading or compressing it
162
+ * Prepares a file for upload: .app directories are zipped to a temp file on
163
+ * disk; everything else is described in place. Nothing is read into memory.
145
164
  * @param filePath Path to the file to upload
146
165
  * @param debug Whether debug logging is enabled
147
166
  * @param startTime Timestamp when upload started
148
- * @returns Promise resolving to prepared File object
167
+ * @returns Promise resolving to the upload source descriptor
149
168
  */
150
169
  async function prepareFileForUpload(filePath, debug, startTime) {
151
170
  if (debug) {
152
171
  console.log('[DEBUG] Preparing file for upload...');
153
172
  }
154
- let file;
173
+ let source;
155
174
  if (filePath?.endsWith('.app')) {
156
175
  if (debug) {
157
176
  console.log('[DEBUG] Compressing .app folder to zip...');
158
177
  }
159
- // Validate that the .app directory exists before attempting to compress
160
- // Without this check, archiver silently creates an empty 22-byte zip for non-existent paths
178
+ // Validate that the .app directory exists before attempting to compress
179
+ // zipping a non-existent path silently produces an empty 22-byte zip.
161
180
  try {
162
181
  await (0, promises_1.access)(filePath);
163
182
  }
@@ -181,29 +200,36 @@ async function prepareFileForUpload(filePath, debug, startTime) {
181
200
  .join('\n');
182
201
  throw new Error(errorMessage);
183
202
  }
184
- const zippedAppBlob = await (0, exports.compressFolderToBlob)(filePath);
185
- file = new File([zippedAppBlob], filePath + '.zip');
203
+ const { tempDir, zipPath } = await compressFolderToTempZip(filePath);
204
+ const { size } = await (0, promises_1.stat)(zipPath);
205
+ source = {
206
+ contentType: 'application/zip',
207
+ cleanupDir: tempDir,
208
+ diskPath: zipPath,
209
+ name: filePath + '.zip',
210
+ size,
211
+ };
186
212
  if (debug) {
187
- console.log(`[DEBUG] Compressed file size: ${(zippedAppBlob.size / 1024 / 1024).toFixed(2)} MB`);
213
+ console.log(`[DEBUG] Compressed file size: ${(size / 1024 / 1024).toFixed(2)} MB`);
188
214
  }
189
215
  }
190
216
  else {
217
+ const { size } = await (0, promises_1.stat)(filePath);
191
218
  if (debug) {
192
- console.log('[DEBUG] Reading binary file...');
193
- }
194
- const fileBuffer = await (0, promises_1.readFile)(filePath);
195
- if (debug) {
196
- console.log(`[DEBUG] File size: ${(fileBuffer.length / 1024 / 1024).toFixed(2)} MB`);
197
- }
198
- const binaryBlob = new Blob([new Uint8Array(fileBuffer)], {
199
- type: mimeTypeLookupByExtension[filePath.split('.').pop()],
200
- });
201
- file = new File([binaryBlob], filePath);
219
+ console.log(`[DEBUG] File size: ${(size / 1024 / 1024).toFixed(2)} MB`);
220
+ }
221
+ source = {
222
+ contentType: mimeTypeLookupByExtension[filePath.split('.').pop()] ||
223
+ 'application/octet-stream',
224
+ diskPath: filePath,
225
+ name: filePath,
226
+ size,
227
+ };
202
228
  }
203
229
  if (debug) {
204
230
  console.log(`[DEBUG] File preparation completed in ${Date.now() - startTime}ms`);
205
231
  }
206
- return file;
232
+ return source;
207
233
  }
208
234
  /**
209
235
  * Calculates SHA-256 hash for a file
@@ -212,13 +238,13 @@ async function prepareFileForUpload(filePath, debug, startTime) {
212
238
  * @param log Whether to log warnings
213
239
  * @returns Promise resolving to SHA-256 hash or undefined if failed
214
240
  */
215
- async function calculateFileHash(file, debug, log) {
241
+ async function calculateFileHash(source, debug, log) {
216
242
  try {
217
243
  if (debug) {
218
244
  console.log('[DEBUG] Calculating SHA-256 hash...');
219
245
  }
220
246
  const hashStartTime = Date.now();
221
- const sha = await getFileHashFromFile(file);
247
+ const sha = await getFileHashFromPath(source.diskPath);
222
248
  if (debug) {
223
249
  console.log(`[DEBUG] SHA-256 hash: ${sha}`);
224
250
  console.log(`[DEBUG] Hash calculation completed in ${Date.now() - hashStartTime}ms`);
@@ -238,19 +264,19 @@ async function calculateFileHash(file, debug, log) {
238
264
  /**
239
265
  * Checks if an upload with the same SHA already exists
240
266
  * @param apiUrl API base URL
241
- * @param apiKey API authentication key
267
+ * @param auth AuthContext carrying request headers
242
268
  * @param sha SHA-256 hash to check
243
269
  * @param debug Whether debug logging is enabled
244
270
  * @returns Promise resolving to object with exists flag and optional binaryId
245
271
  */
246
- async function checkExistingUpload(apiUrl, apiKey, sha, debug) {
272
+ async function checkExistingUpload(apiUrl, auth, sha, debug) {
247
273
  try {
248
274
  if (debug) {
249
275
  console.log('[DEBUG] Checking for existing upload with matching SHA...');
250
276
  console.log(`[DEBUG] Target endpoint: ${apiUrl}/uploads/checkForExistingUpload`);
251
277
  }
252
278
  const shaCheckStartTime = Date.now();
253
- const { appBinaryId, exists } = await api_gateway_1.ApiGateway.checkForExistingUpload(apiUrl, apiKey, sha);
279
+ const { appBinaryId, exists } = await api_gateway_1.ApiGateway.checkForExistingUpload(apiUrl, auth, sha);
254
280
  if (debug) {
255
281
  console.log(`[DEBUG] SHA check completed in ${Date.now() - shaCheckStartTime}ms`);
256
282
  console.log(`[DEBUG] Existing binary found: ${exists}`);
@@ -261,6 +287,11 @@ async function checkExistingUpload(apiUrl, apiKey, sha, debug) {
261
287
  return { binaryId: appBinaryId, exists };
262
288
  }
263
289
  catch (error) {
290
+ // Invalid credentials will fail every subsequent request — surface now
291
+ // rather than after the user has waited through a potentially huge upload.
292
+ if (error instanceof api_gateway_1.ApiError && (error.status === 401 || error.status === 403)) {
293
+ throw error;
294
+ }
264
295
  if (debug) {
265
296
  console.error('[DEBUG] === SHA CHECK FAILED ===');
266
297
  console.error('[DEBUG] Continuing with upload despite SHA check failure');
@@ -286,19 +317,19 @@ async function checkExistingUpload(apiUrl, apiKey, sha, debug) {
286
317
  * @param debug - Enable debug logging
287
318
  * @returns Upload result with success status and any error
288
319
  */
289
- async function uploadToSupabase(env, tempPath, file, debug) {
320
+ async function uploadToSupabase(env, tempPath, source, debug) {
290
321
  if (debug) {
291
322
  console.log(`[DEBUG] Uploading to Supabase storage (${env}) using resumable uploads...`);
292
323
  console.log(`[DEBUG] Staging path: ${tempPath}`);
293
- console.log(`[DEBUG] File size: ${(file.size / 1024 / 1024).toFixed(2)} MB`);
324
+ console.log(`[DEBUG] File size: ${(source.size / 1024 / 1024).toFixed(2)} MB`);
294
325
  }
295
326
  try {
296
327
  const uploadStartTime = Date.now();
297
- await supabase_gateway_1.SupabaseGateway.uploadResumable(env, tempPath, file, debug);
328
+ await supabase_gateway_1.SupabaseGateway.uploadResumable(env, tempPath, source, debug);
298
329
  if (debug) {
299
330
  const uploadDuration = Date.now() - uploadStartTime;
300
331
  const uploadDurationSeconds = uploadDuration / 1000;
301
- const uploadSpeed = (file.size / 1024 / 1024) / uploadDurationSeconds;
332
+ const uploadSpeed = (source.size / 1024 / 1024) / uploadDurationSeconds;
302
333
  console.log(`[DEBUG] Supabase resumable upload completed in ${uploadDurationSeconds.toFixed(2)}s (${uploadDuration}ms)`);
303
334
  console.log(`[DEBUG] Average upload speed: ${uploadSpeed.toFixed(2)} MB/s`);
304
335
  }
@@ -314,7 +345,7 @@ async function uploadToSupabase(env, tempPath, file, debug) {
314
345
  console.error(`[DEBUG] Error stack:\n${uploadError.stack}`);
315
346
  }
316
347
  console.error(`[DEBUG] Staging path: ${tempPath}`);
317
- console.error(`[DEBUG] File size: ${file.size} bytes`);
348
+ console.error(`[DEBUG] File size: ${source.size} bytes`);
318
349
  console.log('[DEBUG] Will attempt Backblaze fallback if available...');
319
350
  }
320
351
  return { error: uploadError, success: false };
@@ -326,7 +357,7 @@ async function uploadToSupabase(env, tempPath, file, debug) {
326
357
  * @returns Upload result with success status and any error
327
358
  */
328
359
  async function handleBackblazeUpload(config) {
329
- const { b2, apiUrl, apiKey, finalPath, file, filePath, debug } = config;
360
+ const { b2, apiUrl, auth, finalPath, source, debug } = config;
330
361
  if (!b2) {
331
362
  if (debug) {
332
363
  console.log('[DEBUG] Backblaze not configured, will fall back to Supabase');
@@ -341,19 +372,18 @@ async function handleBackblazeUpload(config) {
341
372
  let backblazeSuccess = false;
342
373
  if (b2.strategy === 'simple' && b2.simple) {
343
374
  const simple = b2.simple;
344
- backblazeSuccess = await uploadToBackblaze(simple.uploadUrl, simple.authorizationToken, `organizations/${finalPath}`, file, debug);
375
+ backblazeSuccess = await uploadToBackblaze(simple.uploadUrl, simple.authorizationToken, `organizations/${finalPath}`, source, debug);
345
376
  }
346
377
  else if (b2.strategy === 'large' && b2.large) {
347
378
  const large = b2.large;
348
379
  backblazeSuccess = await uploadLargeFileToBackblaze({
349
- apiKey,
380
+ auth,
350
381
  apiUrl,
351
382
  debug,
352
383
  fileId: large.fileId,
353
384
  fileName: `organizations/${finalPath}`,
354
- fileObject: file,
355
- filePath,
356
- fileSize: file.size,
385
+ filePath: source.diskPath,
386
+ fileSize: source.size,
357
387
  uploadPartUrls: large.uploadPartUrls,
358
388
  });
359
389
  }
@@ -383,13 +413,13 @@ async function handleBackblazeUpload(config) {
383
413
  /**
384
414
  * Requests upload URL and paths from API
385
415
  * @param apiUrl - API base URL
386
- * @param apiKey - API authentication key
416
+ * @param auth - AuthContext carrying request headers
387
417
  * @param filePath - Path to the file being uploaded
388
418
  * @param fileSize - Size of the file in bytes
389
419
  * @param debug - Enable debug logging
390
420
  * @returns Promise resolving to upload paths and configuration
391
421
  */
392
- async function requestUploadPaths(apiUrl, apiKey, filePath, fileSize, debug) {
422
+ async function requestUploadPaths(apiUrl, auth, filePath, fileSize, debug) {
393
423
  const platform = filePath?.endsWith('.apk') ? 'android' : 'ios';
394
424
  if (debug) {
395
425
  console.log('[DEBUG] Requesting upload URL...');
@@ -398,7 +428,7 @@ async function requestUploadPaths(apiUrl, apiKey, filePath, fileSize, debug) {
398
428
  }
399
429
  try {
400
430
  const urlRequestStartTime = Date.now();
401
- const { id, tempPath, finalPath, b2 } = await api_gateway_1.ApiGateway.getBinaryUploadUrl(apiUrl, apiKey, platform, fileSize);
431
+ const { id, tempPath, finalPath, b2 } = await api_gateway_1.ApiGateway.getBinaryUploadUrl(apiUrl, auth, platform, fileSize);
402
432
  if (debug) {
403
433
  const hasStrategy = b2 && typeof b2 === 'object' && 'strategy' in b2;
404
434
  console.log(`[DEBUG] Upload URL request completed in ${Date.now() - urlRequestStartTime}ms`);
@@ -478,21 +508,20 @@ function validateUploadResults(supabaseSuccess, backblazeSuccess, lastError, b2,
478
508
  * @returns Promise resolving to upload ID
479
509
  */
480
510
  async function performUpload(config) {
481
- const { filePath, apiUrl, apiKey, file, sha, debug, startTime } = config;
511
+ const { filePath, apiUrl, auth, source, sha, debug, startTime } = config;
482
512
  // Request upload URL and paths
483
- const { id, tempPath, finalPath, b2 } = await requestUploadPaths(apiUrl, apiKey, filePath, file.size, debug);
513
+ const { id, tempPath, finalPath, b2 } = await requestUploadPaths(apiUrl, auth, filePath, source.size, debug);
484
514
  // Extract app metadata
485
515
  const metadata = await extractBinaryMetadata(filePath, debug);
486
- const env = apiUrl === 'https://api.devicecloud.dev' ? 'prod' : 'dev';
516
+ const env = (0, environments_1.inferEnvFromApiUrl)(apiUrl);
487
517
  // Upload to Backblaze first (primary)
488
518
  const backblazeResult = await handleBackblazeUpload({
489
- apiKey,
519
+ auth,
490
520
  apiUrl,
491
521
  b2: b2,
492
522
  debug,
493
- file,
494
- filePath,
495
523
  finalPath,
524
+ source,
496
525
  });
497
526
  let lastError = backblazeResult.error;
498
527
  // Always upload to Supabase (re-enabled as always-on alongside Backblaze)
@@ -500,7 +529,7 @@ async function performUpload(config) {
500
529
  if (debug) {
501
530
  console.log('[DEBUG] Uploading to Supabase...');
502
531
  }
503
- supabaseResult = await uploadToSupabase(env, tempPath, file, debug);
532
+ supabaseResult = await uploadToSupabase(env, tempPath, source, debug);
504
533
  if (!supabaseResult.success && supabaseResult.error) {
505
534
  lastError = supabaseResult.error;
506
535
  }
@@ -521,14 +550,15 @@ async function performUpload(config) {
521
550
  // Finalize upload
522
551
  const finalizeStartTime = Date.now();
523
552
  await api_gateway_1.ApiGateway.finaliseUpload({
524
- apiKey,
553
+ auth,
525
554
  backblazeSuccess: backblazeResult.success,
526
555
  baseUrl: apiUrl,
527
- bytes: file.size,
556
+ bytes: source.size,
528
557
  id,
529
558
  metadata,
530
559
  path: tempPath,
531
- sha: sha,
560
+ // sha is undefined when hash calculation failed — omit it explicitly
561
+ ...(sha ? { sha } : {}),
532
562
  supabaseSuccess: supabaseResult.success,
533
563
  });
534
564
  if (debug) {
@@ -542,16 +572,20 @@ async function performUpload(config) {
542
572
  * @param uploadUrl - Backblaze upload URL
543
573
  * @param authorizationToken - Authorization token for the upload
544
574
  * @param fileName - Name/path of the file
545
- * @param file - File to upload
575
+ * @param source - Upload source descriptor (streamed from disk)
546
576
  * @param debug - Whether debug logging is enabled
547
577
  * @returns Promise that resolves when upload completes or fails gracefully
548
578
  */
549
- async function uploadToBackblaze(uploadUrl, authorizationToken, fileName, file, debug) {
579
+ async function uploadToBackblaze(uploadUrl, authorizationToken, fileName, source, debug) {
550
580
  try {
551
- const arrayBuffer = await file.arrayBuffer();
581
+ // The API only picks the simple strategy for small binaries (large files
582
+ // get the multi-part path), so one transient buffer here is bounded.
583
+ // S3 pre-signed PUTs reject chunked transfer encoding, which rules out a
584
+ // plain stream body.
585
+ const body = await (0, promises_1.readFile)(source.diskPath);
552
586
  // Calculate SHA1 hash for Backblaze (B2 requires SHA1, not SHA256)
553
587
  const sha1 = (0, node_crypto_1.createHash)('sha1');
554
- sha1.update(Buffer.from(arrayBuffer));
588
+ sha1.update(body);
555
589
  const sha1Hex = sha1.digest('hex');
556
590
  // Detect if this is an S3 pre-signed URL (authorization token is empty)
557
591
  const isS3PreSignedUrl = !authorizationToken || authorizationToken === '';
@@ -563,8 +597,8 @@ async function uploadToBackblaze(uploadUrl, authorizationToken, fileName, file,
563
597
  }
564
598
  // Build headers based on upload method
565
599
  const headers = {
566
- 'Content-Length': file.size.toString(),
567
- 'Content-Type': file.type || 'application/octet-stream',
600
+ 'Content-Length': body.length.toString(),
601
+ 'Content-Type': source.contentType || 'application/octet-stream',
568
602
  'X-Bz-Content-Sha1': sha1Hex,
569
603
  };
570
604
  // S3 pre-signed URLs have auth embedded in URL, native B2 uses Authorization header
@@ -573,7 +607,8 @@ async function uploadToBackblaze(uploadUrl, authorizationToken, fileName, file,
573
607
  headers['X-Bz-File-Name'] = encodeURIComponent(fileName);
574
608
  }
575
609
  const response = await fetch(uploadUrl, {
576
- body: arrayBuffer,
610
+ // Zero-copy view over the buffer (new Uint8Array(buffer) would clone it).
611
+ body: new Uint8Array(body.buffer, body.byteOffset, body.byteLength),
577
612
  headers,
578
613
  method: isS3PreSignedUrl ? 'PUT' : 'POST',
579
614
  });
@@ -637,31 +672,6 @@ async function readFileChunk(filePath, start, end) {
637
672
  });
638
673
  });
639
674
  }
640
- /**
641
- * Helper function to read a chunk from a File/Blob object
642
- * @param file - File or Blob object
643
- * @param start - Start byte position
644
- * @param end - End byte position (exclusive)
645
- * @returns Promise resolving to Buffer containing the chunk
646
- */
647
- async function readFileObjectChunk(file, start, end) {
648
- const slice = file.slice(start, end);
649
- const arrayBuffer = await slice.arrayBuffer();
650
- return Buffer.from(arrayBuffer);
651
- }
652
- /**
653
- * Reads a file chunk from either a File object or disk
654
- * @param fileObject - Optional File object to read from
655
- * @param filePath - Path to file on disk
656
- * @param start - Start byte position
657
- * @param end - End byte position
658
- * @returns Promise resolving to Buffer containing the chunk
659
- */
660
- async function readChunk(fileObject, filePath, start, end) {
661
- return fileObject
662
- ? readFileObjectChunk(fileObject, start, end)
663
- : readFileChunk(filePath, start, end);
664
- }
665
675
  /**
666
676
  * Calculates SHA1 hash for a buffer
667
677
  * @param buffer - Buffer to hash
@@ -744,14 +754,14 @@ function logBackblazeUploadError(error, debug) {
744
754
  * @returns Promise that resolves when upload completes or fails gracefully
745
755
  */
746
756
  async function uploadLargeFileToBackblaze(config) {
747
- const { apiUrl, apiKey, fileId, uploadPartUrls, filePath, fileSize, debug, fileObject } = config;
757
+ const { apiUrl, auth, fileId, uploadPartUrls, filePath, fileSize, debug } = config;
748
758
  try {
749
759
  const partSha1Array = [];
750
760
  const partSize = Math.ceil(fileSize / uploadPartUrls.length);
751
761
  if (debug) {
752
762
  console.log(`[DEBUG] Uploading large file in ${uploadPartUrls.length} parts (streaming mode)`);
753
763
  console.log(`[DEBUG] Part size: ${(partSize / 1024 / 1024).toFixed(2)} MB`);
754
- console.log(`[DEBUG] Reading from: ${fileObject ? 'in-memory File object' : filePath}`);
764
+ console.log(`[DEBUG] Reading from: ${filePath}`);
755
765
  }
756
766
  // Upload each part using streaming to avoid loading entire file into memory
757
767
  for (let i = 0; i < uploadPartUrls.length; i++) {
@@ -762,7 +772,7 @@ async function uploadLargeFileToBackblaze(config) {
762
772
  if (debug) {
763
773
  console.log(`[DEBUG] Reading part ${partNumber}/${uploadPartUrls.length} bytes ${start}-${end}`);
764
774
  }
765
- const partBuffer = await readChunk(fileObject, filePath, start, end);
775
+ const partBuffer = await readFileChunk(filePath, start, end);
766
776
  const sha1Hex = calculateSha1(partBuffer);
767
777
  partSha1Array.push(sha1Hex);
768
778
  await uploadPartToBackblaze({
@@ -776,18 +786,11 @@ async function uploadLargeFileToBackblaze(config) {
776
786
  uploadUrl: uploadPartUrls[i].uploadUrl,
777
787
  });
778
788
  }
779
- // Validate all parts were uploaded
780
- if (partSha1Array.length !== uploadPartUrls.length) {
781
- const errorMsg = `Part count mismatch: uploaded ${partSha1Array.length} parts but expected ${uploadPartUrls.length}`;
782
- if (debug)
783
- console.error(`[DEBUG] ${errorMsg}`);
784
- throw new Error(errorMsg);
785
- }
786
789
  if (debug) {
787
790
  console.log('[DEBUG] Finishing large file upload...');
788
791
  console.log(`[DEBUG] Finalizing ${partSha1Array.length} parts with fileId: ${fileId}`);
789
792
  }
790
- await api_gateway_1.ApiGateway.finishLargeFile(apiUrl, apiKey, fileId, partSha1Array);
793
+ await api_gateway_1.ApiGateway.finishLargeFile(apiUrl, auth, fileId, partSha1Array);
791
794
  if (debug)
792
795
  console.log('[DEBUG] Large file upload completed successfully');
793
796
  return true;
@@ -797,27 +800,12 @@ async function uploadLargeFileToBackblaze(config) {
797
800
  return false;
798
801
  }
799
802
  }
800
- async function getFileHashFromFile(file) {
801
- return new Promise((resolve, reject) => {
802
- const hash = (0, node_crypto_1.createHash)('sha256');
803
- const stream = file.stream();
804
- const reader = stream.getReader();
805
- const processChunks = async () => {
806
- try {
807
- let readerResult = await reader.read();
808
- while (!readerResult.done) {
809
- const { value } = readerResult;
810
- hash.update(value);
811
- readerResult = await reader.read();
812
- }
813
- resolve(hash.digest('hex'));
814
- }
815
- catch (error) {
816
- reject(error);
817
- }
818
- };
819
- processChunks();
820
- });
803
+ async function getFileHashFromPath(filePath) {
804
+ const hash = (0, node_crypto_1.createHash)('sha256');
805
+ for await (const chunk of (0, node_fs_1.createReadStream)(filePath)) {
806
+ hash.update(chunk);
807
+ }
808
+ return hash.digest('hex');
821
809
  }
822
810
  /**
823
811
  * Writes JSON data to a file with error handling
@@ -828,6 +816,10 @@ async function getFileHashFromFile(file) {
828
816
  */
829
817
  const writeJSONFile = (filePath, data, logger) => {
830
818
  try {
819
+ const directory = path.dirname(filePath);
820
+ if (directory !== '.') {
821
+ (0, node_fs_1.mkdirSync)(directory, { recursive: true });
822
+ }
831
823
  (0, node_fs_1.writeFileSync)(filePath, JSON.stringify(data, null, 2));
832
824
  logger.log(styling_1.colors.dim('JSON output written to: ') + styling_1.colors.highlight(path.resolve(filePath)));
833
825
  }
@@ -835,7 +827,7 @@ const writeJSONFile = (filePath, data, logger) => {
835
827
  const errorMessage = error instanceof Error ? error.message : String(error);
836
828
  const isPermissionError = errorMessage.includes('EACCES') || errorMessage.includes('EPERM');
837
829
  const isNoSuchFileError = errorMessage.includes('ENOENT');
838
- logger.warn(styling_1.colors.warning('⚠') + ' ' + styling_1.colors.error(`Failed to write JSON output to file: ${filePath}`));
830
+ logger.warn(styling_1.colors.error(`Failed to write JSON output to file: ${filePath}`));
839
831
  if (isPermissionError) {
840
832
  logger.warn(styling_1.colors.dim(' Permission denied - check file/directory write permissions'));
841
833
  logger.warn(styling_1.colors.dim(' Try running with appropriate permissions or choose a different output location'));