securewithtrace 0.44.0 → 0.45.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/dist/index.js CHANGED
@@ -22640,7 +22640,11 @@ var agentSessionContract = c2.router({
22640
22640
  });
22641
22641
 
22642
22642
  // ../../packages/api-contracts/src/schemas/api/v1/agent-task.api.ts
22643
- var AGENT_TASK_TYPES = ["ENGAGEMENT_BRIEF", "VERIFY_APPLICATION_SETUP"];
22643
+ var AGENT_TASK_TYPES = [
22644
+ "ENGAGEMENT_BRIEF",
22645
+ "VERIFY_APPLICATION_SETUP",
22646
+ "TRIAGE_DISTILL"
22647
+ ];
22644
22648
  var AgentTaskTypeSchema = external_exports.enum(AGENT_TASK_TYPES);
22645
22649
  var TRIGGERABLE_AGENT_TASK_TYPES = ["ENGAGEMENT_BRIEF"];
22646
22650
  var TriggerableAgentTaskTypeSchema = external_exports.enum(TRIGGERABLE_AGENT_TASK_TYPES);
@@ -22701,6 +22705,7 @@ var AgentTaskResponseSchema = external_exports.object({
22701
22705
  status: AgentTaskStatusSchema,
22702
22706
  subjectPentestId: external_exports.uuid().nullable(),
22703
22707
  subjectApplicationId: external_exports.uuid().nullable(),
22708
+ subjectVulnerabilityId: external_exports.uuid().nullable(),
22704
22709
  capabilities: external_exports.array(external_exports.string()),
22705
22710
  /**
22706
22711
  * Null until the worker claims the task and records what it actually ran —
@@ -23178,10 +23183,22 @@ var RepositoryResponseSchema = external_exports.object({
23178
23183
  primaryLanguage: external_exports.string().nullable().optional(),
23179
23184
  /** Staff-set opt-in: automated PR scans run on this repo even without a FULL SAST baseline. */
23180
23185
  prScansEnabled: external_exports.boolean(),
23186
+ /** Customer-set gate: false = no scans of any kind run for this repo until it is included again. */
23187
+ includedInScanning: external_exports.boolean(),
23181
23188
  createdAt: external_exports.date(),
23182
23189
  updatedAt: external_exports.date()
23183
23190
  });
23184
23191
  var ListRepositoriesResponseSchema = external_exports.array(RepositoryResponseSchema);
23192
+ var SetScanningInclusionRequestSchema = external_exports.object({
23193
+ repositoryIds: external_exports.array(external_exports.uuid()).min(1).max(500),
23194
+ includedInScanning: external_exports.boolean()
23195
+ });
23196
+ var SetScanningInclusionResponseSchema = external_exports.object({
23197
+ /** Rows whose flag now matches the request (already-matching rows count). */
23198
+ updatedCount: external_exports.number().int().min(0),
23199
+ /** IDs from the request that are not repositories of this organization. */
23200
+ notFoundRepositoryIds: external_exports.array(external_exports.uuid())
23201
+ });
23185
23202
  var SetPrScansEnabledRequestSchema = external_exports.object({
23186
23203
  enabled: external_exports.boolean()
23187
23204
  });
