@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.
- package/README.md +40 -2
- package/dist/commands/artifacts.d.ts +47 -18
- package/dist/commands/artifacts.js +68 -60
- package/dist/commands/cloud.d.ts +228 -88
- package/dist/commands/cloud.js +389 -288
- package/dist/commands/list.d.ts +39 -38
- package/dist/commands/list.js +122 -127
- package/dist/commands/live.d.ts +2 -0
- package/dist/commands/live.js +513 -0
- package/dist/commands/login.d.ts +17 -0
- package/dist/commands/login.js +250 -0
- package/dist/commands/logout.d.ts +2 -0
- package/dist/commands/logout.js +32 -0
- package/dist/commands/status.d.ts +23 -42
- package/dist/commands/status.js +162 -173
- package/dist/commands/switch-org.d.ts +12 -0
- package/dist/commands/switch-org.js +78 -0
- package/dist/commands/upgrade.d.ts +2 -0
- package/dist/commands/upgrade.js +122 -0
- package/dist/commands/upload.d.ts +33 -18
- package/dist/commands/upload.js +62 -67
- package/dist/commands/whoami.d.ts +2 -0
- package/dist/commands/whoami.js +34 -0
- package/dist/config/environments.d.ts +31 -0
- package/dist/config/environments.js +58 -0
- package/dist/config/flags/api.flags.d.ts +10 -2
- package/dist/config/flags/api.flags.js +12 -10
- package/dist/config/flags/binary.flags.d.ts +17 -4
- package/dist/config/flags/binary.flags.js +13 -14
- package/dist/config/flags/device.flags.d.ts +49 -11
- package/dist/config/flags/device.flags.js +41 -33
- package/dist/config/flags/environment.flags.d.ts +27 -6
- package/dist/config/flags/environment.flags.js +23 -25
- package/dist/config/flags/execution.flags.d.ts +35 -8
- package/dist/config/flags/execution.flags.js +30 -37
- package/dist/config/flags/github.flags.d.ts +23 -5
- package/dist/config/flags/github.flags.js +18 -11
- package/dist/config/flags/output.flags.d.ts +57 -13
- package/dist/config/flags/output.flags.js +47 -43
- package/dist/constants.d.ts +218 -51
- package/dist/constants.js +2 -2
- package/dist/gateways/api-gateway.d.ts +43 -12
- package/dist/gateways/api-gateway.js +240 -100
- package/dist/gateways/cli-auth-gateway.d.ts +13 -0
- package/dist/gateways/cli-auth-gateway.js +57 -0
- package/dist/gateways/supabase-gateway.d.ts +11 -11
- package/dist/gateways/supabase-gateway.js +15 -39
- package/dist/index.d.ts +2 -1
- package/dist/index.js +93 -2
- package/dist/methods.d.ts +3 -5
- package/dist/methods.js +170 -178
- package/dist/services/device-validation.service.d.ts +8 -0
- package/dist/services/device-validation.service.js +55 -35
- package/dist/services/execution-plan.service.js +27 -15
- package/dist/services/execution-plan.utils.d.ts +3 -0
- package/dist/services/execution-plan.utils.js +10 -32
- package/dist/services/metadata-extractor.service.d.ts +0 -2
- package/dist/services/metadata-extractor.service.js +57 -57
- package/dist/services/moropo.service.js +25 -24
- package/dist/services/report-download.service.d.ts +12 -1
- package/dist/services/report-download.service.js +31 -20
- package/dist/services/results-polling.service.d.ts +6 -7
- package/dist/services/results-polling.service.js +80 -33
- package/dist/services/telemetry.service.d.ts +40 -0
- package/dist/services/telemetry.service.js +230 -0
- package/dist/services/test-submission.service.js +2 -1
- package/dist/services/version.service.d.ts +3 -2
- package/dist/services/version.service.js +27 -11
- package/dist/types/domain/auth.types.d.ts +12 -0
- package/dist/types/{schema.types.js → domain/auth.types.js} +0 -1
- package/dist/types/domain/live.types.d.ts +76 -0
- package/dist/types/domain/live.types.js +4 -0
- package/dist/utils/auth.d.ts +13 -0
- package/dist/utils/auth.js +142 -0
- package/dist/utils/cli.d.ts +35 -0
- package/dist/utils/cli.js +127 -0
- package/dist/utils/compatibility.d.ts +2 -1
- package/dist/utils/compatibility.js +2 -2
- package/dist/utils/config-store.d.ts +35 -0
- package/dist/utils/config-store.js +125 -0
- package/dist/utils/connectivity.js +7 -3
- package/dist/utils/expo.js +14 -3
- package/dist/utils/orgs.d.ts +11 -0
- package/dist/utils/orgs.js +40 -0
- package/dist/utils/paths.d.ts +11 -0
- package/dist/utils/paths.js +24 -0
- package/dist/utils/progress.d.ts +13 -0
- package/dist/utils/progress.js +50 -0
- package/dist/utils/styling.d.ts +13 -5
- package/dist/utils/styling.js +37 -7
- package/package.json +26 -38
- package/bin/dev.cmd +0 -3
- package/bin/dev.js +0 -6
- package/bin/run.cmd +0 -3
- package/bin/run.js +0 -7
- package/dist/types/schema.types.d.ts +0 -2702
- 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 =
|
|
4
|
-
const
|
|
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
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
chunks.push(chunk);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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,
|
|
101
|
+
const { filePath, apiUrl, auth, ignoreShaCheck = false, log = true, debug = false } = config;
|
|
82
102
|
if (log) {
|
|
83
|
-
|
|
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
|
-
|
|
117
|
+
source = await prepareFileForUpload(filePath, debug, startTime);
|
|
97
118
|
// Calculate SHA hash
|
|
98
|
-
const sha = await calculateFileHash(
|
|
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,
|
|
122
|
+
const { exists, binaryId } = await checkExistingUpload(apiUrl, auth, sha, debug);
|
|
102
123
|
if (exists && binaryId) {
|
|
103
124
|
if (log) {
|
|
104
|
-
|
|
105
|
-
|
|
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({
|
|
132
|
+
const uploadId = await performUpload({ auth, apiUrl, debug, filePath, sha, source, startTime });
|
|
112
133
|
if (log) {
|
|
113
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
|
185
|
-
|
|
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: ${(
|
|
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(
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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: ${(
|
|
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,
|
|
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 = (
|
|
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: ${
|
|
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,
|
|
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}`,
|
|
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
|
-
|
|
380
|
+
auth,
|
|
350
381
|
apiUrl,
|
|
351
382
|
debug,
|
|
352
383
|
fileId: large.fileId,
|
|
353
384
|
fileName: `organizations/${finalPath}`,
|
|
354
|
-
|
|
355
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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 =
|
|
516
|
+
const env = (0, environments_1.inferEnvFromApiUrl)(apiUrl);
|
|
487
517
|
// Upload to Backblaze first (primary)
|
|
488
518
|
const backblazeResult = await handleBackblazeUpload({
|
|
489
|
-
|
|
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,
|
|
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
|
-
|
|
553
|
+
auth,
|
|
525
554
|
backblazeSuccess: backblazeResult.success,
|
|
526
555
|
baseUrl: apiUrl,
|
|
527
|
-
bytes:
|
|
556
|
+
bytes: source.size,
|
|
528
557
|
id,
|
|
529
558
|
metadata,
|
|
530
559
|
path: tempPath,
|
|
531
|
-
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
|
|
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,
|
|
579
|
+
async function uploadToBackblaze(uploadUrl, authorizationToken, fileName, source, debug) {
|
|
550
580
|
try {
|
|
551
|
-
|
|
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(
|
|
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':
|
|
567
|
-
'Content-Type':
|
|
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
|
-
|
|
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,
|
|
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: ${
|
|
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
|
|
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,
|
|
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
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
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.
|
|
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'));
|