securewithtrace 0.40.0 → 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 +301 -17
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -22144,7 +22144,7 @@ var AgentSessionFailureReasonValues = [
22144
22144
  "INTERNAL"
22145
22145
  ];
22146
22146
  var AgentSessionFailureReason = external_exports.enum(AgentSessionFailureReasonValues);
22147
- var AgentSessionProviderValues = ["BEDROCK", "ANTHROPIC"];
22147
+ var AgentSessionProviderValues = ["BEDROCK", "ANTHROPIC", "OPENAI"];
22148
22148
  var AgentSessionProvider = external_exports.enum(AgentSessionProviderValues);
22149
22149
  var AgentSessionEventKindValues = [
22150
22150
  "AGENT_TEXT",
@@ -22283,6 +22283,9 @@ var AgentSessionSummarySchema = external_exports.object({
22283
22283
  status: AgentSessionStatus,
22284
22284
  waitingReason: AgentSessionWaitingReason.nullable(),
22285
22285
  failureReason: AgentSessionFailureReason.nullable(),
22286
+ /** Sanitized, shape-only failure description (e.g. a provider refusal message) — never customer
22287
+ * data. In the summary so the staff console can show WHY a listed session failed. */
22288
+ failureDetails: external_exports.string().nullable(),
22286
22289
  objective: external_exports.string(),
22287
22290
  provider: AgentSessionProvider,
22288
22291
  model: external_exports.string(),
@@ -22305,8 +22308,6 @@ var AgentSessionRepositoryPinSchema = external_exports.object({
22305
22308
  commitSha: external_exports.string()
22306
22309
  });
22307
22310
  var AgentSessionDetailSchema = AgentSessionSummarySchema.extend({
22308
- /** Sanitized, shape-only failure description — never customer data. */
22309
- failureDetails: external_exports.string().nullable(),
22310
22311
  sdkSessionId: external_exports.string().nullable(),
22311
22312
  promptVersion: external_exports.string(),
22312
22313
  toolsetVersion: external_exports.string(),
@@ -29593,6 +29594,11 @@ var ResumeCodeUploadResponseSchema = external_exports.object({
29593
29594
  var ListCodeUploadsResponseSchema = external_exports.object({
29594
29595
  uploads: external_exports.array(CodeUploadSchema)
29595
29596
  });
29597
+ var DownloadCodeUploadResponseSchema = external_exports.object({
29598
+ upload: CodeUploadSchema,
29599
+ downloadUrl: external_exports.url(),
29600
+ expiresAt: external_exports.coerce.date()
29601
+ });
29596
29602
 
29597
29603
  // ../../packages/api-contracts/src/contracts/v1/code-upload.ts
29598
29604
  var c12 = initContract();
@@ -29646,6 +29652,15 @@ var codeUploadContract = c12.router({
29646
29652
  ...commonErrors
29647
29653
  }
29648
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
+ },
29649
29664
  deleteCodeUpload: {
29650
29665
  method: "DELETE",
29651
29666
  path: "/organizations/:organizationId/code-uploads/:codeUploadId",
@@ -37240,7 +37255,7 @@ async function requestDeviceAuthorization(clientId) {
37240
37255
  }
37241
37256
  return await response.json();
37242
37257
  }
37243
- var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
37258
+ var sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
37244
37259
  async function pollForToken(clientId, deviceCode, options = {}) {
37245
37260
  const expiresIn = options.expiresIn ?? 300;
37246
37261
  let interval = options.interval ?? 5;
@@ -37925,18 +37940,18 @@ var proto = Object.defineProperties(() => {
37925
37940
  }
37926
37941
  }
37927
37942
  });
37928
- var createStyler = (open2, close, parent) => {
37943
+ var createStyler = (open3, close, parent) => {
37929
37944
  let openAll;
37930
37945
  let closeAll;
37931
37946
  if (parent === void 0) {
37932
- openAll = open2;
37947
+ openAll = open3;
37933
37948
  closeAll = close;
37934
37949
  } else {
37935
- openAll = parent.openAll + open2;
37950
+ openAll = parent.openAll + open3;
37936
37951
  closeAll = close + parent.closeAll;
37937
37952
  }
37938
37953
  return {
37939
- open: open2,
37954
+ open: open3,
37940
37955
  close,
37941
37956
  openAll,
37942
37957
  closeAll,
@@ -47290,6 +47305,11 @@ Examples:
47290
47305
  });
47291
47306
  }
47292
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
+
47293
47313
  // src/upload/prepare.ts
47294
47314
  import { createHash } from "crypto";
47295
47315
  import { createReadStream, statSync as statSync4 } from "fs";
@@ -47433,6 +47453,128 @@ async function prepareUploadFile(filePath, options = {}) {
47433
47453
  };
47434
47454
  }
47435
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
+
47436
47578
  // src/upload/multipart.ts
47437
47579
  var MAX_CONCURRENT_UPLOADS = 3;
47438
47580
  var MAX_PART_ATTEMPTS = 3;
@@ -47445,7 +47587,7 @@ async function uploadCodeArchiveParts(filePath, totalBytes, partSizeBytes, parts
47445
47587
  if (!Number.isSafeInteger(uploadedBytes) || uploadedBytes < 0 || uploadedBytes > totalBytes) {
47446
47588
  throw new Error("Invalid multipart progress: uploaded byte count is outside the archive.");
47447
47589
  }
47448
- options.onProgress?.(progress(uploadedBytes, totalBytes));
47590
+ options.onProgress?.(progress2(uploadedBytes, totalBytes));
47449
47591
  if (parts.length === 0) return;
47450
47592
  const urls = new Map(parts.map((part) => [part.partNumber, part.uploadUrl]));
47451
47593
  const refreshUrls = createUrlRefresher(urls, options.refreshParts);
@@ -47462,7 +47604,7 @@ async function uploadCodeArchiveParts(filePath, totalBytes, partSizeBytes, parts
47462
47604
  const body = await readFileSlice(filePath, start, end);
47463
47605
  await uploadPartWithRetry(part.partNumber, urls, body, fetchPart, wait, refreshUrls);
47464
47606
  uploadedBytes += body.byteLength;
47465
- options.onProgress?.(progress(uploadedBytes, totalBytes));
47607
+ options.onProgress?.(progress2(uploadedBytes, totalBytes));
47466
47608
  }
47467
47609
  })
47468
47610
  );
@@ -47550,7 +47692,7 @@ function assertPresignedUploadUrl(value) {
47550
47692
  throw new Error("Invalid multipart upload: part URLs must use HTTPS.");
47551
47693
  }
47552
47694
  }
47553
- function progress(uploadedBytes, totalBytes) {
47695
+ function progress2(uploadedBytes, totalBytes) {
47554
47696
  return {
47555
47697
  uploadedBytes,
47556
47698
  totalBytes,
@@ -47558,13 +47700,13 @@ function progress(uploadedBytes, totalBytes) {
47558
47700
  };
47559
47701
  }
47560
47702
  function defaultWait(milliseconds) {
47561
- return new Promise((resolve) => setTimeout(resolve, milliseconds));
47703
+ return new Promise((resolve2) => setTimeout(resolve2, milliseconds));
47562
47704
  }
47563
47705
 
47564
47706
  // src/upload/commands.ts
47565
47707
  var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["READY", "REJECTED", "FAILED", "DELETING"]);
47566
47708
  var DEFAULT_RUNTIME = {
47567
- wait: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
47709
+ wait: (milliseconds) => new Promise((resolve2) => setTimeout(resolve2, milliseconds)),
47568
47710
  pollIntervalMs: 3e3,
47569
47711
  fetch: (input, init) => globalThis.fetch(input, init)
47570
47712
  };
@@ -47677,6 +47819,53 @@ function failReadOp5(action, status, body) {
47677
47819
  );
47678
47820
  process.exit(1);
47679
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
+ }
47680
47869
  function resumeHint(codeUploadId, filePath) {
47681
47870
  return `Resume with: tracecli upload resume ${codeUploadId} ${filePath}`;
47682
47871
  }
@@ -47766,11 +47955,18 @@ function outputUpload(upload, output) {
47766
47955
  }
47767
47956
  console.log(renderUploadDetail(upload));
47768
47957
  }
47769
- function writeProgress(progress2) {
47958
+ function writeProgress(progress3) {
47959
+ if (!process.stderr.isTTY) return;
47960
+ const pct = Math.floor(progress3.fraction * 100);
47961
+ process.stderr.write(
47962
+ `\rUploading ${pct}% (${formatArchiveBytes(progress3.uploadedBytes)} / ${formatArchiveBytes(progress3.totalBytes)})`
47963
+ );
47964
+ }
47965
+ function writeDownloadProgress(progress3) {
47770
47966
  if (!process.stderr.isTTY) return;
47771
- const pct = Math.floor(progress2.fraction * 100);
47967
+ const pct = Math.floor(progress3.fraction * 100);
47772
47968
  process.stderr.write(
47773
- `\rUploading ${pct}% (${formatArchiveBytes(progress2.uploadedBytes)} / ${formatArchiveBytes(progress2.totalBytes)})`
47969
+ `\rDownloading ${pct}% (${formatArchiveBytes(progress3.downloadedBytes)} / ${formatArchiveBytes(progress3.totalBytes)})`
47774
47970
  );
47775
47971
  }
47776
47972
  function finishProgress() {
@@ -48084,6 +48280,76 @@ async function runGet4(codeUploadIdArg, options) {
48084
48280
  const upload = await requireUpload(organizationId, codeUploadId);
48085
48281
  outputUpload(upload, output);
48086
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
+ }
48087
48353
  async function runWait(codeUploadIdArg, options, runtime) {
48088
48354
  const output = parseOutput(options.output);
48089
48355
  const codeUploadId = requireUuid(codeUploadIdArg, "code upload id");
@@ -48208,6 +48474,23 @@ Examples:
48208
48474
  await runResume(codeUploadId, file2, options, runtime);
48209
48475
  });
48210
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
+ }
48211
48494
  function createWaitCommand(runtime) {
48212
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) => {
48213
48496
  await runWait(codeUploadId, options, runtime);
@@ -48233,11 +48516,12 @@ Examples:
48233
48516
  }
48234
48517
  function createUploadCommand(runtime = {}) {
48235
48518
  const resolved = resolveRuntime(runtime);
48236
- 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");
48237
48520
  upload.addCommand(createCreateCommand5(resolved));
48238
48521
  upload.addCommand(createBuildCommand(resolved));
48239
48522
  upload.addCommand(createListCommand7());
48240
48523
  upload.addCommand(createGetCommand4());
48524
+ upload.addCommand(createDownloadCommand(resolved));
48241
48525
  upload.addCommand(createResumeCommand(resolved));
48242
48526
  upload.addCommand(createWaitCommand(resolved));
48243
48527
  upload.addCommand(createDeleteCommand5());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securewithtrace",
3
- "version": "0.40.0",
3
+ "version": "0.41.0",
4
4
  "description": "Official Trace CLI for vulnerability intelligence in your terminal",
5
5
  "type": "module",
6
6
  "bin": {