@@ -23666,6 +23683,7 @@ var ReportPdfResponseSchema = external_exports.object({
23666
23683
  var GetReportPdfResponseSchema = external_exports.object({
23667
23684
  report: ReportPdfResponseSchema.nullable()
23668
23685
  });
23686
+ var CvssVersionSchema = external_exports.enum(["3.1", "4.0"]);
23669
23687
  var ReportCanvasFindingSchema = external_exports.object({
23670
23688
  pentestFindingId: external_exports.uuid(),
23671
23689
  displayId: external_exports.int().min(1),
@@ -23819,6 +23837,12 @@ var ReportCanvasResponseSchema = external_exports.object({
23819
23837
  * project the default (true) — the value the first render will use. */
23820
23838
  includeRemediationTimeline: external_exports.boolean(),
23821
23839
  findings: external_exports.array(ReportCanvasFindingSchema),
23840
+ /** The CVSS version this engagement scores with (its stored vectors, else the
23841
+ * producer default). The editor renders this version's metric grid; the api
23842
+ * rejects the other version on every write, so no finding can differ from it.
23843
+ * Optional for the same reason as `boundApplications` above; consumers default
23844
+ * it to the producer default, which is what an api that predates it scored with. */
23845
+ cvssVersion: CvssVersionSchema.optional(),
23822
23846
  // Counts reuse the module-local Severity/Status counts + totalFindings trio.
23823
23847
  liveSeverityCounts: SeverityCountsSchema,
23824
23848
  liveStatusCounts: StatusCountsSchema,
@@ -24017,6 +24041,11 @@ var IssuedReportSnapshotSchema = external_exports.object({
24017
24041
  * parses those rows for the changelog diff — a required field would throw
24018
24042
  * there and break issuing a new version of any pre-feature report. */
24019
24043
  testType: PentestTestType.optional(),
24044
+ /** The CVSS version the engagement was scored with at issuance: the archival
24045
+ * record of which version the delivered PDF names when its rendered findings
24046
+ * carry no vector to read it from. Optional because every pre-feature snapshot
24047
+ * lacks it, and those were all issued under 3.1. */
24048
+ cvssVersion: CvssVersionSchema.optional(),
24020
24049
  /** Types of the applications bound at issuance: the archival record of which
24021
24050
  * OWASP families the delivered PDF named. Optional for the same reason as
24022
24051
  * `testType` above: a pre-feature snapshot has no value, and
@@ -24813,8 +24842,11 @@ var FindingProseSchema = FindingRewriteJsonSchema.pick({
24813
24842
  remediation: true,
24814
24843
  verification: true
24815
24844
  });
24845
+ var FindingProsePatchSchema = FindingProseSchema.partial().extend({
24846
+ summary: FindingRewriteJsonSchema.shape.summary.unwrap().optional()
24847
+ });
24816
24848
  var ApplyFindingProseRequestSchema = external_exports.object({
24817
- prose: FindingProseSchema,
24849
+ prose: FindingProsePatchSchema,
24818
24850
  /**
24819
24851
  * Optimistic precondition — the finding's `updatedAt` at edit-open. A mismatch
24820
24852
  * means a concurrent writer (staff / report agent / retest) moved the row →
@@ -24836,7 +24868,9 @@ var CreatePentestFindingRequestSchema = external_exports.object({
24836
24868
  category: external_exports.string().min(1).max(200),
24837
24869
  status: PentestFindingStatus.optional(),
24838
24870
  cweIds: external_exports.array(external_exports.string()).optional(),
24871
+ /** `CVSS:3.1/…` or `CVSS:4.0/…`; scored server-side and checked against the engagement's version. */
24839
24872
  cvssVector: external_exports.string().max(200).optional(),
24873
+ /** Accepted for compatibility and ignored: the score is always derived from `cvssVector`. */
24840
24874
  cvssScore: external_exports.number().min(0).max(10).optional(),
24841
24875
  owaspCategory: external_exports.string().max(200).optional(),
24842
24876
  proofOfConcept: external_exports.string().max(5e4).optional(),
@@ -24860,7 +24894,9 @@ var UpdatePentestFindingRequestSchema = external_exports.object({
24860
24894
  category: external_exports.string().min(1).max(200).optional(),
24861
24895
  status: PentestFindingStatus.optional(),
24862
24896
  cweIds: external_exports.array(external_exports.string()).optional(),
24897
+ /** `CVSS:3.1/…` or `CVSS:4.0/…` (scored server-side, version-checked), or null to clear both columns. */
24863
24898
  cvssVector: external_exports.string().max(200).nullable().optional(),
24899
+ /** Accepted for compatibility and ignored: the score is always derived from `cvssVector`. */
24864
24900
  cvssScore: external_exports.number().min(0).max(10).nullable().optional(),
24865
24901
  owaspCategory: external_exports.string().max(200).nullable().optional(),
24866
24902
  proofOfConcept: external_exports.string().max(5e4).nullable().optional(),
@@ -25268,6 +25304,12 @@ var PentestResponseSchema = external_exports.object({
25268
25304
  perHypothesisBudgetCapUsd: external_exports.number(),
25269
25305
  startedAt: external_exports.coerce.date().nullable(),
25270
25306
  endedAt: external_exports.coerce.date().nullable(),
25307
+ /**
25308
+ * Set while staff have curtained the engagement from every customer
25309
+ * principal; null = the customer can open it. `.optional()` for the same
25310
+ * rolling-deploy reason as `applications` above.
25311
+ */
25312
+ hiddenFromCustomerAt: external_exports.coerce.date().nullable().optional(),
25271
25313
  createdById: external_exports.uuid(),
25272
25314
  createdByName: external_exports.string(),
25273
25315
  createdAt: external_exports.coerce.date(),
@@ -25699,6 +25741,27 @@ var PasskeySecretSchema = external_exports.object({
25699
25741
  /** largeBlob extension payload, base64. */
25700
25742
  largeBlob: external_exports.string().max(8192).optional()
25701
25743
  }).strict();
25744
+ var CredentialPasskeySummarySchema = external_exports.object({
25745
+ credentialPasskeyId: external_exports.uuid(),
25746
+ /** Relying party id: the hostname the passkey answers for. */
25747
+ rpId: external_exports.string().min(1).max(253),
25748
+ /**
25749
+ * The user name the site set at creation, read from the envelope. Null when the
25750
+ * site set none, or when `unreadable` is true.
25751
+ */
25752
+ userName: external_exports.string().max(320).nullable(),
25753
+ /**
25754
+ * The envelope decrypted but did not parse as a passkey document: a permanent
25755
+ * state, and the agent skips this row too. A decrypt failure (KMS, a bad auth tag)
25756
+ * is not this flag; it fails the read, as every other envelope read does.
25757
+ */
25758
+ unreadable: external_exports.boolean(),
25759
+ // Coerced, not `z.date()`: the web parses this out of the wire body with
25760
+ // safeParse, where the value is a string.
25761
+ createdAt: external_exports.coerce.date(),
25762
+ /** Moves only when the signature counter moved, the one observable sign of a use. */
25763
+ lastAssertedAt: external_exports.coerce.date().nullable()
25764
+ }).strict();
25702
25765
  var CredentialMfaKindValues = ["TOTP"];
25703
25766
  var CredentialMfaKind = external_exports.enum(CredentialMfaKindValues);
25704
25767
  var MFA_CAPABLE_CREDENTIAL_TYPES = ["USERNAME_PASSWORD", "MAGIC_AUTH"];
@@ -26252,12 +26315,14 @@ var RecordValidationResultRequestSchema = external_exports.object({
26252
26315
  */
26253
26316
  owaspCategory: external_exports.string().min(1).max(120).nullable().optional(),
26254
26317
  /**
26255
- * Agent-assigned CVSS v3.1 base vector for the demonstrated impact
26256
- * (PROVEN-only). The api derives and persists the canonical vector + base
26257
- * score AND the finding severity (from the score's band) server-side; an
26258
- * unparseable explicit vector is rejected (400). The score is NEVER
26259
- * client-supplied. Absent → both cvss columns null and severity falls back
26260
- * to the agent's `severity` field (informational findings carry no vector).
26318
+ * Agent-assigned CVSS base vector (`CVSS:3.1/…` or `CVSS:4.0/…`) for the
26319
+ * demonstrated impact (PROVEN-only). The api derives and persists the
26320
+ * canonical vector + base score AND the finding severity (from the score's
26321
+ * band) server-side; an unparseable explicit vector is rejected (400
26322
+ * INVALID_CVSS_VECTOR), and so is a vector of the other version than the
26323
+ * engagement's stored vectors (400 CVSS_VERSION_MISMATCH). The score is
26324
+ * NEVER client-supplied. Absent → both cvss columns null and severity falls
26325
+ * back to the agent's `severity` field (informational findings carry no vector).
26261
26326
  */
26262
26327
  cvssVector: external_exports.string().min(1).max(120).nullable().optional(),
26263
26328
  /**
@@ -27839,6 +27904,7 @@ var agentSessionInternalContract = c4.router({
27839
27904
  body: OperatorApplyFindingCvssRequestSchema,
27840
27905
  responses: {
27841
27906
  200: OperatorFindingReadSchema,
27907
+ // INVALID_CVSS_VECTOR or CVSS_VERSION_MISMATCH (see the staff applyFindingCvss route).
27842
27908
  400: ErrorResponseSchema,
27843
27909
  401: ErrorResponseSchema,
27844
27910
  404: ErrorResponseSchema,
@@ -29865,11 +29931,12 @@ var CodeSizeLanguageTallySchema = external_exports.object({
29865
29931
  files: external_exports.number().int().nonnegative(),
29866
29932
  code: external_exports.number().int().nonnegative()
29867
29933
  });
29868
- var CodeSizePlatformValues = ["GITHUB", "BITBUCKET"];
29934
+ var CodeSizePlatformValues = ["GITHUB", "BITBUCKET", "UPLOAD"];
29869
29935
  var CodeSizeRepositorySchema = external_exports.object({
29870
29936
  repositoryId: external_exports.uuid(),
29871
29937
  repositoryUrl: external_exports.url(),
29872
29938
  platform: external_exports.enum(CodeSizePlatformValues),
29939
+ displayName: external_exports.string(),
29873
29940
  /** Billable SLOC of the current resolved branch. null = not measured yet. */
29874
29941
  billableSloc: external_exports.number().int().nonnegative().nullable()
29875
29942
  });
@@ -29881,7 +29948,8 @@ var CodeSizeBucketsSchema = external_exports.object({
29881
29948
  vendored_generated: CodeSizeBucketTallySchema
29882
29949
  });
29883
29950
  var OrganizationCodeSizeResponseSchema = external_exports.object({
29884
- /** All CONNECTED GitHub/Bitbucket repos. Server-sorted: measured first by
29951
+ /** All CONNECTED GitHub/Bitbucket repos plus READY source uploads.
29952
+ * Server-sorted: measured first by
29885
29953
  * billableSloc desc (ties repositoryUrl asc), then unmeasured by
29886
29954
  * repositoryUrl asc — clients render as-is (spec-pr-2 §3.3). */
29887
29955
  repositories: external_exports.array(CodeSizeRepositorySchema),
@@ -29902,7 +29970,9 @@ var StaffCodeSizeRepositorySchema = external_exports.object({
29902
29970
  repositoryId: external_exports.uuid(),
29903
29971
  repositoryUrl: external_exports.url(),
29904
29972
  platform: external_exports.enum(CodeSizePlatformValues),
29905
- /** Resolved branch the measurement is keyed on (scanningBranch ?? default). */
29973
+ displayName: external_exports.string(),
29974
+ /** Resolved branch the measurement is keyed on (scanningBranch ?? default;
29975
+ * `snapshot` for uploads). */
29906
29976
  branch: external_exports.string(),
29907
29977
  /** null = no successful measurement for the current resolved branch. */
29908
29978
  billableSloc: external_exports.number().int().nonnegative().nullable(),
@@ -29920,7 +29990,8 @@ var StaffOrganizationCodeSizeResponseSchema = external_exports.object({
29920
29990
  totals: external_exports.object({
29921
29991
  billableSloc: external_exports.number().int().nonnegative(),
29922
29992
  rawLines: external_exports.number().int().nonnegative(),
29923
- /** Connected GitHub/Bitbucket repos — the re-measure `all` scope size. */
29993
+ /** Connected GitHub/Bitbucket repos plus READY source uploads: the
29994
+ * re-measure `all` scope size. */
29924
29995
  repositoryCount: external_exports.number().int().nonnegative(),
29925
29996
  /** Repos with a measurement for their current resolved branch. */
29926
29997
  measuredCount: external_exports.number().int().nonnegative(),
@@ -30211,6 +30282,9 @@ var CredentialIdParamsSchema = OrgIdParamsSchema.extend({
30211
30282
  environmentId: external_exports.uuid(),
30212
30283
  credentialId: external_exports.uuid()
30213
30284
  });
30285
+ var CredentialPasskeyIdParamsSchema = CredentialIdParamsSchema.extend({
30286
+ credentialPasskeyId: external_exports.uuid()
30287
+ });
30214
30288
  var EnvironmentCredentialsParamsSchema = OrgIdParamsSchema.extend({
30215
30289
  environmentId: external_exports.uuid()
30216
30290
  });
@@ -30369,7 +30443,12 @@ var CredentialResponseSchema = external_exports.object({
30369
30443
  * parameters only — the seed lives in its own KMS envelope on the row and
30370
30444
  * has no reveal endpoint at all (see the reveal union below).
30371
30445
  */
30372
- mfa: CredentialMfaSummarySchema.nullable()
30446
+ mfa: CredentialMfaSummarySchema.nullable(),
30447
+ /**
30448
+ * Passkeys an agent created on this account, oldest first. Always present; empty
30449
+ * for every credential type that cannot hold one. Summaries only.
30450
+ */
30451
+ passkeys: external_exports.array(CredentialPasskeySummarySchema)
30373
30452
  }).strict();
30374
30453
  var ListCredentialsResponseSchema = external_exports.array(CredentialResponseSchema);
30375
30454
  var UpdateCredentialRequestSchema = external_exports.object({
@@ -30592,6 +30671,27 @@ var credentialContract = c13.router({
30592
30671
  500: ErrorResponseSchema
30593
30672
  }
30594
30673
  },
30674
+ // Remove ONE passkey Trace holds for the account. Not idempotent: the id names a
30675
+ // row, and a second call 404s like deleteCredential. Allowed mid-engagement like
30676
+ // clearCredentialMfa, and for the same reason: the customer decides what Trace
30677
+ // holds on their account. A session already armed keeps the key in Chrome's
30678
+ // memory until that browser closes, but cannot recreate the row (a counter tick
30679
+ // carries no key, a rejected tick is never re-sent, and a fresh arm reads the
30680
+ // store), so the removal sticks. Trace's copy only: the site keeps its own
30681
+ // registration of the public key until the customer removes it there.
30682
+ deleteCredentialPasskey: {
30683
+ method: "DELETE",
30684
+ path: "/organizations/:organizationId/environments/:environmentId/credentials/:credentialId/passkeys/:credentialPasskeyId",
30685
+ pathParams: CredentialPasskeyIdParamsSchema,
30686
+ responses: {
30687
+ // The refreshed credential, so the dashboard's row state comes from the api.
30688
+ 200: CredentialResponseSchema,
30689
+ 401: ErrorResponseSchema,
30690
+ 403: ErrorResponseSchema,
30691
+ 404: ErrorResponseSchema,
30692
+ 500: ErrorResponseSchema
30693
+ }
30694
+ },
30595
30695
  deleteCredential: {
30596
30696
  method: "DELETE",
30597
30697
  path: "/organizations/:organizationId/environments/:environmentId/credentials/:credentialId",
@@ -31082,6 +31182,11 @@ var EngagementCardsSchema = external_exports.object({
31082
31182
  statusChangesViaPrComment: external_exports.number().int(),
31083
31183
  botCommentsPosted: external_exports.number().int(),
31084
31184
  structuralReplies: external_exports.number().int(),
31185
+ /**
31186
+ * Always 0. Its only source is the `issue_comment` webhook, which GitHub
31187
+ * does not deliver to any Trace app (GITHUB_APP_PERMISSIONS.md §Issue
31188
+ * comment). Read a reply figure as `structuralReplies` alone.
31189
+ */
31085
31190
  heuristicReplies: external_exports.number().int(),
31086
31191
  /** structuralReplies / max(1, botCommentsPosted), clamped to [0, 1]. */
31087
31192
  replyRate: external_exports.number().min(0).max(1)
@@ -34759,6 +34864,7 @@ var pentestFindingContract = c36.router({
34759
34864
  body: CreatePentestFindingRequestSchema,
34760
34865
  responses: {
34761
34866
  201: PentestFindingResponseSchema,
34867
+ // INVALID_CVSS_VECTOR / CVSS_VERSION_MISMATCH when a cvssVector is sent, or schema validation.
34762
34868
  400: ErrorResponseSchema,
34763
34869
  401: ErrorResponseSchema,
34764
34870
  403: ErrorResponseSchema,
@@ -34934,6 +35040,7 @@ var pentestFindingContract = c36.router({
34934
35040
  body: UpdatePentestFindingRequestSchema,
34935
35041
  responses: {
34936
35042
  200: PentestFindingResponseSchema,
35043
+ // INVALID_CVSS_VECTOR / CVSS_VERSION_MISMATCH when a cvssVector is sent, or schema validation.
34937
35044
  400: ErrorResponseSchema,
34938
35045
  401: ErrorResponseSchema,
34939
35046
  403: ErrorResponseSchema,
@@ -34980,7 +35087,8 @@ var pentestFindingContract = c36.router({
34980
35087
  body: ApplyFindingCvssRequestSchema,
34981
35088
  responses: {
34982
35089
  200: PentestFindingResponseSchema,
34983
- // INVALID_CVSS_VECTOR (unparseable vector) or schema validation.
35090
+ // INVALID_CVSS_VECTOR (unparseable vector), CVSS_VERSION_MISMATCH (the
35091
+ // engagement's stored vectors carry the other CVSS version), or schema validation.
34984
35092
  400: ErrorResponseSchema,
34985
35093
  401: ErrorResponseSchema,
34986
35094
  403: ErrorResponseSchema,
@@ -36198,6 +36306,25 @@ var repositoryContract = c42.router({
36198
36306
  500: ErrorResponseSchema
36199
36307
  }
36200
36308
  },
36309
+ /**
36310
+ * Include or exclude repositories from scanning. Excluded repos stop
36311
+ * receiving scans of every trigger type until included again; the flag is
36312
+ * customer-owned and survives provider re-syncs.
36313
+ */
36314
+ setScanningInclusion: {
36315
+ method: "PUT",
36316
+ path: "/organizations/:organizationId/repositories/scanning-inclusion",
36317
+ pathParams: OrgIdParamsSchema,
36318
+ body: SetScanningInclusionRequestSchema,
36319
+ responses: {
36320
+ 200: SetScanningInclusionResponseSchema,
36321
+ 400: ErrorResponseSchema,
36322
+ 401: ErrorResponseSchema,
36323
+ 403: ErrorResponseSchema,
36324
+ 404: ErrorResponseSchema,
36325
+ 500: ErrorResponseSchema
36326
+ }
36327
+ },
36201
36328
  getScanningBranchPreference: {
36202
36329
  method: "GET",
36203
36330
  path: "/organizations/:organizationId/repositories/scanning-branch/preference",
@@ -36521,11 +36648,17 @@ var StaffOrganizationPentestSummarySchema = external_exports.object({
36521
36648
  findingCount: external_exports.int().nonnegative(),
36522
36649
  startedAt: external_exports.iso.datetime().nullable(),
36523
36650
  endedAt: external_exports.iso.datetime().nullable(),
36524
- createdAt: external_exports.iso.datetime()
36651
+ createdAt: external_exports.iso.datetime(),
36652
+ /** Set while the engagement is curtained from every customer principal; null = visible. */
36653
+ hiddenFromCustomerAt: external_exports.iso.datetime().nullable()
36525
36654
  }).strict();
36526
36655
  var ListStaffOrganizationPentestsResponseSchema = external_exports.array(
36527
36656
  StaffOrganizationPentestSummarySchema
36528
36657
  );
36658
+ var SetStaffOrganizationPentestCustomerVisibilityRequestSchema = external_exports.object({
36659
+ /** false hides the engagement from the customer entirely; true shows it again. */
36660
+ visible: external_exports.boolean()
36661
+ }).strict();
36529
36662
 
36530
36663
  // ../../packages/api-contracts/src/contracts/v1/staff-pentests.ts
36531
36664
  var c45 = initContract();
@@ -36555,6 +36688,21 @@ var staffPentestsContract = c45.router({
36555
36688
  500: ErrorResponseSchema
36556
36689
  },
36557
36690
  summary: "Soft-delete a pentest from the admin console (Trace staff only)"
36691
+ },
36692
+ setStaffOrganizationPentestCustomerVisibility: {
36693
+ method: "POST",
36694
+ path: "/organizations/:organizationId/staff/pentests/:pentestId/customer-visibility",
36695
+ pathParams: StaffOrganizationPentestParamsSchema,
36696
+ body: SetStaffOrganizationPentestCustomerVisibilityRequestSchema,
36697
+ responses: {
36698
+ 200: StaffOrganizationPentestSummarySchema,
36699
+ 401: ErrorResponseSchema,
36700
+ 403: ErrorResponseSchema,
36701
+ 404: ErrorResponseSchema,
36702
+ 409: ErrorResponseSchema,
36703
+ 500: ErrorResponseSchema
36704
+ },
36705
+ summary: "Hide a pentest from the customer or show it again (Trace staff only)"
36558
36706
  }
36559
36707
  });
36560
36708
 
@@ -37079,7 +37227,10 @@ var VulnerabilityLatestFindingSchema = external_exports.object({
37079
37227
  classification: external_exports.enum(["vulnerability", "defense_in_depth", "context_dependent"]).nullable(),
37080
37228
  prerequisiteAccess: external_exports.string().nullable(),
37081
37229
  capabilityGain: external_exports.string().nullable(),
37082
- technicalSeverity: VulnerabilitySeverity.nullable()
37230
+ technicalSeverity: VulnerabilitySeverity.nullable(),
37231
+ // Whether the scan found an entry point that reaches the code; null when
37232
+ // no scan assessed it (reads as reachable).
37233
+ reachability: external_exports.enum(["reachable", "unreachable"]).nullable()
37083
37234
  });
37084
37235
  var DependencyGraphSchema = external_exports.object({
37085
37236
  vulnerablePackage: external_exports.string(),
@@ -37451,6 +37602,7 @@ var vulnerabilityContract = c50.router({
37451
37602
  402: ErrorResponseSchema,
37452
37603
  403: ErrorResponseSchema,
37453
37604
  404: ErrorResponseSchema,
37605
+ 409: ErrorResponseSchema,
37454
37606
  500: ErrorResponseSchema
37455
37607
  }
37456
37608
  },
@@ -38234,7 +38386,7 @@ async function requestDeviceAuthorization(clientId) {
38234
38386
  }
38235
38387
  return await response.json();
38236
38388
  }
38237
- var sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
38389
+ var sleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
38238
38390
  async function pollForToken(clientId, deviceCode, options = {}) {
38239
38391
  const expiresIn = options.expiresIn ?? 300;
38240
38392
  let interval = options.interval ?? 5;
@@ -38919,18 +39071,18 @@ var proto = Object.defineProperties(() => {
38919
39071
  }
38920
39072
  }
38921
39073
  });
38922
- var createStyler = (open3, close, parent) => {
39074
+ var createStyler = (open4, close, parent) => {
38923
39075
  let openAll;
38924
39076
  let closeAll;
38925
39077
  if (parent === void 0) {
38926
- openAll = open3;
39078
+ openAll = open4;
38927
39079
  closeAll = close;
38928
39080
  } else {
38929
- openAll = parent.openAll + open3;
39081
+ openAll = parent.openAll + open4;
38930
39082
  closeAll = close + parent.closeAll;
38931
39083
  }
38932
39084
  return {
38933
- open: open3,
39085
+ open: open4,
38934
39086
  close,
38935
39087
  openAll,
38936
39088
  closeAll,
@@ -39513,6 +39665,9 @@ function formatStatus(status) {
39513
39665
  function tsvSafe(value) {
39514
39666
  return (value ?? "").replace(/[\t\r\n]/g, " ");
39515
39667
  }
39668
+ function formulaSafe(value) {
39669
+ return /^[=+\-@]/.test(value) ? `'${value}` : value;
39670
+ }
39516
39671
 
39517
39672
  // src/pager.ts
39518
39673
  function isInteractiveEnvironment() {
@@ -39627,6 +39782,21 @@ function extractRepoName(input) {
39627
39782
  }
39628
39783
  return input;
39629
39784
  }
39785
+ function normalizeRepoUrlForMatch(url2) {
39786
+ let normalized = url2.trim().toLowerCase();
39787
+ const scp = normalized.match(/^[a-z0-9._-]+@([^:/]+):(.+)$/);
39788
+ if (scp) {
39789
+ normalized = `${scp[1]}/${scp[2]}`;
39790
+ } else {
39791
+ normalized = normalized.replace(/^[a-z][a-z0-9+.-]*:\/\//, "").replace(/^[a-z0-9._-]+@/, "");
39792
+ }
39793
+ let previous;
39794
+ do {
39795
+ previous = normalized;
39796
+ normalized = normalized.replace(/\/+$/, "").replace(/\.git$/, "");
39797
+ } while (normalized !== previous);
39798
+ return normalized;
39799
+ }
39630
39800
  var BRANCH_NAME_PATTERN = /^[a-zA-Z0-9._/+-]+$/;
39631
39801
  var TRACE_GIT_ASKPASS_TOKEN_ENV = "TRACE_GIT_ASKPASS_TOKEN";
39632
39802
  var DEFAULT_LIMIT = 30;
@@ -39634,6 +39804,8 @@ var DEFAULT_PAGE = 1;
39634
39804
  var MAX_LIMIT = 100;
39635
39805
  var REPOSITORY_PLATFORMS = ["GITHUB", "GITLAB", "BITBUCKET"];
39636
39806
  var REPOSITORY_STATUSES = ["CONNECTED", "DISCONNECTED"];
39807
+ var SCANNING_INCLUSIONS = ["included", "excluded"];
39808
+ var SCANNING_INCLUSION_BATCH_SIZE = 500;
39637
39809
  function validateBranchName(branch) {
39638
39810
  if (branch.startsWith("-")) {
39639
39811
  throw new Error(`Invalid branch name: '${branch}' \u2014 branch names cannot start with a dash`);
@@ -39687,6 +39859,18 @@ function normalizeStatus(value, source) {
39687
39859
  `${source} must be one of: ${REPOSITORY_STATUSES.join(", ")}. Received '${value}'.`
39688
39860
  );
39689
39861
  }
39862
+ function normalizeScanningInclusion(value, source) {
39863
+ const normalized = value.trim().toLowerCase();
39864
+ if (SCANNING_INCLUSIONS.includes(normalized)) {
39865
+ return normalized;
39866
+ }
39867
+ throw new Error(
39868
+ `${source} must be one of: ${SCANNING_INCLUSIONS.join(", ")}. Received '${value}'.`
39869
+ );
39870
+ }
39871
+ function scanningInclusionOf(repo) {
39872
+ return repo.includedInScanning ? "included" : "excluded";
39873
+ }
39690
39874
  function normalizeFilterField(rawField) {
39691
39875
  const field = rawField.trim().toLowerCase();
39692
39876
  switch (field) {
@@ -39711,9 +39895,14 @@ function normalizeFilterField(rawField) {
39711
39895
  case "primarylanguage":
39712
39896
  case "primary_language":
39713
39897
  return "language";
39898
+ case "scanning":
39899
+ case "inclusion":
39900
+ case "included_in_scanning":
39901
+ case "includedinscanning":
39902
+ return "scanning";
39714
39903
  default:
39715
39904
  throw new Error(
39716
- `Unknown repository filter field '${rawField}'. Supported fields: platform, status, url, repo, branch, language.`
39905
+ `Unknown repository filter field '${rawField}'. Supported fields: platform, status, url, repo, branch, language, scanning.`
39717
39906
  );
39718
39907
  }
39719
39908
  }
@@ -39757,6 +39946,9 @@ function parseRepositoryFilterExpression(expression) {
39757
39946
  if (field === "status") {
39758
39947
  normalizeStatus(value, "--filter");
39759
39948
  }
39949
+ if (field === "scanning") {
39950
+ normalizeScanningInclusion(value, "--filter");
39951
+ }
39760
39952
  return { field, operator, value };
39761
39953
  }
39762
39954
  function doesValueMatchFilter(candidate, filterValue, operator, exactMatchOnly) {
@@ -39764,7 +39956,7 @@ function doesValueMatchFilter(candidate, filterValue, operator, exactMatchOnly)
39764
39956
  const normalizedFilter = normalizeForMatching(filterValue);
39765
39957
  if (exactMatchOnly || operator === "exact" || operator === "not_exact") {
39766
39958
  const matches2 = normalizedCandidate === normalizedFilter;
39767
- return operator === "not_exact" ? !matches2 : matches2;
39959
+ return operator === "not_exact" || operator === "not_contains" ? !matches2 : matches2;
39768
39960
  }
39769
39961
  const matches = normalizedCandidate.includes(normalizedFilter);
39770
39962
  return operator === "not_contains" ? !matches : matches;
@@ -39800,6 +39992,13 @@ function matchParsedFilter(repo, parsedFilter) {
39800
39992
  const language = repo.primaryLanguage ?? "";
39801
39993
  return doesValueMatchFilter(language, parsedFilter.value, parsedFilter.operator, false);
39802
39994
  }
39995
+ case "scanning":
39996
+ return doesValueMatchFilter(
39997
+ scanningInclusionOf(repo),
39998
+ parsedFilter.value,
39999
+ parsedFilter.operator,
40000
+ true
40001
+ );
39803
40002
  }
39804
40003
  }
39805
40004
  function filterRepositories(repositories, options) {
@@ -39811,6 +40010,7 @@ function filterRepositories(repositories, options) {
39811
40010
  const normalizedQuery = options.query ? normalizeForMatching(options.query) : void 0;
39812
40011
  const normalizedDefaultBranch = options.githubDefaultBranch ? normalizeForMatching(options.githubDefaultBranch) : void 0;
39813
40012
  const normalizedLanguage = options.language ? normalizeForMatching(options.language) : void 0;
40013
+ const normalizedScanning = options.scanning ? normalizeScanningInclusion(options.scanning, "--scanning") : void 0;
39814
40014
  return repositories.filter((repo) => {
39815
40015
  if (normalizedPlatform && repo.platform.toUpperCase() !== normalizedPlatform) {
39816
40016
  return false;
@@ -39818,6 +40018,9 @@ function filterRepositories(repositories, options) {
39818
40018
  if (normalizedStatus && repo.status.toUpperCase() !== normalizedStatus) {
39819
40019
  return false;
39820
40020
  }
40021
+ if (normalizedScanning && scanningInclusionOf(repo) !== normalizedScanning) {
40022
+ return false;
40023
+ }
39821
40024
  if (normalizedDefaultBranch && !normalizeForMatching(repo.githubDefaultBranch).includes(normalizedDefaultBranch)) {
39822
40025
  return false;
39823
40026
  }
@@ -39890,7 +40093,9 @@ function paginateResults(items, options) {
39890
40093
  };
39891
40094
  }
39892
40095
  function printRepositoriesTsv(repositories) {
39893
- console.log("repositoryUrl platform status githubDefaultBranch scanningBranch language");
40096
+ console.log(
40097
+ "repositoryUrl platform status githubDefaultBranch scanningBranch language scanning"
40098
+ );
39894
40099
  for (const repo of repositories) {
39895
40100
  console.log(
39896
40101
  [
@@ -39899,18 +40104,20 @@ function printRepositoriesTsv(repositories) {
39899
40104
  tsvSafe(repo.status),
39900
40105
  tsvSafe(repo.githubDefaultBranch),
39901
40106
  tsvSafe(repo.scanningBranch ?? ""),
39902
- tsvSafe(repo.primaryLanguage)
40107
+ tsvSafe(repo.primaryLanguage),
40108
+ scanningInclusionOf(repo)
39903
40109
  ].join(" ")
39904
40110
  );
39905
40111
  }
39906
40112
  }
39907
40113
  function buildRepositoriesTable(repositories) {
39908
- const headers = ["Repository URL", "Platform", "Status"];
40114
+ const headers = ["Repository URL", "Platform", "Status", "Scanning"];
39909
40115
  const rows = repositories.map(
39910
40116
  (repo) => [
39911
40117
  stripAnsi(String(repo.repositoryUrl)),
39912
40118
  stripAnsi(String(repo.platform)).toUpperCase(),
39913
- repo.status === "CONNECTED" ? "connected" : "disconnected"
40119
+ repo.status === "CONNECTED" ? "connected" : "disconnected",
40120
+ scanningInclusionOf(repo)
39914
40121
  ]
39915
40122
  );
39916
40123
  const colWidths = headers.map(
@@ -40039,13 +40246,27 @@ async function maybeRefreshCloneTokenSessionForOrganization(organizationId, list
40039
40246
  }
40040
40247
  return refreshForOrganization(workosOrganizationId);
40041
40248
  }
40249
+ function resolveScanningFlag(options) {
40250
+ if (options.included && options.excluded) {
40251
+ throw new Error("Pass only one of --included / --excluded.");
40252
+ }
40253
+ if (options.included) {
40254
+ return "included";
40255
+ }
40256
+ if (options.excluded) {
40257
+ return "excluded";
40258
+ }
40259
+ return void 0;
40260
+ }
40042
40261
  function toRepositoryListFilterOptions(options) {
40262
+ const scanning = resolveScanningFlag(options);
40043
40263
  return {
40044
40264
  ...options.platform ? { platform: options.platform } : {},
40045
40265
  ...options.status ? { status: options.status } : {},
40046
40266
  ...options.query ? { query: options.query } : {},
40047
40267
  ...options.defaultBranch ? { githubDefaultBranch: options.defaultBranch } : {},
40048
40268
  ...options.language ? { language: options.language } : {},
40269
+ ...scanning ? { scanning } : {},
40049
40270
  ...options.filter.length > 0 ? { filters: options.filter } : {}
40050
40271
  };
40051
40272
  }
@@ -40058,9 +40279,9 @@ function addRepositoryFilterOptions(command) {
40058
40279
  "--status <status>",
40059
40280
  "Filter by status (CONNECTED, DISCONNECTED)",
40060
40281
  (value) => value.toUpperCase()
40061
- ).option("--query <query>", "Case-insensitive search across URL, repo, branch, and language").option("--default-branch <branch>", "Filter by default branch (case-insensitive contains)").option("--language <language>", "Filter by primary language (case-insensitive contains)").option(
40282
+ ).option("--query <query>", "Case-insensitive search across URL, repo, branch, and language").option("--default-branch <branch>", "Filter by default branch (case-insensitive contains)").option("--language <language>", "Filter by primary language (case-insensitive contains)").option("--included", "Only repositories included in scanning").option("--excluded", "Only repositories excluded from scanning").option(
40062
40283
  "-f, --filter <expression>",
40063
- "Advanced filter expression. Repeatable. Examples: platform=GITHUB, repo:trace, status!=DISCONNECTED",
40284
+ "Advanced filter expression. Repeatable. Examples: platform=GITHUB, repo:trace, status!=DISCONNECTED, scanning=excluded",
40064
40285
  collectFilterExpression,
40065
40286
  []
40066
40287
  ).option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").option(
@@ -40088,6 +40309,14 @@ async function runRepositoryListCommand(options) {
40088
40309
  }
40089
40310
  return;
40090
40311
  }
40312
+ let filterOptions;
40313
+ try {
40314
+ filterOptions = toRepositoryListFilterOptions(options);
40315
+ } catch (error48) {
40316
+ const message = error48 instanceof Error ? error48.message : "Unknown filter parsing error.";
40317
+ console.error(`Invalid repository filter: ${message}`);
40318
+ process.exit(1);
40319
+ }
40091
40320
  const organizationId = await resolveOrganizationId();
40092
40321
  if (options.dependency) {
40093
40322
  await runDependencyFilter(organizationId, {
@@ -40111,14 +40340,14 @@ async function runRepositoryListCommand(options) {
40111
40340
  process.exit(1);
40112
40341
  }
40113
40342
  try {
40114
- filteredRepositories = filterRepositories(result.body, toRepositoryListFilterOptions(options));
40343
+ filteredRepositories = filterRepositories(result.body, filterOptions);
40115
40344
  } catch (error48) {
40116
40345
  const message = error48 instanceof Error ? error48.message : "Unknown filter parsing error.";
40117
40346
  console.error(`Invalid repository filter: ${message}`);
40118
40347
  process.exit(1);
40119
40348
  }
40120
40349
  if (filteredRepositories.length === 0) {
40121
- if (options.platform || options.status || options.query || options.defaultBranch || options.language || options.filter.length > 0) {
40350
+ if (options.platform || options.status || options.query || options.defaultBranch || options.language || options.included || options.excluded || options.filter.length > 0) {
40122
40351
  console.log("No repositories found matching the provided filters.");
40123
40352
  return;
40124
40353
  }
@@ -40252,6 +40481,8 @@ Examples:
40252
40481
  tracecli repo list --output json | jq '.[].repositoryUrl'
40253
40482
  tracecli repo list --output tsv | cut -f1
40254
40483
  tracecli repo list --filter "language:typescript" --limit 10
40484
+ tracecli repo list --excluded
40485
+ tracecli repo list --query payments --included --output json
40255
40486
  tracecli repo list --dependency lodash@4.17.21
40256
40487
  tracecli repo list --web`
40257
40488
  ).action(async (options) => {
@@ -40269,6 +40500,7 @@ Examples:
40269
40500
  tracecli repo filter --page 2 --limit 30
40270
40501
  tracecli repo filter --all
40271
40502
  tracecli repo filter --language typescript --limit 10
40503
+ tracecli repo filter --filter "scanning=excluded"
40272
40504
  tracecli repo filter --output json | jq '.[].repositoryUrl'
40273
40505
  tracecli repo filter --output tsv | cut -f1
40274
40506
  tracecli repo filter --web`
@@ -40631,12 +40863,158 @@ Examples:
40631
40863
  await runSetScanningBranchCommand(repositoryUrl, options);
40632
40864
  });
40633
40865
  }
40866
+ function resolveRepositoryReferences(repositories, references) {
40867
+ const byId = /* @__PURE__ */ new Map();
40868
+ const byUrl = /* @__PURE__ */ new Map();
40869
+ for (const repo of repositories) {
40870
+ byId.set(repo.repositoryId.toLowerCase(), repo);
40871
+ byUrl.set(normalizeRepoUrlForMatch(repo.repositoryUrl), repo);
40872
+ }
40873
+ const matched = /* @__PURE__ */ new Map();
40874
+ const unresolved = [];
40875
+ for (const reference of references) {
40876
+ const trimmed = reference.trim();
40877
+ if (trimmed.length === 0) {
40878
+ continue;
40879
+ }
40880
+ const repo = byId.get(trimmed.toLowerCase()) ?? byUrl.get(normalizeRepoUrlForMatch(resolveRepoUrl(trimmed)));
40881
+ if (!repo) {
40882
+ unresolved.push(trimmed);
40883
+ continue;
40884
+ }
40885
+ if (!matched.has(repo.repositoryId)) {
40886
+ matched.set(repo.repositoryId, repo);
40887
+ }
40888
+ }
40889
+ return { matched: [...matched.values()], unresolved };
40890
+ }
40891
+ function scanningInclusionVerb(includedInScanning) {
40892
+ return includedInScanning ? { past: "Included", noun: "included in scanning" } : { past: "Excluded", noun: "excluded from scanning" };
40893
+ }
40894
+ async function runSetScanningInclusionCommand(references, includedInScanning, options) {
40895
+ const organizationId = await resolveOrganizationId();
40896
+ const listResult = await authenticatedRequest(
40897
+ (client) => client.repository.listRepositories({ params: { organizationId } })
40898
+ );
40899
+ if (listResult.status === 401) {
40900
+ console.error("Authentication failed. Run `tracecli auth login` to re-authenticate.");
40901
+ process.exit(1);
40902
+ }
40903
+ if (listResult.status !== 200) {
40904
+ console.error(`Failed to list repositories (status ${listResult.status}).`);
40905
+ process.exit(1);
40906
+ }
40907
+ const { matched, unresolved } = resolveRepositoryReferences(listResult.body, references);
40908
+ if (unresolved.length > 0) {
40909
+ console.error(
40910
+ colorError(
40911
+ `${unresolved.length === 1 ? "Repository" : "Repositories"} not found in this organization: ${unresolved.join(", ")}`
40912
+ )
40913
+ );
40914
+ console.error(
40915
+ "Pass a repository ID, URL, or owner/repo shorthand. Run `tracecli repo list` to see connected repositories."
40916
+ );
40917
+ process.exit(1);
40918
+ }
40919
+ if (matched.length === 0) {
40920
+ console.error(colorError("No repositories given."));
40921
+ process.exit(1);
40922
+ }
40923
+ const outcomes = matched.map((repo) => ({
40924
+ repositoryId: repo.repositoryId,
40925
+ repositoryUrl: repo.repositoryUrl,
40926
+ previouslyIncludedInScanning: repo.includedInScanning
40927
+ }));
40928
+ const ids = matched.map((repo) => repo.repositoryId);
40929
+ let updatedCount = 0;
40930
+ const notFoundRepositoryIds = [];
40931
+ const verb = scanningInclusionVerb(includedInScanning);
40932
+ for (let offset = 0; offset < ids.length; offset += SCANNING_INCLUSION_BATCH_SIZE) {
40933
+ const result = await authenticatedRequest(
40934
+ (client) => client.repository.setScanningInclusion({
40935
+ params: { organizationId },
40936
+ body: {
40937
+ repositoryIds: ids.slice(offset, offset + SCANNING_INCLUSION_BATCH_SIZE),
40938
+ includedInScanning
40939
+ }
40940
+ })
40941
+ );
40942
+ if (result.status === 200) {
40943
+ updatedCount += result.body.updatedCount;
40944
+ notFoundRepositoryIds.push(...result.body.notFoundRepositoryIds);
40945
+ continue;
40946
+ }
40947
+ const committedSuffix = offset > 0 ? ` ${updatedCount} of ${ids.length} repositories were already updated.` : "";
40948
+ if (result.status === 401) {
40949
+ console.error("Authentication failed. Run `tracecli auth login` to re-authenticate.");
40950
+ } else if (result.status === 403) {
40951
+ console.error(
40952
+ colorError(
40953
+ `You don't have permission to change repository scanning in this organization (analyst or admin role required).${committedSuffix}`
40954
+ )
40955
+ );
40956
+ } else {
40957
+ const message = extractErrorMessage(result.body);
40958
+ console.error(
40959
+ colorError(
40960
+ `Failed to update repository scanning (status ${result.status})${message ? `: ${message}` : "."}${committedSuffix}`
40961
+ )
40962
+ );
40963
+ }
40964
+ process.exit(1);
40965
+ }
40966
+ if (options.output === "json") {
40967
+ console.log(
40968
+ JSON.stringify(
40969
+ { includedInScanning, updatedCount, notFoundRepositoryIds, repositories: outcomes },
40970
+ null,
40971
+ 2
40972
+ )
40973
+ );
40974
+ return;
40975
+ }
40976
+ const notFound = new Set(notFoundRepositoryIds);
40977
+ for (const outcome of outcomes) {
40978
+ if (notFound.has(outcome.repositoryId)) {
40979
+ console.log(
40980
+ `${colorWarningPrefix("Skipped")} ${outcome.repositoryUrl}: no longer connected to this organization`
40981
+ );
40982
+ } else if (outcome.previouslyIncludedInScanning === includedInScanning) {
40983
+ console.log(`Already ${verb.noun}: ${outcome.repositoryUrl}`);
40984
+ } else {
40985
+ console.log(`${colorSuccess(verb.past)} ${outcome.repositoryUrl}`);
40986
+ }
40987
+ }
40988
+ const changedCount = outcomes.filter(
40989
+ (outcome) => !notFound.has(outcome.repositoryId) && outcome.previouslyIncludedInScanning !== includedInScanning
40990
+ ).length;
40991
+ console.log(
40992
+ `${changedCount} of ${outcomes.length} ${outcomes.length === 1 ? "repository" : "repositories"} changed to ${verb.noun}.${includedInScanning ? "" : " Excluded repositories are skipped by every scan until you include them again."}`
40993
+ );
40994
+ }
40995
+ function createScanningInclusionCommand(includedInScanning) {
40996
+ const name = includedInScanning ? "include" : "exclude";
40997
+ const description = includedInScanning ? "Include repositories in scanning" : "Exclude repositories from scanning (no scans of any kind run until included again)";
40998
+ return new Command(name).description(description).argument("<repository...>", "Repository ID, URL, or owner/repo shorthand (repeatable)").option("-o, --output <format>", "Output format: table (default), json", "table").addHelpText(
40999
+ "after",
41000
+ `
41001
+ Examples:
41002
+ tracecli repo ${name} acme/api
41003
+ tracecli repo ${name} https://github.com/acme/api https://bitbucket.org/acme/worker
41004
+ tracecli repo ${name} 3f1c6c2e-9b7a-4a6f-8f1d-2d4e5a6b7c8d --output json
41005
+ tracecli repo list --query legacy --output json | jq -r '.[].repositoryUrl' | xargs tracecli repo ${name}`
41006
+ ).action(async (repositories, options) => {
41007
+ await runSetScanningInclusionCommand(repositories, includedInScanning, options);
41008
+ });
41009
+ }
40634
41010
  function createRepoCommand() {
40635
41011
  const repo = new Command("repo").description("Manage repositories");
40636
41012
  repo.addCommand(createListCommand());
40637
41013
  repo.addCommand(createFilterCommand());
40638
41014
  repo.addCommand(createCloneCommand());
40639
41015
  repo.addCommand(createSetScanningBranchCommand());
41016
+ repo.addCommand(createScanningInclusionCommand(true));
41017
+ repo.addCommand(createScanningInclusionCommand(false));
40640
41018
  return repo;
40641
41019
  }
40642
41020
 
@@ -40745,21 +41123,6 @@ function validateCreateTypeUrl(type, url2) {
40745
41123
  }
40746
41124
  return null;
40747
41125
  }
40748
- function normalizeRepoUrlForMatch(url2) {
40749
- let normalized = url2.trim().toLowerCase();
40750
- const scp = normalized.match(/^[a-z0-9._-]+@([^:/]+):(.+)$/);
40751
- if (scp) {
40752
- normalized = `${scp[1]}/${scp[2]}`;
40753
- } else {
40754
- normalized = normalized.replace(/^[a-z][a-z0-9+.-]*:\/\//, "").replace(/^[a-z0-9._-]+@/, "");
40755
- }
40756
- let previous;
40757
- do {
40758
- previous = normalized;
40759
- normalized = normalized.replace(/\/+$/, "").replace(/\.git$/, "");
40760
- } while (normalized !== previous);
40761
- return normalized;
40762
- }
40763
41126
  function buildCreateApplicationBody(input) {
40764
41127
  return {
40765
41128
  name: input.name,
@@ -43713,6 +44076,9 @@ function repoDisplayName(repositoryUrl) {
43713
44076
  return repositoryUrl;
43714
44077
  }
43715
44078
  }
44079
+ function repoLabel(repo) {
44080
+ return sanitizeDisplay(repo.displayName ?? repoDisplayName(repo.repositoryUrl));
44081
+ }
43716
44082
  function countedRows(totals) {
43717
44083
  const b = totals.buckets;
43718
44084
  return [
@@ -43769,7 +44135,7 @@ function renderCodeSizeTable(codeSize, options) {
43769
44135
  lines.push(
43770
44136
  ...padValues(
43771
44137
  shown.map((repo) => ({
43772
- label: repoDisplayName(repo.repositoryUrl),
44138
+ label: repoLabel(repo),
43773
44139
  value: repo.billableSloc ?? 0
43774
44140
  }))
43775
44141
  )
@@ -43784,7 +44150,8 @@ function renderCodeSizeTsv(codeSize) {
43784
44150
  return [
43785
44151
  "repository billableSloc",
43786
44152
  ...codeSize.repositories.map(
43787
- (repo) => `${repoDisplayName(repo.repositoryUrl)} ${repo.billableSloc === null ? "" : repo.billableSloc}`
44153
+ // Upload names are customer-authored, and TSV lands in spreadsheets.
44154
+ (repo) => `${formulaSafe(repoLabel(repo))} ${repo.billableSloc === null ? "" : repo.billableSloc}`
43788
44155
  )
43789
44156
  ];
43790
44157
  }
@@ -46657,185 +47024,159 @@ async function runCreateFinding(options) {
46657
47024
  });
46658
47025
  }
46659
47026
  function createCreateFindingCommand() {
46660
- return new Command("create").description("Create a pentest finding from a contractor write-up (staff)").requiredOption("--pentest <id>", "Pentest display ID (e.g. PT-3) or UUID").option("--title <text>", "Finding title").option("--description <text>", "Finding description").option("--description-file <path>", "Read description from a file").option("--severity <severity>", `Severity (${SEVERITY_LIST})`).option("--category <text>", "Finding category (e.g. Broken Access Control)").option("--cwe <id>", "CWE identifier (repeatable)", collectCwe).option("--cvss-vector <vector>", "CVSS v3 vector string").option("--cvss-score <n>", "CVSS base score (0\u201310)").option("--owasp <category>", "OWASP category").option("--poc <text>", "Proof of concept").option("--poc-file <path>", "Read proof of concept from a file").option("--remediation <text>", "Remediation guidance").option("--remediation-file <path>", "Read remediation from a file").option("--file <path>", "JSON object of finding fields; flags override").option("-o, --output <format>", "Output format: text, json (default: text)", "text").action(async (options) => {
47027
+ return new Command("create").description("Create a pentest finding from a contractor write-up (staff)").requiredOption("--pentest <id>", "Pentest display ID (e.g. PT-3) or UUID").option("--title <text>", "Finding title").option("--description <text>", "Finding description").option("--description-file <path>", "Read description from a file").option("--severity <severity>", `Severity (${SEVERITY_LIST})`).option("--category <text>", "Finding category (e.g. Broken Access Control)").option("--cwe <id>", "CWE identifier (repeatable)", collectCwe).option(
47028
+ "--cvss-vector <vector>",
47029
+ "CVSS base vector (CVSS:3.1/... or CVSS:4.0/...); the score is derived server-side"
47030
+ ).option("--cvss-score <n>", "Ignored: the score is derived from --cvss-vector").option("--owasp <category>", "OWASP category").option("--poc <text>", "Proof of concept").option("--poc-file <path>", "Read proof of concept from a file").option("--remediation <text>", "Remediation guidance").option("--remediation-file <path>", "Read remediation from a file").option("--file <path>", "JSON object of finding fields; flags override").option("-o, --output <format>", "Output format: text, json (default: text)", "text").action(async (options) => {
46661
47031
  await runCreateFinding(options);
46662
47032
  });
46663
47033
  }
46664
47034
 
46665
- // src/pentest/evidence.ts
46666
- import { readFileSync as readFileSync6, statSync as statSync3 } from "fs";
47035
+ // src/pentest/deliverables.ts
47036
+ import { open as open2, rename, rm, stat } from "fs/promises";
47037
+ import { basename as basename3, join as join6, resolve } from "path";
47038
+
47039
+ // src/upload/prepare.ts
47040
+ import { createHash } from "crypto";
47041
+ import { createReadStream, statSync as statSync3 } from "fs";
47042
+ import { open } from "fs/promises";
46667
47043
  import { basename as basename2 } from "path";
46668
- var SECTION_LIST = ValidationEvidenceSectionValues.join(", ");
46669
- var KIND_LIST = PentestExploitArtifactKindValues.join(", ");
46670
- function isSection(value) {
46671
- return ValidationEvidenceSectionValues.includes(value);
47044
+ var CodeUploadPrepareError = class extends Error {
47045
+ constructor(message) {
47046
+ super(message);
47047
+ this.name = "CodeUploadPrepareError";
47048
+ }
47049
+ };
47050
+ var KIND_BY_INPUT = {
47051
+ source: "SOURCE_ARCHIVE",
47052
+ code: "SOURCE_ARCHIVE",
47053
+ "source-archive": "SOURCE_ARCHIVE",
47054
+ source_archive: "SOURCE_ARCHIVE",
47055
+ SOURCE_ARCHIVE: "SOURCE_ARCHIVE",
47056
+ apk: "ANDROID_APK",
47057
+ android: "ANDROID_APK",
47058
+ "android-apk": "ANDROID_APK",
47059
+ android_apk: "ANDROID_APK",
47060
+ ANDROID_APK: "ANDROID_APK",
47061
+ ipa: "IOS_IPA",
47062
+ ios: "IOS_IPA",
47063
+ "ios-ipa": "IOS_IPA",
47064
+ ios_ipa: "IOS_IPA",
47065
+ IOS_IPA: "IOS_IPA"
47066
+ };
47067
+ function normalizeArtifactKind(input) {
47068
+ const trimmed = input.trim();
47069
+ return KIND_BY_INPUT[trimmed] ?? KIND_BY_INPUT[trimmed.toLowerCase()] ?? null;
46672
47070
  }
46673
- function isKind(value) {
46674
- return PentestExploitArtifactKindValues.includes(value);
47071
+ function inferArtifactKindFromFilename(filename) {
47072
+ const lower = filename.toLowerCase();
47073
+ if (lower.endsWith(".zip")) return "SOURCE_ARCHIVE";
47074
+ if (lower.endsWith(".apk")) return "ANDROID_APK";
47075
+ if (lower.endsWith(".ipa")) return "IOS_IPA";
47076
+ return null;
46675
47077
  }
46676
- function formatBytes2(bytes) {
46677
- if (bytes < 1024) return `${bytes} B`;
46678
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
46679
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
47078
+ function requiredExtensionForKind(kind) {
47079
+ if (kind === "ANDROID_APK") return ".apk";
47080
+ if (kind === "IOS_IPA") return ".ipa";
47081
+ return ".zip";
46680
47082
  }
46681
- async function resolveFindingId(organizationId, pentestId, identifier) {
46682
- if (isUuid(identifier)) {
46683
- return identifier;
47083
+ function defaultDisplayName(originalFilename) {
47084
+ const base = originalFilename.replace(/\.(zip|apk|ipa)$/i, "").trim();
47085
+ return base.slice(0, 200) || "source";
47086
+ }
47087
+ function formatArchiveBytes(bytes) {
47088
+ if (bytes < 1024) return `${bytes} B`;
47089
+ if (bytes < 1024 * 1024) {
47090
+ const kb = bytes / 1024;
47091
+ return `${kb.toFixed(Number.isInteger(kb) ? 0 : 1)} KB`;
46684
47092
  }
46685
- const numericMatch = identifier.match(/^(?:PTF-)?(\d+)$/i);
46686
- if (!numericMatch?.[1]) {
46687
- console.error(
46688
- colorError(
46689
- `Invalid finding identifier '${sanitizeDisplay(identifier)}'. Use a finding number (e.g. PTF-7) or UUID.`
46690
- )
46691
- );
46692
- process.exit(1);
46693
- }
46694
- const displayId = Number.parseInt(numericMatch[1], 10);
46695
- const result = await authenticatedRequest(
46696
- (client) => client.pentestFinding.listFindings({ params: { organizationId, pentestId } })
46697
- );
46698
- if (result.status === UNAUTHORIZED_STATUS2) {
46699
- exitAuthFailed();
47093
+ if (bytes < 1024 * 1024 * 1024) {
47094
+ const mb = bytes / (1024 * 1024);
47095
+ return `${mb.toFixed(Number.isInteger(mb) ? 0 : 1)} MB`;
46700
47096
  }
46701
- if (result.status !== 200) {
46702
- console.error(colorError(`Failed to list findings (status ${result.status}).`));
46703
- process.exit(1);
47097
+ const gb = bytes / (1024 * 1024 * 1024);
47098
+ return `${gb.toFixed(Number.isInteger(gb) ? 0 : 1)} GB`;
47099
+ }
47100
+ function archiveTooLargeMessage() {
47101
+ return `The file must be ${formatArchiveBytes(CODE_UPLOAD_MAX_BYTES)} or smaller.`;
47102
+ }
47103
+ async function hashFileSha256(filePath) {
47104
+ const hash2 = createHash("sha256");
47105
+ const stream = createReadStream(filePath);
47106
+ for await (const chunk of stream) {
47107
+ hash2.update(chunk);
46704
47108
  }
46705
- const match = result.body.find((f) => f.displayId === displayId);
46706
- if (!match || !("pentestFindingId" in match)) {
46707
- console.error(
46708
- colorError(
46709
- `No finding PTF-${displayId} in this pentest. Unpublished findings have no number yet \u2014 pass the finding UUID instead (\`tracecli pentest findings list --pentest <id> --output json\`).`
46710
- )
47109
+ return hash2.digest("hex");
47110
+ }
47111
+ async function readFileSlice(filePath, start, endExclusive) {
47112
+ const length = endExclusive - start;
47113
+ if (!Number.isSafeInteger(start) || !Number.isSafeInteger(length) || start < 0 || length < 1) {
47114
+ throw new Error(
47115
+ `Invalid file slice: start=${String(start)} endExclusive=${String(endExclusive)}.`
46711
47116
  );
46712
- process.exit(1);
46713
47117
  }
46714
- return match.pentestFindingId;
46715
- }
46716
- async function runAttachEvidence(options) {
46717
- if (options.section !== void 0 && !isSection(options.section)) {
46718
- console.error(colorError(`Invalid --section '${sanitizeDisplay(options.section)}'.`));
46719
- console.error(`Valid sections: ${SECTION_LIST}`);
46720
- process.exit(1);
47118
+ const buffer = Buffer.alloc(length);
47119
+ const handle = await open(filePath, "r");
47120
+ try {
47121
+ const { bytesRead } = await handle.read(buffer, 0, length, start);
47122
+ if (bytesRead !== length) {
47123
+ throw new Error(
47124
+ `Expected to read ${String(length)} bytes at offset ${String(start)}, got ${String(bytesRead)}.`
47125
+ );
47126
+ }
47127
+ return buffer;
47128
+ } finally {
47129
+ await handle.close();
46721
47130
  }
46722
- if (options.kind !== void 0 && !isKind(options.kind)) {
46723
- console.error(colorError(`Invalid --kind '${sanitizeDisplay(options.kind)}'.`));
46724
- console.error(`Valid kinds: ${KIND_LIST}`);
46725
- process.exit(1);
47131
+ }
47132
+ function assertPreparedMatchesUpload(upload, prepared) {
47133
+ if (upload.sizeBytes !== prepared.sizeBytes || upload.archiveSha256 !== prepared.archiveSha256) {
47134
+ throw new CodeUploadPrepareError(
47135
+ "The file does not match the interrupted upload (size or SHA-256 differs)."
47136
+ );
46726
47137
  }
47138
+ }
47139
+ async function prepareUploadFile(filePath, options = {}) {
46727
47140
  let sizeBytes;
46728
47141
  try {
46729
- const stats = statSync3(options.file);
47142
+ const stats = statSync3(filePath);
46730
47143
  if (!stats.isFile()) {
46731
- console.error(colorError(`--file '${sanitizeDisplay(options.file)}' is not a regular file.`));
46732
- process.exit(1);
47144
+ throw new CodeUploadPrepareError("Path is not a regular file. Pass a .zip, .apk, or .ipa.");
46733
47145
  }
46734
47146
  sizeBytes = stats.size;
46735
- } catch {
46736
- console.error(colorError(`Cannot read --file '${sanitizeDisplay(options.file)}'.`));
46737
- process.exit(1);
46738
- }
46739
- if (sizeBytes === 0) {
46740
- console.error(colorError(`--file '${sanitizeDisplay(options.file)}' is empty.`));
46741
- process.exit(1);
46742
- }
46743
- if (sizeBytes > MAX_STAFF_EVIDENCE_UPLOAD_BYTES) {
46744
- console.error(
46745
- colorError(
46746
- `File is ${formatBytes2(sizeBytes)}; the limit for one exhibit is ${formatBytes2(MAX_STAFF_EVIDENCE_UPLOAD_BYTES)}.`
46747
- )
46748
- );
46749
- process.exit(1);
46750
- }
46751
- const organizationId = await resolveOrganizationId();
46752
- const pentestId = await resolvePentestId(organizationId, options.pentest);
46753
- const pentestFindingId = await resolveFindingId(organizationId, pentestId, options.finding);
46754
- const params = { organizationId, pentestId, pentestFindingId };
46755
- const filename = basename2(options.file);
46756
- const slot = await authenticatedRequest(
46757
- (client) => client.pentestFinding.createFindingEvidenceUpload({
46758
- params,
46759
- body: { filename, sizeBytes }
46760
- })
46761
- );
46762
- if (slot.status === UNAUTHORIZED_STATUS2) {
46763
- exitAuthFailed();
46764
- }
46765
- if (slot.status !== 201) {
46766
- const { message } = readErrorFields(slot.body);
46767
- console.error(
46768
- colorError(message ?? `Failed to start the evidence upload (status ${slot.status}).`)
46769
- );
46770
- process.exit(1);
47147
+ } catch (error48) {
47148
+ if (error48 instanceof CodeUploadPrepareError) throw error48;
47149
+ throw new CodeUploadPrepareError("Cannot read that file.");
46771
47150
  }
46772
- const body = readFileSync6(options.file);
46773
- const put = await fetch(slot.body.uploadUrl, { method: "PUT", body: new Uint8Array(body) });
46774
- if (!put.ok) {
46775
- console.error(
46776
- colorError(
46777
- `Upload to storage failed (status ${put.status}). The upload link is short-lived \u2014 re-run the command.`
46778
- )
46779
- );
46780
- process.exit(1);
47151
+ if (!Number.isSafeInteger(sizeBytes) || sizeBytes < 1) {
47152
+ throw new CodeUploadPrepareError("The file is empty.");
46781
47153
  }
46782
- const attached = await authenticatedRequest(
46783
- (client) => client.pentestFinding.attachFindingEvidence({
46784
- params,
46785
- body: {
46786
- key: slot.body.key,
46787
- ...options.caption !== void 0 ? { caption: options.caption } : {},
46788
- ...options.section !== void 0 && isSection(options.section) ? { section: options.section } : {},
46789
- ...options.kind !== void 0 && isKind(options.kind) ? { kind: options.kind } : {},
46790
- ...options.customerFacing ? { customerFacing: true } : {}
46791
- }
46792
- })
46793
- );
46794
- if (attached.status === UNAUTHORIZED_STATUS2) {
46795
- exitAuthFailed();
47154
+ if (sizeBytes > CODE_UPLOAD_MAX_BYTES) {
47155
+ throw new CodeUploadPrepareError(archiveTooLargeMessage());
46796
47156
  }
46797
- if (attached.status !== 201) {
46798
- const { message } = readErrorFields(attached.body);
46799
- console.error(
46800
- colorError(message ?? `Failed to attach the evidence (status ${attached.status}).`)
46801
- );
46802
- process.exit(1);
47157
+ const originalFilename = basename2(filePath);
47158
+ const inferred = inferArtifactKindFromFilename(originalFilename);
47159
+ const artifactKind = options.artifactKind ?? inferred;
47160
+ if (artifactKind == null) {
47161
+ throw new CodeUploadPrepareError("Pass a .zip source archive, an .apk, or an .ipa.");
46803
47162
  }
46804
- if (options.output === "json") {
46805
- console.log(JSON.stringify(attached.body, null, 2));
46806
- return;
47163
+ const expected = requiredExtensionForKind(artifactKind);
47164
+ if (!originalFilename.toLowerCase().endsWith(expected)) {
47165
+ const label = isApplicationBuildArtifactKind(artifactKind) ? artifactKind === "ANDROID_APK" ? "Android APK" : "iOS package" : "source archive";
47166
+ throw new CodeUploadPrepareError(`A ${label} must end in ${expected}.`);
46807
47167
  }
46808
- const visibility = attached.body.customerFacing ? "customer-facing" : "staff-internal";
46809
- const placement = attached.body.section ? ` \xB7 ${attached.body.section}` : "";
46810
- console.log(
46811
- colorSuccess(
46812
- `Attached ${attached.body.kind} \xB7 ${formatBytes2(attached.body.sizeBytes)} \xB7 ${visibility}${placement}`
46813
- )
46814
- );
46815
- console.log(` artifact ${attached.body.artifactId}`);
46816
- if (attached.body.kind === "HAR") {
46817
- console.log(" HAR captures stay internal \u2014 they can carry a live session token.");
46818
- } else if (!attached.body.customerFacing) {
46819
- console.log(" Publish it from the finding\u2019s evidence panel in the staff console.");
47168
+ const displayName = (options.displayName ?? defaultDisplayName(originalFilename)).trim();
47169
+ if (displayName.length < 1 || displayName.length > 200) {
47170
+ throw new CodeUploadPrepareError("Display name must be 1 to 200 characters.");
46820
47171
  }
46821
- }
46822
- function createAttachEvidenceCommand() {
46823
- return new Command("attach-evidence").description("Attach a screenshot, recording, or capture to a pentest finding (staff)").requiredOption("--pentest <id>", "Pentest display ID (e.g. PT-3) or UUID").requiredOption("--finding <id>", "Finding number (e.g. PTF-7) or UUID").requiredOption("--file <path>", "Path to the evidence file").option("--caption <text>", "What the exhibit shows; rendered as its label").option("--section <section>", `Report section it backs (${SECTION_LIST})`).option("--kind <kind>", `Override the derived exhibit kind (${KIND_LIST})`).option("--customer-facing", "Publish to the customer (not allowed for HAR or video)").option("-o, --output <format>", "Output format: text, json (default: text)", "text").addHelpText(
46824
- "after",
46825
- `
46826
- The file uploads straight to storage, so recordings of any reasonable size work.
46827
- Videos and HAR captures always land staff-internal \u2014 publish a video with the
46828
- toggle in the staff console's evidence panel after reviewing it for redaction.
46829
-
46830
- Examples:
46831
- tracecli pentest findings attach-evidence --pentest PT-3 --finding 7 --file ./poc.png
46832
- tracecli pentest findings attach-evidence --pentest PT-3 --finding PTF-7 --file ./poc.mov \\
46833
- --caption "Cookie survives logout, replayed 40s later" --section proofOfConcept
46834
- tracecli pentest findings attach-evidence --pentest PT-3 --finding 7 --file ./roster.json \\
46835
- --caption "Full member roster returned to a non-member" --customer-facing --output json`
46836
- ).action(async (options) => {
46837
- await runAttachEvidence(options);
46838
- });
47172
+ return {
47173
+ path: filePath,
47174
+ displayName,
47175
+ originalFilename,
47176
+ sizeBytes,
47177
+ archiveSha256: await hashFileSha256(filePath),
47178
+ artifactKind
47179
+ };
46839
47180
  }
46840
47181
 
46841
47182
  // src/pentest/status-update.ts
@@ -46954,68 +47295,738 @@ async function runStatusUpdate(options) {
46954
47295
  }
46955
47296
  if (note !== void 0 && target === "IN_PROGRESS") {
46956
47297
  console.error(
46957
- colorError("--note is only used when reporting remediation (--status REMEDIATED).")
47298
+ colorError("--note is only used when reporting remediation (--status REMEDIATED).")
47299
+ );
47300
+ process.exit(1);
47301
+ }
47302
+ const viaApiKey = requireAuth().kind === "api_key";
47303
+ const organizationId = await resolveOrganizationId();
47304
+ const params = {
47305
+ organizationId,
47306
+ pentestId: options.pentest,
47307
+ pentestFindingId: options.finding
47308
+ };
47309
+ const result = viaApiKey ? await authenticatedRequest(
47310
+ (client) => client.pentestFinding.automationStatusUpdate({
47311
+ params,
47312
+ body: { status: target, ...note !== void 0 ? { note } : {} }
47313
+ })
47314
+ ) : target === "IN_PROGRESS" ? await authenticatedRequest(
47315
+ (client) => client.pentestFinding.startProgress({ params, body: {} })
47316
+ ) : await authenticatedRequest(
47317
+ (client) => client.pentestFinding.reportRemediation({
47318
+ params,
47319
+ body: note !== void 0 ? { note } : {}
47320
+ })
47321
+ );
47322
+ if (result.status !== 200) {
47323
+ failStatusUpdate(result.status, result.body, viaApiKey);
47324
+ }
47325
+ if (options.output === "json") {
47326
+ console.log(JSON.stringify(result.body, null, 2));
47327
+ return;
47328
+ }
47329
+ const displayId = result.body.displayId;
47330
+ const status = sanitizeDisplay(result.body.status);
47331
+ console.log(colorSuccess(`PTF-${displayId} is now ${status}`));
47332
+ }
47333
+ function createStatusUpdateCommand() {
47334
+ return new Command("status-update").description("Move a pentest finding to IN_PROGRESS or REMEDIATED").requiredOption("--pentest <id>", "Pentest display ID (e.g. PT-3) or UUID").requiredOption("--finding <id>", "Finding number (e.g. PTF-7) or UUID").requiredOption(
47335
+ "--status <status>",
47336
+ "IN_PROGRESS (start work) or REMEDIATED (ready for retest)"
47337
+ ).option("--note <text>", "Optional context when reporting remediation").option("-o, --output <format>", "Output format: text, json (default: text)", "text").addHelpText(
47338
+ "after",
47339
+ `
47340
+ Customer-facing status writes \u2014 the same transitions as the dashboard. An agent
47341
+ can mark a finding in progress when it starts the fix, then remediated when the
47342
+ fix is ready for Trace to re-test. Leaving the finding IN_PROGRESS (and not
47343
+ sending REMEDIATED) keeps it out of the retest queue.
47344
+
47345
+ IN_PROGRESS is allowed from OPEN or VERIFIED_OPEN. REMEDIATED is allowed from
47346
+ OPEN, IN_PROGRESS, or VERIFIED_OPEN.
47347
+
47348
+ A signed-in session (\`tracecli auth login\`) attributes the change to you. A
47349
+ Trace API key works too, once Trace enables finding automation for your
47350
+ organization: those writes are recorded as \`api-key:<key name>\` and show up as
47351
+ yours in the finding's history.
47352
+
47353
+ Examples:
47354
+ tracecli pentest findings status-update --pentest PT-3 --finding 7 --status IN_PROGRESS
47355
+ tracecli pentest findings status-update --pentest PT-3 --finding PTF-7 --status REMEDIATED
47356
+ tracecli pentest findings status-update --pentest PT-3 --finding 7 --status REMEDIATED \\
47357
+ --note "Fixed in PR 412" --output json`
47358
+ ).action(async (options) => {
47359
+ await runStatusUpdate(options);
47360
+ });
47361
+ }
47362
+
47363
+ // src/pentest/deliverables.ts
47364
+ var PentestDeliverableDownloadError = class extends Error {
47365
+ constructor(message) {
47366
+ super(message);
47367
+ this.name = "PentestDeliverableDownloadError";
47368
+ }
47369
+ };
47370
+ function safeDeliverableFilename(documentId, extension, fallbackStem) {
47371
+ const stripped = Array.from(basename3(documentId.replace(/\\/g, "/")), (char) => {
47372
+ const code = char.codePointAt(0) ?? 0;
47373
+ return code <= 31 || code === 127 ? "_" : char;
47374
+ }).join("").trim();
47375
+ const stem = stripped.length === 0 || stripped === "." || stripped === ".." ? fallbackStem : stripped;
47376
+ return stem.toLowerCase().endsWith(extension) ? stem : `${stem}${extension}`;
47377
+ }
47378
+ function reportFileName(report, artifact) {
47379
+ if (artifact === "docx") {
47380
+ return safeDeliverableFilename(report.documentId, ".docx", "pentest-report");
47381
+ }
47382
+ const extension = report.downloadFormat === "bundle" ? ".zip" : ".pdf";
47383
+ return safeDeliverableFilename(report.documentId, extension, "pentest-report");
47384
+ }
47385
+ function attestationFileName(attestation) {
47386
+ return safeDeliverableFilename(attestation.documentId, ".pdf", "letter-of-attestation");
47387
+ }
47388
+ async function resolveDeliverableDestination(fileName, outOption) {
47389
+ if (outOption === void 0) return resolve(process.cwd(), fileName);
47390
+ const target = resolve(outOption);
47391
+ try {
47392
+ const info = await stat(target);
47393
+ if (info.isDirectory()) return join6(target, fileName);
47394
+ } catch {
47395
+ }
47396
+ return target;
47397
+ }
47398
+ async function pathExists(path) {
47399
+ try {
47400
+ await stat(path);
47401
+ return true;
47402
+ } catch {
47403
+ return false;
47404
+ }
47405
+ }
47406
+ async function downloadDeliverableToFile(downloadUrl, destination, options = {}) {
47407
+ const fetchImpl = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
47408
+ const partialPath = `${destination}.part`;
47409
+ let response;
47410
+ try {
47411
+ response = await fetchImpl(downloadUrl, { method: "GET", redirect: "manual" });
47412
+ } catch (error48) {
47413
+ const detail = error48 instanceof Error ? error48.message : "network error";
47414
+ throw new PentestDeliverableDownloadError(`Download from storage failed: ${detail}.`);
47415
+ }
47416
+ if (!response.ok || response.body === null) {
47417
+ if (response.body !== null) await response.body.cancel();
47418
+ throw new PentestDeliverableDownloadError(
47419
+ `Storage answered HTTP ${response.status} instead of the document. Retry; the download link may have expired.`
47420
+ );
47421
+ }
47422
+ const contentLength = response.headers.get("content-length");
47423
+ let totalBytes = null;
47424
+ if (contentLength !== null) {
47425
+ const declared = Number(contentLength);
47426
+ if (!Number.isSafeInteger(declared) || declared < 0) {
47427
+ await response.body.cancel();
47428
+ throw new PentestDeliverableDownloadError(
47429
+ `Storage declared an unreadable Content-Length ('${contentLength}').`
47430
+ );
47431
+ }
47432
+ totalBytes = declared;
47433
+ }
47434
+ await rm(partialPath, { force: true });
47435
+ const file2 = await open2(partialPath, "wx");
47436
+ const reader = response.body.getReader();
47437
+ let downloadedBytes = 0;
47438
+ options.onProgress?.({ downloadedBytes, totalBytes });
47439
+ try {
47440
+ for (; ; ) {
47441
+ const chunk = await reader.read();
47442
+ if (chunk.done) break;
47443
+ downloadedBytes += chunk.value.byteLength;
47444
+ if (totalBytes !== null && downloadedBytes > totalBytes) {
47445
+ await reader.cancel();
47446
+ throw new PentestDeliverableDownloadError(
47447
+ `Storage sent more than the ${totalBytes} bytes it declared.`
47448
+ );
47449
+ }
47450
+ await writeAll(file2, chunk.value);
47451
+ options.onProgress?.({ downloadedBytes, totalBytes });
47452
+ }
47453
+ await file2.close();
47454
+ if (totalBytes !== null && downloadedBytes !== totalBytes) {
47455
+ throw new PentestDeliverableDownloadError(
47456
+ `Download ended after ${downloadedBytes} of ${totalBytes} bytes.`
47457
+ );
47458
+ }
47459
+ if (downloadedBytes === 0) {
47460
+ throw new PentestDeliverableDownloadError("Storage returned an empty document.");
47461
+ }
47462
+ await rename(partialPath, destination);
47463
+ return { bytes: downloadedBytes };
47464
+ } catch (error48) {
47465
+ await file2.close().catch(() => void 0);
47466
+ await rm(partialPath, { force: true });
47467
+ if (error48 instanceof PentestDeliverableDownloadError) throw error48;
47468
+ const detail = error48 instanceof Error ? error48.message : "unknown error";
47469
+ throw new PentestDeliverableDownloadError(`Download from storage failed: ${detail}.`);
47470
+ }
47471
+ }
47472
+ async function writeAll(file2, bytes) {
47473
+ let offset = 0;
47474
+ while (offset < bytes.byteLength) {
47475
+ const { bytesWritten } = await file2.write(bytes, offset, bytes.byteLength - offset);
47476
+ if (bytesWritten === 0) {
47477
+ throw new PentestDeliverableDownloadError("Writing the document to disk stalled.");
47478
+ }
47479
+ offset += bytesWritten;
47480
+ }
47481
+ }
47482
+ var DELIVERABLE_LABEL = {
47483
+ report: "report",
47484
+ attestation: "letter of attestation"
47485
+ };
47486
+ function requirePentestIdentifier(value) {
47487
+ if (!isPentestIdentifier(value)) {
47488
+ console.error(
47489
+ colorError(`Invalid pentest identifier '${value}'. Use a display ID (e.g. PT-3) or UUID.`)
47490
+ );
47491
+ process.exit(1);
47492
+ }
47493
+ }
47494
+ function failDeliverable(status, body, deliverable, pentestIdentifier, action) {
47495
+ const { message, errorCode } = readErrorFields(body);
47496
+ const label = DELIVERABLE_LABEL[deliverable];
47497
+ if (status === UNAUTHORIZED_STATUS2) {
47498
+ exitAuthFailed();
47499
+ }
47500
+ if (status === 403) {
47501
+ console.error(
47502
+ colorError(
47503
+ message ?? `You do not have permission to ${action === "list" ? "view" : "download"} ${label}s for this organization.`
47504
+ )
47505
+ );
47506
+ process.exit(1);
47507
+ }
47508
+ if (status === 404) {
47509
+ if (message === "Report not found") {
47510
+ console.error(
47511
+ colorError(
47512
+ `No report has been shared for pentest ${pentestIdentifier} yet. Trace publishes it to your dashboard when the engagement's report is ready.`
47513
+ )
47514
+ );
47515
+ process.exit(1);
47516
+ }
47517
+ if (message === "Attestation not found") {
47518
+ console.error(
47519
+ colorError(
47520
+ `That letter of attestation is not available for pentest ${pentestIdentifier}. Run \`tracecli pentest attestation list ${pentestIdentifier}\` to see the letters shared with you.`
47521
+ )
47522
+ );
47523
+ process.exit(1);
47524
+ }
47525
+ console.error(
47526
+ colorError(
47527
+ `Pentest '${pentestIdentifier}' not found. Verify the ID with \`tracecli pentest list\`.`
47528
+ )
47529
+ );
47530
+ process.exit(1);
47531
+ }
47532
+ if (status === 409) {
47533
+ if (errorCode === "DOCX_NOT_RENDERED") {
47534
+ console.error(
47535
+ colorError(
47536
+ "No Word document is available for this report. Drop --docx to download the PDF."
47537
+ )
47538
+ );
47539
+ process.exit(1);
47540
+ }
47541
+ console.error(colorError(message ?? `Conflict (status ${status}).`));
47542
+ process.exit(1);
47543
+ }
47544
+ if (status === 400) {
47545
+ console.error(colorError(message ?? "Invalid request (status 400)."));
47546
+ process.exit(1);
47547
+ }
47548
+ console.error(
47549
+ colorError(
47550
+ `Failed to ${action} ${label}${action === "list" ? "s" : ""} (status ${status})${message ? `: ${message}` : "."}`
47551
+ )
47552
+ );
47553
+ process.exit(1);
47554
+ }
47555
+ function writeDownloadProgress(progress3) {
47556
+ if (!process.stderr.isTTY) return;
47557
+ const downloaded = formatArchiveBytes(progress3.downloadedBytes);
47558
+ if (progress3.totalBytes === null || progress3.totalBytes === 0) {
47559
+ process.stderr.write(`\rDownloading ${downloaded}`);
47560
+ return;
47561
+ }
47562
+ const pct = Math.floor(progress3.downloadedBytes / progress3.totalBytes * 100);
47563
+ process.stderr.write(
47564
+ `\rDownloading ${pct}% (${downloaded} / ${formatArchiveBytes(progress3.totalBytes)})`
47565
+ );
47566
+ }
47567
+ function finishProgress() {
47568
+ if (process.stderr.isTTY) process.stderr.write("\n");
47569
+ }
47570
+ async function saveDeliverable(downloadUrl, fileName, options) {
47571
+ const destination = await resolveDeliverableDestination(fileName, options.out);
47572
+ if (!options.force && await pathExists(destination)) {
47573
+ console.error(
47574
+ colorError(`${destination} already exists. Pass --force to overwrite it or --out <path>.`)
47575
+ );
47576
+ process.exit(1);
47577
+ }
47578
+ if (options.output === "table") {
47579
+ console.error(`Downloading ${sanitizeDisplay(fileName)} to ${destination}`);
47580
+ }
47581
+ try {
47582
+ const { bytes } = await downloadDeliverableToFile(downloadUrl, destination, {
47583
+ onProgress: writeDownloadProgress
47584
+ });
47585
+ finishProgress();
47586
+ return { destination, bytes };
47587
+ } catch (error48) {
47588
+ finishProgress();
47589
+ const detail = error48 instanceof Error ? error48.message : "Download from storage failed.";
47590
+ console.error(colorError(detail));
47591
+ process.exit(1);
47592
+ }
47593
+ }
47594
+ async function runReportDownload(pentestIdentifier, options) {
47595
+ const output = parseOutput(options.output);
47596
+ requirePentestIdentifier(pentestIdentifier);
47597
+ const artifact = options.docx ? "docx" : "primary";
47598
+ const organizationId = await resolveOrganizationId();
47599
+ const result = await authenticatedRequest(
47600
+ (client) => client.pentestReportPdf.downloadCustomer({
47601
+ params: { organizationId, pentestId: pentestIdentifier },
47602
+ query: {
47603
+ disposition: "attachment",
47604
+ ...artifact === "docx" ? { artifact: "docx" } : {}
47605
+ }
47606
+ })
47607
+ );
47608
+ if (result.status !== SUCCESS_STATUS) {
47609
+ failDeliverable(result.status, result.body, "report", pentestIdentifier, "download");
47610
+ }
47611
+ const { report, downloadUrl } = result.body;
47612
+ const fileName = reportFileName(report, artifact);
47613
+ const { destination, bytes } = await saveDeliverable(downloadUrl, fileName, {
47614
+ ...options.out !== void 0 ? { out: options.out } : {},
47615
+ ...options.force !== void 0 ? { force: options.force } : {},
47616
+ output
47617
+ });
47618
+ if (output === "json") {
47619
+ console.log(JSON.stringify({ report, artifact, path: destination, sizeBytes: bytes }, null, 2));
47620
+ return;
47621
+ }
47622
+ if (output === "tsv") {
47623
+ console.log(["documentId", "version", "artifact", "path", "sizeBytes"].join(" "));
47624
+ console.log(
47625
+ [
47626
+ tsvSafe(sanitizeDisplay(report.documentId)),
47627
+ tsvSafe(sanitizeDisplay(report.version)),
47628
+ artifact,
47629
+ tsvSafe(destination),
47630
+ String(bytes)
47631
+ ].join(" ")
47632
+ );
47633
+ return;
47634
+ }
47635
+ console.log(
47636
+ colorSuccess(
47637
+ `Saved ${sanitizeDisplay(report.documentId)} (${sanitizeDisplay(report.version)}) to ${destination} (${formatArchiveBytes(bytes)}).`
47638
+ )
47639
+ );
47640
+ }
47641
+ function createReportDownloadCommand() {
47642
+ return new Command("download").description("Download the pentest report shared with your organization").argument("<pentestId>", "Pentest display ID (e.g. PT-3) or UUID").option("--out <path>", "File to write, or an existing directory (default: working directory)").option("-f, --force", "Overwrite an existing file").option("--docx", "Download the Word document instead of the PDF (when your report ships one)").option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").addHelpText(
47643
+ "after",
47644
+ `
47645
+ Downloads the report Trace has published for the engagement: the issued version
47646
+ when one exists, otherwise the current shared draft. The file is named after the
47647
+ report's document ID (for example TR-PT-2026-03-ACME.pdf). Organizations on
47648
+ a partner report template receive a zip carrying the PDF, the Word document, the
47649
+ markdown report, and evidence files.
47650
+
47651
+ Works with a browser session from \`tracecli auth login\` or a Trace API key
47652
+ (TRACE_API_KEY), so it can run unattended in CI.
47653
+
47654
+ Examples:
47655
+ tracecli pentest report download PT-3
47656
+ tracecli pentest report download PT-3 --out ./reports/
47657
+ tracecli pentest report download PT-3 --out ./acme-q3.pdf --force
47658
+ tracecli pentest report download PT-3 --docx
47659
+ tracecli pentest report download PT-3 --output json | jq '.path'`
47660
+ ).action(async (pentestId, options) => {
47661
+ await runReportDownload(pentestId, options);
47662
+ });
47663
+ }
47664
+ function createReportCommand() {
47665
+ const report = new Command("report").description("Download the pentest report");
47666
+ report.addCommand(createReportDownloadCommand());
47667
+ return report;
47668
+ }
47669
+ function sortAttestationsNewestFirst(attestations) {
47670
+ return [...attestations].sort((a2, b) => {
47671
+ const delta = new Date(b.createdAt).getTime() - new Date(a2.createdAt).getTime();
47672
+ return delta !== 0 ? delta : a2.attestationId.localeCompare(b.attestationId);
47673
+ });
47674
+ }
47675
+ function renderAttestationLines(attestation) {
47676
+ const counts = attestation.severityCounts;
47677
+ return [
47678
+ ` ${sanitizeDisplay(attestation.documentId)} (${sanitizeDisplay(attestation.version)}, ${sanitizeDisplay(attestation.classification)})`,
47679
+ ` Issued: ${formatDate(attestation.issuedDate)} | Findings: ${attestation.totalFindings} (C${counts.critical} H${counts.high} M${counts.medium} L${counts.low} I${counts.info}) | ${formatArchiveBytes(attestation.fileSizeBytes)}`,
47680
+ ` ID: ${attestation.attestationId}`,
47681
+ ""
47682
+ ];
47683
+ }
47684
+ var ATTESTATION_TSV_HEADER = [
47685
+ "attestationId",
47686
+ "documentId",
47687
+ "version",
47688
+ "classification",
47689
+ "issuedDate",
47690
+ "totalFindings",
47691
+ "fileSizeBytes",
47692
+ "createdAt"
47693
+ ].join(" ");
47694
+ function attestationTsvRow(attestation) {
47695
+ return [
47696
+ attestation.attestationId,
47697
+ tsvSafe(sanitizeDisplay(attestation.documentId)),
47698
+ tsvSafe(sanitizeDisplay(attestation.version)),
47699
+ tsvSafe(sanitizeDisplay(attestation.classification)),
47700
+ attestation.issuedDate,
47701
+ String(attestation.totalFindings),
47702
+ String(attestation.fileSizeBytes),
47703
+ toIso(attestation.createdAt)
47704
+ ].join(" ");
47705
+ }
47706
+ async function fetchAttestations(organizationId, pentestIdentifier) {
47707
+ const result = await authenticatedRequest(
47708
+ (client) => client.pentestAttestation.listCustomerAttestations({
47709
+ params: { organizationId, pentestId: pentestIdentifier }
47710
+ })
47711
+ );
47712
+ if (result.status !== SUCCESS_STATUS) {
47713
+ failDeliverable(result.status, result.body, "attestation", pentestIdentifier, "list");
47714
+ }
47715
+ return sortAttestationsNewestFirst(result.body.attestations);
47716
+ }
47717
+ async function runAttestationList(pentestIdentifier, options) {
47718
+ const output = parseOutput(options.output);
47719
+ requirePentestIdentifier(pentestIdentifier);
47720
+ const organizationId = await resolveOrganizationId();
47721
+ const attestations = await fetchAttestations(organizationId, pentestIdentifier);
47722
+ if (output === "json") {
47723
+ console.log(JSON.stringify(attestations, null, 2));
47724
+ return;
47725
+ }
47726
+ if (output === "tsv") {
47727
+ console.log(ATTESTATION_TSV_HEADER);
47728
+ for (const attestation of attestations) {
47729
+ console.log(attestationTsvRow(attestation));
47730
+ }
47731
+ return;
47732
+ }
47733
+ if (attestations.length === 0) {
47734
+ console.log(`No letters of attestation have been shared for pentest ${pentestIdentifier}.`);
47735
+ return;
47736
+ }
47737
+ const header = `Found ${attestations.length} letter${attestations.length === 1 ? "" : "s"} of attestation (newest first):`;
47738
+ await runInteractivePager({
47739
+ items: attestations,
47740
+ header,
47741
+ renderItem: (attestation) => renderAttestationLines(attestation),
47742
+ disabled: options.pager === false
47743
+ });
47744
+ }
47745
+ function createAttestationListCommand() {
47746
+ return new Command("list").description("List the letters of attestation shared for a pentest").argument("<pentestId>", "Pentest display ID (e.g. PT-3) or UUID").option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").option("--no-pager", "Disable the interactive arrow-key pager for table output").addHelpText(
47747
+ "after",
47748
+ `
47749
+ Works with a browser session from \`tracecli auth login\` or a Trace API key
47750
+ (TRACE_API_KEY).
47751
+
47752
+ Examples:
47753
+ tracecli pentest attestation list PT-3
47754
+ tracecli pentest attestation list PT-3 --output json | jq '.[0].attestationId'
47755
+ tracecli pentest attestation list PT-3 --output tsv | cut -f1,2`
47756
+ ).action(async (pentestId, options) => {
47757
+ await runAttestationList(pentestId, options);
47758
+ });
47759
+ }
47760
+ async function runAttestationDownload(pentestIdentifier, options) {
47761
+ const output = parseOutput(options.output);
47762
+ requirePentestIdentifier(pentestIdentifier);
47763
+ if (options.attestation !== void 0 && !isUuid(options.attestation)) {
47764
+ console.error(
47765
+ colorError(
47766
+ `Invalid --attestation '${options.attestation}'. Pass the UUID shown by \`tracecli pentest attestation list ${pentestIdentifier}\`.`
47767
+ )
47768
+ );
47769
+ process.exit(1);
47770
+ }
47771
+ const organizationId = await resolveOrganizationId();
47772
+ let attestationId = options.attestation;
47773
+ if (attestationId === void 0) {
47774
+ const attestations = await fetchAttestations(organizationId, pentestIdentifier);
47775
+ const newest = attestations[0];
47776
+ if (newest === void 0) {
47777
+ console.error(
47778
+ colorError(
47779
+ `No letters of attestation have been shared for pentest ${pentestIdentifier} yet.`
47780
+ )
47781
+ );
47782
+ process.exit(1);
47783
+ }
47784
+ if (attestations.length > 1 && output === "table") {
47785
+ console.error(
47786
+ `Downloading the newest of ${attestations.length} letters (${sanitizeDisplay(newest.documentId)}). Pass --attestation <id> for another.`
47787
+ );
47788
+ }
47789
+ attestationId = newest.attestationId;
47790
+ }
47791
+ const result = await authenticatedRequest(
47792
+ (client) => client.pentestAttestation.downloadCustomerAttestation({
47793
+ params: { organizationId, pentestId: pentestIdentifier, attestationId },
47794
+ query: { disposition: "attachment" }
47795
+ })
47796
+ );
47797
+ if (result.status !== SUCCESS_STATUS) {
47798
+ failDeliverable(result.status, result.body, "attestation", pentestIdentifier, "download");
47799
+ }
47800
+ const { attestation, downloadUrl } = result.body;
47801
+ const fileName = attestationFileName(attestation);
47802
+ const { destination, bytes } = await saveDeliverable(downloadUrl, fileName, {
47803
+ ...options.out !== void 0 ? { out: options.out } : {},
47804
+ ...options.force !== void 0 ? { force: options.force } : {},
47805
+ output
47806
+ });
47807
+ if (output === "json") {
47808
+ console.log(JSON.stringify({ attestation, path: destination, sizeBytes: bytes }, null, 2));
47809
+ return;
47810
+ }
47811
+ if (output === "tsv") {
47812
+ console.log(["attestationId", "documentId", "version", "path", "sizeBytes"].join(" "));
47813
+ console.log(
47814
+ [
47815
+ attestation.attestationId,
47816
+ tsvSafe(sanitizeDisplay(attestation.documentId)),
47817
+ tsvSafe(sanitizeDisplay(attestation.version)),
47818
+ tsvSafe(destination),
47819
+ String(bytes)
47820
+ ].join(" ")
47821
+ );
47822
+ return;
47823
+ }
47824
+ console.log(
47825
+ colorSuccess(
47826
+ `Saved ${sanitizeDisplay(attestation.documentId)} (${sanitizeDisplay(attestation.version)}) to ${destination} (${formatArchiveBytes(bytes)}).`
47827
+ )
47828
+ );
47829
+ }
47830
+ function createAttestationDownloadCommand() {
47831
+ return new Command("download").description("Download a letter of attestation (newest by default)").argument("<pentestId>", "Pentest display ID (e.g. PT-3) or UUID").option(
47832
+ "--attestation <id>",
47833
+ "Attestation UUID from `pentest attestation list` (default: the newest letter)"
47834
+ ).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(
47835
+ "after",
47836
+ `
47837
+ The file is named after the letter's document ID (for example
47838
+ TR-LOA-2026-03-ACME-1A2B3C.pdf). Works with a browser session from \`tracecli auth
47839
+ login\` or a Trace API key (TRACE_API_KEY), so it can run unattended in CI.
47840
+
47841
+ Examples:
47842
+ tracecli pentest attestation download PT-3
47843
+ tracecli pentest attestation download PT-3 --out ./compliance/
47844
+ tracecli pentest attestation download PT-3 --attestation 6f1c... --out ./loa.pdf --force
47845
+ tracecli pentest attestation download PT-3 --output json | jq '.path'`
47846
+ ).action(async (pentestId, options) => {
47847
+ await runAttestationDownload(pentestId, options);
47848
+ });
47849
+ }
47850
+ function createAttestationCommand() {
47851
+ const attestation = new Command("attestation").alias("loa").description("List and download letters of attestation");
47852
+ attestation.addCommand(createAttestationListCommand());
47853
+ attestation.addCommand(createAttestationDownloadCommand());
47854
+ return attestation;
47855
+ }
47856
+
47857
+ // src/pentest/evidence.ts
47858
+ import { readFileSync as readFileSync6, statSync as statSync4 } from "fs";
47859
+ import { basename as basename4 } from "path";
47860
+ var SECTION_LIST = ValidationEvidenceSectionValues.join(", ");
47861
+ var KIND_LIST = PentestExploitArtifactKindValues.join(", ");
47862
+ function isSection(value) {
47863
+ return ValidationEvidenceSectionValues.includes(value);
47864
+ }
47865
+ function isKind(value) {
47866
+ return PentestExploitArtifactKindValues.includes(value);
47867
+ }
47868
+ function formatBytes2(bytes) {
47869
+ if (bytes < 1024) return `${bytes} B`;
47870
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
47871
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
47872
+ }
47873
+ async function resolveFindingId(organizationId, pentestId, identifier) {
47874
+ if (isUuid(identifier)) {
47875
+ return identifier;
47876
+ }
47877
+ const numericMatch = identifier.match(/^(?:PTF-)?(\d+)$/i);
47878
+ if (!numericMatch?.[1]) {
47879
+ console.error(
47880
+ colorError(
47881
+ `Invalid finding identifier '${sanitizeDisplay(identifier)}'. Use a finding number (e.g. PTF-7) or UUID.`
47882
+ )
47883
+ );
47884
+ process.exit(1);
47885
+ }
47886
+ const displayId = Number.parseInt(numericMatch[1], 10);
47887
+ const result = await authenticatedRequest(
47888
+ (client) => client.pentestFinding.listFindings({ params: { organizationId, pentestId } })
47889
+ );
47890
+ if (result.status === UNAUTHORIZED_STATUS2) {
47891
+ exitAuthFailed();
47892
+ }
47893
+ if (result.status !== 200) {
47894
+ console.error(colorError(`Failed to list findings (status ${result.status}).`));
47895
+ process.exit(1);
47896
+ }
47897
+ const match = result.body.find((f) => f.displayId === displayId);
47898
+ if (!match || !("pentestFindingId" in match)) {
47899
+ console.error(
47900
+ colorError(
47901
+ `No finding PTF-${displayId} in this pentest. Unpublished findings have no number yet \u2014 pass the finding UUID instead (\`tracecli pentest findings list --pentest <id> --output json\`).`
47902
+ )
47903
+ );
47904
+ process.exit(1);
47905
+ }
47906
+ return match.pentestFindingId;
47907
+ }
47908
+ async function runAttachEvidence(options) {
47909
+ if (options.section !== void 0 && !isSection(options.section)) {
47910
+ console.error(colorError(`Invalid --section '${sanitizeDisplay(options.section)}'.`));
47911
+ console.error(`Valid sections: ${SECTION_LIST}`);
47912
+ process.exit(1);
47913
+ }
47914
+ if (options.kind !== void 0 && !isKind(options.kind)) {
47915
+ console.error(colorError(`Invalid --kind '${sanitizeDisplay(options.kind)}'.`));
47916
+ console.error(`Valid kinds: ${KIND_LIST}`);
47917
+ process.exit(1);
47918
+ }
47919
+ let sizeBytes;
47920
+ try {
47921
+ const stats = statSync4(options.file);
47922
+ if (!stats.isFile()) {
47923
+ console.error(colorError(`--file '${sanitizeDisplay(options.file)}' is not a regular file.`));
47924
+ process.exit(1);
47925
+ }
47926
+ sizeBytes = stats.size;
47927
+ } catch {
47928
+ console.error(colorError(`Cannot read --file '${sanitizeDisplay(options.file)}'.`));
47929
+ process.exit(1);
47930
+ }
47931
+ if (sizeBytes === 0) {
47932
+ console.error(colorError(`--file '${sanitizeDisplay(options.file)}' is empty.`));
47933
+ process.exit(1);
47934
+ }
47935
+ if (sizeBytes > MAX_STAFF_EVIDENCE_UPLOAD_BYTES) {
47936
+ console.error(
47937
+ colorError(
47938
+ `File is ${formatBytes2(sizeBytes)}; the limit for one exhibit is ${formatBytes2(MAX_STAFF_EVIDENCE_UPLOAD_BYTES)}.`
47939
+ )
47940
+ );
47941
+ process.exit(1);
47942
+ }
47943
+ const organizationId = await resolveOrganizationId();
47944
+ const pentestId = await resolvePentestId(organizationId, options.pentest);
47945
+ const pentestFindingId = await resolveFindingId(organizationId, pentestId, options.finding);
47946
+ const params = { organizationId, pentestId, pentestFindingId };
47947
+ const filename = basename4(options.file);
47948
+ const slot = await authenticatedRequest(
47949
+ (client) => client.pentestFinding.createFindingEvidenceUpload({
47950
+ params,
47951
+ body: { filename, sizeBytes }
47952
+ })
47953
+ );
47954
+ if (slot.status === UNAUTHORIZED_STATUS2) {
47955
+ exitAuthFailed();
47956
+ }
47957
+ if (slot.status !== 201) {
47958
+ const { message } = readErrorFields(slot.body);
47959
+ console.error(
47960
+ colorError(message ?? `Failed to start the evidence upload (status ${slot.status}).`)
47961
+ );
47962
+ process.exit(1);
47963
+ }
47964
+ const body = readFileSync6(options.file);
47965
+ const put = await fetch(slot.body.uploadUrl, { method: "PUT", body: new Uint8Array(body) });
47966
+ if (!put.ok) {
47967
+ console.error(
47968
+ colorError(
47969
+ `Upload to storage failed (status ${put.status}). The upload link is short-lived \u2014 re-run the command.`
47970
+ )
46958
47971
  );
46959
47972
  process.exit(1);
46960
47973
  }
46961
- const viaApiKey = requireAuth().kind === "api_key";
46962
- const organizationId = await resolveOrganizationId();
46963
- const params = {
46964
- organizationId,
46965
- pentestId: options.pentest,
46966
- pentestFindingId: options.finding
46967
- };
46968
- const result = viaApiKey ? await authenticatedRequest(
46969
- (client) => client.pentestFinding.automationStatusUpdate({
46970
- params,
46971
- body: { status: target, ...note !== void 0 ? { note } : {} }
46972
- })
46973
- ) : target === "IN_PROGRESS" ? await authenticatedRequest(
46974
- (client) => client.pentestFinding.startProgress({ params, body: {} })
46975
- ) : await authenticatedRequest(
46976
- (client) => client.pentestFinding.reportRemediation({
47974
+ const attached = await authenticatedRequest(
47975
+ (client) => client.pentestFinding.attachFindingEvidence({
46977
47976
  params,
46978
- body: note !== void 0 ? { note } : {}
47977
+ body: {
47978
+ key: slot.body.key,
47979
+ ...options.caption !== void 0 ? { caption: options.caption } : {},
47980
+ ...options.section !== void 0 && isSection(options.section) ? { section: options.section } : {},
47981
+ ...options.kind !== void 0 && isKind(options.kind) ? { kind: options.kind } : {},
47982
+ ...options.customerFacing ? { customerFacing: true } : {}
47983
+ }
46979
47984
  })
46980
47985
  );
46981
- if (result.status !== 200) {
46982
- failStatusUpdate(result.status, result.body, viaApiKey);
47986
+ if (attached.status === UNAUTHORIZED_STATUS2) {
47987
+ exitAuthFailed();
47988
+ }
47989
+ if (attached.status !== 201) {
47990
+ const { message } = readErrorFields(attached.body);
47991
+ console.error(
47992
+ colorError(message ?? `Failed to attach the evidence (status ${attached.status}).`)
47993
+ );
47994
+ process.exit(1);
46983
47995
  }
46984
47996
  if (options.output === "json") {
46985
- console.log(JSON.stringify(result.body, null, 2));
47997
+ console.log(JSON.stringify(attached.body, null, 2));
46986
47998
  return;
46987
47999
  }
46988
- const displayId = result.body.displayId;
46989
- const status = sanitizeDisplay(result.body.status);
46990
- console.log(colorSuccess(`PTF-${displayId} is now ${status}`));
48000
+ const visibility = attached.body.customerFacing ? "customer-facing" : "staff-internal";
48001
+ const placement = attached.body.section ? ` \xB7 ${attached.body.section}` : "";
48002
+ console.log(
48003
+ colorSuccess(
48004
+ `Attached ${attached.body.kind} \xB7 ${formatBytes2(attached.body.sizeBytes)} \xB7 ${visibility}${placement}`
48005
+ )
48006
+ );
48007
+ console.log(` artifact ${attached.body.artifactId}`);
48008
+ if (attached.body.kind === "HAR") {
48009
+ console.log(" HAR captures stay internal \u2014 they can carry a live session token.");
48010
+ } else if (!attached.body.customerFacing) {
48011
+ console.log(" Publish it from the finding\u2019s evidence panel in the staff console.");
48012
+ }
46991
48013
  }
46992
- function createStatusUpdateCommand() {
46993
- return new Command("status-update").description("Move a pentest finding to IN_PROGRESS or REMEDIATED").requiredOption("--pentest <id>", "Pentest display ID (e.g. PT-3) or UUID").requiredOption("--finding <id>", "Finding number (e.g. PTF-7) or UUID").requiredOption(
46994
- "--status <status>",
46995
- "IN_PROGRESS (start work) or REMEDIATED (ready for retest)"
46996
- ).option("--note <text>", "Optional context when reporting remediation").option("-o, --output <format>", "Output format: text, json (default: text)", "text").addHelpText(
48014
+ function createAttachEvidenceCommand() {
48015
+ return new Command("attach-evidence").description("Attach a screenshot, recording, or capture to a pentest finding (staff)").requiredOption("--pentest <id>", "Pentest display ID (e.g. PT-3) or UUID").requiredOption("--finding <id>", "Finding number (e.g. PTF-7) or UUID").requiredOption("--file <path>", "Path to the evidence file").option("--caption <text>", "What the exhibit shows; rendered as its label").option("--section <section>", `Report section it backs (${SECTION_LIST})`).option("--kind <kind>", `Override the derived exhibit kind (${KIND_LIST})`).option("--customer-facing", "Publish to the customer (not allowed for HAR or video)").option("-o, --output <format>", "Output format: text, json (default: text)", "text").addHelpText(
46997
48016
  "after",
46998
48017
  `
46999
- Customer-facing status writes \u2014 the same transitions as the dashboard. An agent
47000
- can mark a finding in progress when it starts the fix, then remediated when the
47001
- fix is ready for Trace to re-test. Leaving the finding IN_PROGRESS (and not
47002
- sending REMEDIATED) keeps it out of the retest queue.
47003
-
47004
- IN_PROGRESS is allowed from OPEN or VERIFIED_OPEN. REMEDIATED is allowed from
47005
- OPEN, IN_PROGRESS, or VERIFIED_OPEN.
47006
-
47007
- A signed-in session (\`tracecli auth login\`) attributes the change to you. A
47008
- Trace API key works too, once Trace enables finding automation for your
47009
- organization: those writes are recorded as \`api-key:<key name>\` and show up as
47010
- yours in the finding's history.
48018
+ The file uploads straight to storage, so recordings of any reasonable size work.
48019
+ Videos and HAR captures always land staff-internal \u2014 publish a video with the
48020
+ toggle in the staff console's evidence panel after reviewing it for redaction.
47011
48021
 
47012
48022
  Examples:
47013
- tracecli pentest findings status-update --pentest PT-3 --finding 7 --status IN_PROGRESS
47014
- tracecli pentest findings status-update --pentest PT-3 --finding PTF-7 --status REMEDIATED
47015
- tracecli pentest findings status-update --pentest PT-3 --finding 7 --status REMEDIATED \\
47016
- --note "Fixed in PR 412" --output json`
48023
+ tracecli pentest findings attach-evidence --pentest PT-3 --finding 7 --file ./poc.png
48024
+ tracecli pentest findings attach-evidence --pentest PT-3 --finding PTF-7 --file ./poc.mov \\
48025
+ --caption "Cookie survives logout, replayed 40s later" --section proofOfConcept
48026
+ tracecli pentest findings attach-evidence --pentest PT-3 --finding 7 --file ./roster.json \\
48027
+ --caption "Full member roster returned to a non-member" --customer-facing --output json`
47017
48028
  ).action(async (options) => {
47018
- await runStatusUpdate(options);
48029
+ await runAttachEvidence(options);
47019
48030
  });
47020
48031
  }
47021
48032
 
@@ -48130,12 +49141,14 @@ function createPentestCommand() {
48130
49141
  pentest.addCommand(createUpdateCommand4());
48131
49142
  pentest.addCommand(createQuestionnaireCommand());
48132
49143
  pentest.addCommand(createFindingsCommand());
49144
+ pentest.addCommand(createReportCommand());
49145
+ pentest.addCommand(createAttestationCommand());
48133
49146
  return pentest;
48134
49147
  }
48135
49148
 
48136
49149
  // src/update/commands.ts
48137
49150
  import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
48138
- import { dirname as dirname3, join as join6 } from "path";
49151
+ import { dirname as dirname3, join as join7 } from "path";
48139
49152
  import { createInterface as createInterface4 } from "readline/promises";
48140
49153
  var UPDATE_CACHE_FILE = "update-check.json";
48141
49154
  var UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
@@ -48144,7 +49157,7 @@ var DISABLE_UPDATE_NOTIFIER_ENV = "TRACECLI_DISABLE_UPDATE_NOTIFIER";
48144
49157
  var REGISTRY_URL_ENV = "TRACECLI_NPM_REGISTRY_URL";
48145
49158
  var noticePrinted = false;
48146
49159
  function getUpdateCheckCachePath() {
48147
- return join6(getTraceDir(), UPDATE_CACHE_FILE);
49160
+ return join7(getTraceDir(), UPDATE_CACHE_FILE);
48148
49161
  }
48149
49162
  function readUpdateCheckCache() {
48150
49163
  const cachePath = getUpdateCheckCachePath();
@@ -48518,153 +49531,8 @@ Examples:
48518
49531
 
48519
49532
  // src/upload/download.ts
48520
49533
  import { createHash as createHash2 } from "crypto";
48521
- import { open as open2, rename, rm, stat } from "fs/promises";
48522
- import { basename as basename4, join as join7, resolve } from "path";
48523
-
48524
- // src/upload/prepare.ts
48525
- import { createHash } from "crypto";
48526
- import { createReadStream, statSync as statSync4 } from "fs";
48527
- import { open } from "fs/promises";
48528
- import { basename as basename3 } from "path";
48529
- var CodeUploadPrepareError = class extends Error {
48530
- constructor(message) {
48531
- super(message);
48532
- this.name = "CodeUploadPrepareError";
48533
- }
48534
- };
48535
- var KIND_BY_INPUT = {
48536
- source: "SOURCE_ARCHIVE",
48537
- code: "SOURCE_ARCHIVE",
48538
- "source-archive": "SOURCE_ARCHIVE",
48539
- source_archive: "SOURCE_ARCHIVE",
48540
- SOURCE_ARCHIVE: "SOURCE_ARCHIVE",
48541
- apk: "ANDROID_APK",
48542
- android: "ANDROID_APK",
48543
- "android-apk": "ANDROID_APK",
48544
- android_apk: "ANDROID_APK",
48545
- ANDROID_APK: "ANDROID_APK",
48546
- ipa: "IOS_IPA",
48547
- ios: "IOS_IPA",
48548
- "ios-ipa": "IOS_IPA",
48549
- ios_ipa: "IOS_IPA",
48550
- IOS_IPA: "IOS_IPA"
48551
- };
48552
- function normalizeArtifactKind(input) {
48553
- const trimmed = input.trim();
48554
- return KIND_BY_INPUT[trimmed] ?? KIND_BY_INPUT[trimmed.toLowerCase()] ?? null;
48555
- }
48556
- function inferArtifactKindFromFilename(filename) {
48557
- const lower = filename.toLowerCase();
48558
- if (lower.endsWith(".zip")) return "SOURCE_ARCHIVE";
48559
- if (lower.endsWith(".apk")) return "ANDROID_APK";
48560
- if (lower.endsWith(".ipa")) return "IOS_IPA";
48561
- return null;
48562
- }
48563
- function requiredExtensionForKind(kind) {
48564
- if (kind === "ANDROID_APK") return ".apk";
48565
- if (kind === "IOS_IPA") return ".ipa";
48566
- return ".zip";
48567
- }
48568
- function defaultDisplayName(originalFilename) {
48569
- const base = originalFilename.replace(/\.(zip|apk|ipa)$/i, "").trim();
48570
- return base.slice(0, 200) || "source";
48571
- }
48572
- function formatArchiveBytes(bytes) {
48573
- if (bytes < 1024) return `${bytes} B`;
48574
- if (bytes < 1024 * 1024) {
48575
- const kb = bytes / 1024;
48576
- return `${kb.toFixed(Number.isInteger(kb) ? 0 : 1)} KB`;
48577
- }
48578
- if (bytes < 1024 * 1024 * 1024) {
48579
- const mb = bytes / (1024 * 1024);
48580
- return `${mb.toFixed(Number.isInteger(mb) ? 0 : 1)} MB`;
48581
- }
48582
- const gb = bytes / (1024 * 1024 * 1024);
48583
- return `${gb.toFixed(Number.isInteger(gb) ? 0 : 1)} GB`;
48584
- }
48585
- function archiveTooLargeMessage() {
48586
- return `The file must be ${formatArchiveBytes(CODE_UPLOAD_MAX_BYTES)} or smaller.`;
48587
- }
48588
- async function hashFileSha256(filePath) {
48589
- const hash2 = createHash("sha256");
48590
- const stream = createReadStream(filePath);
48591
- for await (const chunk of stream) {
48592
- hash2.update(chunk);
48593
- }
48594
- return hash2.digest("hex");
48595
- }
48596
- async function readFileSlice(filePath, start, endExclusive) {
48597
- const length = endExclusive - start;
48598
- if (!Number.isSafeInteger(start) || !Number.isSafeInteger(length) || start < 0 || length < 1) {
48599
- throw new Error(
48600
- `Invalid file slice: start=${String(start)} endExclusive=${String(endExclusive)}.`
48601
- );
48602
- }
48603
- const buffer = Buffer.alloc(length);
48604
- const handle = await open(filePath, "r");
48605
- try {
48606
- const { bytesRead } = await handle.read(buffer, 0, length, start);
48607
- if (bytesRead !== length) {
48608
- throw new Error(
48609
- `Expected to read ${String(length)} bytes at offset ${String(start)}, got ${String(bytesRead)}.`
48610
- );
48611
- }
48612
- return buffer;
48613
- } finally {
48614
- await handle.close();
48615
- }
48616
- }
48617
- function assertPreparedMatchesUpload(upload, prepared) {
48618
- if (upload.sizeBytes !== prepared.sizeBytes || upload.archiveSha256 !== prepared.archiveSha256) {
48619
- throw new CodeUploadPrepareError(
48620
- "The file does not match the interrupted upload (size or SHA-256 differs)."
48621
- );
48622
- }
48623
- }
48624
- async function prepareUploadFile(filePath, options = {}) {
48625
- let sizeBytes;
48626
- try {
48627
- const stats = statSync4(filePath);
48628
- if (!stats.isFile()) {
48629
- throw new CodeUploadPrepareError("Path is not a regular file. Pass a .zip, .apk, or .ipa.");
48630
- }
48631
- sizeBytes = stats.size;
48632
- } catch (error48) {
48633
- if (error48 instanceof CodeUploadPrepareError) throw error48;
48634
- throw new CodeUploadPrepareError("Cannot read that file.");
48635
- }
48636
- if (!Number.isSafeInteger(sizeBytes) || sizeBytes < 1) {
48637
- throw new CodeUploadPrepareError("The file is empty.");
48638
- }
48639
- if (sizeBytes > CODE_UPLOAD_MAX_BYTES) {
48640
- throw new CodeUploadPrepareError(archiveTooLargeMessage());
48641
- }
48642
- const originalFilename = basename3(filePath);
48643
- const inferred = inferArtifactKindFromFilename(originalFilename);
48644
- const artifactKind = options.artifactKind ?? inferred;
48645
- if (artifactKind == null) {
48646
- throw new CodeUploadPrepareError("Pass a .zip source archive, an .apk, or an .ipa.");
48647
- }
48648
- const expected = requiredExtensionForKind(artifactKind);
48649
- if (!originalFilename.toLowerCase().endsWith(expected)) {
48650
- const label = isApplicationBuildArtifactKind(artifactKind) ? artifactKind === "ANDROID_APK" ? "Android APK" : "iOS package" : "source archive";
48651
- throw new CodeUploadPrepareError(`A ${label} must end in ${expected}.`);
48652
- }
48653
- const displayName = (options.displayName ?? defaultDisplayName(originalFilename)).trim();
48654
- if (displayName.length < 1 || displayName.length > 200) {
48655
- throw new CodeUploadPrepareError("Display name must be 1 to 200 characters.");
48656
- }
48657
- return {
48658
- path: filePath,
48659
- displayName,
48660
- originalFilename,
48661
- sizeBytes,
48662
- archiveSha256: await hashFileSha256(filePath),
48663
- artifactKind
48664
- };
48665
- }
48666
-
48667
- // src/upload/download.ts
49534
+ import { open as open3, rename as rename2, rm as rm2, stat as stat2 } from "fs/promises";
49535
+ import { basename as basename5, join as join8, resolve as resolve2 } from "path";
48668
49536
  var CodeUploadDownloadError = class extends Error {
48669
49537
  constructor(message) {
48670
49538
  super(message);
@@ -48674,7 +49542,7 @@ var CodeUploadDownloadError = class extends Error {
48674
49542
  function safeDownloadFilename(upload) {
48675
49543
  const extension = requiredExtensionForKind(upload.artifactKind);
48676
49544
  const fallback = `app${extension}`;
48677
- const stripped = Array.from(basename4(upload.originalFilename.replace(/\\/g, "/")), (char) => {
49545
+ const stripped = Array.from(basename5(upload.originalFilename.replace(/\\/g, "/")), (char) => {
48678
49546
  const code = char.codePointAt(0) ?? 0;
48679
49547
  return code <= 31 || code === 127 ? "_" : char;
48680
49548
  }).join("").trim();
@@ -48683,18 +49551,18 @@ function safeDownloadFilename(upload) {
48683
49551
  }
48684
49552
  async function resolveDownloadDestination(upload, outOption) {
48685
49553
  const filename = safeDownloadFilename(upload);
48686
- if (outOption === void 0) return resolve(process.cwd(), filename);
48687
- const target = resolve(outOption);
49554
+ if (outOption === void 0) return resolve2(process.cwd(), filename);
49555
+ const target = resolve2(outOption);
48688
49556
  try {
48689
- const info = await stat(target);
48690
- if (info.isDirectory()) return join7(target, filename);
49557
+ const info = await stat2(target);
49558
+ if (info.isDirectory()) return join8(target, filename);
48691
49559
  } catch {
48692
49560
  }
48693
49561
  return target;
48694
49562
  }
48695
- async function pathExists(path) {
49563
+ async function pathExists2(path) {
48696
49564
  try {
48697
- await stat(path);
49565
+ await stat2(path);
48698
49566
  return true;
48699
49567
  } catch {
48700
49568
  return false;
@@ -48726,8 +49594,8 @@ async function downloadCodeUploadToFile(upload, downloadUrl, destination, option
48726
49594
  );
48727
49595
  }
48728
49596
  }
48729
- await rm(partialPath, { force: true });
48730
- const file2 = await open2(partialPath, "wx");
49597
+ await rm2(partialPath, { force: true });
49598
+ const file2 = await open3(partialPath, "wx");
48731
49599
  const hash2 = createHash2("sha256");
48732
49600
  const reader = response.body.getReader();
48733
49601
  let downloadedBytes = 0;
@@ -48744,7 +49612,7 @@ async function downloadCodeUploadToFile(upload, downloadUrl, destination, option
48744
49612
  );
48745
49613
  }
48746
49614
  hash2.update(chunk.value);
48747
- await writeAll(file2, chunk.value);
49615
+ await writeAll2(file2, chunk.value);
48748
49616
  options.onProgress?.(progress(downloadedBytes, upload.sizeBytes));
48749
49617
  }
48750
49618
  await file2.close();
@@ -48759,10 +49627,10 @@ async function downloadCodeUploadToFile(upload, downloadUrl, destination, option
48759
49627
  "Downloaded bytes do not match the SHA-256 recorded at upload time."
48760
49628
  );
48761
49629
  }
48762
- await rename(partialPath, destination);
49630
+ await rename2(partialPath, destination);
48763
49631
  } catch (error48) {
48764
49632
  await file2.close().catch(() => void 0);
48765
- await rm(partialPath, { force: true });
49633
+ await rm2(partialPath, { force: true });
48766
49634
  if (error48 instanceof CodeUploadDownloadError) throw error48;
48767
49635
  const detail = error48 instanceof Error ? error48.message : "unknown error";
48768
49636
  throw new CodeUploadDownloadError(`Download from storage failed: ${detail}.`);
@@ -48775,7 +49643,7 @@ function progress(downloadedBytes, totalBytes) {
48775
49643
  fraction: totalBytes === 0 ? 1 : downloadedBytes / totalBytes
48776
49644
  };
48777
49645
  }
48778
- async function writeAll(file2, bytes) {
49646
+ async function writeAll2(file2, bytes) {
48779
49647
  let offset = 0;
48780
49648
  while (offset < bytes.byteLength) {
48781
49649
  const { bytesWritten } = await file2.write(bytes, offset, bytes.byteLength - offset);
@@ -48911,13 +49779,13 @@ function progress2(uploadedBytes, totalBytes) {
48911
49779
  };
48912
49780
  }
48913
49781
  function defaultWait(milliseconds) {
48914
- return new Promise((resolve2) => setTimeout(resolve2, milliseconds));
49782
+ return new Promise((resolve3) => setTimeout(resolve3, milliseconds));
48915
49783
  }
48916
49784
 
48917
49785
  // src/upload/commands.ts
48918
49786
  var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["READY", "REJECTED", "FAILED", "DELETING"]);
48919
49787
  var DEFAULT_RUNTIME = {
48920
- wait: (milliseconds) => new Promise((resolve2) => setTimeout(resolve2, milliseconds)),
49788
+ wait: (milliseconds) => new Promise((resolve3) => setTimeout(resolve3, milliseconds)),
48921
49789
  pollIntervalMs: 3e3,
48922
49790
  fetch: (input, init) => globalThis.fetch(input, init)
48923
49791
  };
@@ -49173,14 +50041,14 @@ function writeProgress(progress3) {
49173
50041
  `\rUploading ${pct}% (${formatArchiveBytes(progress3.uploadedBytes)} / ${formatArchiveBytes(progress3.totalBytes)})`
49174
50042
  );
49175
50043
  }
49176
- function writeDownloadProgress(progress3) {
50044
+ function writeDownloadProgress2(progress3) {
49177
50045
  if (!process.stderr.isTTY) return;
49178
50046
  const pct = Math.floor(progress3.fraction * 100);
49179
50047
  process.stderr.write(
49180
50048
  `\rDownloading ${pct}% (${formatArchiveBytes(progress3.downloadedBytes)} / ${formatArchiveBytes(progress3.totalBytes)})`
49181
50049
  );
49182
50050
  }
49183
- function finishProgress() {
50051
+ function finishProgress2() {
49184
50052
  if (process.stderr.isTTY) process.stderr.write("\n");
49185
50053
  }
49186
50054
  async function listUploads(organizationId, query = {}) {
@@ -49310,9 +50178,9 @@ async function runTransfer(filePath, options, runtime) {
49310
50178
  refreshParts: () => refreshRemainingParts(organizationId, codeUploadId)
49311
50179
  }
49312
50180
  );
49313
- finishProgress();
50181
+ finishProgress2();
49314
50182
  } catch (error48) {
49315
- finishProgress();
50183
+ finishProgress2();
49316
50184
  const detail = error48 instanceof Error ? error48.message : "Upload to storage failed.";
49317
50185
  console.error(colorError(detail));
49318
50186
  console.error(resumeHint(codeUploadId, filePath));
@@ -49425,9 +50293,9 @@ async function runResume(codeUploadIdArg, filePath, options, runtime) {
49425
50293
  refreshParts: () => refreshRemainingParts(organizationId, codeUploadId)
49426
50294
  }
49427
50295
  );
49428
- finishProgress();
50296
+ finishProgress2();
49429
50297
  } catch (error48) {
49430
- finishProgress();
50298
+ finishProgress2();
49431
50299
  const detail = error48 instanceof Error ? error48.message : "Upload to storage failed.";
49432
50300
  console.error(colorError(detail));
49433
50301
  console.error(resumeHint(codeUploadId, filePath));
@@ -49505,7 +50373,7 @@ async function runDownload(codeUploadIdArg, options, runtime) {
49505
50373
  }
49506
50374
  const { upload, downloadUrl } = result.body;
49507
50375
  const destination = await resolveDownloadDestination(upload, options.out);
49508
- if (!options.force && await pathExists(destination)) {
50376
+ if (!options.force && await pathExists2(destination)) {
49509
50377
  console.error(
49510
50378
  colorError(`${destination} already exists. Pass --force to overwrite it or --out <path>.`)
49511
50379
  );
@@ -49519,11 +50387,11 @@ async function runDownload(codeUploadIdArg, options, runtime) {
49519
50387
  try {
49520
50388
  await downloadCodeUploadToFile(upload, downloadUrl, destination, {
49521
50389
  fetch: runtime.fetch,
49522
- onProgress: writeDownloadProgress
50390
+ onProgress: writeDownloadProgress2
49523
50391
  });
49524
- finishProgress();
50392
+ finishProgress2();
49525
50393
  } catch (error48) {
49526
- finishProgress();
50394
+ finishProgress2();
49527
50395
  const detail = error48 instanceof Error ? error48.message : "Download from storage failed.";
49528
50396
  console.error(colorError(detail));
49529
50397
  process.exit(1);