securewithtrace 0.40.1 → 0.41.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 (2) hide show
  1. package/dist/index.js +297 -14
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -29594,6 +29594,11 @@ var ResumeCodeUploadResponseSchema = external_exports.object({
29594
29594
  var ListCodeUploadsResponseSchema = external_exports.object({
29595
29595
  uploads: external_exports.array(CodeUploadSchema)
29596
29596
  });
29597
+ var DownloadCodeUploadResponseSchema = external_exports.object({
29598
+ upload: CodeUploadSchema,
29599
+ downloadUrl: external_exports.url(),
29600
+ expiresAt: external_exports.coerce.date()
29601
+ });
29597
29602
 
29598
29603
  // ../../packages/api-contracts/src/contracts/v1/code-upload.ts
29599
29604
  var c12 = initContract();
@@ -29647,6 +29652,15 @@ var codeUploadContract = c12.router({
29647
29652
  ...commonErrors
29648
29653
  }
29649
29654
  },
29655
+ downloadCodeUpload: {
29656
+ method: "GET",
29657
+ path: "/organizations/:organizationId/code-uploads/:codeUploadId/download",
29658
+ pathParams: CodeUploadIdParamsSchema,
29659
+ responses: {
29660
+ 200: DownloadCodeUploadResponseSchema,
29661
+ ...commonErrors
29662
+ }
29663
+ },
29650
29664
  deleteCodeUpload: {
29651
29665
  method: "DELETE",
29652
29666
  path: "/organizations/:organizationId/code-uploads/:codeUploadId",
@@ -37241,7 +37255,7 @@ async function requestDeviceAuthorization(clientId) {
37241
37255
  }
37242
37256
  return await response.json();
37243
37257
  }
37244
- var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
37258
+ var sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
37245
37259
  async function pollForToken(clientId, deviceCode, options = {}) {
37246
37260
  const expiresIn = options.expiresIn ?? 300;
37247
37261
  let interval = options.interval ?? 5;
@@ -37926,18 +37940,18 @@ var proto = Object.defineProperties(() => {
37926
37940
  }
37927
37941
  }
37928
37942
  });
37929
- var createStyler = (open2, close, parent) => {
37943
+ var createStyler = (open3, close, parent) => {
37930
37944
  let openAll;
37931
37945
  let closeAll;
37932
37946
  if (parent === void 0) {
37933
- openAll = open2;
37947
+ openAll = open3;
37934
37948
  closeAll = close;
37935
37949
  } else {
37936
- openAll = parent.openAll + open2;
37950
+ openAll = parent.openAll + open3;
37937
37951
  closeAll = close + parent.closeAll;
37938
37952
  }
37939
37953
  return {
37940
- open: open2,
37954
+ open: open3,
37941
37955
  close,
37942
37956
  openAll,
37943
37957
  closeAll,
@@ -47291,6 +47305,11 @@ Examples:
47291
47305
  });
47292
47306
  }
47293
47307
 
47308
+ // src/upload/download.ts
47309
+ import { createHash as createHash2 } from "crypto";
47310
+ import { open as open2, rename, rm, stat } from "fs/promises";
47311
+ import { basename as basename4, join as join7, resolve } from "path";
47312
+
47294
47313
  // src/upload/prepare.ts
47295
47314
  import { createHash } from "crypto";
47296
47315
  import { createReadStream, statSync as statSync4 } from "fs";
@@ -47434,6 +47453,128 @@ async function prepareUploadFile(filePath, options = {}) {
47434
47453
  };
47435
47454
  }
47436
47455
 
47456
+ // src/upload/download.ts
47457
+ var CodeUploadDownloadError = class extends Error {
47458
+ constructor(message) {
47459
+ super(message);
47460
+ this.name = "CodeUploadDownloadError";
47461
+ }
47462
+ };
47463
+ function safeDownloadFilename(upload) {
47464
+ const extension = requiredExtensionForKind(upload.artifactKind);
47465
+ const fallback = `app${extension}`;
47466
+ const stripped = Array.from(basename4(upload.originalFilename.replace(/\\/g, "/")), (char) => {
47467
+ const code = char.codePointAt(0) ?? 0;
47468
+ return code <= 31 || code === 127 ? "_" : char;
47469
+ }).join("").trim();
47470
+ if (stripped.length === 0 || stripped === "." || stripped === "..") return fallback;
47471
+ return stripped.toLowerCase().endsWith(extension) ? stripped : `${stripped}${extension}`;
47472
+ }
47473
+ async function resolveDownloadDestination(upload, outOption) {
47474
+ const filename = safeDownloadFilename(upload);
47475
+ if (outOption === void 0) return resolve(process.cwd(), filename);
47476
+ const target = resolve(outOption);
47477
+ try {
47478
+ const info = await stat(target);
47479
+ if (info.isDirectory()) return join7(target, filename);
47480
+ } catch {
47481
+ }
47482
+ return target;
47483
+ }
47484
+ async function pathExists(path) {
47485
+ try {
47486
+ await stat(path);
47487
+ return true;
47488
+ } catch {
47489
+ return false;
47490
+ }
47491
+ }
47492
+ async function downloadCodeUploadToFile(upload, downloadUrl, destination, options = {}) {
47493
+ const fetchImpl = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
47494
+ const partialPath = `${destination}.part`;
47495
+ let response;
47496
+ try {
47497
+ response = await fetchImpl(downloadUrl, { method: "GET", redirect: "manual" });
47498
+ } catch (error48) {
47499
+ const detail = error48 instanceof Error ? error48.message : "network error";
47500
+ throw new CodeUploadDownloadError(`Download from storage failed: ${detail}.`);
47501
+ }
47502
+ if (!response.ok || response.body === null) {
47503
+ if (response.body !== null) await response.body.cancel();
47504
+ throw new CodeUploadDownloadError(
47505
+ `Storage answered HTTP ${response.status} instead of the build bytes. Retry; the download link may have expired.`
47506
+ );
47507
+ }
47508
+ const contentLength = response.headers.get("content-length");
47509
+ if (contentLength !== null) {
47510
+ const declared = Number(contentLength);
47511
+ if (!Number.isSafeInteger(declared) || declared !== upload.sizeBytes) {
47512
+ await response.body.cancel();
47513
+ throw new CodeUploadDownloadError(
47514
+ `Storage declared ${contentLength} bytes; the upload record says ${upload.sizeBytes}.`
47515
+ );
47516
+ }
47517
+ }
47518
+ await rm(partialPath, { force: true });
47519
+ const file2 = await open2(partialPath, "wx");
47520
+ const hash2 = createHash2("sha256");
47521
+ const reader = response.body.getReader();
47522
+ let downloadedBytes = 0;
47523
+ options.onProgress?.(progress(0, upload.sizeBytes));
47524
+ try {
47525
+ for (; ; ) {
47526
+ const chunk = await reader.read();
47527
+ if (chunk.done) break;
47528
+ downloadedBytes += chunk.value.byteLength;
47529
+ if (downloadedBytes > upload.sizeBytes) {
47530
+ await reader.cancel();
47531
+ throw new CodeUploadDownloadError(
47532
+ `Storage sent more than the ${upload.sizeBytes} bytes on record.`
47533
+ );
47534
+ }
47535
+ hash2.update(chunk.value);
47536
+ await writeAll(file2, chunk.value);
47537
+ options.onProgress?.(progress(downloadedBytes, upload.sizeBytes));
47538
+ }
47539
+ await file2.close();
47540
+ if (downloadedBytes !== upload.sizeBytes) {
47541
+ throw new CodeUploadDownloadError(
47542
+ `Download ended after ${downloadedBytes} of ${upload.sizeBytes} bytes.`
47543
+ );
47544
+ }
47545
+ const digest = hash2.digest("hex");
47546
+ if (digest !== upload.archiveSha256) {
47547
+ throw new CodeUploadDownloadError(
47548
+ "Downloaded bytes do not match the SHA-256 recorded at upload time."
47549
+ );
47550
+ }
47551
+ await rename(partialPath, destination);
47552
+ } catch (error48) {
47553
+ await file2.close().catch(() => void 0);
47554
+ await rm(partialPath, { force: true });
47555
+ if (error48 instanceof CodeUploadDownloadError) throw error48;
47556
+ const detail = error48 instanceof Error ? error48.message : "unknown error";
47557
+ throw new CodeUploadDownloadError(`Download from storage failed: ${detail}.`);
47558
+ }
47559
+ }
47560
+ function progress(downloadedBytes, totalBytes) {
47561
+ return {
47562
+ downloadedBytes,
47563
+ totalBytes,
47564
+ fraction: totalBytes === 0 ? 1 : downloadedBytes / totalBytes
47565
+ };
47566
+ }
47567
+ async function writeAll(file2, bytes) {
47568
+ let offset = 0;
47569
+ while (offset < bytes.byteLength) {
47570
+ const { bytesWritten } = await file2.write(bytes, offset, bytes.byteLength - offset);
47571
+ if (bytesWritten === 0) {
47572
+ throw new CodeUploadDownloadError("Writing the build to disk stalled.");
47573
+ }
47574
+ offset += bytesWritten;
47575
+ }
47576
+ }
47577
+
47437
47578
  // src/upload/multipart.ts
47438
47579
  var MAX_CONCURRENT_UPLOADS = 3;
47439
47580
  var MAX_PART_ATTEMPTS = 3;
@@ -47446,7 +47587,7 @@ async function uploadCodeArchiveParts(filePath, totalBytes, partSizeBytes, parts
47446
47587
  if (!Number.isSafeInteger(uploadedBytes) || uploadedBytes < 0 || uploadedBytes > totalBytes) {
47447
47588
  throw new Error("Invalid multipart progress: uploaded byte count is outside the archive.");
47448
47589
  }
47449
- options.onProgress?.(progress(uploadedBytes, totalBytes));
47590
+ options.onProgress?.(progress2(uploadedBytes, totalBytes));
47450
47591
  if (parts.length === 0) return;
47451
47592
  const urls = new Map(parts.map((part) => [part.partNumber, part.uploadUrl]));
47452
47593
  const refreshUrls = createUrlRefresher(urls, options.refreshParts);
@@ -47463,7 +47604,7 @@ async function uploadCodeArchiveParts(filePath, totalBytes, partSizeBytes, parts
47463
47604
  const body = await readFileSlice(filePath, start, end);
47464
47605
  await uploadPartWithRetry(part.partNumber, urls, body, fetchPart, wait, refreshUrls);
47465
47606
  uploadedBytes += body.byteLength;
47466
- options.onProgress?.(progress(uploadedBytes, totalBytes));
47607
+ options.onProgress?.(progress2(uploadedBytes, totalBytes));
47467
47608
  }
47468
47609
  })
47469
47610
  );
@@ -47551,7 +47692,7 @@ function assertPresignedUploadUrl(value) {
47551
47692
  throw new Error("Invalid multipart upload: part URLs must use HTTPS.");
47552
47693
  }
47553
47694
  }
47554
- function progress(uploadedBytes, totalBytes) {
47695
+ function progress2(uploadedBytes, totalBytes) {
47555
47696
  return {
47556
47697
  uploadedBytes,
47557
47698
  totalBytes,
@@ -47559,13 +47700,13 @@ function progress(uploadedBytes, totalBytes) {
47559
47700
  };
47560
47701
  }
47561
47702
  function defaultWait(milliseconds) {
47562
- return new Promise((resolve) => setTimeout(resolve, milliseconds));
47703
+ return new Promise((resolve2) => setTimeout(resolve2, milliseconds));
47563
47704
  }
47564
47705
 
47565
47706
  // src/upload/commands.ts
47566
47707
  var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["READY", "REJECTED", "FAILED", "DELETING"]);
47567
47708
  var DEFAULT_RUNTIME = {
47568
- wait: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
47709
+ wait: (milliseconds) => new Promise((resolve2) => setTimeout(resolve2, milliseconds)),
47569
47710
  pollIntervalMs: 3e3,
47570
47711
  fetch: (input, init) => globalThis.fetch(input, init)
47571
47712
  };
@@ -47678,6 +47819,53 @@ function failReadOp5(action, status, body) {
47678
47819
  );
47679
47820
  process.exit(1);
47680
47821
  }
47822
+ function failDownloadOp(codeUploadId, status, body) {
47823
+ const { message, errorCode } = readErrorFields(body);
47824
+ if (status === UNAUTHORIZED_STATUS2) {
47825
+ exitAuthFailed();
47826
+ }
47827
+ if (status === 403) {
47828
+ console.error(
47829
+ colorError(
47830
+ "Only an organization admin (or Trace staff) on a pentest-enabled organization can download builds."
47831
+ )
47832
+ );
47833
+ process.exit(1);
47834
+ }
47835
+ if (status === 404) {
47836
+ console.error(colorError(message ?? "No code upload with that id."));
47837
+ process.exit(1);
47838
+ }
47839
+ if (status === 409) {
47840
+ if (errorCode === "CODE_UPLOAD_NOT_BUILD") {
47841
+ console.error(
47842
+ colorError(
47843
+ message ?? "Only APK and IPA builds can be downloaded. Source archives stay on the server."
47844
+ )
47845
+ );
47846
+ } else if (errorCode === "CODE_UPLOAD_NOT_READY") {
47847
+ console.error(
47848
+ colorError(
47849
+ message ?? "The build must be ready and pass malware scanning before it can be downloaded."
47850
+ )
47851
+ );
47852
+ console.error(`Wait for it with: tracecli upload wait ${codeUploadId}`);
47853
+ } else {
47854
+ console.error(colorError(message ?? "Conflict (status 409)."));
47855
+ }
47856
+ process.exit(1);
47857
+ }
47858
+ if (status === 503) {
47859
+ console.error(
47860
+ colorError(message ?? "Source archive uploads are not configured on this server.")
47861
+ );
47862
+ process.exit(1);
47863
+ }
47864
+ console.error(
47865
+ colorError(`Failed to download build (status ${status})${message ? `: ${message}` : "."}`)
47866
+ );
47867
+ process.exit(1);
47868
+ }
47681
47869
  function resumeHint(codeUploadId, filePath) {
47682
47870
  return `Resume with: tracecli upload resume ${codeUploadId} ${filePath}`;
47683
47871
  }
@@ -47767,11 +47955,18 @@ function outputUpload(upload, output) {
47767
47955
  }
47768
47956
  console.log(renderUploadDetail(upload));
47769
47957
  }
47770
- function writeProgress(progress2) {
47958
+ function writeProgress(progress3) {
47771
47959
  if (!process.stderr.isTTY) return;
47772
- const pct = Math.floor(progress2.fraction * 100);
47960
+ const pct = Math.floor(progress3.fraction * 100);
47773
47961
  process.stderr.write(
47774
- `\rUploading ${pct}% (${formatArchiveBytes(progress2.uploadedBytes)} / ${formatArchiveBytes(progress2.totalBytes)})`
47962
+ `\rUploading ${pct}% (${formatArchiveBytes(progress3.uploadedBytes)} / ${formatArchiveBytes(progress3.totalBytes)})`
47963
+ );
47964
+ }
47965
+ function writeDownloadProgress(progress3) {
47966
+ if (!process.stderr.isTTY) return;
47967
+ const pct = Math.floor(progress3.fraction * 100);
47968
+ process.stderr.write(
47969
+ `\rDownloading ${pct}% (${formatArchiveBytes(progress3.downloadedBytes)} / ${formatArchiveBytes(progress3.totalBytes)})`
47775
47970
  );
47776
47971
  }
47777
47972
  function finishProgress() {
@@ -48085,6 +48280,76 @@ async function runGet4(codeUploadIdArg, options) {
48085
48280
  const upload = await requireUpload(organizationId, codeUploadId);
48086
48281
  outputUpload(upload, output);
48087
48282
  }
48283
+ async function runDownload(codeUploadIdArg, options, runtime) {
48284
+ const output = parseOutput(options.output);
48285
+ const codeUploadId = requireUuid(codeUploadIdArg, "code upload id");
48286
+ const organizationId = await resolveOrganizationId();
48287
+ const result = await authenticatedRequest(
48288
+ (client) => client.codeUpload.downloadCodeUpload({
48289
+ params: { organizationId, codeUploadId }
48290
+ })
48291
+ );
48292
+ if (result.status !== SUCCESS_STATUS) {
48293
+ failDownloadOp(codeUploadId, result.status, result.body);
48294
+ }
48295
+ const { upload, downloadUrl } = result.body;
48296
+ const destination = await resolveDownloadDestination(upload, options.out);
48297
+ if (!options.force && await pathExists(destination)) {
48298
+ console.error(
48299
+ colorError(`${destination} already exists. Pass --force to overwrite it or --out <path>.`)
48300
+ );
48301
+ process.exit(1);
48302
+ }
48303
+ if (output === "table") {
48304
+ console.error(
48305
+ `Downloading ${sanitizeDisplay(upload.displayName)} (${formatArchiveBytes(upload.sizeBytes)}) to ${destination}`
48306
+ );
48307
+ }
48308
+ try {
48309
+ await downloadCodeUploadToFile(upload, downloadUrl, destination, {
48310
+ fetch: runtime.fetch,
48311
+ onProgress: writeDownloadProgress
48312
+ });
48313
+ finishProgress();
48314
+ } catch (error48) {
48315
+ finishProgress();
48316
+ const detail = error48 instanceof Error ? error48.message : "Download from storage failed.";
48317
+ console.error(colorError(detail));
48318
+ process.exit(1);
48319
+ }
48320
+ if (output === "json") {
48321
+ console.log(
48322
+ JSON.stringify(
48323
+ {
48324
+ upload,
48325
+ path: destination,
48326
+ sizeBytes: upload.sizeBytes,
48327
+ archiveSha256: upload.archiveSha256
48328
+ },
48329
+ null,
48330
+ 2
48331
+ )
48332
+ );
48333
+ return;
48334
+ }
48335
+ if (output === "tsv") {
48336
+ console.log(["codeUploadId", "path", "sizeBytes", "archiveSha256"].join(" "));
48337
+ console.log(
48338
+ [
48339
+ tsvSafe(upload.codeUploadId),
48340
+ tsvSafe(destination),
48341
+ String(upload.sizeBytes),
48342
+ tsvSafe(upload.archiveSha256)
48343
+ ].join(" ")
48344
+ );
48345
+ return;
48346
+ }
48347
+ console.log(
48348
+ colorSuccess(
48349
+ `Saved ${sanitizeDisplay(upload.displayName)} to ${destination} (${formatArchiveBytes(upload.sizeBytes)}, SHA-256 verified).`
48350
+ )
48351
+ );
48352
+ }
48088
48353
  async function runWait(codeUploadIdArg, options, runtime) {
48089
48354
  const output = parseOutput(options.output);
48090
48355
  const codeUploadId = requireUuid(codeUploadIdArg, "code upload id");
@@ -48209,6 +48474,23 @@ Examples:
48209
48474
  await runResume(codeUploadId, file2, options, runtime);
48210
48475
  });
48211
48476
  }
48477
+ function createDownloadCommand(runtime) {
48478
+ return new Command("download").description("Download an APK or IPA build that finished malware scanning").argument("<codeUploadId>", "Upload UUID of the build").option("--out <path>", "File to write, or an existing directory (default: working directory)").option("-f, --force", "Overwrite an existing file").option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").addHelpText(
48479
+ "after",
48480
+ `
48481
+ Only READY builds with a clean malware scan can be downloaded. Source ZIPs
48482
+ are not downloadable. The file is written under the name it was uploaded
48483
+ with and its size and SHA-256 are checked against the upload record before
48484
+ it is moved into place.
48485
+
48486
+ Examples:
48487
+ tracecli upload download <codeUploadId>
48488
+ tracecli upload download <codeUploadId> --out ./builds/
48489
+ tracecli upload download <codeUploadId> --out ./release.apk --force`
48490
+ ).action(async (codeUploadId, options) => {
48491
+ await runDownload(codeUploadId, options, runtime);
48492
+ });
48493
+ }
48212
48494
  function createWaitCommand(runtime) {
48213
48495
  return new Command("wait").description("Wait until an upload finishes malware scanning and validation").argument("<codeUploadId>", "Upload UUID").option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").action(async (codeUploadId, options) => {
48214
48496
  await runWait(codeUploadId, options, runtime);
@@ -48234,11 +48516,12 @@ Examples:
48234
48516
  }
48235
48517
  function createUploadCommand(runtime = {}) {
48236
48518
  const resolved = resolveRuntime(runtime);
48237
- const upload = new Command("upload").alias("code-upload").description("Upload source archives (and separately, mobile builds) for pentest");
48519
+ const upload = new Command("upload").alias("code-upload").description("Upload source archives and mobile builds for pentest; download builds back");
48238
48520
  upload.addCommand(createCreateCommand5(resolved));
48239
48521
  upload.addCommand(createBuildCommand(resolved));
48240
48522
  upload.addCommand(createListCommand7());
48241
48523
  upload.addCommand(createGetCommand4());
48524
+ upload.addCommand(createDownloadCommand(resolved));
48242
48525
  upload.addCommand(createResumeCommand(resolved));
48243
48526
  upload.addCommand(createWaitCommand(resolved));
48244
48527
  upload.addCommand(createDeleteCommand5());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securewithtrace",
3
- "version": "0.40.1",
3
+ "version": "0.41.0",
4
4
  "description": "Official Trace CLI for vulnerability intelligence in your terminal",
5
5
  "type": "module",
6
6
  "bin": {