securewithtrace 0.45.1 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +6 -2
  2. package/dist/index.js +2437 -558
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3695,6 +3695,7 @@ var PENTEST_FINDING_STATUS_VALUES = [
3695
3695
  "REMEDIATED",
3696
3696
  "VERIFIED_FIXED",
3697
3697
  "VERIFIED_OPEN",
3698
+ "PARTIALLY_FIXED",
3698
3699
  "ACCEPTED_WITH_CONTROLS",
3699
3700
  "DISMISSED"
3700
3701
  ];
@@ -6292,7 +6293,7 @@ var $ZodBase64 = /* @__PURE__ */ $constructor("$ZodBase64", (inst, def) => {
6292
6293
  function isValidBase64URL(data) {
6293
6294
  if (!base64url.test(data))
6294
6295
  return false;
6295
- const base643 = data.replace(/[-_]/g, (c62) => c62 === "-" ? "+" : "/");
6296
+ const base643 = data.replace(/[-_]/g, (c65) => c65 === "-" ? "+" : "/");
6296
6297
  const padded = base643.padEnd(Math.ceil(base643.length / 4) * 4, "=");
6297
6298
  return isValidBase64(padded);
6298
6299
  }
@@ -16336,9 +16337,9 @@ var ZodDate = /* @__PURE__ */ $constructor("ZodDate", (inst, def) => {
16336
16337
  inst._zod.processJSONSchema = (ctx, json2, params) => dateProcessor(inst, ctx, json2, params);
16337
16338
  inst.min = (value, params) => inst.check(_gte(value, params));
16338
16339
  inst.max = (value, params) => inst.check(_lte(value, params));
16339
- const c62 = inst._zod.bag;
16340
- inst.minDate = c62.minimum ? new Date(c62.minimum) : null;
16341
- inst.maxDate = c62.maximum ? new Date(c62.maximum) : null;
16340
+ const c65 = inst._zod.bag;
16341
+ inst.minDate = c65.minimum ? new Date(c65.minimum) : null;
16342
+ inst.maxDate = c65.maximum ? new Date(c65.maximum) : null;
16342
16343
  });
16343
16344
  function date3(params) {
16344
16345
  return _date(ZodDate, params);
@@ -22162,6 +22163,8 @@ var AgentSessionFailureReasonValues = [
22162
22163
  "INTERNAL"
22163
22164
  ];
22164
22165
  var AgentSessionFailureReason = external_exports.enum(AgentSessionFailureReasonValues);
22166
+ var AgentSessionLaunchTargetValues = ["ECS", "LOCAL"];
22167
+ var AgentSessionLaunchTarget = external_exports.enum(AgentSessionLaunchTargetValues);
22165
22168
  var AgentSessionProviderValues = ["BEDROCK", "ANTHROPIC", "OPENAI"];
22166
22169
  var AgentSessionProvider = external_exports.enum(AgentSessionProviderValues);
22167
22170
  var AgentSessionEventKindValues = [
@@ -22194,6 +22197,9 @@ var MAX_AGENT_SESSION_OBJECTIVE_CHARS = 32e3;
22194
22197
  var CreateAgentSessionCommonSchema = external_exports.object({
22195
22198
  provider: AgentSessionProvider.optional(),
22196
22199
  model: external_exports.string().min(1).max(200).optional(),
22200
+ /** LOCAL leaves the launch for the caller to claim (`claimLocalLaunch`) instead of
22201
+ * submitting a Fargate task. Staff tooling only; the dashboard never sends it. */
22202
+ launchTarget: AgentSessionLaunchTarget.optional(),
22197
22203
  maxUsd: external_exports.number().positive().max(MAX_AGENT_SESSION_MAX_USD).optional(),
22198
22204
  maxWallClockMin: external_exports.number().int().positive().max(24 * 60).optional()
22199
22205
  });
@@ -22280,7 +22286,25 @@ var ResumeAgentSessionRequestSchema = external_exports.object({
22280
22286
  /** Budget raise — required to leave WAITING_ON_STAFF(BUDGET_EXHAUSTED). Same
22281
22287
  * lifetime ceiling as create: a raise past it is not expressible, which is why
22282
22288
  * the create cap had to move for the engagement-wide validator. */
22283
- maxUsd: external_exports.number().positive().max(MAX_AGENT_SESSION_MAX_USD).optional()
22289
+ maxUsd: external_exports.number().positive().max(MAX_AGENT_SESSION_MAX_USD).optional(),
22290
+ /** Move the session between the Fargate path and a staff-claimed local host. Omitted
22291
+ * keeps the row's current target. */
22292
+ launchTarget: AgentSessionLaunchTarget.optional()
22293
+ });
22294
+ var ClaimLocalLaunchResponseSchema = external_exports.object({
22295
+ agentSessionId: external_exports.uuid(),
22296
+ runAttemptId: external_exports.uuid(),
22297
+ runVersion: external_exports.number().int().positive(),
22298
+ /** The host image this environment's task definition runs (`<repo>:<assetHash>` in the
22299
+ * environment's ECR). Null when the environment has no deployed image (the local stack). */
22300
+ imageUri: external_exports.string().nullable(),
22301
+ /** The full container env contract: the same keys the ECS launch passes as container
22302
+ * overrides (`AGENT_SESSION_*` plus the credential env). Carries the run token, so it is
22303
+ * returned once and never logged. */
22304
+ env: external_exports.record(external_exports.string(), external_exports.string())
22305
+ });
22306
+ var LocalHostImageResponseSchema = external_exports.object({
22307
+ imageUri: external_exports.string().nullable()
22284
22308
  });
22285
22309
  var ListAgentSessionEventsQuerySchema = external_exports.object({
22286
22310
  /** Return events with seq > sinceSeq (poll cursor). */
@@ -22315,6 +22339,10 @@ var AgentSessionSummarySchema = external_exports.object({
22315
22339
  launchedAt: external_exports.coerce.date().nullable(),
22316
22340
  lastActivityAt: external_exports.coerce.date().nullable(),
22317
22341
  completedAt: external_exports.coerce.date().nullable(),
22342
+ launchTarget: AgentSessionLaunchTarget,
22343
+ /** Null while parked, and on a LOCAL row until its launch is claimed: LAUNCHING with this
22344
+ * null on a LOCAL row is "awaiting claim". */
22345
+ taskSubmittedAt: external_exports.coerce.date().nullable(),
22318
22346
  createdById: external_exports.uuid(),
22319
22347
  createdAt: external_exports.coerce.date(),
22320
22348
  updatedAt: external_exports.coerce.date()
@@ -22618,6 +22646,43 @@ var agentSessionContract = c2.router({
22618
22646
  500: ErrorResponseSchema
22619
22647
  }
22620
22648
  },
22649
+ // The host image this environment's agent-session task definition runs. Reads
22650
+ // config, mints nothing: it exists so a staff CLI can pull the image BEFORE it
22651
+ // creates or resumes a LOCAL session, keeping the boot deadline off the pull.
22652
+ getLocalHostImage: {
22653
+ method: "GET",
22654
+ path: "/organizations/:organizationId/agent-sessions/local-host-image",
22655
+ pathParams: OrgIdParamsSchema,
22656
+ responses: {
22657
+ 200: LocalHostImageResponseSchema,
22658
+ 401: ErrorResponseSchema,
22659
+ 403: ErrorResponseSchema,
22660
+ 500: ErrorResponseSchema
22661
+ }
22662
+ },
22663
+ // Claim a LOCAL session's launch: mints the run token and returns the container
22664
+ // env the ECS launch would have passed as overrides. Single-shot (a second
22665
+ // claim is 409): two containers must never hold valid tokens for one run.
22666
+ // Verified staff only: the response carries the token and the org's
22667
+ // customer credential env.
22668
+ claimLocalLaunch: {
22669
+ method: "POST",
22670
+ path: "/organizations/:organizationId/pentests/:pentestId/agent-sessions/:agentSessionId/local-launch",
22671
+ pathParams: AgentSessionIdParamsSchema,
22672
+ body: null,
22673
+ responses: {
22674
+ 200: ClaimLocalLaunchResponseSchema,
22675
+ 401: ErrorResponseSchema,
22676
+ 403: ErrorResponseSchema,
22677
+ 404: ErrorResponseSchema,
22678
+ // LAUNCH_TARGET_NOT_LOCAL (an ECS row) or LOCAL_LAUNCH_NOT_PENDING (already
22679
+ // claimed, parked, or terminal).
22680
+ 409: ErrorResponseSchema,
22681
+ 500: ErrorResponseSchema,
22682
+ // LOCAL_LAUNCH_UNCONFIGURED: this environment has no public host api URL.
22683
+ 503: ErrorResponseSchema
22684
+ }
22685
+ },
22621
22686
  // Staff "Review": advance a VALIDATION pentest VALIDATION_RUNNING →
22622
22687
  // AWAITING_VALIDATION_REVIEW WITHOUT terminating the session (it stays
22623
22688
  // IDLE/resumable for a customer-feedback follow-up). Distinct from stop, which
@@ -22858,6 +22923,145 @@ var ListAgentTasksQuerySchema = external_exports.object({
22858
22923
  });
22859
22924
  var ListAgentTasksResponseSchema = external_exports.array(AgentTaskResponseSchema);
22860
22925
 
22926
+ // ../../packages/api-contracts/src/schemas/api/v1/code-upload.api.ts
22927
+ var CODE_UPLOAD_MAX_BYTES = 1024 * 1024 * 1024;
22928
+ var CODE_UPLOAD_PART_SIZE_BYTES = 10 * 1024 * 1024;
22929
+ var CodeUploadStatusValues = [
22930
+ "UPLOADING",
22931
+ "FINALIZING",
22932
+ "SCANNING",
22933
+ "VALIDATING",
22934
+ "READY",
22935
+ "REJECTED",
22936
+ "DELETING",
22937
+ "FAILED"
22938
+ ];
22939
+ var CodeUploadMalwareScanResultValues = [
22940
+ "NO_THREATS_FOUND",
22941
+ "THREATS_FOUND",
22942
+ "UNSUPPORTED",
22943
+ "ACCESS_DENIED",
22944
+ "FAILED"
22945
+ ];
22946
+ var CodeUploadRejectionReasonValues = [
22947
+ "MALWARE_DETECTED",
22948
+ "MALWARE_SCAN_UNSUPPORTED",
22949
+ "MALWARE_SCAN_UNAVAILABLE",
22950
+ "INVALID_ARCHIVE",
22951
+ "INTEGRITY_MISMATCH"
22952
+ ];
22953
+ var CodeUploadArtifactKindValues = ["SOURCE_ARCHIVE", "ANDROID_APK", "IOS_IPA"];
22954
+ function isApplicationBuildArtifactKind(kind) {
22955
+ return kind === "ANDROID_APK" || kind === "IOS_IPA";
22956
+ }
22957
+ var CodeUploadIdParamsSchema = OrgIdParamsSchema.extend({
22958
+ codeUploadId: external_exports.uuid()
22959
+ });
22960
+ var ListCodeUploadsQuerySchema = external_exports.object({
22961
+ artifactKind: external_exports.enum(CodeUploadArtifactKindValues).optional(),
22962
+ applicationId: external_exports.uuid().optional()
22963
+ }).strict();
22964
+ var InitializeCodeUploadRequestSchema = external_exports.object({
22965
+ displayName: external_exports.string().trim().min(1).max(200),
22966
+ originalFilename: external_exports.string().trim().min(1).max(255),
22967
+ sizeBytes: external_exports.number().int().min(1).max(CODE_UPLOAD_MAX_BYTES),
22968
+ archiveSha256: external_exports.string().regex(/^[0-9a-f]{64}$/),
22969
+ // Defaulted so a pre-this-release client that omitted the field still
22970
+ // initializes a source ZIP during a mixed-version rollout.
22971
+ artifactKind: external_exports.enum(CodeUploadArtifactKindValues).default("SOURCE_ARCHIVE"),
22972
+ applicationId: external_exports.uuid().optional()
22973
+ }).strict().superRefine((data, ctx) => {
22974
+ const filename = data.originalFilename.toLowerCase();
22975
+ if (isApplicationBuildArtifactKind(data.artifactKind)) {
22976
+ const extension = data.artifactKind === "ANDROID_APK" ? ".apk" : ".ipa";
22977
+ if (!filename.endsWith(extension)) {
22978
+ ctx.addIssue({
22979
+ code: "custom",
22980
+ path: ["originalFilename"],
22981
+ message: `originalFilename must end in ${extension}`
22982
+ });
22983
+ }
22984
+ if (data.applicationId == null) {
22985
+ ctx.addIssue({
22986
+ code: "custom",
22987
+ path: ["applicationId"],
22988
+ message: "A build upload must target a mobile application."
22989
+ });
22990
+ }
22991
+ return;
22992
+ }
22993
+ if (!filename.endsWith(".zip")) {
22994
+ ctx.addIssue({
22995
+ code: "custom",
22996
+ path: ["originalFilename"],
22997
+ message: "originalFilename must end in .zip"
22998
+ });
22999
+ }
23000
+ if (data.applicationId != null) {
23001
+ ctx.addIssue({
23002
+ code: "custom",
23003
+ path: ["applicationId"],
23004
+ message: "Source archives are linked to an application after they are ready."
23005
+ });
23006
+ }
23007
+ });
23008
+ var LinkedCodeUploadApplicationSchema = external_exports.object({
23009
+ applicationId: external_exports.uuid(),
23010
+ name: external_exports.string()
23011
+ });
23012
+ var CodeUploadSchema = external_exports.object({
23013
+ codeUploadId: external_exports.uuid(),
23014
+ repositoryId: external_exports.uuid(),
23015
+ displayName: external_exports.string(),
23016
+ originalFilename: external_exports.string(),
23017
+ sizeBytes: external_exports.number().int().positive(),
23018
+ archiveSha256: external_exports.string().regex(/^[0-9a-f]{64}$/),
23019
+ status: external_exports.enum(CodeUploadStatusValues),
23020
+ malwareScanResult: external_exports.enum(CodeUploadMalwareScanResultValues).nullable(),
23021
+ rejectionReason: external_exports.enum(CodeUploadRejectionReasonValues).nullable(),
23022
+ // Defaulted so a new client reading a pre-this-release API during a rolling
23023
+ // deploy doesn't reject source-upload rows that omit the field.
23024
+ artifactKind: external_exports.enum(CodeUploadArtifactKindValues).default("SOURCE_ARCHIVE"),
23025
+ applicationId: external_exports.uuid().nullable().default(null),
23026
+ // z.coerce.date(), not z.date(): web server actions safeParse the raw
23027
+ // ts-rest JSON body, where these arrive as ISO strings.
23028
+ scanningStartedAt: external_exports.coerce.date().nullable(),
23029
+ readyAt: external_exports.coerce.date().nullable(),
23030
+ createdAt: external_exports.coerce.date(),
23031
+ linkedApplications: external_exports.array(LinkedCodeUploadApplicationSchema)
23032
+ });
23033
+ var CodeUploadPartUrlSchema = external_exports.object({
23034
+ partNumber: external_exports.number().int().positive(),
23035
+ uploadUrl: external_exports.url()
23036
+ });
23037
+ var UploadedCodeUploadPartSchema = external_exports.object({
23038
+ partNumber: external_exports.number().int().positive(),
23039
+ sizeBytes: external_exports.number().int().positive()
23040
+ });
23041
+ var CodeUploadMultipartSchema = external_exports.object({
23042
+ partSizeBytes: external_exports.literal(CODE_UPLOAD_PART_SIZE_BYTES),
23043
+ parts: external_exports.array(CodeUploadPartUrlSchema),
23044
+ expiresAt: external_exports.coerce.date()
23045
+ });
23046
+ var InitializeCodeUploadResponseSchema = external_exports.object({
23047
+ upload: CodeUploadSchema,
23048
+ multipart: CodeUploadMultipartSchema
23049
+ });
23050
+ var ResumeCodeUploadResponseSchema = external_exports.object({
23051
+ upload: CodeUploadSchema,
23052
+ multipart: CodeUploadMultipartSchema.extend({
23053
+ uploadedParts: external_exports.array(UploadedCodeUploadPartSchema)
23054
+ })
23055
+ });
23056
+ var ListCodeUploadsResponseSchema = external_exports.object({
23057
+ uploads: external_exports.array(CodeUploadSchema)
23058
+ });
23059
+ var DownloadCodeUploadResponseSchema = external_exports.object({
23060
+ upload: CodeUploadSchema,
23061
+ downloadUrl: external_exports.url(),
23062
+ expiresAt: external_exports.coerce.date()
23063
+ });
23064
+
22861
23065
  // ../../packages/api-contracts/src/schemas/api/v1/inbox.api.ts
22862
23066
  var InboxLabelSchema = external_exports.string().regex(
22863
23067
  /^[a-z][a-z0-9-]{0,39}$/,
@@ -22930,6 +23134,87 @@ var GetInboxMessageResponseSchema = external_exports.object({
22930
23134
  })
22931
23135
  });
22932
23136
 
23137
+ // ../../packages/api-contracts/src/schemas/api/v1/payment-card.api.ts
23138
+ var PAYMENT_CARD_NOTES_MAX = 2e3;
23139
+ function passesLuhn(digits) {
23140
+ if (!/^\d+$/.test(digits)) return false;
23141
+ let sum = 0;
23142
+ let double = false;
23143
+ for (let i2 = digits.length - 1; i2 >= 0; i2--) {
23144
+ let d = digits.charCodeAt(i2) - 48;
23145
+ if (double) {
23146
+ d *= 2;
23147
+ if (d > 9) d -= 9;
23148
+ }
23149
+ sum += d;
23150
+ double = !double;
23151
+ }
23152
+ return sum % 10 === 0;
23153
+ }
23154
+ var PaymentCardIdParamsSchema = external_exports.object({
23155
+ paymentCardId: external_exports.uuid()
23156
+ });
23157
+ var PaymentCardBillingAddressSchema = external_exports.object({
23158
+ line1: external_exports.string().min(1).max(200),
23159
+ line2: external_exports.string().max(200).optional(),
23160
+ city: external_exports.string().min(1).max(100),
23161
+ region: external_exports.string().max(100).optional(),
23162
+ postalCode: external_exports.string().min(1).max(20),
23163
+ /** ISO 3166-1 alpha-2, upper-cased by the service. */
23164
+ country: external_exports.string().regex(/^[A-Za-z]{2}$/, "Country is a two-letter code, like US.")
23165
+ }).strict();
23166
+ var CardNumberSchema = external_exports.string().regex(/^\d{12,19}$/, "Card number must be 12 to 19 digits.").refine(passesLuhn, { message: "Card number fails its check digit." });
23167
+ var CardCvcSchema = external_exports.string().regex(/^\d{3,4}$/, "CVC must be 3 or 4 digits.");
23168
+ var CardExpMonthSchema = external_exports.number().int().min(1).max(12);
23169
+ var CardExpYearSchema = external_exports.number().int().min(2026).max(2100);
23170
+ var PaymentCardResponseSchema = external_exports.object({
23171
+ paymentCardId: external_exports.uuid(),
23172
+ label: external_exports.string(),
23173
+ last4: external_exports.string(),
23174
+ expMonth: external_exports.number().int(),
23175
+ expYear: external_exports.number().int(),
23176
+ cardholderName: external_exports.string(),
23177
+ billingAddress: PaymentCardBillingAddressSchema,
23178
+ notes: external_exports.string(),
23179
+ active: external_exports.boolean(),
23180
+ /** Expiry month is before the current UTC month; the gate treats such a card as absent. */
23181
+ expired: external_exports.boolean(),
23182
+ /** Purchases with status AUTHORIZED or COMPLETED, across every engagement. */
23183
+ purchaseCount: external_exports.number().int(),
23184
+ /** Sum of those purchases' amounts, USD. Answers "how close to the issuer limit". */
23185
+ committedUsd: external_exports.number(),
23186
+ /**
23187
+ * False once ANY purchase row references the card, whatever its status: the
23188
+ * FK is Restrict, so delete would be refused. The UI offers Turn off instead.
23189
+ */
23190
+ deletable: external_exports.boolean(),
23191
+ createdAt: external_exports.date(),
23192
+ updatedAt: external_exports.date()
23193
+ }).strict();
23194
+ var ListPaymentCardsResponseSchema = external_exports.array(PaymentCardResponseSchema);
23195
+ var CreatePaymentCardRequestSchema = external_exports.object({
23196
+ label: external_exports.string().min(1).max(200),
23197
+ number: CardNumberSchema,
23198
+ cvc: CardCvcSchema,
23199
+ expMonth: CardExpMonthSchema,
23200
+ expYear: CardExpYearSchema,
23201
+ cardholderName: external_exports.string().min(1).max(200),
23202
+ billingAddress: PaymentCardBillingAddressSchema,
23203
+ notes: external_exports.string().max(PAYMENT_CARD_NOTES_MAX).optional(),
23204
+ /**
23205
+ * Defaults true. Creating an active card deactivates the current active card
23206
+ * in the same transaction.
23207
+ */
23208
+ active: external_exports.boolean().optional()
23209
+ }).strict();
23210
+ var UpdatePaymentCardRequestSchema = external_exports.object({
23211
+ label: external_exports.string().min(1).max(200).optional(),
23212
+ cardholderName: external_exports.string().min(1).max(200).optional(),
23213
+ billingAddress: PaymentCardBillingAddressSchema.optional(),
23214
+ notes: external_exports.string().max(PAYMENT_CARD_NOTES_MAX).optional(),
23215
+ active: external_exports.boolean().optional()
23216
+ }).strict();
23217
+
22933
23218
  // ../../packages/api-contracts/src/schemas/api/v1/pentest-attack-chain.api.ts
22934
23219
  var PentestAttackChainParamsSchema = OrgIdParamsSchema.extend({
22935
23220
  pentestId: external_exports.uuid()
@@ -23185,6 +23470,8 @@ var RepositoryResponseSchema = external_exports.object({
23185
23470
  prScansEnabled: external_exports.boolean(),
23186
23471
  /** Customer-set gate: false = no scans of any kind run for this repo until it is included again. */
23187
23472
  includedInScanning: external_exports.boolean(),
23473
+ /** Host archive flag mirrored from the provider. */
23474
+ isArchived: external_exports.boolean(),
23188
23475
  createdAt: external_exports.date(),
23189
23476
  updatedAt: external_exports.date()
23190
23477
  });
@@ -23715,6 +24002,10 @@ var ReportCanvasFindingSchema = external_exports.object({
23715
24002
  */
23716
24003
  description: external_exports.string(),
23717
24004
  proofOfConcept: external_exports.string().nullable(),
24005
+ /** Customer-visible acceptance rationale a RAW-rendered ACCEPTED_WITH_CONTROLS
24006
+ * finding shows under Compensating Controls; null otherwise. Optional while the
24007
+ * web and API roll independently. */
24008
+ acceptanceRationale: external_exports.string().nullable().optional(),
23718
24009
  // Per-finding prose staleness (precise — keyed to the finding's own columns).
23719
24010
  proseConfirmed: external_exports.boolean(),
23720
24011
  statusFramingStale: external_exports.boolean(),
@@ -23974,6 +24265,9 @@ var IssuedFindingSnapshotSchema = external_exports.object({
23974
24265
  description: external_exports.string(),
23975
24266
  proofOfConcept: external_exports.string().nullable(),
23976
24267
  remediation: external_exports.string().nullable(),
24268
+ /** Compensating Controls text of a RAW-rendered ACCEPTED_WITH_CONTROLS finding;
24269
+ * null otherwise. Optional so pre-feature issue snapshots remain parseable. */
24270
+ acceptanceRationale: external_exports.string().nullable().optional(),
23977
24271
  repositories: external_exports.array(
23978
24272
  external_exports.object({
23979
24273
  repositoryName: external_exports.string(),
@@ -24399,6 +24693,10 @@ var PentestFindingRepositorySchema = external_exports.object({
24399
24693
  startLine: external_exports.number().nullable(),
24400
24694
  endLine: external_exports.number().nullable()
24401
24695
  });
24696
+ var PentestFindingApplicationSchema = external_exports.object({
24697
+ applicationId: external_exports.uuid(),
24698
+ name: external_exports.string()
24699
+ });
24402
24700
  var PentestFindingResponseSchema = external_exports.object({
24403
24701
  pentestFindingId: external_exports.uuid(),
24404
24702
  organizationId: external_exports.uuid(),
@@ -24462,6 +24760,10 @@ var PentestFindingListItemSchema = PentestFindingResponseSchema.omit({
24462
24760
  attackScenario: true,
24463
24761
  severityConsiderations: true,
24464
24762
  verification: true
24763
+ }).extend({
24764
+ // Derived by `loadFindingApplications` for the list reads only; it backs the
24765
+ // application filter. `.default([])` keeps an API that predates it parsing.
24766
+ applications: external_exports.array(PentestFindingApplicationSchema).default([])
24465
24767
  });
24466
24768
  var ListPentestFindingsResponseSchema = external_exports.array(PentestFindingListItemSchema);
24467
24769
  var OperatorFindingEvidenceSchema = external_exports.object({
@@ -24510,7 +24812,8 @@ var OperatorFindingListSchema = PentestFindingListItemSchema.omit({
24510
24812
  createdByName: true,
24511
24813
  reportProseAt: true,
24512
24814
  reportProseStatus: true,
24513
- externalTicket: true
24815
+ externalTicket: true,
24816
+ applications: true
24514
24817
  });
24515
24818
  var CustomerPentestFindingRepositorySchema = PentestFindingRepositorySchema.omit({
24516
24819
  repositoryId: true
@@ -24792,6 +25095,30 @@ var PublishedFindingSchema = external_exports.object({
24792
25095
  var PublishAllFindingsResponseSchema = external_exports.object({
24793
25096
  published: external_exports.array(PublishedFindingSchema)
24794
25097
  });
25098
+ var CopyPentestFindingsRequestSchema = external_exports.object({
25099
+ sourcePentestId: external_exports.uuid(),
25100
+ pentestFindingIds: external_exports.array(external_exports.uuid()).min(1).max(200).optional(),
25101
+ applicationId: external_exports.uuid().optional(),
25102
+ /** Report the selection without writing anything. */
25103
+ dryRun: external_exports.boolean().optional()
25104
+ }).strict().refine((body) => body.pentestFindingIds === void 0 !== (body.applicationId === void 0), {
25105
+ message: "Provide exactly one of pentestFindingIds or applicationId."
25106
+ });
25107
+ var CopiedPentestFindingSchema = external_exports.object({
25108
+ sourcePentestFindingId: external_exports.uuid(),
25109
+ sourceDisplayId: external_exports.int().nullable(),
25110
+ /** The source's status; the copy itself is always OPEN. */
25111
+ sourceStatus: PentestFindingStatus,
25112
+ /** The new draft's id; null on a dry run. */
25113
+ pentestFindingId: external_exports.uuid().nullable(),
25114
+ title: external_exports.string(),
25115
+ severity: PentestFindingSeverity,
25116
+ evidenceCount: external_exports.int().nonnegative()
25117
+ });
25118
+ var CopyPentestFindingsResponseSchema = external_exports.object({
25119
+ dryRun: external_exports.boolean(),
25120
+ findings: external_exports.array(CopiedPentestFindingSchema)
25121
+ });
24795
25122
  var RecoverStrandedFindingsRequestSchema = external_exports.object({
24796
25123
  dryRun: external_exports.boolean().optional()
24797
25124
  });
@@ -25003,12 +25330,22 @@ var StartProgressPentestFindingRequestSchema = external_exports.object({});
25003
25330
  var ReportRemediationPentestFindingRequestSchema = external_exports.object({
25004
25331
  note: external_exports.string().trim().min(1).max(1e4).optional()
25005
25332
  });
25006
- var AutomationFindingTransitionSchema = external_exports.enum(["IN_PROGRESS", "REMEDIATED"]);
25333
+ var AcceptCustomerPentestFindingRequestSchema = external_exports.object({
25334
+ rationale: external_exports.string().trim().min(1).max(1e4)
25335
+ });
25336
+ var AutomationFindingTransitionSchema = external_exports.enum([
25337
+ "IN_PROGRESS",
25338
+ "REMEDIATED",
25339
+ "ACCEPTED_WITH_CONTROLS"
25340
+ ]);
25007
25341
  var AutomationStatusUpdateRequestSchema = external_exports.object({
25008
25342
  status: AutomationFindingTransitionSchema,
25009
25343
  note: external_exports.string().trim().min(1).max(1e4).optional()
25010
- }).refine((value) => value.note === void 0 || value.status === "REMEDIATED", {
25011
- message: "note is only accepted when reporting remediation (status REMEDIATED)",
25344
+ }).refine((value) => value.note === void 0 || value.status !== "IN_PROGRESS", {
25345
+ message: "note is not accepted when starting progress (status IN_PROGRESS)",
25346
+ path: ["note"]
25347
+ }).refine((value) => value.note !== void 0 || value.status !== "ACCEPTED_WITH_CONTROLS", {
25348
+ message: "note is required when accepting a finding (status ACCEPTED_WITH_CONTROLS)",
25012
25349
  path: ["note"]
25013
25350
  });
25014
25351
  var ReopenPentestFindingTargetStatusSchema = external_exports.enum(["OPEN", "IN_PROGRESS"]);
@@ -25025,10 +25362,11 @@ var retestVerificationOnlyOnFixed = (value, ctx) => {
25025
25362
  });
25026
25363
  }
25027
25364
  };
25365
+ var PENTEST_FINDING_RETEST_VERIFICATION_MAX_LENGTH = 8e3;
25028
25366
  var CreatePentestFindingRetestRequestSchema = external_exports.object({
25029
25367
  outcome: ManualRetestOutcome,
25030
25368
  notes: external_exports.string().trim().min(1).max(1e4),
25031
- verification: external_exports.string().trim().min(1).max(8e3).optional()
25369
+ verification: external_exports.string().trim().min(1).max(PENTEST_FINDING_RETEST_VERIFICATION_MAX_LENGTH).optional()
25032
25370
  });
25033
25371
  var CreatePentestFindingRetestBodySchema = CreatePentestFindingRetestRequestSchema.superRefine(retestVerificationOnlyOnFixed);
25034
25372
  var CreatePentestFindingNoteRequestSchema = external_exports.object({
@@ -25224,6 +25562,46 @@ var PromotePentestHypothesisRequestSchema = external_exports.object({
25224
25562
  manualFindingIds: HypothesisSourceIdsSchema.optional()
25225
25563
  }).strict().optional();
25226
25564
 
25565
+ // ../../packages/api-contracts/src/schemas/api/v1/pentest-purchase.api.ts
25566
+ var PurchaseStatusValues = ["AUTHORIZED", "COMPLETED", "DECLINED", "VOIDED"];
25567
+ var PurchaseStatus = external_exports.enum(PurchaseStatusValues);
25568
+ var PurchaseOutcomeValues = ["COMPLETED", "DECLINED"];
25569
+ var PurchaseOutcome = external_exports.enum(PurchaseOutcomeValues);
25570
+ var PentestPurchaseParamsSchema = OrgIdParamsSchema.extend({
25571
+ pentestId: PentestIdOrDisplayIdSchema
25572
+ });
25573
+ var PentestPurchaseIdParamsSchema = PentestPurchaseParamsSchema.extend({
25574
+ purchaseId: external_exports.uuid()
25575
+ });
25576
+ var PentestPurchaseSchema = external_exports.object({
25577
+ purchaseId: external_exports.uuid(),
25578
+ agentSessionId: external_exports.uuid(),
25579
+ agentSessionType: AgentSessionType,
25580
+ paymentCardLabel: external_exports.string(),
25581
+ paymentCardLast4: external_exports.string(),
25582
+ amountUsd: external_exports.number(),
25583
+ merchant: external_exports.string(),
25584
+ purpose: external_exports.string(),
25585
+ status: PurchaseStatus,
25586
+ voidedAt: external_exports.coerce.date().nullable(),
25587
+ voidedByLabel: external_exports.string().nullable(),
25588
+ voidReason: external_exports.string().nullable(),
25589
+ createdAt: external_exports.coerce.date(),
25590
+ updatedAt: external_exports.coerce.date()
25591
+ }).strict();
25592
+ var ListPentestPurchasesResponseSchema = external_exports.object({
25593
+ capUsd: external_exports.number(),
25594
+ /** Every AUTHORIZED and COMPLETED amount, summed in cents. */
25595
+ committedUsd: external_exports.number(),
25596
+ /** `capUsd - committedUsd`, never below 0 (a cap lowered under the committed sum). */
25597
+ remainingUsd: external_exports.number(),
25598
+ /** Newest first. */
25599
+ purchases: external_exports.array(PentestPurchaseSchema)
25600
+ }).strict();
25601
+ var VoidPentestPurchaseRequestSchema = external_exports.object({
25602
+ reason: external_exports.string().trim().min(1).max(1e3)
25603
+ }).strict();
25604
+
25227
25605
  // ../../packages/api-contracts/src/schemas/api/v1/pentest.api.ts
25228
25606
  var PentestIdParamsSchema = OrgIdParamsSchema.extend({
25229
25607
  pentestId: external_exports.string().min(1)
@@ -25243,6 +25621,10 @@ var CustomerPentestStatusValues = [
25243
25621
  "PAUSED"
25244
25622
  ];
25245
25623
  var CustomerPentestStatus = external_exports.enum(CustomerPentestStatusValues);
25624
+ var PentestFindingStatusCountShape = Object.fromEntries(
25625
+ PentestFindingStatusValues.map((status) => [status, external_exports.number().default(0)])
25626
+ );
25627
+ var PentestFindingStatusCountsByStatusSchema = external_exports.object(PentestFindingStatusCountShape).catchall(external_exports.number());
25246
25628
  var BoundPentestApplicationSchema = external_exports.object({
25247
25629
  applicationId: external_exports.uuid(),
25248
25630
  name: external_exports.string(),
@@ -25302,6 +25684,12 @@ var PentestResponseSchema = external_exports.object({
25302
25684
  hypothesisBudgetCapUsd: external_exports.number(),
25303
25685
  validationBudgetCapUsd: external_exports.number(),
25304
25686
  perHypothesisBudgetCapUsd: external_exports.number(),
25687
+ /**
25688
+ * Per-engagement purchase allowance (USD; 0 = purchasing off). `.optional()`
25689
+ * for the rolling-deploy reason `testType` states above: the staff panel
25690
+ * renders the row read-only when an older API omits it.
25691
+ */
25692
+ purchaseCapUsd: external_exports.number().optional(),
25305
25693
  startedAt: external_exports.coerce.date().nullable(),
25306
25694
  endedAt: external_exports.coerce.date().nullable(),
25307
25695
  /**
@@ -25317,7 +25705,7 @@ var PentestResponseSchema = external_exports.object({
25317
25705
  findingCounts: external_exports.object({
25318
25706
  total: external_exports.number(),
25319
25707
  bySeverity: external_exports.record(PentestFindingSeverity, external_exports.number()),
25320
- byStatus: external_exports.record(PentestFindingStatus, external_exports.number())
25708
+ byStatus: PentestFindingStatusCountsByStatusSchema
25321
25709
  })
25322
25710
  });
25323
25711
  var ListPentestsResponseSchema = external_exports.array(PentestResponseSchema);
@@ -25373,7 +25761,7 @@ var CustomerPentestShellResponseSchema = external_exports.object({
25373
25761
  findingCounts: external_exports.object({
25374
25762
  total: external_exports.number(),
25375
25763
  bySeverity: external_exports.record(PentestFindingSeverity, external_exports.number()),
25376
- byStatus: external_exports.record(PentestFindingStatus, external_exports.number())
25764
+ byStatus: PentestFindingStatusCountsByStatusSchema
25377
25765
  })
25378
25766
  });
25379
25767
  var ListCustomerPentestShellsResponseSchema = external_exports.array(CustomerPentestShellResponseSchema);
@@ -25498,7 +25886,8 @@ var CreatePentestRequestSchema = external_exports.object({
25498
25886
  customerPriorityCategories: external_exports.array(CustomerPriorityCategory).max(20).optional(),
25499
25887
  hypothesisBudgetCapUsd: external_exports.number().min(0).max(1e5).optional(),
25500
25888
  validationBudgetCapUsd: external_exports.number().min(0).max(1e5).optional(),
25501
- perHypothesisBudgetCapUsd: external_exports.number().min(0).max(1e5).optional()
25889
+ perHypothesisBudgetCapUsd: external_exports.number().min(0).max(1e5).optional(),
25890
+ purchaseCapUsd: external_exports.number().min(0).max(1e5).optional()
25502
25891
  });
25503
25892
  var UpdatePentestRequestSchema = external_exports.object({
25504
25893
  title: external_exports.string().min(1).max(500).optional(),
@@ -25529,6 +25918,7 @@ var UpdatePentestRequestSchema = external_exports.object({
25529
25918
  hypothesisBudgetCapUsd: external_exports.number().min(0).max(1e5).optional(),
25530
25919
  validationBudgetCapUsd: external_exports.number().min(0).max(1e5).optional(),
25531
25920
  perHypothesisBudgetCapUsd: external_exports.number().min(0).max(1e5).optional(),
25921
+ purchaseCapUsd: external_exports.number().min(0).max(1e5).optional(),
25532
25922
  lifecycleReason: external_exports.string().trim().min(1).max(2e3).optional()
25533
25923
  });
25534
25924
  var BindPentestApplicationRequestSchema = external_exports.object({
@@ -25643,13 +26033,13 @@ var StorageStateSchema = external_exports.object({
25643
26033
  } catch {
25644
26034
  return;
25645
26035
  }
25646
- s.cookies.forEach((c62, i2) => {
25647
- const d = c62.domain.replace(/^\./, "").toLowerCase();
26036
+ s.cookies.forEach((c65, i2) => {
26037
+ const d = c65.domain.replace(/^\./, "").toLowerCase();
25648
26038
  if (d !== host && !host.endsWith(`.${d}`)) {
25649
26039
  ctx.addIssue({
25650
26040
  code: "custom",
25651
26041
  path: ["cookies", i2, "domain"],
25652
- message: `cookie domain "${c62.domain}" is not the capture origin host "${host}" or a parent domain of it`
26042
+ message: `cookie domain "${c65.domain}" is not the capture origin host "${host}" or a parent domain of it`
25653
26043
  });
25654
26044
  }
25655
26045
  });
@@ -25762,6 +26152,9 @@ var CredentialPasskeySummarySchema = external_exports.object({
25762
26152
  /** Moves only when the signature counter moved, the one observable sign of a use. */
25763
26153
  lastAssertedAt: external_exports.coerce.date().nullable()
25764
26154
  }).strict();
26155
+ var TestIntegrationPasskeySummarySchema = CredentialPasskeySummarySchema.omit({
26156
+ credentialPasskeyId: true
26157
+ }).extend({ testIntegrationPasskeyId: external_exports.uuid() }).strict();
25765
26158
  var CredentialMfaKindValues = ["TOTP"];
25766
26159
  var CredentialMfaKind = external_exports.enum(CredentialMfaKindValues);
25767
26160
  var MFA_CAPABLE_CREDENTIAL_TYPES = ["USERNAME_PASSWORD", "MAGIC_AUTH"];
@@ -25876,6 +26269,7 @@ var DesktopTargetStatusValues = [
25876
26269
  "TERMINATED",
25877
26270
  "FAILED"
25878
26271
  ];
26272
+ var DesktopTargetPlatformValues = ["WINDOWS", "MACOS"];
25879
26273
  var DesktopActionStatusValues = [
25880
26274
  "PENDING",
25881
26275
  "CLAIMED",
@@ -26170,24 +26564,40 @@ var RecordCandidateVulnResponseSchema = external_exports.object({
26170
26564
  inserted: external_exports.boolean()
26171
26565
  });
26172
26566
  var ProductContextStatement = external_exports.string().min(1).max(8e3);
26567
+ var ProductEvidenceKindSchema = external_exports.enum(["docs", "source", "live"]);
26568
+ var EvidenceRefSchema = external_exports.object({
26569
+ ref: external_exports.string().min(1).max(512),
26570
+ why: external_exports.string().min(1).max(300)
26571
+ });
26572
+ var ProductFamilyEvidence = {
26573
+ evidence: ProductEvidenceKindSchema.optional(),
26574
+ refs: external_exports.array(EvidenceRefSchema).max(10).optional()
26575
+ };
26173
26576
  var RecordProductContextRequestSchema = external_exports.object({
26174
26577
  runVersion: external_exports.number().int().nonnegative(),
26175
26578
  payload: external_exports.object({
26176
- purpose: external_exports.object({ statement: ProductContextStatement }).optional(),
26177
- authModel: external_exports.object({ statement: ProductContextStatement }).optional(),
26579
+ purpose: external_exports.object({ statement: ProductContextStatement, ...ProductFamilyEvidence }).optional(),
26580
+ authModel: external_exports.object({ statement: ProductContextStatement, ...ProductFamilyEvidence }).optional(),
26178
26581
  techStack: external_exports.object({
26179
26582
  statement: ProductContextStatement,
26180
26583
  languages: external_exports.array(external_exports.string().min(1).max(80)).max(50).optional(),
26181
26584
  frameworks: external_exports.array(external_exports.string().min(1).max(80)).max(50).optional(),
26182
- services: external_exports.array(external_exports.string().min(1).max(80)).max(50).optional()
26585
+ services: external_exports.array(external_exports.string().min(1).max(80)).max(50).optional(),
26586
+ ...ProductFamilyEvidence
26183
26587
  }).optional(),
26184
26588
  dataClassification: external_exports.object({
26185
26589
  statement: ProductContextStatement,
26186
- classes: external_exports.array(DataClassificationClass).min(1).max(40)
26590
+ classes: external_exports.array(DataClassificationClass).min(1).max(40),
26591
+ ...ProductFamilyEvidence
26187
26592
  }).optional(),
26188
- businessImpact: external_exports.object({ statement: ProductContextStatement, level: BusinessImpactLevel }).optional(),
26189
- deployment: external_exports.object({ statement: ProductContextStatement }).optional(),
26190
- architectureNotes: external_exports.array(external_exports.object({ statement: ProductContextStatement })).max(40).optional()
26593
+ businessImpact: external_exports.object({
26594
+ statement: ProductContextStatement,
26595
+ level: BusinessImpactLevel,
26596
+ ...ProductFamilyEvidence
26597
+ }).optional(),
26598
+ deployment: external_exports.object({ statement: ProductContextStatement, ...ProductFamilyEvidence }).optional(),
26599
+ // The bare-array shape is kept for v1 payloads; each note carries its own evidence.
26600
+ architectureNotes: external_exports.array(external_exports.object({ statement: ProductContextStatement, ...ProductFamilyEvidence })).max(40).optional()
26191
26601
  })
26192
26602
  });
26193
26603
  var RecordProductContextResponseSchema = external_exports.object({
@@ -26796,6 +27206,103 @@ var AgentSessionSshPrivateKeyResponseSchema = external_exports.object({
26796
27206
  */
26797
27207
  secret: SshPrivateKeySecretSchema
26798
27208
  });
27209
+ var AgentSessionAndroidBuildSchema = CodeUploadSchema.pick({
27210
+ codeUploadId: true,
27211
+ displayName: true,
27212
+ originalFilename: true,
27213
+ archiveSha256: true,
27214
+ status: true,
27215
+ malwareScanResult: true
27216
+ }).extend({
27217
+ applicationId: external_exports.uuid(),
27218
+ applicationName: external_exports.string(),
27219
+ sizeBytes: external_exports.number().int().positive().max(CODE_UPLOAD_MAX_BYTES),
27220
+ createdAt: external_exports.iso.datetime()
27221
+ });
27222
+ var AgentSessionListAndroidBuildsRequestSchema = external_exports.object({
27223
+ runVersion: external_exports.number().int().nonnegative(),
27224
+ afterCodeUploadId: external_exports.uuid().optional()
27225
+ });
27226
+ var AgentSessionListAndroidBuildsResponseSchema = external_exports.object({
27227
+ builds: external_exports.array(AgentSessionAndroidBuildSchema).max(50),
27228
+ nextAfterCodeUploadId: external_exports.uuid().nullable()
27229
+ });
27230
+ var AgentSessionGetAndroidBuildRequestSchema = external_exports.object({
27231
+ runVersion: external_exports.number().int().nonnegative(),
27232
+ codeUploadId: external_exports.uuid()
27233
+ });
27234
+ var AgentSessionAndroidBuildGrantSchema = AgentSessionAndroidBuildSchema.pick({
27235
+ codeUploadId: true,
27236
+ applicationId: true,
27237
+ archiveSha256: true,
27238
+ sizeBytes: true
27239
+ }).extend({
27240
+ downloadUrl: external_exports.url(),
27241
+ urlExpiresAt: external_exports.iso.datetime()
27242
+ });
27243
+ var AgentSessionListTestIntegrationsRequestSchema = external_exports.object({
27244
+ runVersion: external_exports.number().int().nonnegative()
27245
+ });
27246
+ var AgentSessionListTestIntegrationsResponseSchema = external_exports.object({
27247
+ /** Every Trace test integration by label, ascending. Empty when none exist. */
27248
+ labels: external_exports.array(external_exports.string())
27249
+ });
27250
+ var AgentSessionGetTestIntegrationRequestSchema = external_exports.object({
27251
+ runVersion: external_exports.number().int().nonnegative(),
27252
+ label: external_exports.string().min(1).max(200)
27253
+ });
27254
+ var AgentSessionTestIntegrationResponseSchema = external_exports.object({
27255
+ testIntegrationId: external_exports.uuid(),
27256
+ label: external_exports.string(),
27257
+ /** Staff-written, non-secret access instructions. */
27258
+ notes: external_exports.string(),
27259
+ /** Verbatim as stored: the host seeds its redactor with these exact bytes. */
27260
+ apiKey: external_exports.string().optional(),
27261
+ login: external_exports.object({ username: external_exports.string(), password: external_exports.string() }).optional()
27262
+ });
27263
+ var AgentSessionRequestPaymentCardRequestSchema = external_exports.object({
27264
+ runVersion: external_exports.number().int().nonnegative(),
27265
+ /** Minted by the host per tool call; reused across that call's HTTP retries. */
27266
+ requestKey: external_exports.uuid(),
27267
+ /** USD, two decimals: what the agent expects the merchant to charge. */
27268
+ amountUsd: external_exports.number().positive().max(1e4).multipleOf(0.01),
27269
+ merchant: external_exports.string().min(1).max(200),
27270
+ purpose: external_exports.string().min(1).max(1e3)
27271
+ });
27272
+ var AgentSessionPaymentCardResponseSchema = external_exports.object({
27273
+ purchaseId: external_exports.uuid(),
27274
+ /** True when this response replays an authorization an earlier attempt already committed. */
27275
+ replayed: external_exports.boolean(),
27276
+ paymentCardId: external_exports.uuid(),
27277
+ label: external_exports.string(),
27278
+ last4: external_exports.string(),
27279
+ expMonth: external_exports.number().int(),
27280
+ expYear: external_exports.number().int(),
27281
+ cardholderName: external_exports.string(),
27282
+ billingAddress: PaymentCardBillingAddressSchema,
27283
+ /** Staff-written, non-secret: the billing email to give, what the issuer declines. */
27284
+ notes: external_exports.string(),
27285
+ /** Cap minus every AUTHORIZED and COMPLETED amount for this pentest, this one included. */
27286
+ remainingUsd: external_exports.number(),
27287
+ capUsd: external_exports.number(),
27288
+ /** Verbatim as stored: the host seeds its redactor with these exact bytes. */
27289
+ number: external_exports.string(),
27290
+ cvc: external_exports.string()
27291
+ });
27292
+ var AgentSessionReportPurchaseOutcomeRequestSchema = external_exports.object({
27293
+ runVersion: external_exports.number().int().nonnegative(),
27294
+ purchaseId: external_exports.uuid(),
27295
+ outcome: PurchaseOutcome
27296
+ });
27297
+ var AgentSessionPurchaseOutcomeResponseSchema = external_exports.object({
27298
+ purchaseId: external_exports.uuid(),
27299
+ /** The row's status after the call. */
27300
+ status: PurchaseStatus,
27301
+ /** True when the row already carried this outcome (a retried report). */
27302
+ replayed: external_exports.boolean(),
27303
+ remainingUsd: external_exports.number(),
27304
+ capUsd: external_exports.number()
27305
+ });
26799
27306
  var AgentSessionPasskeySchema = external_exports.object({
26800
27307
  /** Relying party id (a hostname). */
26801
27308
  rpId: external_exports.string().min(1).max(253),
@@ -26823,18 +27330,54 @@ var AgentSessionStoredPasskeySchema = AgentSessionPasskeySchema.extend({
26823
27330
  privateKey: external_exports.string().min(1).max(4096),
26824
27331
  userHandle: external_exports.string().max(128)
26825
27332
  });
26826
- var AgentSessionListPasskeysResponseSchema = external_exports.object({
26827
- credentialId: external_exports.uuid(),
26828
- credentialLabel: external_exports.string().max(200),
26829
- /** The account types a browser logs in as; the same set that can carry a second factor. */
26830
- credentialType: external_exports.enum(MFA_CAPABLE_CREDENTIAL_TYPES),
26831
- passkeys: external_exports.array(AgentSessionStoredPasskeySchema),
26832
- /**
26833
- * Stored rows whose envelope could not be read. Reported rather than swallowed so a
26834
- * partial list is never presented to the agent as the whole store, and rather than
26835
- * failing the call so the readable passkeys still load.
26836
- */
26837
- unreadable: external_exports.number().int().min(0)
27333
+ var AgentSessionIntegrationPasskeySchema = AgentSessionPasskeySchema.extend({
27334
+ testIntegrationPasskeyId: external_exports.uuid(),
27335
+ privateKey: external_exports.string().min(1).max(4096),
27336
+ userHandle: external_exports.string().max(128)
27337
+ });
27338
+ var AgentSessionListPasskeysResponseSchema = external_exports.discriminatedUnion("owner", [
27339
+ external_exports.object({
27340
+ owner: external_exports.literal("credential"),
27341
+ credentialId: external_exports.uuid(),
27342
+ credentialLabel: external_exports.string().max(200),
27343
+ /** The account types a browser logs in as; the same set that can carry a second factor. */
27344
+ credentialType: external_exports.enum(MFA_CAPABLE_CREDENTIAL_TYPES),
27345
+ passkeys: external_exports.array(AgentSessionStoredPasskeySchema),
27346
+ /**
27347
+ * Stored rows whose envelope could not be read. Reported rather than swallowed so a
27348
+ * partial list is never presented to the agent as the whole store, and rather than
27349
+ * failing the call so the readable passkeys still load.
27350
+ */
27351
+ unreadable: external_exports.number().int().min(0)
27352
+ }),
27353
+ external_exports.object({
27354
+ owner: external_exports.literal("testIntegration"),
27355
+ testIntegrationId: external_exports.uuid(),
27356
+ /** The integration's `label`, under the field name the host already keys on. */
27357
+ credentialLabel: external_exports.string().max(200),
27358
+ passkeys: external_exports.array(AgentSessionIntegrationPasskeySchema),
27359
+ unreadable: external_exports.number().int().min(0)
27360
+ })
27361
+ ]);
27362
+ var AgentSessionTestIntegrationPasskeyUseSchema = AgentSessionPasskeySchema.pick({
27363
+ rpId: true,
27364
+ webauthnCredentialId: true,
27365
+ signCount: true,
27366
+ backupEligibility: true,
27367
+ backupState: true
27368
+ }).strict();
27369
+ var AgentSessionRecordTestIntegrationPasskeyUseRequestSchema = external_exports.object({
27370
+ runVersion: external_exports.number().int().nonnegative(),
27371
+ /** From the list response, never agent-supplied. */
27372
+ testIntegrationId: external_exports.uuid(),
27373
+ passkey: AgentSessionTestIntegrationPasskeyUseSchema
27374
+ });
27375
+ var AgentSessionRecordTestIntegrationPasskeyUseResponseSchema = external_exports.object({
27376
+ testIntegrationPasskeyId: external_exports.uuid(),
27377
+ /** The stored counter after the call, never lower than before. */
27378
+ signCount: external_exports.number().int().min(0),
27379
+ /** False when the stored counter was already at or past the reported one. */
27380
+ counterMoved: external_exports.boolean()
26838
27381
  });
26839
27382
  var AgentSessionUpsertPasskeyRequestSchema = external_exports.object({
26840
27383
  runVersion: external_exports.number().int().nonnegative(),
@@ -27203,8 +27746,9 @@ var ReportUpdateScopeRequestSchema = external_exports.object({
27203
27746
  });
27204
27747
  var RequestDesktopTargetRequestSchema = external_exports.object({
27205
27748
  runVersion: external_exports.number().int().min(0),
27206
- /** Only Windows exists; a literal keeps the macOS follow-on additive. */
27207
- platform: external_exports.literal("WINDOWS")
27749
+ /** A MACOS request is also gated on the session's pinned toolset (the api's
27750
+ * `DESKTOP_MACOS_CAPABLE_TOOLSETS`); the schema alone does not authorize it. */
27751
+ platform: external_exports.enum(DesktopTargetPlatformValues)
27208
27752
  });
27209
27753
  var DesktopCredentialRefSchema = external_exports.object({
27210
27754
  credentialId: external_exports.uuid(),
@@ -27231,7 +27775,11 @@ var DesktopTargetStatusRequestSchema = external_exports.object({
27231
27775
  });
27232
27776
  var DesktopTargetStatusResponseSchema = external_exports.object({
27233
27777
  status: external_exports.enum(DesktopTargetStatusValues),
27234
- failureReason: external_exports.string().nullable()
27778
+ failureReason: external_exports.string().nullable(),
27779
+ /** How the host learns the platform of a target it did not request in this
27780
+ * process (a resumed session): the key chords, job paths and recording rules
27781
+ * it applies all depend on it. */
27782
+ platform: external_exports.enum(DesktopTargetPlatformValues)
27235
27783
  });
27236
27784
  var DESKTOP_ACTION_MAX_WAIT_MS = 9e4;
27237
27785
  var EnqueueDesktopActionRequestSchema = external_exports.object({
@@ -27688,6 +28236,112 @@ var agentSessionInternalContract = c4.router({
27688
28236
  500: ErrorResponseSchema
27689
28237
  }
27690
28238
  },
28239
+ listAndroidBuilds: {
28240
+ method: "POST",
28241
+ path: "/internal/v1/agent-sessions/:agentSessionId/android-builds/list",
28242
+ pathParams: AgentSessionInternalParamsSchema,
28243
+ body: AgentSessionListAndroidBuildsRequestSchema,
28244
+ responses: {
28245
+ 200: AgentSessionListAndroidBuildsResponseSchema,
28246
+ 400: ErrorResponseSchema,
28247
+ 401: ErrorResponseSchema,
28248
+ 403: ErrorResponseSchema,
28249
+ 409: ErrorResponseSchema,
28250
+ 500: ErrorResponseSchema
28251
+ }
28252
+ },
28253
+ getAndroidBuild: {
28254
+ method: "POST",
28255
+ path: "/internal/v1/agent-sessions/:agentSessionId/android-builds/get",
28256
+ pathParams: AgentSessionInternalParamsSchema,
28257
+ body: AgentSessionGetAndroidBuildRequestSchema,
28258
+ responses: {
28259
+ 200: AgentSessionAndroidBuildGrantSchema,
28260
+ 400: ErrorResponseSchema,
28261
+ 401: ErrorResponseSchema,
28262
+ 403: ErrorResponseSchema,
28263
+ 404: ErrorResponseSchema,
28264
+ 409: ErrorResponseSchema,
28265
+ 422: ErrorResponseSchema,
28266
+ 500: ErrorResponseSchema,
28267
+ 503: ErrorResponseSchema
28268
+ }
28269
+ },
28270
+ // get_test_integration, no name: every Trace test integration by label. Gated on the
28271
+ // session type (403 for a type that receives none) and on the run still being live.
28272
+ listTestIntegrations: {
28273
+ method: "POST",
28274
+ path: "/internal/v1/agent-sessions/:agentSessionId/test-integrations",
28275
+ pathParams: AgentSessionInternalParamsSchema,
28276
+ body: AgentSessionListTestIntegrationsRequestSchema,
28277
+ responses: {
28278
+ 200: AgentSessionListTestIntegrationsResponseSchema,
28279
+ 400: ErrorResponseSchema,
28280
+ 401: ErrorResponseSchema,
28281
+ 403: ErrorResponseSchema,
28282
+ 409: ErrorResponseSchema,
28283
+ 500: ErrorResponseSchema
28284
+ }
28285
+ },
28286
+ // get_test_integration, with a name: that integration's notes and decrypted facets.
28287
+ // Same gates as the list. A label that matches no integration is a 404 whose message
28288
+ // names the available labels, which are Trace data, not customer data.
28289
+ getTestIntegration: {
28290
+ method: "POST",
28291
+ path: "/internal/v1/agent-sessions/:agentSessionId/test-integration",
28292
+ pathParams: AgentSessionInternalParamsSchema,
28293
+ body: AgentSessionGetTestIntegrationRequestSchema,
28294
+ responses: {
28295
+ 200: AgentSessionTestIntegrationResponseSchema,
28296
+ 400: ErrorResponseSchema,
28297
+ 401: ErrorResponseSchema,
28298
+ 403: ErrorResponseSchema,
28299
+ 404: ErrorResponseSchema,
28300
+ 409: ErrorResponseSchema,
28301
+ 500: ErrorResponseSchema
28302
+ }
28303
+ },
28304
+ // request_payment_card: declare a purchase against the engagement's cap and, when
28305
+ // it fits, receive the active card's facets. 402 PURCHASE_CAP_EXCEEDED carries
28306
+ // `details: { remainingUsd, capUsd }`; 403 when the run's pinned toolset is not
28307
+ // purchase-capable; 409 RUN_REVOKED, PAYMENT_CARD_UNAVAILABLE (no active card, or
28308
+ // the active card is expired or changed since a replayed request), or
28309
+ // ENGAGEMENT_ENDED (the engagement was cancelled or closed while the session ran).
28310
+ requestPaymentCard: {
28311
+ method: "POST",
28312
+ path: "/internal/v1/agent-sessions/:agentSessionId/payment-card",
28313
+ pathParams: AgentSessionInternalParamsSchema,
28314
+ body: AgentSessionRequestPaymentCardRequestSchema,
28315
+ responses: {
28316
+ 200: AgentSessionPaymentCardResponseSchema,
28317
+ 400: ErrorResponseSchema,
28318
+ 401: ErrorResponseSchema,
28319
+ 402: ErrorResponseSchema,
28320
+ 403: ErrorResponseSchema,
28321
+ 409: ErrorResponseSchema,
28322
+ 500: ErrorResponseSchema
28323
+ }
28324
+ },
28325
+ // report_purchase_outcome: COMPLETED or DECLINED for a purchase this session
28326
+ // authorized. 403 when the run's pinned toolset is not purchase-capable; 404
28327
+ // 404 (no code) for any purchase this session did not make; 409 RUN_REVOKED,
28328
+ // or PURCHASE_OUTCOME_CONFLICT with `details: { status }` when the row was already
28329
+ // reported differently or staff voided it. A repeated identical report replays.
28330
+ reportPurchaseOutcome: {
28331
+ method: "POST",
28332
+ path: "/internal/v1/agent-sessions/:agentSessionId/purchase-outcome",
28333
+ pathParams: AgentSessionInternalParamsSchema,
28334
+ body: AgentSessionReportPurchaseOutcomeRequestSchema,
28335
+ responses: {
28336
+ 200: AgentSessionPurchaseOutcomeResponseSchema,
28337
+ 400: ErrorResponseSchema,
28338
+ 401: ErrorResponseSchema,
28339
+ 403: ErrorResponseSchema,
28340
+ 404: ErrorResponseSchema,
28341
+ 409: ErrorResponseSchema,
28342
+ 500: ErrorResponseSchema
28343
+ }
28344
+ },
27691
28345
  // provision_passkey, read half: every passkey Trace holds for one of the run's
27692
28346
  // accounts, decrypted, so the host can load them into the virtual authenticator
27693
28347
  // it arms on the agent's browser. Same resolution and ownership gate as
@@ -27738,6 +28392,27 @@ var agentSessionInternalContract = c4.router({
27738
28392
  500: ErrorResponseSchema
27739
28393
  }
27740
28394
  },
28395
+ // provision_passkey, integration write half: a stored test-integration passkey's
28396
+ // counter moved on the armed authenticator. Counter-only by shape (no key field on
28397
+ // the body); idempotent on (testIntegrationId, rpId, webauthnCredentialId).
28398
+ recordTestIntegrationPasskeyUse: {
28399
+ method: "POST",
28400
+ path: "/internal/v1/agent-sessions/:agentSessionId/test-integration-passkeys/use",
28401
+ pathParams: AgentSessionInternalParamsSchema,
28402
+ body: AgentSessionRecordTestIntegrationPasskeyUseRequestSchema,
28403
+ responses: {
28404
+ 200: AgentSessionRecordTestIntegrationPasskeyUseResponseSchema,
28405
+ 400: ErrorResponseSchema,
28406
+ 401: ErrorResponseSchema,
28407
+ // Session type receives no account credentials, or may not fetch test integrations.
28408
+ 403: ErrorResponseSchema,
28409
+ // No stored passkey with that natural key on that integration (staff deleted it).
28410
+ 404: ErrorResponseSchema,
28411
+ // RUN_REVOKED.
28412
+ 409: ErrorResponseSchema,
28413
+ 500: ErrorResponseSchema
28414
+ }
28415
+ },
27741
28416
  // renew_cloud_credentials (GCP integration PR 7): agent-invoked re-mint of the
27742
28417
  // ~1 h AWS/GCP recon creds. Run-token authed; org+pentest from the session row;
27743
28418
  // 403 for a non-credentialed session type (ADHOC/CODE_REVIEW/REPORT).
@@ -29966,6 +30641,19 @@ var OrganizationCodeSizeResponseSchema = external_exports.object({
29966
30641
  languageCount: external_exports.number().int().nonnegative()
29967
30642
  })
29968
30643
  });
30644
+ var CodeSizeRecalculateResponseSchema = external_exports.object({
30645
+ /** Repos the request covered. */
30646
+ requested: external_exports.number().int().nonnegative(),
30647
+ /** Messages the queue accepted. Lower than `requested` is a partial send,
30648
+ * reported rather than masked. */
30649
+ enqueued: external_exports.number().int().nonnegative()
30650
+ });
30651
+ var CodeSizeRecalculateLimitedResponseSchema = ErrorResponseSchema.extend({
30652
+ /** Recalculates allowed per org in any 24 hours. */
30653
+ limit: external_exports.number().int().positive(),
30654
+ /** When the oldest request in the window ages out and frees a slot. */
30655
+ retryAt: external_exports.iso.datetime()
30656
+ });
29969
30657
  var StaffCodeSizeRepositorySchema = external_exports.object({
29970
30658
  repositoryId: external_exports.uuid(),
29971
30659
  repositoryUrl: external_exports.url(),
@@ -30009,7 +30697,11 @@ var StaffCodeSizeRemeasureResponseSchema = external_exports.object({
30009
30697
  requested: external_exports.number().int().nonnegative(),
30010
30698
  /** Messages SQS accepted. Lower than `requested` means a partial send —
30011
30699
  * reported rather than masked (the panel states this number). */
30012
- enqueued: external_exports.number().int().nonnegative()
30700
+ enqueued: external_exports.number().int().nonnegative(),
30701
+ /** Repos re-read from GitHub (default branch, archive state) before the
30702
+ * scope was selected. Null when no refresh ran: scope `unmeasured`, or no
30703
+ * GitHub App credentials on this environment. */
30704
+ refreshedRepoCount: external_exports.number().int().nonnegative().nullable()
30013
30705
  });
30014
30706
 
30015
30707
  // ../../packages/api-contracts/src/contracts/v1/code-size.ts
@@ -30027,6 +30719,23 @@ var codeSizeContract = c11.router({
30027
30719
  500: ErrorResponseSchema
30028
30720
  }
30029
30721
  },
30722
+ // Customer Recalculate (MANAGE_INTEGRATED_REPOSITORIES). Enqueue-only.
30723
+ recalculateOrganizationCodeSize: {
30724
+ method: "POST",
30725
+ path: "/organizations/:organizationId/code-size/recalculate",
30726
+ pathParams: OrgIdParamsSchema,
30727
+ body: c11.noBody(),
30728
+ responses: {
30729
+ 200: CodeSizeRecalculateResponseSchema,
30730
+ 401: ErrorResponseSchema,
30731
+ 403: ErrorResponseSchema,
30732
+ 404: ErrorResponseSchema,
30733
+ 429: CodeSizeRecalculateLimitedResponseSchema,
30734
+ 500: ErrorResponseSchema,
30735
+ // Measurement enqueueing is unconfigured (REPO_METADATA_QUEUE_URL unset).
30736
+ 503: ErrorResponseSchema
30737
+ }
30738
+ },
30030
30739
  // Staff-only: per-repo measurement freshness for the admin Controls panel.
30031
30740
  getStaffOrganizationCodeSize: {
30032
30741
  method: "GET",
@@ -30053,151 +30762,15 @@ var codeSizeContract = c11.router({
30053
30762
  403: ErrorResponseSchema,
30054
30763
  404: ErrorResponseSchema,
30055
30764
  500: ErrorResponseSchema,
30765
+ // Scope `all` could not refresh repository state from GitHub; nothing
30766
+ // was re-measured.
30767
+ 502: ErrorResponseSchema,
30056
30768
  // Measurement enqueueing is unconfigured (REPO_METADATA_QUEUE_URL unset).
30057
30769
  503: ErrorResponseSchema
30058
30770
  }
30059
30771
  }
30060
30772
  });
30061
30773
 
30062
- // ../../packages/api-contracts/src/schemas/api/v1/code-upload.api.ts
30063
- var CODE_UPLOAD_MAX_BYTES = 1024 * 1024 * 1024;
30064
- var CODE_UPLOAD_PART_SIZE_BYTES = 10 * 1024 * 1024;
30065
- var CodeUploadStatusValues = [
30066
- "UPLOADING",
30067
- "FINALIZING",
30068
- "SCANNING",
30069
- "VALIDATING",
30070
- "READY",
30071
- "REJECTED",
30072
- "DELETING",
30073
- "FAILED"
30074
- ];
30075
- var CodeUploadMalwareScanResultValues = [
30076
- "NO_THREATS_FOUND",
30077
- "THREATS_FOUND",
30078
- "UNSUPPORTED",
30079
- "ACCESS_DENIED",
30080
- "FAILED"
30081
- ];
30082
- var CodeUploadRejectionReasonValues = [
30083
- "MALWARE_DETECTED",
30084
- "MALWARE_SCAN_UNSUPPORTED",
30085
- "MALWARE_SCAN_UNAVAILABLE",
30086
- "INVALID_ARCHIVE",
30087
- "INTEGRITY_MISMATCH"
30088
- ];
30089
- var CodeUploadArtifactKindValues = ["SOURCE_ARCHIVE", "ANDROID_APK", "IOS_IPA"];
30090
- function isApplicationBuildArtifactKind(kind) {
30091
- return kind === "ANDROID_APK" || kind === "IOS_IPA";
30092
- }
30093
- var CodeUploadIdParamsSchema = OrgIdParamsSchema.extend({
30094
- codeUploadId: external_exports.uuid()
30095
- });
30096
- var ListCodeUploadsQuerySchema = external_exports.object({
30097
- artifactKind: external_exports.enum(CodeUploadArtifactKindValues).optional(),
30098
- applicationId: external_exports.uuid().optional()
30099
- }).strict();
30100
- var InitializeCodeUploadRequestSchema = external_exports.object({
30101
- displayName: external_exports.string().trim().min(1).max(200),
30102
- originalFilename: external_exports.string().trim().min(1).max(255),
30103
- sizeBytes: external_exports.number().int().min(1).max(CODE_UPLOAD_MAX_BYTES),
30104
- archiveSha256: external_exports.string().regex(/^[0-9a-f]{64}$/),
30105
- // Defaulted so a pre-this-release client that omitted the field still
30106
- // initializes a source ZIP during a mixed-version rollout.
30107
- artifactKind: external_exports.enum(CodeUploadArtifactKindValues).default("SOURCE_ARCHIVE"),
30108
- applicationId: external_exports.uuid().optional()
30109
- }).strict().superRefine((data, ctx) => {
30110
- const filename = data.originalFilename.toLowerCase();
30111
- if (isApplicationBuildArtifactKind(data.artifactKind)) {
30112
- const extension = data.artifactKind === "ANDROID_APK" ? ".apk" : ".ipa";
30113
- if (!filename.endsWith(extension)) {
30114
- ctx.addIssue({
30115
- code: "custom",
30116
- path: ["originalFilename"],
30117
- message: `originalFilename must end in ${extension}`
30118
- });
30119
- }
30120
- if (data.applicationId == null) {
30121
- ctx.addIssue({
30122
- code: "custom",
30123
- path: ["applicationId"],
30124
- message: "A build upload must target a mobile application."
30125
- });
30126
- }
30127
- return;
30128
- }
30129
- if (!filename.endsWith(".zip")) {
30130
- ctx.addIssue({
30131
- code: "custom",
30132
- path: ["originalFilename"],
30133
- message: "originalFilename must end in .zip"
30134
- });
30135
- }
30136
- if (data.applicationId != null) {
30137
- ctx.addIssue({
30138
- code: "custom",
30139
- path: ["applicationId"],
30140
- message: "Source archives are linked to an application after they are ready."
30141
- });
30142
- }
30143
- });
30144
- var LinkedCodeUploadApplicationSchema = external_exports.object({
30145
- applicationId: external_exports.uuid(),
30146
- name: external_exports.string()
30147
- });
30148
- var CodeUploadSchema = external_exports.object({
30149
- codeUploadId: external_exports.uuid(),
30150
- repositoryId: external_exports.uuid(),
30151
- displayName: external_exports.string(),
30152
- originalFilename: external_exports.string(),
30153
- sizeBytes: external_exports.number().int().positive(),
30154
- archiveSha256: external_exports.string().regex(/^[0-9a-f]{64}$/),
30155
- status: external_exports.enum(CodeUploadStatusValues),
30156
- malwareScanResult: external_exports.enum(CodeUploadMalwareScanResultValues).nullable(),
30157
- rejectionReason: external_exports.enum(CodeUploadRejectionReasonValues).nullable(),
30158
- // Defaulted so a new client reading a pre-this-release API during a rolling
30159
- // deploy doesn't reject source-upload rows that omit the field.
30160
- artifactKind: external_exports.enum(CodeUploadArtifactKindValues).default("SOURCE_ARCHIVE"),
30161
- applicationId: external_exports.uuid().nullable().default(null),
30162
- // z.coerce.date(), not z.date(): web server actions safeParse the raw
30163
- // ts-rest JSON body, where these arrive as ISO strings.
30164
- scanningStartedAt: external_exports.coerce.date().nullable(),
30165
- readyAt: external_exports.coerce.date().nullable(),
30166
- createdAt: external_exports.coerce.date(),
30167
- linkedApplications: external_exports.array(LinkedCodeUploadApplicationSchema)
30168
- });
30169
- var CodeUploadPartUrlSchema = external_exports.object({
30170
- partNumber: external_exports.number().int().positive(),
30171
- uploadUrl: external_exports.url()
30172
- });
30173
- var UploadedCodeUploadPartSchema = external_exports.object({
30174
- partNumber: external_exports.number().int().positive(),
30175
- sizeBytes: external_exports.number().int().positive()
30176
- });
30177
- var CodeUploadMultipartSchema = external_exports.object({
30178
- partSizeBytes: external_exports.literal(CODE_UPLOAD_PART_SIZE_BYTES),
30179
- parts: external_exports.array(CodeUploadPartUrlSchema),
30180
- expiresAt: external_exports.coerce.date()
30181
- });
30182
- var InitializeCodeUploadResponseSchema = external_exports.object({
30183
- upload: CodeUploadSchema,
30184
- multipart: CodeUploadMultipartSchema
30185
- });
30186
- var ResumeCodeUploadResponseSchema = external_exports.object({
30187
- upload: CodeUploadSchema,
30188
- multipart: CodeUploadMultipartSchema.extend({
30189
- uploadedParts: external_exports.array(UploadedCodeUploadPartSchema)
30190
- })
30191
- });
30192
- var ListCodeUploadsResponseSchema = external_exports.object({
30193
- uploads: external_exports.array(CodeUploadSchema)
30194
- });
30195
- var DownloadCodeUploadResponseSchema = external_exports.object({
30196
- upload: CodeUploadSchema,
30197
- downloadUrl: external_exports.url(),
30198
- expiresAt: external_exports.coerce.date()
30199
- });
30200
-
30201
30774
  // ../../packages/api-contracts/src/contracts/v1/code-upload.ts
30202
30775
  var c12 = initContract();
30203
30776
  var commonErrors = {
@@ -31399,7 +31972,9 @@ var environmentContract = c17.router({
31399
31972
  403: ErrorResponseSchema,
31400
31973
  404: ErrorResponseSchema,
31401
31974
  // 409 — environment target is frozen by a non-terminal pentest
31402
- // (errorCode ENVIRONMENT_TARGET_FROZEN_BY_PENTEST).
31975
+ // (errorCode ENVIRONMENT_TARGET_FROZEN_BY_PENTEST), or a live pentest
31976
+ // still links the environment (errorCode ENVIRONMENT_IN_USE_BY_PENTEST,
31977
+ // details.pentestDisplayIds names the holders).
31403
31978
  409: ErrorResponseSchema,
31404
31979
  500: ErrorResponseSchema
31405
31980
  }
@@ -31579,7 +32154,9 @@ var GitHubRepoSchema = external_exports.object({
31579
32154
  private: external_exports.boolean(),
31580
32155
  htmlUrl: external_exports.string().url(),
31581
32156
  githubDefaultBranch: external_exports.string(),
31582
- language: external_exports.string().nullable()
32157
+ language: external_exports.string().nullable(),
32158
+ /** Archived on GitHub. Archived repos stay listed; archiving excludes them once. */
32159
+ archived: external_exports.boolean()
31583
32160
  });
31584
32161
  var ListGitHubInstallationReposResponseSchema = external_exports.array(GitHubRepoSchema);
31585
32162
 
@@ -32870,9 +33447,10 @@ var PrScanAnalyticsDeveloperSchema = external_exports.object({
32870
33447
  criticalHighFixedCount: external_exports.number().int().nonnegative(),
32871
33448
  falsePositiveCount: external_exports.number().int().nonnegative()
32872
33449
  });
33450
+ var PrScanAnalyticsMonthKeySchema = external_exports.string().regex(/^[1-9]\d{3}-(0[1-9]|1[0-2])$/);
32873
33451
  var PrScanAnalyticsMonthSchema = external_exports.object({
32874
33452
  /** UTC calendar month, `YYYY-MM`. */
32875
- month: external_exports.string().regex(/^\d{4}-\d{2}$/),
33453
+ month: PrScanAnalyticsMonthKeySchema,
32876
33454
  prCount: external_exports.number().int().nonnegative(),
32877
33455
  developerCount: external_exports.number().int().nonnegative(),
32878
33456
  findingsCount: external_exports.number().int().nonnegative(),
@@ -32911,28 +33489,92 @@ var PrScanAnalyticsSummarySchema = external_exports.object({
32911
33489
  /** One row per severity, CRITICAL first. */
32912
33490
  timeToFix: external_exports.array(PrScanAnalyticsTimeToFixSchema).length(VulnerabilitySeverityValues.length)
32913
33491
  });
33492
+ var PrScanAnalyticsFindingSchema = external_exports.object({
33493
+ vulnerabilityId: external_exports.uuid(),
33494
+ title: external_exports.string(),
33495
+ severity: VulnerabilitySeverity,
33496
+ status: VulnerabilityStatus,
33497
+ /** The PR the finding was first detected on; its review comment lives there. */
33498
+ pullRequestNumber: external_exports.number().int().positive().nullable()
33499
+ });
33500
+ var PrScanAnalyticsFindingsQuerySchema = external_exports.object({
33501
+ category: external_exports.string().min(1).max(100),
33502
+ asOf: external_exports.iso.datetime(),
33503
+ afterStatus: VulnerabilityStatus.optional(),
33504
+ afterSeverity: VulnerabilitySeverity.optional(),
33505
+ afterCreatedAt: external_exports.iso.datetime().optional(),
33506
+ afterVulnerabilityId: external_exports.uuid().optional()
33507
+ }).refine(
33508
+ ({ afterStatus, afterSeverity, afterCreatedAt, afterVulnerabilityId }) => {
33509
+ const provided = [afterStatus, afterSeverity, afterCreatedAt, afterVulnerabilityId].filter(
33510
+ (field) => field !== void 0
33511
+ ).length;
33512
+ return provided === 0 || provided === 4;
33513
+ },
33514
+ { message: "Provide all four cursor fields or none", path: ["afterCreatedAt"] }
33515
+ );
33516
+ var PrScanAnalyticsFindingsResponseSchema = external_exports.object({
33517
+ /** Open first (then In progress, Fixed, Archived, False positive, Dismissed), most severe first within a status, then newest. */
33518
+ findings: external_exports.array(PrScanAnalyticsFindingSchema).max(25),
33519
+ total: external_exports.number().int().nonnegative(),
33520
+ nextCursor: external_exports.object({
33521
+ status: VulnerabilityStatus,
33522
+ severity: VulnerabilitySeverity,
33523
+ firstDetectedAt: external_exports.iso.datetime(),
33524
+ vulnerabilityId: external_exports.uuid()
33525
+ }).nullable()
33526
+ });
33527
+ var PrScanAnalyticsFindingCommentParamsSchema = external_exports.object({
33528
+ organizationId: external_exports.uuid(),
33529
+ vulnerabilityId: external_exports.uuid()
33530
+ });
33531
+ var PrScanAnalyticsFindingCommentResponseSchema = external_exports.object({
33532
+ url: external_exports.url(),
33533
+ target: external_exports.enum(["comment", "pull_request"])
33534
+ });
33535
+ var PrScanAnalyticsTotalsSchema = external_exports.object({
33536
+ prCount: external_exports.number().int().nonnegative(),
33537
+ findingsCount: external_exports.number().int().nonnegative(),
33538
+ criticalHighCount: external_exports.number().int().nonnegative(),
33539
+ remediatedCount: external_exports.number().int().nonnegative(),
33540
+ byClass: external_exports.array(PrScanAnalyticsClassCountSchema)
33541
+ });
32914
33542
  var PrScanAnalyticsResponseSchema = external_exports.object({
32915
33543
  generatedAt: external_exports.iso.datetime(),
32916
33544
  months: external_exports.array(PrScanAnalyticsMonthSchema).length(12),
32917
- summary: PrScanAnalyticsSummarySchema
33545
+ summary: PrScanAnalyticsSummarySchema,
33546
+ totals: PrScanAnalyticsTotalsSchema.optional()
32918
33547
  });
32919
33548
  var ProductPlanParamsSchema = external_exports.object({
32920
33549
  organizationId: external_exports.uuid(),
32921
33550
  product: external_exports.enum(ProductValues)
32922
33551
  });
33552
+ var ProseSpellingValues = ["US", "UK"];
33553
+ var ProseSpellingSchema = external_exports.enum(ProseSpellingValues);
32923
33554
  var ReferrerSchema = external_exports.object({
32924
33555
  referrerId: external_exports.uuid(),
32925
33556
  name: external_exports.string(),
32926
33557
  slug: external_exports.string(),
32927
- reportTemplate: PentestReportTemplateSchema
33558
+ reportTemplate: PentestReportTemplateSchema,
33559
+ /** Spelling the agents write customer-facing prose in for this partner's orgs. */
33560
+ proseSpelling: ProseSpellingSchema
33561
+ });
33562
+ var ReferrerParamsSchema = external_exports.object({
33563
+ referrerId: external_exports.uuid()
32928
33564
  });
33565
+ var UpdateReferrerRequestSchema = external_exports.object({
33566
+ proseSpelling: ProseSpellingSchema
33567
+ });
33568
+ var UpdateReferrerResponseSchema = ReferrerSchema;
32929
33569
  var ListReferrersResponseSchema = external_exports.object({
32930
33570
  referrers: external_exports.array(ReferrerSchema)
32931
33571
  });
32932
33572
  var CreateReferrerRequestSchema = external_exports.object({
32933
33573
  name: external_exports.string().trim().min(2, "Partner name must be at least 2 characters").max(100, "Partner name must be 100 characters or fewer"),
32934
33574
  /** Report template for orgs this partner refers. Defaults to TRACE. */
32935
- reportTemplate: PentestReportTemplateSchema.optional()
33575
+ reportTemplate: PentestReportTemplateSchema.optional(),
33576
+ /** Prose spelling for orgs this partner refers. Defaults to US. */
33577
+ proseSpelling: ProseSpellingSchema.optional()
32936
33578
  });
32937
33579
  var CreateReferrerResponseSchema = ReferrerSchema;
32938
33580
  var SetOrganizationReferrerRequestSchema = external_exports.object({
@@ -32993,9 +33635,14 @@ var CreatePrReviewerSubscriptionRequestSchema = external_exports.object({
32993
33635
  /** Calendar day the subscription starts, `YYYY-MM-DD`, UTC. Today or in the past. */
32994
33636
  startDate: external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/)
32995
33637
  });
33638
+ var BillingCollectionMethodValues = ["charge_automatically", "send_invoice"];
32996
33639
  var PrReviewerSubscriptionResponseSchema = external_exports.object({
32997
33640
  subscriptionId: external_exports.string(),
32998
33641
  status: external_exports.string(),
33642
+ /** Optional for rolling-deploy tolerance: an older API omits the key. */
33643
+ collectionMethod: external_exports.enum(BillingCollectionMethodValues).optional(),
33644
+ /** The customer opted into autopay but the saved card didn't pay the first invoice, so it was emailed. */
33645
+ firstChargeDeclined: external_exports.boolean().optional(),
32999
33646
  invoice: external_exports.object({
33000
33647
  id: external_exports.string(),
33001
33648
  status: external_exports.string(),
@@ -33044,6 +33691,33 @@ var BillingSubscriptionSchema = external_exports.object({
33044
33691
  currentPeriodEnd: external_exports.date().nullable(),
33045
33692
  cancelAtPeriodEnd: external_exports.boolean()
33046
33693
  });
33694
+ var BillingPaymentMethodSchema = external_exports.object({
33695
+ /** Stripe PaymentMethod type, e.g. `card` or `us_bank_account`. */
33696
+ type: external_exports.string(),
33697
+ /** Card network or bank name, when Stripe exposes one. */
33698
+ brand: external_exports.string().nullable(),
33699
+ last4: external_exports.string().nullable(),
33700
+ expMonth: external_exports.number().int().nullable(),
33701
+ expYear: external_exports.number().int().nullable()
33702
+ });
33703
+ var BillingAutopayStateValues = ["off", "card_only", "on"];
33704
+ var BillingAutopaySchema = external_exports.object({
33705
+ state: external_exports.enum(BillingAutopayStateValues),
33706
+ /** The customer's default card; null when state is `off`. */
33707
+ paymentMethod: BillingPaymentMethodSchema.nullable(),
33708
+ /**
33709
+ * Open PR reviewer invoices that enabling autopay charges right away, once
33710
+ * the admin confirms them. Enabling charges these ids and no others.
33711
+ */
33712
+ chargeOnEnable: external_exports.object({
33713
+ invoiceIds: external_exports.array(external_exports.string()).min(1),
33714
+ invoiceCount: external_exports.number().int().positive(),
33715
+ amountCents: external_exports.number().int().positive(),
33716
+ currency: external_exports.string(),
33717
+ /** At least one was already charged and declined, rather than never attempted. */
33718
+ declined: external_exports.boolean()
33719
+ }).nullable()
33720
+ });
33047
33721
  var OrganizationBillingSchema = external_exports.object({
33048
33722
  prReviewerBillingRequired: external_exports.boolean(),
33049
33723
  hasStripeCustomer: external_exports.boolean(),
@@ -33053,11 +33727,44 @@ var OrganizationBillingSchema = external_exports.object({
33053
33727
  /** Optional for rolling-deploy tolerance. */
33054
33728
  subscription: BillingSubscriptionSchema.nullable().optional(),
33055
33729
  /** Optional for rolling-deploy tolerance: an older API omits the key. */
33056
- paymentDue: BillingPaymentDueSchema.nullable().optional()
33730
+ paymentDue: BillingPaymentDueSchema.nullable().optional(),
33731
+ /**
33732
+ * Null when there's no Stripe customer, or the card lookup failed (the row is
33733
+ * hidden rather than offering setup over a card that may exist). Optional for
33734
+ * rolling-deploy tolerance.
33735
+ */
33736
+ autopay: BillingAutopaySchema.nullable().optional()
33057
33737
  });
33058
33738
  var BillingPortalResponseSchema = external_exports.object({
33059
33739
  url: external_exports.url()
33060
33740
  });
33741
+ var STRIPE_CHECKOUT_SESSION_ID_PATTERN = /^cs_(test|live)_[A-Za-z0-9]+$/;
33742
+ var EnableAutopayRequestSchema = external_exports.object({
33743
+ /**
33744
+ * Checkout session (setup mode) the admin just completed. Omitted when the
33745
+ * customer already has a default card on file and only the opt-in and the
33746
+ * PR reviewer subscriptions need to switch.
33747
+ */
33748
+ checkoutSessionId: external_exports.string().regex(STRIPE_CHECKOUT_SESSION_ID_PATTERN).optional(),
33749
+ /**
33750
+ * Card-on-file path: the open invoices the admin confirmed charging. Ignored
33751
+ * with a Checkout session, which carries its own confirmation.
33752
+ */
33753
+ confirmedInvoiceIds: external_exports.array(external_exports.string().regex(/^in_[A-Za-z0-9]+$/)).max(15).optional()
33754
+ });
33755
+ var CreateAutopaySetupSessionRequestSchema = external_exports.object({
33756
+ /** Open invoices the admin confirmed; the Checkout return charges these and no others. */
33757
+ confirmedInvoiceIds: external_exports.array(external_exports.string().regex(/^in_[A-Za-z0-9]+$/)).max(15)
33758
+ });
33759
+ var EnableAutopayResponseSchema = external_exports.object({
33760
+ paymentMethod: BillingPaymentMethodSchema,
33761
+ /** PR reviewer subscriptions moved to charge the customer's default card. */
33762
+ subscriptionsUpdated: external_exports.number().int(),
33763
+ /** Open PR reviewer invoices Stripe confirmed as paid. */
33764
+ openInvoicesPaid: external_exports.number().int(),
33765
+ /** Open PR reviewer invoices the card didn't pay; they stay open for manual payment. */
33766
+ openInvoicesDeclined: external_exports.number().int()
33767
+ });
33061
33768
  var ListOrganizationsResponseSchema = external_exports.array(OrganizationResponseSchema);
33062
33769
  var StaffOrganizationSummarySchema = external_exports.object({
33063
33770
  organizationId: external_exports.uuid(),
@@ -33075,6 +33782,14 @@ var CreateOrganizationRequestSchema = external_exports.object({
33075
33782
  homepageUrl: external_exports.url("Homepage must be a full URL, like https://acme.com").max(2048).nullable().optional()
33076
33783
  });
33077
33784
  var LogoFetchOutcomeValues = ["skipped", "stored", "not_found", "failed"];
33785
+ var RenameOrganizationRequestSchema = external_exports.object({
33786
+ name: external_exports.string().trim().min(2, "Organization name must be at least 2 characters").max(100, "Organization name must be 100 characters or fewer")
33787
+ }).strict();
33788
+ var RenameOrganizationResponseSchema = external_exports.object({
33789
+ organizationId: external_exports.uuid(),
33790
+ slug: external_exports.string(),
33791
+ name: external_exports.string()
33792
+ });
33078
33793
  var CreateOrganizationResponseSchema = external_exports.object({
33079
33794
  organizationId: external_exports.uuid(),
33080
33795
  slug: external_exports.string(),
@@ -33162,6 +33877,23 @@ var organizationContract = c27.router({
33162
33877
  },
33163
33878
  summary: "Create an organization (Trace staff only)"
33164
33879
  },
33880
+ // Staff-only, cross-org: renames the tenant in Trace and WorkOS together.
33881
+ // The slug stays as it was.
33882
+ renameOrganizationAsStaff: {
33883
+ method: "PATCH",
33884
+ path: "/staff/organizations/:organizationId",
33885
+ pathParams: OrgIdParamsSchema,
33886
+ body: RenameOrganizationRequestSchema,
33887
+ responses: {
33888
+ 200: RenameOrganizationResponseSchema,
33889
+ 400: ErrorResponseSchema,
33890
+ 401: ErrorResponseSchema,
33891
+ 403: ErrorResponseSchema,
33892
+ 404: ErrorResponseSchema,
33893
+ 500: ErrorResponseSchema
33894
+ },
33895
+ summary: "Rename an organization (Trace staff only)"
33896
+ },
33165
33897
  // Staff-only, cross-org: removes the tenant from Trace and WorkOS together.
33166
33898
  // 409 when members, repositories, or retained audit history still reference
33167
33899
  // the organization; those must be removed first (or cannot be, for WORM rows).
@@ -33280,6 +34012,22 @@ var organizationContract = c27.router({
33280
34012
  500: ErrorResponseSchema
33281
34013
  }
33282
34014
  },
34015
+ // Staff-only: change a partner's prose spelling. Reaches sessions launched
34016
+ // after the change; running sessions keep the header they started with.
34017
+ updateReferrer: {
34018
+ method: "PATCH",
34019
+ path: "/referrers/:referrerId",
34020
+ pathParams: ReferrerParamsSchema,
34021
+ body: UpdateReferrerRequestSchema,
34022
+ responses: {
34023
+ 200: UpdateReferrerResponseSchema,
34024
+ 400: ErrorResponseSchema,
34025
+ 401: ErrorResponseSchema,
34026
+ 403: ErrorResponseSchema,
34027
+ 404: ErrorResponseSchema,
34028
+ 500: ErrorResponseSchema
34029
+ }
34030
+ },
33283
34031
  // Staff-only: set (or clear with null) the org's referral partner.
33284
34032
  // Drives "Trace x <partner>" co-branding on pentest report covers.
33285
34033
  setOrganizationReferrer: {
@@ -33443,6 +34191,42 @@ var organizationContract = c27.router({
33443
34191
  500: ErrorResponseSchema
33444
34192
  }
33445
34193
  },
34194
+ // Signed-in admins only: mint a Stripe Checkout session (setup mode, cards)
34195
+ // that collects the autopay card. Stripe returns to the web's redemption route.
34196
+ createAutopaySetupSession: {
34197
+ method: "POST",
34198
+ path: "/organizations/:organizationId/billing/autopay/session",
34199
+ pathParams: OrgIdParamsSchema,
34200
+ body: CreateAutopaySetupSessionRequestSchema,
34201
+ responses: {
34202
+ 200: BillingPortalResponseSchema,
34203
+ 401: ErrorResponseSchema,
34204
+ 403: ErrorResponseSchema,
34205
+ 404: ErrorResponseSchema,
34206
+ 409: ErrorResponseSchema,
34207
+ 503: ErrorResponseSchema,
34208
+ 500: ErrorResponseSchema
34209
+ }
34210
+ },
34211
+ // Signed-in admins only: make the collected card the customer default, opt the
34212
+ // customer in, move live PR reviewer subscriptions onto it, and charge their
34213
+ // open invoices. The web confirms the charge before calling.
34214
+ enableAutopay: {
34215
+ method: "POST",
34216
+ path: "/organizations/:organizationId/billing/autopay",
34217
+ pathParams: OrgIdParamsSchema,
34218
+ body: EnableAutopayRequestSchema,
34219
+ responses: {
34220
+ 200: EnableAutopayResponseSchema,
34221
+ 400: ErrorResponseSchema,
34222
+ 401: ErrorResponseSchema,
34223
+ 403: ErrorResponseSchema,
34224
+ 404: ErrorResponseSchema,
34225
+ 409: ErrorResponseSchema,
34226
+ 503: ErrorResponseSchema,
34227
+ 500: ErrorResponseSchema
34228
+ }
34229
+ },
33446
34230
  // Member-readable: the caller's own org SCANNING plan + spend, for the
33447
34231
  // customer trial-credits meter. Scoped by requireOrgAccess (no staff gate).
33448
34232
  getScanUsage: {
@@ -33460,6 +34244,34 @@ var organizationContract = c27.router({
33460
34244
  // Member-readable: last 12 UTC months of PR scanner usage (unique PRs,
33461
34245
  // developers, PR-first-detected findings + remediations). Gated by
33462
34246
  // requireOrgAccess + VIEW_RESULTS + AUTO_PR_SCANS.
34247
+ getPrScanAnalyticsFindings: {
34248
+ method: "GET",
34249
+ path: "/organizations/:organizationId/pr-scan-analytics/findings",
34250
+ pathParams: OrgIdParamsSchema,
34251
+ query: PrScanAnalyticsFindingsQuerySchema,
34252
+ responses: {
34253
+ 200: PrScanAnalyticsFindingsResponseSchema,
34254
+ 400: ErrorResponseSchema,
34255
+ 401: ErrorResponseSchema,
34256
+ 403: ErrorResponseSchema,
34257
+ 404: ErrorResponseSchema,
34258
+ 500: ErrorResponseSchema
34259
+ }
34260
+ },
34261
+ // Resolves a finding's PR review comment on GitHub (by the hidden
34262
+ // vulnerability marker) so the analytics row can link straight to it.
34263
+ getPrScanAnalyticsFindingComment: {
34264
+ method: "GET",
34265
+ path: "/organizations/:organizationId/pr-scan-analytics/findings/:vulnerabilityId/comment",
34266
+ pathParams: PrScanAnalyticsFindingCommentParamsSchema,
34267
+ responses: {
34268
+ 200: PrScanAnalyticsFindingCommentResponseSchema,
34269
+ 401: ErrorResponseSchema,
34270
+ 403: ErrorResponseSchema,
34271
+ 404: ErrorResponseSchema,
34272
+ 500: ErrorResponseSchema
34273
+ }
34274
+ },
33463
34275
  getPrScanAnalytics: {
33464
34276
  method: "GET",
33465
34277
  path: "/organizations/:organizationId/pr-scan-analytics",
@@ -33504,6 +34316,65 @@ var organizationContract = c27.router({
33504
34316
  }
33505
34317
  });
33506
34318
 
34319
+ // ../../packages/api-contracts/src/contracts/v1/payment-card.ts
34320
+ var c28 = initContract();
34321
+ var paymentCardContract = c28.router({
34322
+ listPaymentCards: {
34323
+ method: "GET",
34324
+ path: "/staff/payment-cards",
34325
+ responses: {
34326
+ 200: ListPaymentCardsResponseSchema,
34327
+ 401: ErrorResponseSchema,
34328
+ 403: ErrorResponseSchema,
34329
+ 500: ErrorResponseSchema
34330
+ }
34331
+ },
34332
+ createPaymentCard: {
34333
+ method: "POST",
34334
+ path: "/staff/payment-cards",
34335
+ body: CreatePaymentCardRequestSchema,
34336
+ responses: {
34337
+ 201: PaymentCardResponseSchema,
34338
+ 400: ErrorResponseSchema,
34339
+ // PAYMENT_CARD_EXPIRED, or a contract failure
34340
+ 401: ErrorResponseSchema,
34341
+ 403: ErrorResponseSchema,
34342
+ 409: ErrorResponseSchema,
34343
+ // PAYMENT_CARD_LABEL_TAKEN, PAYMENT_CARD_ACTIVE_CONFLICT
34344
+ 500: ErrorResponseSchema
34345
+ }
34346
+ },
34347
+ updatePaymentCard: {
34348
+ method: "PATCH",
34349
+ path: "/staff/payment-cards/:paymentCardId",
34350
+ pathParams: PaymentCardIdParamsSchema,
34351
+ body: UpdatePaymentCardRequestSchema,
34352
+ responses: {
34353
+ 200: PaymentCardResponseSchema,
34354
+ 400: ErrorResponseSchema,
34355
+ 401: ErrorResponseSchema,
34356
+ 403: ErrorResponseSchema,
34357
+ 404: ErrorResponseSchema,
34358
+ 409: ErrorResponseSchema,
34359
+ 500: ErrorResponseSchema
34360
+ }
34361
+ },
34362
+ deletePaymentCard: {
34363
+ method: "DELETE",
34364
+ path: "/staff/payment-cards/:paymentCardId",
34365
+ pathParams: PaymentCardIdParamsSchema,
34366
+ responses: {
34367
+ 204: DeleteResponseSchema,
34368
+ 401: ErrorResponseSchema,
34369
+ 403: ErrorResponseSchema,
34370
+ 404: ErrorResponseSchema,
34371
+ 409: ErrorResponseSchema,
34372
+ // PAYMENT_CARD_HAS_PURCHASES
34373
+ 500: ErrorResponseSchema
34374
+ }
34375
+ }
34376
+ });
34377
+
33507
34378
  // ../../packages/api-contracts/src/schemas/api/v1/pentest-cost-rollup.api.ts
33508
34379
  var PentestCostRollupResponseSchema = external_exports.object({
33509
34380
  pentestId: external_exports.uuid(),
@@ -33796,8 +34667,8 @@ var ScanReuseSuggestionsResponseSchema = external_exports.object({
33796
34667
  });
33797
34668
 
33798
34669
  // ../../packages/api-contracts/src/contracts/v1/pentest.ts
33799
- var c28 = initContract();
33800
- var pentestContract = c28.router({
34670
+ var c29 = initContract();
34671
+ var pentestContract = c29.router({
33801
34672
  listPentests: {
33802
34673
  method: "GET",
33803
34674
  path: "/organizations/:organizationId/pentests",
@@ -34107,8 +34978,8 @@ var pentestContract = c28.router({
34107
34978
  });
34108
34979
 
34109
34980
  // ../../packages/api-contracts/src/contracts/v1/pentest-attack-chain.ts
34110
- var c29 = initContract();
34111
- var pentestAttackChainContract = c29.router({
34981
+ var c30 = initContract();
34982
+ var pentestAttackChainContract = c30.router({
34112
34983
  listAttackChains: {
34113
34984
  method: "GET",
34114
34985
  path: "/organizations/:organizationId/pentests/:pentestId/attack-chains",
@@ -34227,7 +35098,7 @@ var pentestAttackChainContract = c29.router({
34227
35098
  method: "DELETE",
34228
35099
  path: "/organizations/:organizationId/pentests/:pentestId/attack-chains/:attackChainId",
34229
35100
  pathParams: PentestAttackChainIdParamsSchema,
34230
- body: c29.noBody(),
35101
+ body: c30.noBody(),
34231
35102
  responses: {
34232
35103
  200: DeleteResponseSchema,
34233
35104
  401: ErrorResponseSchema,
@@ -34388,8 +35259,8 @@ var ReadAttestationDownloadResponseSchema = external_exports.union([
34388
35259
  ]);
34389
35260
 
34390
35261
  // ../../packages/api-contracts/src/contracts/v1/pentest-attestation.ts
34391
- var c30 = initContract();
34392
- var pentestAttestationContract = c30.router({
35262
+ var c31 = initContract();
35263
+ var pentestAttestationContract = c31.router({
34393
35264
  getDefaults: {
34394
35265
  method: "GET",
34395
35266
  path: "/organizations/:organizationId/pentests/:pentestId/attestations/defaults",
@@ -34532,8 +35403,8 @@ var ListPentestAuditLogResponseSchema = external_exports.object({
34532
35403
  });
34533
35404
 
34534
35405
  // ../../packages/api-contracts/src/contracts/v1/pentest-audit-log.ts
34535
- var c31 = initContract();
34536
- var pentestAuditLogContract = c31.router({
35406
+ var c32 = initContract();
35407
+ var pentestAuditLogContract = c32.router({
34537
35408
  listAuditLog: {
34538
35409
  method: "GET",
34539
35410
  path: "/organizations/:organizationId/pentests/:pentestId/audit-log",
@@ -34580,8 +35451,8 @@ var CreatePentestAuthorizedDomainRequestSchema = external_exports.object({
34580
35451
  }).strict();
34581
35452
 
34582
35453
  // ../../packages/api-contracts/src/contracts/v1/pentest-authorized-domain.ts
34583
- var c32 = initContract();
34584
- var pentestAuthorizedDomainContract = c32.router({
35454
+ var c33 = initContract();
35455
+ var pentestAuthorizedDomainContract = c33.router({
34585
35456
  getPentestAuthorizedScope: {
34586
35457
  method: "GET",
34587
35458
  path: "/organizations/:organizationId/pentests/:pentestId/authorized-scope",
@@ -34635,8 +35506,8 @@ var PentestEgressResponseSchema = external_exports.object({
34635
35506
  }).strict();
34636
35507
 
34637
35508
  // ../../packages/api-contracts/src/contracts/v1/pentest-egress.ts
34638
- var c33 = initContract();
34639
- var pentestEgressContract = c33.router({
35509
+ var c34 = initContract();
35510
+ var pentestEgressContract = c34.router({
34640
35511
  getPentestEgress: {
34641
35512
  method: "GET",
34642
35513
  path: "/staff/pentest-egress",
@@ -34675,8 +35546,8 @@ var CreatePentestEngagementExclusionRequestSchema = external_exports.object({
34675
35546
  }).strict();
34676
35547
 
34677
35548
  // ../../packages/api-contracts/src/contracts/v1/pentest-engagement-exclusion.ts
34678
- var c34 = initContract();
34679
- var pentestEngagementExclusionContract = c34.router({
35549
+ var c35 = initContract();
35550
+ var pentestEngagementExclusionContract = c35.router({
34680
35551
  listPentestEngagementExclusions: {
34681
35552
  method: "GET",
34682
35553
  path: "/organizations/:organizationId/pentests/:pentestId/engagement-exclusions",
@@ -34740,8 +35611,8 @@ var CreatePentestEnvironmentRequestSchema = external_exports.object({
34740
35611
  }).strict();
34741
35612
 
34742
35613
  // ../../packages/api-contracts/src/contracts/v1/pentest-environment.ts
34743
- var c35 = initContract();
34744
- var pentestEnvironmentContract = c35.router({
35614
+ var c36 = initContract();
35615
+ var pentestEnvironmentContract = c36.router({
34745
35616
  listPentestEnvironments: {
34746
35617
  method: "GET",
34747
35618
  path: "/organizations/:organizationId/pentests/:pentestId/environments",
@@ -34790,8 +35661,8 @@ var pentestEnvironmentContract = c35.router({
34790
35661
  });
34791
35662
 
34792
35663
  // ../../packages/api-contracts/src/contracts/v1/pentest-finding.ts
34793
- var c36 = initContract();
34794
- var pentestFindingContract = c36.router({
35664
+ var c37 = initContract();
35665
+ var pentestFindingContract = c37.router({
34795
35666
  // ─── Findings ───
34796
35667
  listFindings: {
34797
35668
  method: "GET",
@@ -35019,6 +35890,23 @@ var pentestFindingContract = c36.router({
35019
35890
  500: ErrorResponseSchema
35020
35891
  }
35021
35892
  },
35893
+ // Staff-only. `:pentestId` is the target; the source is in the body.
35894
+ copyFindings: {
35895
+ method: "POST",
35896
+ path: "/organizations/:organizationId/pentests/:pentestId/findings/copies",
35897
+ pathParams: PentestFindingParamsSchema,
35898
+ body: CopyPentestFindingsRequestSchema,
35899
+ responses: {
35900
+ 200: CopyPentestFindingsResponseSchema,
35901
+ 201: CopyPentestFindingsResponseSchema,
35902
+ 400: ErrorResponseSchema,
35903
+ 401: ErrorResponseSchema,
35904
+ 403: ErrorResponseSchema,
35905
+ 404: ErrorResponseSchema,
35906
+ 409: ErrorResponseSchema,
35907
+ 500: ErrorResponseSchema
35908
+ }
35909
+ },
35022
35910
  recoverStrandedFindings: {
35023
35911
  method: "POST",
35024
35912
  path: "/organizations/:organizationId/pentests/:pentestId/findings/recover-stranded",
@@ -35213,6 +36101,26 @@ var pentestFindingContract = c36.router({
35213
36101
  500: ErrorResponseSchema
35214
36102
  }
35215
36103
  },
36104
+ // Customer accepts the finding → ACCEPTED_WITH_CONTROLS without a dispute or a
36105
+ // staff decision. Human principals here; an API key sends the same transition
36106
+ // through `automationStatusUpdate` with status ACCEPTED_WITH_CONTROLS.
36107
+ accept: {
36108
+ method: "POST",
36109
+ path: "/organizations/:organizationId/customer-pentests/:pentestId/findings/:pentestFindingId/accept",
36110
+ pathParams: CustomerPentestFindingIdParamsSchema,
36111
+ body: AcceptCustomerPentestFindingRequestSchema,
36112
+ responses: {
36113
+ 200: CustomerPentestFindingResponseSchema,
36114
+ 400: ErrorResponseSchema,
36115
+ 401: ErrorResponseSchema,
36116
+ 403: ErrorResponseSchema,
36117
+ 404: ErrorResponseSchema,
36118
+ // FINDING_TRANSITION_NOT_ALLOWED (bad source status / dispute pending),
36119
+ // FINDING_STALE (concurrent write), or PENTEST_CLOSED.
36120
+ 409: ErrorResponseSchema,
36121
+ 500: ErrorResponseSchema
36122
+ }
36123
+ },
35216
36124
  // ─── Automation lifecycle transition (API keys) ───
35217
36125
  // A separate route rather than a widened `start-progress` / `report-remediation`:
35218
36126
  // those stay human-only, and everything that makes a machine-driven write
@@ -35423,8 +36331,8 @@ var pentestFindingContract = c36.router({
35423
36331
  });
35424
36332
 
35425
36333
  // ../../packages/api-contracts/src/contracts/v1/pentest-hypothesis.ts
35426
- var c37 = initContract();
35427
- var pentestHypothesisContract = c37.router({
36334
+ var c38 = initContract();
36335
+ var pentestHypothesisContract = c38.router({
35428
36336
  listHypotheses: {
35429
36337
  method: "GET",
35430
36338
  path: "/organizations/:organizationId/pentests/:pentestId/hypotheses",
@@ -35579,8 +36487,8 @@ var DownloadPentestIntakeDocumentResponseSchema = external_exports.object({
35579
36487
  });
35580
36488
 
35581
36489
  // ../../packages/api-contracts/src/contracts/v1/pentest-intake-document.ts
35582
- var c38 = initContract();
35583
- var pentestIntakeDocumentContract = c38.router({
36490
+ var c39 = initContract();
36491
+ var pentestIntakeDocumentContract = c39.router({
35584
36492
  listDocuments: {
35585
36493
  method: "GET",
35586
36494
  path: "/organizations/:organizationId/customer-pentests/:pentestId/questionnaire/documents",
@@ -35666,8 +36574,8 @@ var pentestIntakeDocumentContract = c38.router({
35666
36574
  });
35667
36575
 
35668
36576
  // ../../packages/api-contracts/src/contracts/v1/pentest-manual-finding.ts
35669
- var c39 = initContract();
35670
- var pentestManualFindingContract = c39.router({
36577
+ var c40 = initContract();
36578
+ var pentestManualFindingContract = c40.router({
35671
36579
  listManualFindings: {
35672
36580
  method: "GET",
35673
36581
  path: "/organizations/:organizationId/pentests/:pentestId/manual-findings",
@@ -35753,9 +36661,42 @@ var pentestManualFindingContract = c39.router({
35753
36661
  }
35754
36662
  });
35755
36663
 
36664
+ // ../../packages/api-contracts/src/contracts/v1/pentest-purchase.ts
36665
+ var c41 = initContract();
36666
+ var pentestPurchaseContract = c41.router({
36667
+ listPurchases: {
36668
+ method: "GET",
36669
+ path: "/organizations/:organizationId/pentests/:pentestId/purchases",
36670
+ pathParams: PentestPurchaseParamsSchema,
36671
+ responses: {
36672
+ 200: ListPentestPurchasesResponseSchema,
36673
+ 401: ErrorResponseSchema,
36674
+ 403: ErrorResponseSchema,
36675
+ 404: ErrorResponseSchema,
36676
+ 500: ErrorResponseSchema
36677
+ }
36678
+ },
36679
+ voidPurchase: {
36680
+ method: "POST",
36681
+ path: "/organizations/:organizationId/pentests/:pentestId/purchases/:purchaseId/void",
36682
+ pathParams: PentestPurchaseIdParamsSchema,
36683
+ body: VoidPentestPurchaseRequestSchema,
36684
+ responses: {
36685
+ 200: PentestPurchaseSchema,
36686
+ 400: ErrorResponseSchema,
36687
+ 401: ErrorResponseSchema,
36688
+ 403: ErrorResponseSchema,
36689
+ 404: ErrorResponseSchema,
36690
+ 409: ErrorResponseSchema,
36691
+ // PURCHASE_NOT_VOIDABLE, details: { status }
36692
+ 500: ErrorResponseSchema
36693
+ }
36694
+ }
36695
+ });
36696
+
35756
36697
  // ../../packages/api-contracts/src/contracts/v1/pentest-report-pdf.ts
35757
- var c40 = initContract();
35758
- var pentestReportPdfContract = c40.router({
36698
+ var c42 = initContract();
36699
+ var pentestReportPdfContract = c42.router({
35759
36700
  getCurrent: {
35760
36701
  method: "GET",
35761
36702
  path: "/organizations/:organizationId/pentests/:pentestId/report-pdf",
@@ -35848,7 +36789,7 @@ var pentestReportPdfContract = c40.router({
35848
36789
  // No body: the render always reads current DB state (cover derived via
35849
36790
  // deriveReportCover(), narrative/scope from the row's columns). The legacy
35850
36791
  // dialog payload was retired with the Generate dialog (report-session RS5).
35851
- body: c40.noBody(),
36792
+ body: c42.noBody(),
35852
36793
  responses: {
35853
36794
  202: StartReportPdfGenerationTaskResponseSchema,
35854
36795
  400: ErrorResponseSchema,
@@ -36046,8 +36987,8 @@ var pentestReportPdfContract = c40.router({
36046
36987
  });
36047
36988
 
36048
36989
  // ../../packages/api-contracts/src/contracts/v1/pentest-validation.ts
36049
- var c41 = initContract();
36050
- var pentestValidationContract = c41.router({
36990
+ var c43 = initContract();
36991
+ var pentestValidationContract = c43.router({
36051
36992
  listValidationRuns: {
36052
36993
  method: "GET",
36053
36994
  path: "/organizations/:organizationId/pentests/:pentestId/validation-runs",
@@ -36155,8 +37096,8 @@ var pentestValidationContract = c41.router({
36155
37096
  });
36156
37097
 
36157
37098
  // ../../packages/api-contracts/src/contracts/v1/repository.ts
36158
- var c42 = initContract();
36159
- var repositoryContract = c42.router({
37099
+ var c44 = initContract();
37100
+ var repositoryContract = c44.router({
36160
37101
  createRepository: {
36161
37102
  method: "POST",
36162
37103
  path: "/organizations/:organizationId/repositories",
@@ -36361,7 +37302,7 @@ var repositoryContract = c42.router({
36361
37302
  method: "POST",
36362
37303
  path: "/organizations/:organizationId/repositories/scanning-branch/confirm-defaults",
36363
37304
  pathParams: OrgIdParamsSchema,
36364
- body: c42.noBody(),
37305
+ body: c44.noBody(),
36365
37306
  responses: {
36366
37307
  200: ConfirmScanningBranchDefaultsResponseSchema,
36367
37308
  401: ErrorResponseSchema,
@@ -36495,8 +37436,8 @@ var ListScanLogsResponseSchema = external_exports.object({
36495
37436
  });
36496
37437
 
36497
37438
  // ../../packages/api-contracts/src/contracts/v1/scan.ts
36498
- var c43 = initContract();
36499
- var scanContract = c43.router({
37439
+ var c45 = initContract();
37440
+ var scanContract = c45.router({
36500
37441
  createScan: {
36501
37442
  method: "POST",
36502
37443
  path: "/organizations/:organizationId/scans",
@@ -36617,8 +37558,8 @@ var StaffPentestStatsResponseSchema = external_exports.object({
36617
37558
  }).strict();
36618
37559
 
36619
37560
  // ../../packages/api-contracts/src/contracts/v1/staff-pentest-stats.ts
36620
- var c44 = initContract();
36621
- var staffPentestStatsContract = c44.router({
37561
+ var c46 = initContract();
37562
+ var staffPentestStatsContract = c46.router({
36622
37563
  getStaffPentestStats: {
36623
37564
  method: "GET",
36624
37565
  path: "/staff/pentest-stats",
@@ -36659,10 +37600,13 @@ var SetStaffOrganizationPentestCustomerVisibilityRequestSchema = external_export
36659
37600
  /** false hides the engagement from the customer entirely; true shows it again. */
36660
37601
  visible: external_exports.boolean()
36661
37602
  }).strict();
37603
+ var RenameStaffOrganizationPentestRequestSchema = external_exports.object({
37604
+ title: external_exports.string().trim().min(1, "Title is required").max(500, "Title must be 500 characters or fewer")
37605
+ }).strict();
36662
37606
 
36663
37607
  // ../../packages/api-contracts/src/contracts/v1/staff-pentests.ts
36664
- var c45 = initContract();
36665
- var staffPentestsContract = c45.router({
37608
+ var c47 = initContract();
37609
+ var staffPentestsContract = c47.router({
36666
37610
  listStaffOrganizationPentests: {
36667
37611
  method: "GET",
36668
37612
  path: "/organizations/:organizationId/staff/pentests",
@@ -36689,6 +37633,22 @@ var staffPentestsContract = c45.router({
36689
37633
  },
36690
37634
  summary: "Soft-delete a pentest from the admin console (Trace staff only)"
36691
37635
  },
37636
+ renameStaffOrganizationPentest: {
37637
+ method: "PATCH",
37638
+ path: "/organizations/:organizationId/staff/pentests/:pentestId",
37639
+ pathParams: StaffOrganizationPentestParamsSchema,
37640
+ body: RenameStaffOrganizationPentestRequestSchema,
37641
+ responses: {
37642
+ 200: StaffOrganizationPentestSummarySchema,
37643
+ 400: ErrorResponseSchema,
37644
+ 401: ErrorResponseSchema,
37645
+ 403: ErrorResponseSchema,
37646
+ 404: ErrorResponseSchema,
37647
+ 409: ErrorResponseSchema,
37648
+ 500: ErrorResponseSchema
37649
+ },
37650
+ summary: "Rename a pentest from the admin console (Trace staff only)"
37651
+ },
36692
37652
  setStaffOrganizationPentestCustomerVisibility: {
36693
37653
  method: "POST",
36694
37654
  path: "/organizations/:organizationId/staff/pentests/:pentestId/customer-visibility",
@@ -36813,8 +37773,8 @@ var StaffQueueResponseSchema = external_exports.object({
36813
37773
  }).strict();
36814
37774
 
36815
37775
  // ../../packages/api-contracts/src/contracts/v1/staff-queue.ts
36816
- var c46 = initContract();
36817
- var staffQueueContract = c46.router({
37776
+ var c48 = initContract();
37777
+ var staffQueueContract = c48.router({
36818
37778
  getStaffQueue: {
36819
37779
  method: "GET",
36820
37780
  path: "/staff/queue",
@@ -36833,11 +37793,8 @@ var NOTES_MAX = 2e3;
36833
37793
  var TestIntegrationIdParamsSchema = external_exports.object({
36834
37794
  testIntegrationId: external_exports.uuid()
36835
37795
  });
36836
- var PentestTestIntegrationsParamsSchema = OrgIdParamsSchema.extend({
36837
- pentestId: external_exports.uuid()
36838
- });
36839
- var PentestTestIntegrationIdParamsSchema = PentestTestIntegrationsParamsSchema.extend({
36840
- testIntegrationId: external_exports.uuid()
37796
+ var TestIntegrationPasskeyIdParamsSchema = TestIntegrationIdParamsSchema.extend({
37797
+ testIntegrationPasskeyId: external_exports.uuid()
36841
37798
  });
36842
37799
  var TestIntegrationResponseSchema = external_exports.object({
36843
37800
  testIntegrationId: external_exports.uuid(),
@@ -36846,20 +37803,20 @@ var TestIntegrationResponseSchema = external_exports.object({
36846
37803
  notes: external_exports.string(),
36847
37804
  createdAt: external_exports.date(),
36848
37805
  updatedAt: external_exports.date(),
36849
- /** How many pentests this fixture is attached to — blast-radius context. */
36850
- attachedPentestCount: external_exports.number().int()
37806
+ /**
37807
+ * The second factor's NON-secret parameters, or null when the integration has
37808
+ * none (or its stored parameters are unreadable). The setup key is never
37809
+ * returned by any route.
37810
+ */
37811
+ mfa: CredentialMfaSummarySchema.nullable(),
37812
+ /** Passkeys staff stored for the integration's account. A count only: nothing here is decrypted. */
37813
+ passkeyCount: external_exports.number().int().min(0)
36851
37814
  }).strict();
36852
37815
  var ListTestIntegrationsResponseSchema = external_exports.array(TestIntegrationResponseSchema);
36853
- var PentestTestIntegrationResponseSchema = external_exports.object({
36854
- testIntegrationId: external_exports.uuid(),
36855
- label: external_exports.string(),
36856
- notes: external_exports.string(),
36857
- attachedById: external_exports.uuid(),
36858
- attachedAt: external_exports.date()
37816
+ var TestIntegrationPasskeyImportSchema = AgentSessionPasskeySchema.required({
37817
+ privateKey: true,
37818
+ userHandle: true
36859
37819
  }).strict();
36860
- var ListPentestTestIntegrationsResponseSchema = external_exports.array(
36861
- PentestTestIntegrationResponseSchema
36862
- );
36863
37820
  var TestIntegrationLoginSchema = external_exports.object({
36864
37821
  username: external_exports.string().min(1).max(512),
36865
37822
  password: external_exports.string().min(1).max(1024)
@@ -36879,13 +37836,10 @@ var UpdateTestIntegrationRequestSchema = external_exports.object({
36879
37836
  apiKey: external_exports.string().min(1).max(4096).optional(),
36880
37837
  login: TestIntegrationLoginSchema.optional()
36881
37838
  }).strict();
36882
- var AttachTestIntegrationRequestSchema = external_exports.object({
36883
- testIntegrationId: external_exports.uuid()
36884
- }).strict();
36885
37839
 
36886
37840
  // ../../packages/api-contracts/src/contracts/v1/test-integration.ts
36887
- var c47 = initContract();
36888
- var testIntegrationContract = c47.router({
37841
+ var c49 = initContract();
37842
+ var testIntegrationContract = c49.router({
36889
37843
  // -- Fixture CRUD (org-less, staff-only) ----------------------------------
36890
37844
  listTestIntegrations: {
36891
37845
  method: "GET",
@@ -36938,37 +37892,96 @@ var testIntegrationContract = c47.router({
36938
37892
  500: ErrorResponseSchema
36939
37893
  }
36940
37894
  },
36941
- // -- Attach edges (org-scoped, staff-only) --------------------------------
36942
- listPentestTestIntegrations: {
36943
- method: "GET",
36944
- path: "/organizations/:organizationId/staff/pentests/:pentestId/test-integrations",
36945
- pathParams: PentestTestIntegrationsParamsSchema,
37895
+ // -- Second factor (org-less, verified staff) ------------------------------
37896
+ // The same three routes as a customer credential's MFA (credential.ts), for
37897
+ // the same reasons: PUT carries the whole factor so re-sending is idempotent;
37898
+ // the code is a POST so it never sits in an access log or a cacheable read;
37899
+ // DELETE is idempotent. Agents never reach these: they get codes through
37900
+ // get_totp_code on the run-token channel, and never write a factor here.
37901
+ setTestIntegrationMfa: {
37902
+ method: "PUT",
37903
+ path: "/staff/test-integrations/:testIntegrationId/mfa",
37904
+ pathParams: TestIntegrationIdParamsSchema,
37905
+ body: SetCredentialMfaRequestSchema,
36946
37906
  responses: {
36947
- 200: ListPentestTestIntegrationsResponseSchema,
37907
+ // Never echoes the setup key: the integration shape, whose `mfa` carries
37908
+ // the derived non-secret parameters.
37909
+ 200: TestIntegrationResponseSchema,
37910
+ 400: ErrorResponseSchema,
37911
+ // CREDENTIAL_MFA_SETUP_KEY_INVALID
36948
37912
  401: ErrorResponseSchema,
36949
37913
  403: ErrorResponseSchema,
36950
37914
  404: ErrorResponseSchema,
36951
37915
  500: ErrorResponseSchema
36952
37916
  }
36953
37917
  },
36954
- attachTestIntegration: {
37918
+ generateTestIntegrationMfaCode: {
36955
37919
  method: "POST",
36956
- path: "/organizations/:organizationId/staff/pentests/:pentestId/test-integrations",
36957
- pathParams: PentestTestIntegrationsParamsSchema,
36958
- body: AttachTestIntegrationRequestSchema,
37920
+ path: "/staff/test-integrations/:testIntegrationId/mfa/code",
37921
+ pathParams: TestIntegrationIdParamsSchema,
37922
+ body: c49.noBody(),
37923
+ responses: {
37924
+ 200: CredentialMfaCodeResponseSchema,
37925
+ 401: ErrorResponseSchema,
37926
+ 403: ErrorResponseSchema,
37927
+ 404: ErrorResponseSchema,
37928
+ 422: ErrorResponseSchema,
37929
+ // CREDENTIAL_MFA_NOT_ENROLLED
37930
+ 500: ErrorResponseSchema
37931
+ }
37932
+ },
37933
+ clearTestIntegrationMfa: {
37934
+ method: "DELETE",
37935
+ path: "/staff/test-integrations/:testIntegrationId/mfa",
37936
+ pathParams: TestIntegrationIdParamsSchema,
37937
+ responses: {
37938
+ 200: TestIntegrationResponseSchema,
37939
+ 401: ErrorResponseSchema,
37940
+ 403: ErrorResponseSchema,
37941
+ 404: ErrorResponseSchema,
37942
+ 500: ErrorResponseSchema
37943
+ }
37944
+ },
37945
+ // -- Passkeys (org-less, verified staff) -----------------------------------
37946
+ // Staff enroll a passkey on the integration's account with the capture script
37947
+ // and import the captured file here. Agents never reach these routes; the agent
37948
+ // read path is provision_passkey on the run-token channel. A stored key signs in
37949
+ // as a Trace-owned account, so every route takes the MFA routes' verified-staff
37950
+ // bar. Import is never an upsert: a duplicate is a mistake to surface.
37951
+ importTestIntegrationPasskey: {
37952
+ method: "POST",
37953
+ path: "/staff/test-integrations/:testIntegrationId/passkeys",
37954
+ pathParams: TestIntegrationIdParamsSchema,
37955
+ body: TestIntegrationPasskeyImportSchema,
37956
+ responses: {
37957
+ 201: TestIntegrationPasskeySummarySchema,
37958
+ 400: ErrorResponseSchema,
37959
+ 401: ErrorResponseSchema,
37960
+ 403: ErrorResponseSchema,
37961
+ 404: ErrorResponseSchema,
37962
+ 409: ErrorResponseSchema,
37963
+ // TEST_INTEGRATION_PASSKEY_EXISTS
37964
+ 500: ErrorResponseSchema
37965
+ }
37966
+ },
37967
+ listTestIntegrationPasskeys: {
37968
+ method: "GET",
37969
+ path: "/staff/test-integrations/:testIntegrationId/passkeys",
37970
+ pathParams: TestIntegrationIdParamsSchema,
36959
37971
  responses: {
36960
- 201: PentestTestIntegrationResponseSchema,
37972
+ 200: external_exports.array(TestIntegrationPasskeySummarySchema),
36961
37973
  401: ErrorResponseSchema,
36962
37974
  403: ErrorResponseSchema,
36963
37975
  404: ErrorResponseSchema,
36964
- // pentest or fixture not found
36965
37976
  500: ErrorResponseSchema
36966
37977
  }
36967
37978
  },
36968
- detachTestIntegration: {
37979
+ // Not idempotent: the id names a specific row, and a second delete should say it
37980
+ // is gone. The passkey on the site's side stays.
37981
+ deleteTestIntegrationPasskey: {
36969
37982
  method: "DELETE",
36970
- path: "/organizations/:organizationId/staff/pentests/:pentestId/test-integrations/:testIntegrationId",
36971
- pathParams: PentestTestIntegrationIdParamsSchema,
37983
+ path: "/staff/test-integrations/:testIntegrationId/passkeys/:testIntegrationPasskeyId",
37984
+ pathParams: TestIntegrationPasskeyIdParamsSchema,
36972
37985
  responses: {
36973
37986
  204: DeleteResponseSchema,
36974
37987
  401: ErrorResponseSchema,
@@ -37017,8 +38030,8 @@ var MyContextResponseSchema = external_exports.object({
37017
38030
  });
37018
38031
 
37019
38032
  // ../../packages/api-contracts/src/contracts/v1/user.ts
37020
- var c48 = initContract();
37021
- var userContract = c48.router({
38033
+ var c50 = initContract();
38034
+ var userContract = c50.router({
37022
38035
  getStaffProfile: {
37023
38036
  method: "GET",
37024
38037
  path: "/staff/profile",
@@ -37133,8 +38146,8 @@ var VantaIntegrationResponseSchema = external_exports.object({
37133
38146
  });
37134
38147
 
37135
38148
  // ../../packages/api-contracts/src/contracts/v1/vanta-integration.ts
37136
- var c49 = initContract();
37137
- var vantaIntegrationContract = c49.router({
38149
+ var c51 = initContract();
38150
+ var vantaIntegrationContract = c51.router({
37138
38151
  createVantaIntegration: {
37139
38152
  method: "POST",
37140
38153
  path: "/organizations/:organizationId/vanta-integration",
@@ -37476,8 +38489,8 @@ var VerifySharedVulnerabilityEmailCodeResponseSchema = external_exports.object({
37476
38489
  });
37477
38490
 
37478
38491
  // ../../packages/api-contracts/src/contracts/v1/vulnerability.ts
37479
- var c50 = initContract();
37480
- var vulnerabilityContract = c50.router({
38492
+ var c52 = initContract();
38493
+ var vulnerabilityContract = c52.router({
37481
38494
  filterVulnerabilities: {
37482
38495
  method: "POST",
37483
38496
  path: "/organizations/:organizationId/vulnerabilities/filter",
@@ -37702,8 +38715,8 @@ var UpdateVulnerabilityNoteRequestSchema = external_exports.object({
37702
38715
  });
37703
38716
 
37704
38717
  // ../../packages/api-contracts/src/contracts/v1/vulnerability-note.ts
37705
- var c51 = initContract();
37706
- var vulnerabilityNoteContract = c51.router({
38718
+ var c53 = initContract();
38719
+ var vulnerabilityNoteContract = c53.router({
37707
38720
  listNotes: {
37708
38721
  method: "GET",
37709
38722
  path: "/organizations/:organizationId/vulnerabilities/:vulnerabilityId/notes",
@@ -37884,8 +38897,8 @@ var ListWebhookDeliveriesResponseSchema = external_exports.object({
37884
38897
  });
37885
38898
 
37886
38899
  // ../../packages/api-contracts/src/contracts/v1/webhook-settings.ts
37887
- var c52 = initContract();
37888
- var webhookSettingsContract = c52.router({
38900
+ var c54 = initContract();
38901
+ var webhookSettingsContract = c54.router({
37889
38902
  listWebhookEndpoints: {
37890
38903
  method: "GET",
37891
38904
  path: "/organizations/:organizationId/webhook-settings/endpoints",
@@ -38023,8 +39036,8 @@ var webhookSettingsContract = c52.router({
38023
39036
  });
38024
39037
 
38025
39038
  // ../../packages/api-contracts/src/contracts/v1/index.ts
38026
- var c53 = initContract();
38027
- var v1Contract = c53.router(
39039
+ var c55 = initContract();
39040
+ var v1Contract = c55.router(
38028
39041
  {
38029
39042
  admin: adminContract,
38030
39043
  cloudIntegration: cloudIntegrationContract,
@@ -38074,6 +39087,8 @@ var v1Contract = c53.router(
38074
39087
  inviteLink: inviteLinkContract,
38075
39088
  engagement: engagementContract,
38076
39089
  testIntegration: testIntegrationContract,
39090
+ paymentCard: paymentCardContract,
39091
+ pentestPurchase: pentestPurchaseContract,
38077
39092
  pentestEgress: pentestEgressContract,
38078
39093
  staffPentestStats: staffPentestStatsContract,
38079
39094
  staffPentests: staffPentestsContract,
@@ -38085,19 +39100,60 @@ var v1Contract = c53.router(
38085
39100
  );
38086
39101
 
38087
39102
  // ../../packages/api-contracts/src/contracts/index.ts
38088
- var c54 = initContract();
38089
- var contract = c54.router({
39103
+ var c56 = initContract();
39104
+ var contract = c56.router({
38090
39105
  v1: v1Contract
38091
39106
  });
38092
39107
 
39108
+ // ../../packages/api-contracts/src/contracts/v1/desktop-host-internal.ts
39109
+ var c57 = initContract();
39110
+ var MAX_VMS_PER_SYNC = 16;
39111
+ var DesktopHostSyncRequestSchema = external_exports.object({
39112
+ /** Integer protocol the runner speaks. Below the api's minimum, the host gets
39113
+ * no new targets but keeps serving the ones it holds. */
39114
+ protocolVersion: external_exports.number().int().min(1),
39115
+ runnerVersion: external_exports.string().min(1).max(200),
39116
+ /** Target ids with a VM on disk OR being created right now. */
39117
+ presentVmIds: external_exports.array(external_exports.uuid()).max(MAX_VMS_PER_SYNC)
39118
+ });
39119
+ var DesktopHostSyncTargetSchema = external_exports.object({
39120
+ desktopTargetId: external_exports.uuid(),
39121
+ status: external_exports.enum(["PROVISIONING", "READY"]),
39122
+ runVersion: external_exports.number().int().min(0),
39123
+ runToken: external_exports.string().min(1),
39124
+ /** Epoch millis the host deletes the VM by, whatever the api says later. */
39125
+ localDeadlineMs: external_exports.number().int().positive(),
39126
+ heartbeatSec: external_exports.number().int().positive()
39127
+ });
39128
+ var DesktopHostSyncResponseSchema = external_exports.object({
39129
+ syncIntervalMs: external_exports.number().int().positive(),
39130
+ /** This host's PROVISIONING and READY targets. Any VM the runner holds for an
39131
+ * id not listed here is deleted. */
39132
+ targets: external_exports.array(DesktopHostSyncTargetSchema).max(MAX_VMS_PER_SYNC)
39133
+ });
39134
+ var desktopHostInternalContract = c57.router({
39135
+ sync: {
39136
+ method: "POST",
39137
+ path: "/internal/v1/desktop-hosts/sync",
39138
+ body: DesktopHostSyncRequestSchema,
39139
+ responses: {
39140
+ 200: DesktopHostSyncResponseSchema,
39141
+ 400: ErrorResponseSchema,
39142
+ // Bad, expired or revoked host token, or a decommissioned host.
39143
+ 401: ErrorResponseSchema,
39144
+ 500: ErrorResponseSchema
39145
+ }
39146
+ }
39147
+ });
39148
+
38093
39149
  // ../../packages/api-contracts/src/schemas/api/v1/github-webhook.api.ts
38094
39150
  var GitHubWebhookResponseSchema = external_exports.object({
38095
39151
  ok: external_exports.boolean()
38096
39152
  });
38097
39153
 
38098
39154
  // ../../packages/api-contracts/src/contracts/webhooks/github.ts
38099
- var c55 = initContract();
38100
- var githubWebhookContract = c55.router(
39155
+ var c58 = initContract();
39156
+ var githubWebhookContract = c58.router(
38101
39157
  {
38102
39158
  handleWebhook: {
38103
39159
  method: "POST",
@@ -38115,7 +39171,7 @@ var githubWebhookContract = c55.router(
38115
39171
  pathPrefix: "/webhooks"
38116
39172
  }
38117
39173
  );
38118
- var githubReadonlyWebhookContract = c55.router(
39174
+ var githubReadonlyWebhookContract = c58.router(
38119
39175
  {
38120
39176
  handleWebhook: {
38121
39177
  method: "POST",
@@ -38135,9 +39191,9 @@ var githubReadonlyWebhookContract = c55.router(
38135
39191
  );
38136
39192
 
38137
39193
  // ../../packages/api-contracts/src/contracts/webhooks/linear.ts
38138
- var c56 = initContract();
39194
+ var c59 = initContract();
38139
39195
  var LinearWebhookResponseSchema = external_exports.object({ ok: external_exports.boolean() });
38140
- var linearWebhookContract = c56.router(
39196
+ var linearWebhookContract = c59.router(
38141
39197
  {
38142
39198
  handleWebhook: {
38143
39199
  method: "POST",
@@ -38157,7 +39213,7 @@ var linearWebhookContract = c56.router(
38157
39213
  );
38158
39214
 
38159
39215
  // ../../packages/api-contracts/src/contracts/forge/bitbucket.ts
38160
- var c57 = initContract();
39216
+ var c60 = initContract();
38161
39217
  var InstallEventSchema = external_exports.object({
38162
39218
  eventType: external_exports.enum(["avi:forge:installed:app", "avi:forge:upgraded:app"]),
38163
39219
  context: external_exports.object({ cloudId: external_exports.string().min(1) }).loose(),
@@ -38186,7 +39242,7 @@ var ForgePairingCodeResponseSchema = external_exports.discriminatedUnion("state"
38186
39242
  expiresInSeconds: external_exports.number().int()
38187
39243
  })
38188
39244
  ]);
38189
- var forgeBitbucketContract = c57.router(
39245
+ var forgeBitbucketContract = c60.router(
38190
39246
  {
38191
39247
  lifecycle: {
38192
39248
  method: "POST",
@@ -38233,8 +39289,8 @@ var HealthResponseSchema = external_exports.object({
38233
39289
  });
38234
39290
 
38235
39291
  // ../../packages/api-contracts/src/contracts/health.ts
38236
- var c58 = initContract();
38237
- var healthContract = c58.router({
39292
+ var c61 = initContract();
39293
+ var healthContract = c61.router({
38238
39294
  getHealth: {
38239
39295
  method: "GET",
38240
39296
  path: "/health",
@@ -38246,8 +39302,8 @@ var healthContract = c58.router({
38246
39302
  });
38247
39303
 
38248
39304
  // ../../packages/api-contracts/src/contracts/v1/vulnerability-share.ts
38249
- var c59 = initContract();
38250
- var vulnerabilityShareContract = c59.router(
39305
+ var c62 = initContract();
39306
+ var vulnerabilityShareContract = c62.router(
38251
39307
  {
38252
39308
  resolveShareLink: {
38253
39309
  method: "GET",
@@ -38334,8 +39390,8 @@ var UpdatePrVulnerabilityRequestSchema = external_exports.object({
38334
39390
  });
38335
39391
 
38336
39392
  // ../../packages/api-contracts/src/contracts/v1/pr-vulnerability.ts
38337
- var c60 = initContract();
38338
- var prVulnerabilityContract = c60.router(
39393
+ var c63 = initContract();
39394
+ var prVulnerabilityContract = c63.router(
38339
39395
  {
38340
39396
  listPrVulnerabilities: {
38341
39397
  method: "GET",
@@ -41079,22 +42135,30 @@ function toIso(value) {
41079
42135
  return Number.isNaN(date5.getTime()) ? String(value) : date5.toISOString();
41080
42136
  }
41081
42137
  async function confirmDeletion(description, skip) {
41082
- if (skip) {
42138
+ return confirmAction(`Delete ${description}?`, { skip, verb: "delete" });
42139
+ }
42140
+ async function confirmAction(question, options) {
42141
+ if (options.skip) {
41083
42142
  return true;
41084
42143
  }
41085
42144
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
41086
- console.error(colorError("Refusing to delete without --yes in a non-interactive shell."));
42145
+ console.error(
42146
+ colorError(`Refusing to ${options.verb} without --yes in a non-interactive shell.`)
42147
+ );
41087
42148
  process.exit(1);
41088
42149
  }
41089
42150
  const readline = createInterface({ input: process.stdin, output: process.stdout });
41090
42151
  try {
41091
- const answer = await readline.question(`Delete ${description}? [y/N] `);
42152
+ const answer = await readline.question(`${question} [y/N] `);
41092
42153
  const normalized = answer.trim().toLowerCase();
41093
42154
  return normalized === "y" || normalized === "yes";
41094
42155
  } finally {
41095
42156
  readline.close();
41096
42157
  }
41097
42158
  }
42159
+ function collectRepeatable(value, previous) {
42160
+ return [...previous ?? [], value];
42161
+ }
41098
42162
 
41099
42163
  // src/application/commands.ts
41100
42164
  var APPLICATION_TYPE_BY_INPUT = {
@@ -41253,12 +42317,12 @@ function buildApplicationsTable(apps) {
41253
42317
  repos: String(a2.repositories.length),
41254
42318
  url: a2.applicationUrl ? sanitizeDisplay(a2.applicationUrl) : "\u2014"
41255
42319
  }));
41256
- const idWidth = Math.max(2, ...cells.map((c62) => c62.id.length));
41257
- const nameWidth = Math.max(4, ...cells.map((c62) => c62.name.length));
41258
- const typeWidth = Math.max(4, ...cells.map((c62) => c62.type.length));
42320
+ const idWidth = Math.max(2, ...cells.map((c65) => c65.id.length));
42321
+ const nameWidth = Math.max(4, ...cells.map((c65) => c65.name.length));
42322
+ const typeWidth = Math.max(4, ...cells.map((c65) => c65.type.length));
41259
42323
  const header = `${"ID".padEnd(idWidth)} ${"NAME".padEnd(nameWidth)} ${"TYPE".padEnd(typeWidth)} REPOS URL`;
41260
42324
  const rows = cells.map(
41261
- (c62) => `${c62.id.padEnd(idWidth)} ${c62.name.padEnd(nameWidth)} ${c62.type.padEnd(typeWidth)} ${c62.repos.padStart(5)} ${c62.url}`
42325
+ (c65) => `${c65.id.padEnd(idWidth)} ${c65.name.padEnd(nameWidth)} ${c65.type.padEnd(typeWidth)} ${c65.repos.padStart(5)} ${c65.url}`
41262
42326
  );
41263
42327
  return { header, rows };
41264
42328
  }
@@ -41278,12 +42342,12 @@ function buildApplicationRepositoriesTable(links) {
41278
42342
  path: l2.repositoryPath ? sanitizeDisplay(l2.repositoryPath) : "\u2014",
41279
42343
  status: formatStatus(l2.repositoryStatus)
41280
42344
  }));
41281
- const nameWidth = Math.max(10, ...cells.map((c62) => c62.name.length));
41282
- const roleWidth = Math.max(4, ...cells.map((c62) => c62.role.length));
41283
- const pathWidth = Math.max(4, ...cells.map((c62) => c62.path.length));
42345
+ const nameWidth = Math.max(10, ...cells.map((c65) => c65.name.length));
42346
+ const roleWidth = Math.max(4, ...cells.map((c65) => c65.role.length));
42347
+ const pathWidth = Math.max(4, ...cells.map((c65) => c65.path.length));
41284
42348
  const header = `${"REPOSITORY".padEnd(nameWidth)} SCAN ${"ROLE".padEnd(roleWidth)} ${"PATH".padEnd(pathWidth)} STATUS`;
41285
42349
  const rows = cells.map(
41286
- (c62) => `${c62.name.padEnd(nameWidth)} ${c62.scan.padEnd(4)} ${c62.role.padEnd(roleWidth)} ${c62.path.padEnd(pathWidth)} ${c62.status}`
42350
+ (c65) => `${c65.name.padEnd(nameWidth)} ${c65.scan.padEnd(4)} ${c65.role.padEnd(roleWidth)} ${c65.path.padEnd(pathWidth)} ${c65.status}`
41287
42351
  );
41288
42352
  return { header, rows };
41289
42353
  }
@@ -42068,8 +43132,8 @@ function wrapTextWithPrefix(e, r2, o2, t2 = o2, s = o2, n3) {
42068
43132
  hard: true,
42069
43133
  trim: false
42070
43134
  }).split(`
42071
- `).map((c62, i2, m2) => {
42072
- const d = n3 ? n3(c62, i2) : c62;
43135
+ `).map((c65, i2, m2) => {
43136
+ const d = n3 ? n3(c65, i2) : c65;
42073
43137
  return i2 === 0 ? `${t2}${d}` : i2 === m2.length - 1 ? `${s}${d}` : `${o2}${d}`;
42074
43138
  }).join(`
42075
43139
  `);
@@ -42567,14 +43631,14 @@ var limitOptions = ({
42567
43631
  Math.min(l2 - a2 + 3, e.length - a2),
42568
43632
  0
42569
43633
  ));
42570
- let d = a2 < e.length && f > 0, c62 = a2 < e.length && f + a2 < e.length;
43634
+ let d = a2 < e.length && f > 0, c65 = a2 < e.length && f + a2 < e.length;
42571
43635
  const W = Math.min(
42572
43636
  f + a2,
42573
43637
  e.length
42574
43638
  ), s = [];
42575
43639
  let g = 0;
42576
- d && g++, c62 && g++;
42577
- const T = f + (d ? 1 : 0), y = W - (c62 ? 1 : 0);
43640
+ d && g++, c65 && g++;
43641
+ const T = f + (d ? 1 : 0), y = W - (c65 ? 1 : 0);
42578
43642
  for (let t2 = T; t2 < y; t2++) {
42579
43643
  const n3 = e[t2], o2 = n3 ? w(n3, t2 === l2) : "", h2 = wrapAnsi(o2, i2, {
42580
43644
  hard: true,
@@ -42595,14 +43659,14 @@ var limitOptions = ({
42595
43659
  u4,
42596
43660
  true
42597
43661
  );
42598
- d ? ({ lineCount: o2, removals: t2 } = L(), o2 > u4 && (c62 || (u4 -= 1), { lineCount: o2, removals: n3 } = E())) : (c62 || (u4 -= 1), { lineCount: o2, removals: n3 } = E(), o2 > u4 && (u4 -= 1, { lineCount: o2, removals: t2 } = L())), t2 > 0 && (d = true, s.splice(0, t2)), n3 > 0 && (c62 = true, s.splice(s.length - n3, n3));
43662
+ d ? ({ lineCount: o2, removals: t2 } = L(), o2 > u4 && (c65 || (u4 -= 1), { lineCount: o2, removals: n3 } = E())) : (c65 || (u4 -= 1), { lineCount: o2, removals: n3 } = E(), o2 > u4 && (u4 -= 1, { lineCount: o2, removals: t2 } = L())), t2 > 0 && (d = true, s.splice(0, t2)), n3 > 0 && (c65 = true, s.splice(s.length - n3, n3));
42599
43663
  }
42600
43664
  const x = [];
42601
43665
  d && x.push(M);
42602
43666
  for (const t2 of s)
42603
43667
  for (const n3 of t2)
42604
43668
  x.push(n3);
42605
- return c62 && x.push(M), x;
43669
+ return c65 && x.push(M), x;
42606
43670
  };
42607
43671
  var confirm = (i2) => {
42608
43672
  const a2 = i2.active ?? "Yes", s = i2.inactive ?? "No";
@@ -42621,15 +43685,15 @@ var confirm = (i2) => {
42621
43685
  u4
42622
43686
  ), o2 = `${e ? `${styleText2("gray", S_BAR)}
42623
43687
  ` : ""}${f}
42624
- `, c62 = this.value ? a2 : s;
43688
+ `, c65 = this.value ? a2 : s;
42625
43689
  switch (this.state) {
42626
43690
  case "submit": {
42627
43691
  const r2 = e ? `${styleText2("gray", S_BAR)} ` : "";
42628
- return `${o2}${r2}${styleText2("dim", c62)}`;
43692
+ return `${o2}${r2}${styleText2("dim", c65)}`;
42629
43693
  }
42630
43694
  case "cancel": {
42631
43695
  const r2 = e ? `${styleText2("gray", S_BAR)} ` : "";
42632
- return `${o2}${r2}${styleText2(["strikethrough", "dim"], c62)}${e ? `
43696
+ return `${o2}${r2}${styleText2(["strikethrough", "dim"], c65)}${e ? `
42633
43697
  ${styleText2("gray", S_BAR)}` : ""}`;
42634
43698
  }
42635
43699
  default: {
@@ -42777,13 +43841,13 @@ var multiline = (e) => new h({
42777
43841
  `, m2 = e.placeholder && e.placeholder.length > 0 ? (
42778
43842
  // biome-ignore lint/style/noNonNullAssertion: guarded by placeholder.length > 0
42779
43843
  styleText2("inverse", e.placeholder[0]) + styleText2("dim", e.placeholder.slice(1))
42780
- ) : styleText2(["inverse", "hidden"], "_"), a2 = this.userInput ? this.userInputWithCursor : m2, l2 = this.value ?? "", c62 = e.showSubmit ? `
43844
+ ) : styleText2(["inverse", "hidden"], "_"), a2 = this.userInput ? this.userInputWithCursor : m2, l2 = this.value ?? "", c65 = e.showSubmit ? `
42781
43845
  ${styleText2(this.focused === "submit" ? "cyan" : "dim", "[ submit ]")}` : "";
42782
43846
  switch (this.state) {
42783
43847
  case "error": {
42784
43848
  const n3 = `${styleText2("yellow", S_BAR)} `, r2 = i2 ? wrapTextWithPrefix(e.output, a2, n3, void 0) : a2, u4 = styleText2("yellow", S_BAR_END);
42785
43849
  return `${o2}${r2}
42786
- ${u4} ${styleText2("yellow", this.error)}${c62}
43850
+ ${u4} ${styleText2("yellow", this.error)}${c65}
42787
43851
  `;
42788
43852
  }
42789
43853
  case "submit": {
@@ -42811,7 +43875,7 @@ ${u4} ${styleText2("yellow", this.error)}${c62}
42811
43875
  default: {
42812
43876
  const n3 = i2 ? `${styleText2("cyan", S_BAR)} ` : "", r2 = i2 ? styleText2("cyan", S_BAR_END) : "", u4 = i2 ? wrapTextWithPrefix(e.output, a2, n3) : a2;
42813
43877
  return `${o2}${u4}
42814
- ${r2}${c62}
43878
+ ${r2}${c65}
42815
43879
  `;
42816
43880
  }
42817
43881
  }
@@ -42826,7 +43890,7 @@ var password = (r2) => new u$1({
42826
43890
  render() {
42827
43891
  const e = r2.withGuide ?? settings.withGuide, o2 = `${e ? `${styleText2("gray", S_BAR)}
42828
43892
  ` : ""}${symbol2(this.state)} ${r2.message}
42829
- `, c62 = this.userInputWithCursor, i2 = this.masked;
43893
+ `, c65 = this.userInputWithCursor, i2 = this.masked;
42830
43894
  switch (this.state) {
42831
43895
  case "error": {
42832
43896
  const s = e ? `${styleText2("yellow", S_BAR)} ` : "", n3 = e ? `${styleText2("yellow", S_BAR_END)} ` : "", l2 = i2 ?? "";
@@ -42846,7 +43910,7 @@ ${styleText2("gray", S_BAR)}` : ""}`;
42846
43910
  }
42847
43911
  default: {
42848
43912
  const s = e ? `${styleText2("cyan", S_BAR)} ` : "", n3 = e ? styleText2("cyan", S_BAR_END) : "";
42849
- return `${o2}${s}${c62}
43913
+ return `${o2}${s}${c65}
42850
43914
  ${n3}
42851
43915
  `;
42852
43916
  }
@@ -42862,7 +43926,7 @@ var SELECT_INSTRUCTIONS = [
42862
43926
  `${styleText2("dim", "\u2191/\u2193")} to navigate`,
42863
43927
  `${styleText2("dim", "Enter:")} confirm`
42864
43928
  ];
42865
- var c61 = (t2, o2) => t2.includes(`
43929
+ var c64 = (t2, o2) => t2.includes(`
42866
43930
  `) ? t2.split(`
42867
43931
  `).map((d) => o2(d)).join(`
42868
43932
  `) : o2(t2);
@@ -42873,15 +43937,15 @@ var select = (t2) => {
42873
43937
  const s = n3.label ?? String(n3.value);
42874
43938
  switch (m2) {
42875
43939
  case "disabled":
42876
- return `${styleText2("gray", S_RADIO_INACTIVE)} ${c61(s, (i2) => styleText2("gray", i2))}${n3.hint ? ` ${styleText2("dim", `(${n3.hint ?? "disabled"})`)}` : ""}`;
43940
+ return `${styleText2("gray", S_RADIO_INACTIVE)} ${c64(s, (i2) => styleText2("gray", i2))}${n3.hint ? ` ${styleText2("dim", `(${n3.hint ?? "disabled"})`)}` : ""}`;
42877
43941
  case "selected":
42878
- return `${c61(s, (i2) => styleText2("dim", i2))}`;
43942
+ return `${c64(s, (i2) => styleText2("dim", i2))}`;
42879
43943
  case "active":
42880
43944
  return `${styleText2("green", S_RADIO_ACTIVE)} ${s}${n3.hint ? ` ${styleText2("dim", `(${n3.hint})`)}` : ""}`;
42881
43945
  case "cancelled":
42882
- return `${c61(s, (i2) => styleText2(["strikethrough", "dim"], i2))}`;
43946
+ return `${c64(s, (i2) => styleText2(["strikethrough", "dim"], i2))}`;
42883
43947
  default:
42884
- return `${styleText2("dim", S_RADIO_INACTIVE)} ${c61(s, (i2) => styleText2("dim", i2))}`;
43948
+ return `${styleText2("dim", S_RADIO_INACTIVE)} ${c64(s, (i2) => styleText2("dim", i2))}`;
42885
43949
  }
42886
43950
  }, d = t2.showInstructions ?? true;
42887
43951
  return new n$1({
@@ -44213,7 +45277,7 @@ Examples:
44213
45277
  }
44214
45278
  const { organizationId, slug } = resolution.organization;
44215
45279
  if (options.web) {
44216
- const url2 = `${getAppBaseUrl()}/dashboard/${slug}/settings/integrations/github`;
45280
+ const url2 = `${getAppBaseUrl()}/dashboard/${slug}/settings/integrations/source-code`;
44217
45281
  const opened = await openBrowser(url2);
44218
45282
  if (!opened) {
44219
45283
  console.log(`Open in browser: ${url2}`);
@@ -44259,7 +45323,7 @@ function extractCommandTree(cmd) {
44259
45323
  description: o2.description,
44260
45324
  takesValue: o2.required || o2.optional
44261
45325
  }));
44262
- const subcommands = cmd.commands.filter((c62) => !c62._hidden).map((c62) => extractCommandTree(c62));
45326
+ const subcommands = cmd.commands.filter((c65) => !c65._hidden).map((c65) => extractCommandTree(c65));
44263
45327
  return {
44264
45328
  name: cmd.name(),
44265
45329
  description: cmd.description(),
@@ -44325,7 +45389,7 @@ function generateBashCompletion(tree, binName) {
44325
45389
  }
44326
45390
  function collectBashCases(tree, prefix) {
44327
45391
  const cases = /* @__PURE__ */ new Map();
44328
- const subcommandNames = tree.subcommands.map((c62) => c62.name);
45392
+ const subcommandNames = tree.subcommands.map((c65) => c65.name);
44329
45393
  const optionNames = tree.options.flatMap((o2) => o2.short ? [o2.long, o2.short] : [o2.long]);
44330
45394
  const extraOptions = prefix === "" ? ["--help", "--version"] : ["--help"];
44331
45395
  const allOptions = [.../* @__PURE__ */ new Set([...optionNames, ...extraOptions])];
@@ -44740,9 +45804,9 @@ function buildInboxesTable(inboxes) {
44740
45804
  address: sanitizeDisplay(inbox.address),
44741
45805
  label: sanitizeDisplay(inbox.label)
44742
45806
  }));
44743
- const addressWidth = Math.max(7, ...cells.map((c62) => c62.address.length));
45807
+ const addressWidth = Math.max(7, ...cells.map((c65) => c65.address.length));
44744
45808
  const header = `${"ADDRESS".padEnd(addressWidth)} LABEL`;
44745
- const rows = cells.map((c62) => `${c62.address.padEnd(addressWidth)} ${c62.label}`);
45809
+ const rows = cells.map((c65) => `${c65.address.padEnd(addressWidth)} ${c65.label}`);
44746
45810
  return { header, rows };
44747
45811
  }
44748
45812
  function formatInboxQuota(quota) {
@@ -44766,14 +45830,14 @@ function buildInboxEmailsTable(messages) {
44766
45830
  subject: sanitizeDisplay(message.subject ?? ""),
44767
45831
  unread: message.unread ? "yes" : "no"
44768
45832
  }));
44769
- const idWidth = Math.max(2, ...cells.map((c62) => c62.id.length));
44770
- const receivedWidth = Math.max(8, ...cells.map((c62) => c62.received.length));
44771
- const fromWidth = Math.max(4, ...cells.map((c62) => c62.from.length));
44772
- const toWidth = Math.max(2, ...cells.map((c62) => c62.to.length));
44773
- const subjectWidth = Math.max(7, ...cells.map((c62) => c62.subject.length));
45833
+ const idWidth = Math.max(2, ...cells.map((c65) => c65.id.length));
45834
+ const receivedWidth = Math.max(8, ...cells.map((c65) => c65.received.length));
45835
+ const fromWidth = Math.max(4, ...cells.map((c65) => c65.from.length));
45836
+ const toWidth = Math.max(2, ...cells.map((c65) => c65.to.length));
45837
+ const subjectWidth = Math.max(7, ...cells.map((c65) => c65.subject.length));
44774
45838
  const header = `${"ID".padEnd(idWidth)} ${"RECEIVED".padEnd(receivedWidth)} ${"FROM".padEnd(fromWidth)} ${"TO".padEnd(toWidth)} ${"SUBJECT".padEnd(subjectWidth)} UNREAD`;
44775
45839
  const rows = cells.map(
44776
- (c62) => `${c62.id.padEnd(idWidth)} ${c62.received.padEnd(receivedWidth)} ${c62.from.padEnd(fromWidth)} ${c62.to.padEnd(toWidth)} ${c62.subject.padEnd(subjectWidth)} ${c62.unread}`
45840
+ (c65) => `${c65.id.padEnd(idWidth)} ${c65.received.padEnd(receivedWidth)} ${c65.from.padEnd(fromWidth)} ${c65.to.padEnd(toWidth)} ${c65.subject.padEnd(subjectWidth)} ${c65.unread}`
44777
45841
  );
44778
45842
  return { header, rows };
44779
45843
  }
@@ -45548,16 +46612,16 @@ function printCredentialsTsv(creds) {
45548
46612
  }
45549
46613
  }
45550
46614
  function buildCredentialsTable(creds) {
45551
- const cells = creds.map((c62) => ({
45552
- id: c62.credentialId,
45553
- label: sanitizeDisplay(c62.label),
45554
- type: c62.credentialType
46615
+ const cells = creds.map((c65) => ({
46616
+ id: c65.credentialId,
46617
+ label: sanitizeDisplay(c65.label),
46618
+ type: c65.credentialType
45555
46619
  }));
45556
- const idWidth = Math.max(2, ...cells.map((c62) => c62.id.length));
45557
- const labelWidth = Math.max(5, ...cells.map((c62) => c62.label.length));
46620
+ const idWidth = Math.max(2, ...cells.map((c65) => c65.id.length));
46621
+ const labelWidth = Math.max(5, ...cells.map((c65) => c65.label.length));
45558
46622
  const header = `${"ID".padEnd(idWidth)} ${"LABEL".padEnd(labelWidth)} TYPE`;
45559
46623
  const rows = cells.map(
45560
- (c62) => `${c62.id.padEnd(idWidth)} ${c62.label.padEnd(labelWidth)} ${c62.type}`
46624
+ (c65) => `${c65.id.padEnd(idWidth)} ${c65.label.padEnd(labelWidth)} ${c65.type}`
45561
46625
  );
45562
46626
  return { header, rows };
45563
46627
  }
@@ -46202,6 +47266,12 @@ function failWriteOp4(action, status, body, notFoundLabel) {
46202
47266
  message ?? "This environment is frozen by an active pentest and cannot be changed."
46203
47267
  )
46204
47268
  );
47269
+ } else if (errorCode === "ENVIRONMENT_IN_USE_BY_PENTEST") {
47270
+ console.error(
47271
+ colorError(
47272
+ message ?? "A pentest still uses this environment. Remove it from the engagement first."
47273
+ )
47274
+ );
46205
47275
  } else {
46206
47276
  console.error(colorError(message ?? "Conflict (status 409)."));
46207
47277
  }
@@ -46321,14 +47391,14 @@ function buildEnvironmentTable(envs, leading) {
46321
47391
  url: e.primaryUrl != null ? sanitizeDisplay(e.primaryUrl) : "\u2014"
46322
47392
  }));
46323
47393
  const leadingWidths = leading.map(
46324
- (column, i2) => Math.max(column.header.length, ...cells.map((c62) => c62.leading[i2]?.length ?? 0))
47394
+ (column, i2) => Math.max(column.header.length, ...cells.map((c65) => c65.leading[i2]?.length ?? 0))
46325
47395
  );
46326
- const nameWidth = Math.max(4, ...cells.map((c62) => c62.name.length));
47396
+ const nameWidth = Math.max(4, ...cells.map((c65) => c65.name.length));
46327
47397
  const padLeading = (values) => values.map((v, i2) => v.padEnd(leadingWidths[i2] ?? 0)).join(" ");
46328
47398
  return {
46329
- header: `${padLeading(leading.map((c62) => c62.header))} ${"NAME".padEnd(nameWidth)} PROD URL`,
47399
+ header: `${padLeading(leading.map((c65) => c65.header))} ${"NAME".padEnd(nameWidth)} PROD URL`,
46330
47400
  rows: cells.map(
46331
- (c62) => `${padLeading(c62.leading)} ${c62.name.padEnd(nameWidth)} ${c62.prod.padEnd(4)} ${c62.url}`
47401
+ (c65) => `${padLeading(c65.leading)} ${c65.name.padEnd(nameWidth)} ${c65.prod.padEnd(4)} ${c65.url}`
46332
47402
  )
46333
47403
  };
46334
47404
  }
@@ -46709,11 +47779,46 @@ var CLI_VERSION = packageMetadata.version;
46709
47779
  // src/pentest/commands.ts
46710
47780
  import { readFileSync as readFileSync7 } from "fs";
46711
47781
 
46712
- // src/pentest/create-finding.ts
47782
+ // src/pentest/evidence.ts
46713
47783
  import { readFileSync as readFileSync5, statSync as statSync2 } from "fs";
47784
+ import { basename as basename2 } from "path";
46714
47785
 
46715
47786
  // src/pentest/resolve.ts
46716
47787
  var UUID_PATTERN2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
47788
+ async function resolveApplicationId(organizationId, identifier) {
47789
+ if (UUID_PATTERN2.test(identifier)) {
47790
+ return identifier;
47791
+ }
47792
+ const result = await authenticatedRequest(
47793
+ (client) => client.application.listApplications({ params: { organizationId } })
47794
+ );
47795
+ if (result.status === UNAUTHORIZED_STATUS2) {
47796
+ exitAuthFailed();
47797
+ }
47798
+ if (result.status !== 200) {
47799
+ console.error(colorError(`Failed to list applications (status ${result.status}).`));
47800
+ process.exit(1);
47801
+ }
47802
+ const wanted = identifier.trim().toLowerCase();
47803
+ const matches = result.body.filter((app) => app.name.trim().toLowerCase() === wanted);
47804
+ if (matches.length === 1 && matches[0]) {
47805
+ return matches[0].applicationId;
47806
+ }
47807
+ if (matches.length > 1) {
47808
+ console.error(
47809
+ colorError(
47810
+ `Application name '${sanitizeDisplay(identifier)}' is ambiguous. Pass the application ID instead (\`tracecli app list\`).`
47811
+ )
47812
+ );
47813
+ process.exit(1);
47814
+ }
47815
+ console.error(
47816
+ colorError(
47817
+ `Application '${sanitizeDisplay(identifier)}' not found. Run \`tracecli app list\` to see available applications.`
47818
+ )
47819
+ );
47820
+ process.exit(1);
47821
+ }
46717
47822
  async function resolvePentestId(organizationId, identifier) {
46718
47823
  if (UUID_PATTERN2.test(identifier)) {
46719
47824
  return identifier;
@@ -46750,7 +47855,306 @@ Run \`tracecli pentest list\` to see available pentests.`
46750
47855
  return pentest.pentestId;
46751
47856
  }
46752
47857
 
47858
+ // src/pentest/evidence.ts
47859
+ var SECTION_LIST = ValidationEvidenceSectionValues.join(", ");
47860
+ var KIND_LIST = PentestExploitArtifactKindValues.join(", ");
47861
+ function isSection(value) {
47862
+ return ValidationEvidenceSectionValues.includes(value);
47863
+ }
47864
+ function isKind(value) {
47865
+ return PentestExploitArtifactKindValues.includes(value);
47866
+ }
47867
+ function formatBytes2(bytes) {
47868
+ if (bytes < 1024) return `${bytes} B`;
47869
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
47870
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
47871
+ }
47872
+ async function resolveFindingId(organizationId, pentestId, identifier) {
47873
+ if (isUuid(identifier)) {
47874
+ return identifier;
47875
+ }
47876
+ const numericMatch = identifier.match(/^(?:PTF-)?(\d+)$/i);
47877
+ if (!numericMatch?.[1]) {
47878
+ console.error(
47879
+ colorError(
47880
+ `Invalid finding identifier '${sanitizeDisplay(identifier)}'. Use a finding number (e.g. PTF-7) or UUID.`
47881
+ )
47882
+ );
47883
+ process.exit(1);
47884
+ }
47885
+ const displayId = Number.parseInt(numericMatch[1], 10);
47886
+ const result = await authenticatedRequest(
47887
+ (client) => client.pentestFinding.listFindings({ params: { organizationId, pentestId } })
47888
+ );
47889
+ if (result.status === UNAUTHORIZED_STATUS2) {
47890
+ exitAuthFailed();
47891
+ }
47892
+ if (result.status !== 200) {
47893
+ console.error(colorError(`Failed to list findings (status ${result.status}).`));
47894
+ process.exit(1);
47895
+ }
47896
+ const match = result.body.find((f) => f.displayId === displayId);
47897
+ if (!match || !("pentestFindingId" in match)) {
47898
+ console.error(
47899
+ colorError(
47900
+ `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\`).`
47901
+ )
47902
+ );
47903
+ process.exit(1);
47904
+ }
47905
+ return match.pentestFindingId;
47906
+ }
47907
+ async function runAttachEvidence(options) {
47908
+ if (options.section !== void 0 && !isSection(options.section)) {
47909
+ console.error(colorError(`Invalid --section '${sanitizeDisplay(options.section)}'.`));
47910
+ console.error(`Valid sections: ${SECTION_LIST}`);
47911
+ process.exit(1);
47912
+ }
47913
+ if (options.kind !== void 0 && !isKind(options.kind)) {
47914
+ console.error(colorError(`Invalid --kind '${sanitizeDisplay(options.kind)}'.`));
47915
+ console.error(`Valid kinds: ${KIND_LIST}`);
47916
+ process.exit(1);
47917
+ }
47918
+ let sizeBytes;
47919
+ try {
47920
+ const stats = statSync2(options.file);
47921
+ if (!stats.isFile()) {
47922
+ console.error(colorError(`--file '${sanitizeDisplay(options.file)}' is not a regular file.`));
47923
+ process.exit(1);
47924
+ }
47925
+ sizeBytes = stats.size;
47926
+ } catch {
47927
+ console.error(colorError(`Cannot read --file '${sanitizeDisplay(options.file)}'.`));
47928
+ process.exit(1);
47929
+ }
47930
+ if (sizeBytes === 0) {
47931
+ console.error(colorError(`--file '${sanitizeDisplay(options.file)}' is empty.`));
47932
+ process.exit(1);
47933
+ }
47934
+ if (sizeBytes > MAX_STAFF_EVIDENCE_UPLOAD_BYTES) {
47935
+ console.error(
47936
+ colorError(
47937
+ `File is ${formatBytes2(sizeBytes)}; the limit for one exhibit is ${formatBytes2(MAX_STAFF_EVIDENCE_UPLOAD_BYTES)}.`
47938
+ )
47939
+ );
47940
+ process.exit(1);
47941
+ }
47942
+ const organizationId = await resolveOrganizationId();
47943
+ const pentestId = await resolvePentestId(organizationId, options.pentest);
47944
+ const pentestFindingId = await resolveFindingId(organizationId, pentestId, options.finding);
47945
+ const params = { organizationId, pentestId, pentestFindingId };
47946
+ const filename = basename2(options.file);
47947
+ const slot = await authenticatedRequest(
47948
+ (client) => client.pentestFinding.createFindingEvidenceUpload({
47949
+ params,
47950
+ body: { filename, sizeBytes }
47951
+ })
47952
+ );
47953
+ if (slot.status === UNAUTHORIZED_STATUS2) {
47954
+ exitAuthFailed();
47955
+ }
47956
+ if (slot.status !== 201) {
47957
+ const { message } = readErrorFields(slot.body);
47958
+ console.error(
47959
+ colorError(message ?? `Failed to start the evidence upload (status ${slot.status}).`)
47960
+ );
47961
+ process.exit(1);
47962
+ }
47963
+ const body = readFileSync5(options.file);
47964
+ const put = await fetch(slot.body.uploadUrl, { method: "PUT", body: new Uint8Array(body) });
47965
+ if (!put.ok) {
47966
+ console.error(
47967
+ colorError(
47968
+ `Upload to storage failed (status ${put.status}). The upload link is short-lived \u2014 re-run the command.`
47969
+ )
47970
+ );
47971
+ process.exit(1);
47972
+ }
47973
+ const attached = await authenticatedRequest(
47974
+ (client) => client.pentestFinding.attachFindingEvidence({
47975
+ params,
47976
+ body: {
47977
+ key: slot.body.key,
47978
+ ...options.caption !== void 0 ? { caption: options.caption } : {},
47979
+ ...options.section !== void 0 && isSection(options.section) ? { section: options.section } : {},
47980
+ ...options.kind !== void 0 && isKind(options.kind) ? { kind: options.kind } : {},
47981
+ ...options.customerFacing ? { customerFacing: true } : {}
47982
+ }
47983
+ })
47984
+ );
47985
+ if (attached.status === UNAUTHORIZED_STATUS2) {
47986
+ exitAuthFailed();
47987
+ }
47988
+ if (attached.status !== 201) {
47989
+ const { message } = readErrorFields(attached.body);
47990
+ console.error(
47991
+ colorError(message ?? `Failed to attach the evidence (status ${attached.status}).`)
47992
+ );
47993
+ process.exit(1);
47994
+ }
47995
+ if (options.output === "json") {
47996
+ printJson(attached.body);
47997
+ return;
47998
+ }
47999
+ const visibility = attached.body.customerFacing ? "customer-facing" : "staff-internal";
48000
+ const placement = attached.body.section ? ` \xB7 ${attached.body.section}` : "";
48001
+ console.log(
48002
+ colorSuccess(
48003
+ `Attached ${attached.body.kind} \xB7 ${formatBytes2(attached.body.sizeBytes)} \xB7 ${visibility}${placement}`
48004
+ )
48005
+ );
48006
+ console.log(` artifact ${attached.body.artifactId}`);
48007
+ if (attached.body.kind === "HAR") {
48008
+ console.log(" HAR captures stay internal \u2014 they can carry a live session token.");
48009
+ } else if (!attached.body.customerFacing) {
48010
+ console.log(" Publish it from the finding\u2019s evidence panel in the staff console.");
48011
+ }
48012
+ }
48013
+ function createAttachEvidenceCommand() {
48014
+ 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(
48015
+ "after",
48016
+ `
48017
+ The file uploads straight to storage, so recordings of any reasonable size work.
48018
+ Videos and HAR captures always land staff-internal \u2014 publish a video with the
48019
+ toggle in the staff console's evidence panel after reviewing it for redaction.
48020
+
48021
+ Examples:
48022
+ tracecli pentest findings attach-evidence --pentest PT-3 --finding 7 --file ./poc.png
48023
+ tracecli pentest findings attach-evidence --pentest PT-3 --finding PTF-7 --file ./poc.mov \\
48024
+ --caption "Cookie survives logout, replayed 40s later" --section proofOfConcept
48025
+ tracecli pentest findings attach-evidence --pentest PT-3 --finding 7 --file ./roster.json \\
48026
+ --caption "Full member roster returned to a non-member" --customer-facing --output json`
48027
+ ).action(async (options) => {
48028
+ await runAttachEvidence(options);
48029
+ });
48030
+ }
48031
+
48032
+ // src/pentest/copy-findings.ts
48033
+ function validateSelection(options) {
48034
+ const hasApplication = options.application !== void 0;
48035
+ const hasFindings = options.finding !== void 0 && options.finding.length > 0;
48036
+ if (hasApplication === hasFindings) {
48037
+ return {
48038
+ error: "Pass exactly one of --application <id|name> or --finding <id> (repeatable)."
48039
+ };
48040
+ }
48041
+ return null;
48042
+ }
48043
+ function failCopy(status, body) {
48044
+ const { message, errorCode } = readErrorFields(body);
48045
+ if (status === UNAUTHORIZED_STATUS2) {
48046
+ exitAuthFailed();
48047
+ }
48048
+ if (status === 403) {
48049
+ console.error(
48050
+ colorError(
48051
+ "Only Trace staff can copy pentest findings. Sign in with a staff account via `tracecli auth login`."
48052
+ )
48053
+ );
48054
+ process.exit(1);
48055
+ }
48056
+ if (status === 404) {
48057
+ console.error(colorError(message ?? "Pentest, application, or finding not found."));
48058
+ process.exit(1);
48059
+ }
48060
+ if (status === 400) {
48061
+ console.error(colorError(message ?? "Invalid request (status 400)."));
48062
+ process.exit(1);
48063
+ }
48064
+ if (status === 409) {
48065
+ console.error(
48066
+ colorError(
48067
+ errorCode === "PENTEST_CLOSED" ? message ?? "Closed pentests must be reopened before copying findings into them." : message ?? "Conflict (status 409)."
48068
+ )
48069
+ );
48070
+ process.exit(1);
48071
+ }
48072
+ console.error(
48073
+ colorError(`Failed to copy findings (status ${status})${message ? `: ${message}` : "."}`)
48074
+ );
48075
+ process.exit(1);
48076
+ }
48077
+ function formatCopiedFinding(finding) {
48078
+ const source = finding.sourceDisplayId === null ? "draft" : `PTF-${finding.sourceDisplayId}`;
48079
+ const status = sanitizeDisplay(finding.sourceStatus).toLowerCase().replace(/_/g, " ");
48080
+ const evidence = `${finding.evidenceCount} exhibit${finding.evidenceCount === 1 ? "" : "s"}`;
48081
+ const target = finding.pentestFindingId === null ? "" : ` -> ${finding.pentestFindingId}`;
48082
+ return ` [${sanitizeDisplay(finding.severity)}] ${sanitizeDisplay(finding.title)} (${source}, ${status}, ${evidence})${target}`;
48083
+ }
48084
+ async function copyFindings(options) {
48085
+ const selection = validateSelection(options);
48086
+ if (selection) {
48087
+ console.error(colorError(selection.error));
48088
+ process.exit(1);
48089
+ }
48090
+ const organizationId = await resolveOrganizationId();
48091
+ const sourcePentestId = await resolvePentestId(organizationId, options.from);
48092
+ const targetPentestId = await resolvePentestId(organizationId, options.to);
48093
+ const dryRun = options.dryRun ?? false;
48094
+ const body = options.application !== void 0 ? {
48095
+ sourcePentestId,
48096
+ applicationId: await resolveApplicationId(organizationId, options.application),
48097
+ dryRun
48098
+ } : {
48099
+ sourcePentestId,
48100
+ pentestFindingIds: await Promise.all(
48101
+ (options.finding ?? []).map(
48102
+ (id) => resolveFindingId(organizationId, sourcePentestId, id)
48103
+ )
48104
+ ),
48105
+ dryRun
48106
+ };
48107
+ const result = await authenticatedRequest(
48108
+ (client) => client.pentestFinding.copyFindings({
48109
+ params: { organizationId, pentestId: targetPentestId },
48110
+ body
48111
+ })
48112
+ );
48113
+ if (result.status !== CREATED_STATUS && result.status !== SUCCESS_STATUS) {
48114
+ failCopy(result.status, result.body);
48115
+ }
48116
+ if (options.output === "json") {
48117
+ printJson(result.body);
48118
+ return;
48119
+ }
48120
+ const { findings } = result.body;
48121
+ if (findings.length === 0) {
48122
+ console.log("No findings matched the selection. Nothing copied.");
48123
+ return;
48124
+ }
48125
+ const exhibits = findings.reduce((n3, f) => n3 + f.evidenceCount, 0);
48126
+ const summary = `${findings.length} finding${findings.length === 1 ? "" : "s"} with ${exhibits} exhibit${exhibits === 1 ? "" : "s"}`;
48127
+ console.log(
48128
+ result.body.dryRun ? `Dry run: ${summary} would be copied from ${sanitizeDisplay(options.from)} to ${sanitizeDisplay(options.to)}.` : colorSuccess(
48129
+ `Copied ${summary} from ${sanitizeDisplay(options.from)} to ${sanitizeDisplay(options.to)}.`
48130
+ )
48131
+ );
48132
+ for (const finding of findings) {
48133
+ console.log(formatCopiedFinding(finding));
48134
+ }
48135
+ if (!result.body.dryRun) {
48136
+ console.log(
48137
+ `The copies are unpublished drafts. Run \`tracecli pentest findings publish --pentest ${sanitizeDisplay(options.to)} --all\` to number them and show them to the customer.`
48138
+ );
48139
+ }
48140
+ }
48141
+ function createCopyFindingsCommand() {
48142
+ return new Command("copy").description(
48143
+ "Copy findings (with prose and evidence) from one pentest into another as unpublished drafts (staff)"
48144
+ ).requiredOption("--from <id>", "Source pentest display ID (e.g. PT-1) or UUID").requiredOption("--to <id>", "Target pentest display ID (e.g. PT-2) or UUID").option(
48145
+ "--application <id|name>",
48146
+ "Copy every source finding that cites a repository linked to this application"
48147
+ ).option(
48148
+ "--finding <id>",
48149
+ "Source finding number (e.g. PTF-7) or UUID (repeatable)",
48150
+ collectRepeatable
48151
+ ).option("--dry-run", "Show what would be copied without writing anything").option("-o, --output <format>", "Output format: text, json (default: text)", "text").action(async (options) => {
48152
+ await copyFindings(options);
48153
+ });
48154
+ }
48155
+
46753
48156
  // src/pentest/create-finding.ts
48157
+ import { readFileSync as readFileSync6, statSync as statSync3 } from "fs";
46754
48158
  var SEVERITY_LIST = PentestFindingSeverityValues.join(", ");
46755
48159
  function collectCwe(value, previous) {
46756
48160
  return [...previous ?? [], value];
@@ -46789,14 +48193,14 @@ function parseCreateFindingJsonFile(raw, sourceLabel) {
46789
48193
  }
46790
48194
  function readUtf8File(path, flag) {
46791
48195
  try {
46792
- const stats = statSync2(path);
48196
+ const stats = statSync3(path);
46793
48197
  if (!stats.isFile()) {
46794
48198
  return { error: `${flag} '${sanitizeDisplay(path)}' is not a regular file.` };
46795
48199
  }
46796
48200
  } catch {
46797
48201
  return { error: `Cannot read ${flag} '${sanitizeDisplay(path)}'.` };
46798
48202
  }
46799
- const text = readFileSync5(path, "utf8");
48203
+ const text = readFileSync6(path, "utf8");
46800
48204
  if (text.trim().length === 0) {
46801
48205
  return { error: `${flag} '${sanitizeDisplay(path)}' is empty.` };
46802
48206
  }
@@ -47024,15 +48428,139 @@ function createCreateFindingCommand() {
47024
48428
  });
47025
48429
  }
47026
48430
 
48431
+ // src/pentest/staff-errors.ts
48432
+ function failStaffPentestWrite(action, status, body, options = {}) {
48433
+ const { message, errorCode } = readErrorFields(body);
48434
+ if (status === UNAUTHORIZED_STATUS2) {
48435
+ exitAuthFailed();
48436
+ }
48437
+ if (status === 403) {
48438
+ const staffGuard = message === void 0 || /only trace staff/i.test(message);
48439
+ console.error(
48440
+ colorError(
48441
+ staffGuard ? `Only Trace staff can ${action}. Sign in with a staff account via \`tracecli auth login\`.` : `Cannot ${action}: ${message}`
48442
+ )
48443
+ );
48444
+ process.exit(1);
48445
+ }
48446
+ if (status === 404) {
48447
+ console.error(colorError(options.notFound ?? message ?? "Not found."));
48448
+ process.exit(1);
48449
+ }
48450
+ if (status === 400) {
48451
+ console.error(colorError(message ?? "Invalid request (status 400)."));
48452
+ process.exit(1);
48453
+ }
48454
+ if (status === 409) {
48455
+ console.error(
48456
+ colorError(
48457
+ errorCode === "PENTEST_CLOSED" ? options.closed ?? message ?? "Closed pentests must be reopened first." : message ?? "Conflict (status 409)."
48458
+ )
48459
+ );
48460
+ process.exit(1);
48461
+ }
48462
+ console.error(
48463
+ colorError(`Failed to ${action} (status ${status})${message ? `: ${message}` : "."}`)
48464
+ );
48465
+ process.exit(1);
48466
+ }
48467
+
48468
+ // src/pentest/create-pentest.ts
48469
+ var TEST_TYPE_LIST = PentestTestTypeValues.join(", ");
48470
+ function parseTestType(raw) {
48471
+ const parsed = PentestTestType.safeParse(raw.trim().toUpperCase().replace(/-/g, "_"));
48472
+ if (parsed.success) {
48473
+ return parsed.data;
48474
+ }
48475
+ return { error: `Invalid --test-type '${sanitizeDisplay(raw)}'. Use one of: ${TEST_TYPE_LIST}.` };
48476
+ }
48477
+ async function createPentest(options) {
48478
+ const title = options.title.trim();
48479
+ if (title.length === 0) {
48480
+ console.error(colorError("--title cannot be empty."));
48481
+ process.exit(1);
48482
+ }
48483
+ let testType;
48484
+ if (options.testType !== void 0) {
48485
+ const parsed = parseTestType(options.testType);
48486
+ if (typeof parsed !== "string") {
48487
+ console.error(colorError(parsed.error));
48488
+ process.exit(1);
48489
+ }
48490
+ testType = parsed;
48491
+ }
48492
+ const organizationId = await resolveOrganizationId();
48493
+ const primaryApplicationId = options.application === void 0 ? void 0 : await resolveApplicationId(organizationId, options.application);
48494
+ const created = await authenticatedRequest(
48495
+ (client) => client.pentest.createPentest({
48496
+ params: { organizationId },
48497
+ body: {
48498
+ title,
48499
+ ...options.description !== void 0 ? { description: options.description } : {},
48500
+ ...primaryApplicationId !== void 0 ? { primaryApplicationId } : {},
48501
+ ...testType !== void 0 ? { testType } : {}
48502
+ }
48503
+ })
48504
+ );
48505
+ if (created.status !== CREATED_STATUS) {
48506
+ failStaffPentestWrite("create pentests", created.status, created.body);
48507
+ }
48508
+ const pentest = created.body;
48509
+ let hiddenFromCustomerAt = null;
48510
+ if (options.hidden) {
48511
+ const remedy = `Created pentest PT-${pentest.displayId} (${pentest.pentestId}) but could not hide it, so the customer can see it. Run \`tracecli pentest visibility PT-${pentest.displayId} --hidden\`.`;
48512
+ let hidden;
48513
+ try {
48514
+ hidden = await hidePentest(organizationId, pentest.pentestId);
48515
+ } catch (error48) {
48516
+ console.error(
48517
+ colorError(`${remedy} (${error48 instanceof Error ? error48.message : String(error48)})`)
48518
+ );
48519
+ process.exit(1);
48520
+ }
48521
+ if (hidden.status !== SUCCESS_STATUS) {
48522
+ console.error(colorError(`${remedy} (status ${hidden.status})`));
48523
+ process.exit(1);
48524
+ }
48525
+ hiddenFromCustomerAt = hidden.body.hiddenFromCustomerAt;
48526
+ }
48527
+ if (options.output === "json") {
48528
+ printJson({ ...pentest, hiddenFromCustomerAt });
48529
+ return;
48530
+ }
48531
+ console.log(colorSuccess(`Created pentest PT-${pentest.displayId} (${pentest.pentestId})`));
48532
+ console.log(` ${sanitizeDisplay(pentest.title)}`);
48533
+ const applications = (pentest.applications ?? []).map((app) => sanitizeDisplay(app.name));
48534
+ if (applications.length > 0) {
48535
+ console.log(` Application: ${applications.join(", ")}`);
48536
+ }
48537
+ console.log(
48538
+ hiddenFromCustomerAt === null ? " Visible to the customer. Findings stay unpublished until you publish them." : ` Hidden from the customer until you run \`tracecli pentest visibility PT-${pentest.displayId} --visible\`.`
48539
+ );
48540
+ }
48541
+ function hidePentest(organizationId, pentestId) {
48542
+ return authenticatedRequest(
48543
+ (client) => client.staffPentests.setStaffOrganizationPentestCustomerVisibility({
48544
+ params: { organizationId, pentestId },
48545
+ body: { visible: false }
48546
+ })
48547
+ );
48548
+ }
48549
+ function createCreatePentestCommand() {
48550
+ return new Command("create").description("Create a pentest (staff)").requiredOption("--title <text>", "Engagement title").option("--application <id|name>", "Application under test (bound as the primary application)").option("--test-type <type>", `Declared test type (${TEST_TYPE_LIST}); default WHITE_BOX`).option("--description <text>", "Engagement description").option("--hidden", "Hide the pentest from the customer until you make it visible").option("-o, --output <format>", "Output format: text, json (default: text)", "text").action(async (options) => {
48551
+ await createPentest(options);
48552
+ });
48553
+ }
48554
+
47027
48555
  // src/pentest/deliverables.ts
47028
48556
  import { open as open2, rename, rm, stat } from "fs/promises";
47029
- import { basename as basename3, join as join6, resolve } from "path";
48557
+ import { basename as basename4, join as join6, resolve } from "path";
47030
48558
 
47031
48559
  // src/upload/prepare.ts
47032
48560
  import { createHash } from "crypto";
47033
- import { createReadStream, statSync as statSync3 } from "fs";
48561
+ import { createReadStream, statSync as statSync4 } from "fs";
47034
48562
  import { open } from "fs/promises";
47035
- import { basename as basename2 } from "path";
48563
+ import { basename as basename3 } from "path";
47036
48564
  var CodeUploadPrepareError = class extends Error {
47037
48565
  constructor(message) {
47038
48566
  super(message);
@@ -47131,7 +48659,7 @@ function assertPreparedMatchesUpload(upload, prepared) {
47131
48659
  async function prepareUploadFile(filePath, options = {}) {
47132
48660
  let sizeBytes;
47133
48661
  try {
47134
- const stats = statSync3(filePath);
48662
+ const stats = statSync4(filePath);
47135
48663
  if (!stats.isFile()) {
47136
48664
  throw new CodeUploadPrepareError("Path is not a regular file. Pass a .zip, .apk, or .ipa.");
47137
48665
  }
@@ -47146,7 +48674,7 @@ async function prepareUploadFile(filePath, options = {}) {
47146
48674
  if (sizeBytes > CODE_UPLOAD_MAX_BYTES) {
47147
48675
  throw new CodeUploadPrepareError(archiveTooLargeMessage());
47148
48676
  }
47149
- const originalFilename = basename2(filePath);
48677
+ const originalFilename = basename3(filePath);
47150
48678
  const inferred = inferArtifactKindFromFilename(originalFilename);
47151
48679
  const artifactKind = options.artifactKind ?? inferred;
47152
48680
  if (artifactKind == null) {
@@ -47172,6 +48700,12 @@ async function prepareUploadFile(filePath, options = {}) {
47172
48700
  }
47173
48701
 
47174
48702
  // src/pentest/status-update.ts
48703
+ var STATUS_UPDATE_TARGETS = [
48704
+ "IN_PROGRESS",
48705
+ "REMEDIATED",
48706
+ "ACCEPTED_WITH_CONTROLS"
48707
+ ];
48708
+ var STATUS_UPDATE_TARGET_LIST = STATUS_UPDATE_TARGETS.join(", ");
47175
48709
  var PENTEST_IDENTIFIER_PATTERN = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|pt-\d+|\d+)$/i;
47176
48710
  var FINDING_IDENTIFIER_PATTERN = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|ptf-\d+|\d+)$/i;
47177
48711
  var MAX_NOTE_LENGTH = 1e4;
@@ -47182,11 +48716,15 @@ function parseStatusUpdateTarget(raw) {
47182
48716
  error: "IN_REVIEW is not a finding status. Use IN_PROGRESS while fixing, then REMEDIATED when the fix is ready for retest."
47183
48717
  };
47184
48718
  }
47185
- if (normalized === "IN_PROGRESS" || normalized === "REMEDIATED") {
47186
- return normalized;
48719
+ if (normalized === "ACCEPTED" || normalized === "ACCEPT") {
48720
+ return "ACCEPTED_WITH_CONTROLS";
48721
+ }
48722
+ const target = STATUS_UPDATE_TARGETS.find((candidate) => candidate === normalized);
48723
+ if (target !== void 0) {
48724
+ return target;
47187
48725
  }
47188
48726
  return {
47189
- error: `Invalid --status '${sanitizeDisplay(raw)}'. Use IN_PROGRESS or REMEDIATED.`
48727
+ error: `Invalid --status '${sanitizeDisplay(raw)}'. Use ${STATUS_UPDATE_TARGET_LIST}.`
47190
48728
  };
47191
48729
  }
47192
48730
  function isPentestIdentifier(value) {
@@ -47195,6 +48733,17 @@ function isPentestIdentifier(value) {
47195
48733
  function isFindingIdentifier(value) {
47196
48734
  return FINDING_IDENTIFIER_PATTERN.test(value);
47197
48735
  }
48736
+ function requireAcceptRationale(note) {
48737
+ if (note === void 0) {
48738
+ console.error(
48739
+ colorError(
48740
+ "--note is required when accepting a finding: say why the risk is acceptable or which controls cover it."
48741
+ )
48742
+ );
48743
+ process.exit(1);
48744
+ }
48745
+ return note;
48746
+ }
47198
48747
  function failStatusUpdate(status, body, viaApiKey) {
47199
48748
  const { message, errorCode } = readErrorFields(body);
47200
48749
  if (status === UNAUTHORIZED_STATUS2) {
@@ -47287,10 +48836,13 @@ async function runStatusUpdate(options) {
47287
48836
  }
47288
48837
  if (note !== void 0 && target === "IN_PROGRESS") {
47289
48838
  console.error(
47290
- colorError("--note is only used when reporting remediation (--status REMEDIATED).")
48839
+ colorError(
48840
+ "--note is only used when reporting remediation or accepting (--status REMEDIATED or ACCEPTED_WITH_CONTROLS)."
48841
+ )
47291
48842
  );
47292
48843
  process.exit(1);
47293
48844
  }
48845
+ const rationale = target === "ACCEPTED_WITH_CONTROLS" ? requireAcceptRationale(note) : null;
47294
48846
  const viaApiKey = requireAuth().kind === "api_key";
47295
48847
  const organizationId = await resolveOrganizationId();
47296
48848
  const params = {
@@ -47303,6 +48855,8 @@ async function runStatusUpdate(options) {
47303
48855
  params,
47304
48856
  body: { status: target, ...note !== void 0 ? { note } : {} }
47305
48857
  })
48858
+ ) : rationale !== null ? await authenticatedRequest(
48859
+ (client) => client.pentestFinding.accept({ params, body: { rationale } })
47306
48860
  ) : target === "IN_PROGRESS" ? await authenticatedRequest(
47307
48861
  (client) => client.pentestFinding.startProgress({ params, body: {} })
47308
48862
  ) : await authenticatedRequest(
@@ -47323,19 +48877,24 @@ async function runStatusUpdate(options) {
47323
48877
  console.log(colorSuccess(`PTF-${displayId} is now ${status}`));
47324
48878
  }
47325
48879
  function createStatusUpdateCommand() {
47326
- 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(
48880
+ return new Command("status-update").description("Move a pentest finding to IN_PROGRESS, REMEDIATED, or ACCEPTED_WITH_CONTROLS").requiredOption("--pentest <id>", "Pentest display ID (e.g. PT-3) or UUID").requiredOption("--finding <id>", "Finding number (e.g. PTF-7) or UUID").requiredOption(
47327
48881
  "--status <status>",
47328
- "IN_PROGRESS (start work) or REMEDIATED (ready for retest)"
47329
- ).option("--note <text>", "Optional context when reporting remediation").option("-o, --output <format>", "Output format: text, json (default: text)", "text").addHelpText(
48882
+ "IN_PROGRESS (start work), REMEDIATED (ready for retest), or ACCEPTED_WITH_CONTROLS (accept the risk)"
48883
+ ).option(
48884
+ "--note <text>",
48885
+ "Context when reporting remediation; the required rationale when accepting"
48886
+ ).option("-o, --output <format>", "Output format: text, json (default: text)", "text").addHelpText(
47330
48887
  "after",
47331
48888
  `
47332
48889
  Customer-facing status writes \u2014 the same transitions as the dashboard. An agent
47333
48890
  can mark a finding in progress when it starts the fix, then remediated when the
47334
48891
  fix is ready for Trace to re-test. Leaving the finding IN_PROGRESS (and not
47335
- sending REMEDIATED) keeps it out of the retest queue.
48892
+ sending REMEDIATED) keeps it out of the retest queue. ACCEPTED_WITH_CONTROLS
48893
+ (also spelled ACCEPTED) closes the finding as an accepted risk with no Trace
48894
+ review; --note carries the rationale and is required.
47336
48895
 
47337
- IN_PROGRESS is allowed from OPEN or VERIFIED_OPEN. REMEDIATED is allowed from
47338
- OPEN, IN_PROGRESS, or VERIFIED_OPEN.
48896
+ IN_PROGRESS is allowed from OPEN, VERIFIED_OPEN, or PARTIALLY_FIXED. REMEDIATED and
48897
+ ACCEPTED_WITH_CONTROLS are allowed from OPEN, IN_PROGRESS, VERIFIED_OPEN, or PARTIALLY_FIXED.
47339
48898
 
47340
48899
  A signed-in session (\`tracecli auth login\`) attributes the change to you. A
47341
48900
  Trace API key works too, once Trace enables finding automation for your
@@ -47346,7 +48905,9 @@ Examples:
47346
48905
  tracecli pentest findings status-update --pentest PT-3 --finding 7 --status IN_PROGRESS
47347
48906
  tracecli pentest findings status-update --pentest PT-3 --finding PTF-7 --status REMEDIATED
47348
48907
  tracecli pentest findings status-update --pentest PT-3 --finding 7 --status REMEDIATED \\
47349
- --note "Fixed in PR 412" --output json`
48908
+ --note "Fixed in PR 412" --output json
48909
+ tracecli pentest findings status-update --pentest PT-3 --finding 7 --status ACCEPTED \\
48910
+ --note "Internal-only endpoint behind SSO; residual risk accepted"`
47350
48911
  ).action(async (options) => {
47351
48912
  await runStatusUpdate(options);
47352
48913
  });
@@ -47360,7 +48921,7 @@ var PentestDeliverableDownloadError = class extends Error {
47360
48921
  }
47361
48922
  };
47362
48923
  function safeDeliverableFilename(documentId, extension, fallbackStem) {
47363
- const stripped = Array.from(basename3(documentId.replace(/\\/g, "/")), (char) => {
48924
+ const stripped = Array.from(basename4(documentId.replace(/\\/g, "/")), (char) => {
47364
48925
  const code = char.codePointAt(0) ?? 0;
47365
48926
  return code <= 31 || code === 127 ? "_" : char;
47366
48927
  }).join("").trim();
@@ -47846,182 +49407,6 @@ function createAttestationCommand() {
47846
49407
  return attestation;
47847
49408
  }
47848
49409
 
47849
- // src/pentest/evidence.ts
47850
- import { readFileSync as readFileSync6, statSync as statSync4 } from "fs";
47851
- import { basename as basename4 } from "path";
47852
- var SECTION_LIST = ValidationEvidenceSectionValues.join(", ");
47853
- var KIND_LIST = PentestExploitArtifactKindValues.join(", ");
47854
- function isSection(value) {
47855
- return ValidationEvidenceSectionValues.includes(value);
47856
- }
47857
- function isKind(value) {
47858
- return PentestExploitArtifactKindValues.includes(value);
47859
- }
47860
- function formatBytes2(bytes) {
47861
- if (bytes < 1024) return `${bytes} B`;
47862
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
47863
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
47864
- }
47865
- async function resolveFindingId(organizationId, pentestId, identifier) {
47866
- if (isUuid(identifier)) {
47867
- return identifier;
47868
- }
47869
- const numericMatch = identifier.match(/^(?:PTF-)?(\d+)$/i);
47870
- if (!numericMatch?.[1]) {
47871
- console.error(
47872
- colorError(
47873
- `Invalid finding identifier '${sanitizeDisplay(identifier)}'. Use a finding number (e.g. PTF-7) or UUID.`
47874
- )
47875
- );
47876
- process.exit(1);
47877
- }
47878
- const displayId = Number.parseInt(numericMatch[1], 10);
47879
- const result = await authenticatedRequest(
47880
- (client) => client.pentestFinding.listFindings({ params: { organizationId, pentestId } })
47881
- );
47882
- if (result.status === UNAUTHORIZED_STATUS2) {
47883
- exitAuthFailed();
47884
- }
47885
- if (result.status !== 200) {
47886
- console.error(colorError(`Failed to list findings (status ${result.status}).`));
47887
- process.exit(1);
47888
- }
47889
- const match = result.body.find((f) => f.displayId === displayId);
47890
- if (!match || !("pentestFindingId" in match)) {
47891
- console.error(
47892
- colorError(
47893
- `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\`).`
47894
- )
47895
- );
47896
- process.exit(1);
47897
- }
47898
- return match.pentestFindingId;
47899
- }
47900
- async function runAttachEvidence(options) {
47901
- if (options.section !== void 0 && !isSection(options.section)) {
47902
- console.error(colorError(`Invalid --section '${sanitizeDisplay(options.section)}'.`));
47903
- console.error(`Valid sections: ${SECTION_LIST}`);
47904
- process.exit(1);
47905
- }
47906
- if (options.kind !== void 0 && !isKind(options.kind)) {
47907
- console.error(colorError(`Invalid --kind '${sanitizeDisplay(options.kind)}'.`));
47908
- console.error(`Valid kinds: ${KIND_LIST}`);
47909
- process.exit(1);
47910
- }
47911
- let sizeBytes;
47912
- try {
47913
- const stats = statSync4(options.file);
47914
- if (!stats.isFile()) {
47915
- console.error(colorError(`--file '${sanitizeDisplay(options.file)}' is not a regular file.`));
47916
- process.exit(1);
47917
- }
47918
- sizeBytes = stats.size;
47919
- } catch {
47920
- console.error(colorError(`Cannot read --file '${sanitizeDisplay(options.file)}'.`));
47921
- process.exit(1);
47922
- }
47923
- if (sizeBytes === 0) {
47924
- console.error(colorError(`--file '${sanitizeDisplay(options.file)}' is empty.`));
47925
- process.exit(1);
47926
- }
47927
- if (sizeBytes > MAX_STAFF_EVIDENCE_UPLOAD_BYTES) {
47928
- console.error(
47929
- colorError(
47930
- `File is ${formatBytes2(sizeBytes)}; the limit for one exhibit is ${formatBytes2(MAX_STAFF_EVIDENCE_UPLOAD_BYTES)}.`
47931
- )
47932
- );
47933
- process.exit(1);
47934
- }
47935
- const organizationId = await resolveOrganizationId();
47936
- const pentestId = await resolvePentestId(organizationId, options.pentest);
47937
- const pentestFindingId = await resolveFindingId(organizationId, pentestId, options.finding);
47938
- const params = { organizationId, pentestId, pentestFindingId };
47939
- const filename = basename4(options.file);
47940
- const slot = await authenticatedRequest(
47941
- (client) => client.pentestFinding.createFindingEvidenceUpload({
47942
- params,
47943
- body: { filename, sizeBytes }
47944
- })
47945
- );
47946
- if (slot.status === UNAUTHORIZED_STATUS2) {
47947
- exitAuthFailed();
47948
- }
47949
- if (slot.status !== 201) {
47950
- const { message } = readErrorFields(slot.body);
47951
- console.error(
47952
- colorError(message ?? `Failed to start the evidence upload (status ${slot.status}).`)
47953
- );
47954
- process.exit(1);
47955
- }
47956
- const body = readFileSync6(options.file);
47957
- const put = await fetch(slot.body.uploadUrl, { method: "PUT", body: new Uint8Array(body) });
47958
- if (!put.ok) {
47959
- console.error(
47960
- colorError(
47961
- `Upload to storage failed (status ${put.status}). The upload link is short-lived \u2014 re-run the command.`
47962
- )
47963
- );
47964
- process.exit(1);
47965
- }
47966
- const attached = await authenticatedRequest(
47967
- (client) => client.pentestFinding.attachFindingEvidence({
47968
- params,
47969
- body: {
47970
- key: slot.body.key,
47971
- ...options.caption !== void 0 ? { caption: options.caption } : {},
47972
- ...options.section !== void 0 && isSection(options.section) ? { section: options.section } : {},
47973
- ...options.kind !== void 0 && isKind(options.kind) ? { kind: options.kind } : {},
47974
- ...options.customerFacing ? { customerFacing: true } : {}
47975
- }
47976
- })
47977
- );
47978
- if (attached.status === UNAUTHORIZED_STATUS2) {
47979
- exitAuthFailed();
47980
- }
47981
- if (attached.status !== 201) {
47982
- const { message } = readErrorFields(attached.body);
47983
- console.error(
47984
- colorError(message ?? `Failed to attach the evidence (status ${attached.status}).`)
47985
- );
47986
- process.exit(1);
47987
- }
47988
- if (options.output === "json") {
47989
- printJson(attached.body);
47990
- return;
47991
- }
47992
- const visibility = attached.body.customerFacing ? "customer-facing" : "staff-internal";
47993
- const placement = attached.body.section ? ` \xB7 ${attached.body.section}` : "";
47994
- console.log(
47995
- colorSuccess(
47996
- `Attached ${attached.body.kind} \xB7 ${formatBytes2(attached.body.sizeBytes)} \xB7 ${visibility}${placement}`
47997
- )
47998
- );
47999
- console.log(` artifact ${attached.body.artifactId}`);
48000
- if (attached.body.kind === "HAR") {
48001
- console.log(" HAR captures stay internal \u2014 they can carry a live session token.");
48002
- } else if (!attached.body.customerFacing) {
48003
- console.log(" Publish it from the finding\u2019s evidence panel in the staff console.");
48004
- }
48005
- }
48006
- function createAttachEvidenceCommand() {
48007
- 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(
48008
- "after",
48009
- `
48010
- The file uploads straight to storage, so recordings of any reasonable size work.
48011
- Videos and HAR captures always land staff-internal \u2014 publish a video with the
48012
- toggle in the staff console's evidence panel after reviewing it for redaction.
48013
-
48014
- Examples:
48015
- tracecli pentest findings attach-evidence --pentest PT-3 --finding 7 --file ./poc.png
48016
- tracecli pentest findings attach-evidence --pentest PT-3 --finding PTF-7 --file ./poc.mov \\
48017
- --caption "Cookie survives logout, replayed 40s later" --section proofOfConcept
48018
- tracecli pentest findings attach-evidence --pentest PT-3 --finding 7 --file ./roster.json \\
48019
- --caption "Full member roster returned to a non-member" --customer-facing --output json`
48020
- ).action(async (options) => {
48021
- await runAttachEvidence(options);
48022
- });
48023
- }
48024
-
48025
49410
  // src/pentest/get-finding.ts
48026
49411
  var OUTPUT_FORMATS2 = ["text", "json"];
48027
49412
  function isOutputFormat2(value) {
@@ -48043,7 +49428,9 @@ function sanitizeMarkdown(value) {
48043
49428
  }
48044
49429
  function formatFindingMarkdown(finding) {
48045
49430
  const lines = [];
48046
- lines.push(`# PTF-${finding.displayId}: ${sanitizeDisplay(finding.title)}`);
49431
+ const unpublished = "publishedAt" in finding && finding.publishedAt === null;
49432
+ const label = finding.displayId === null ? "Draft" : `PTF-${finding.displayId}${unpublished ? " (unpublished)" : ""}`;
49433
+ lines.push(`# ${label}: ${sanitizeDisplay(finding.title)}`);
48047
49434
  lines.push("");
48048
49435
  lines.push(`- Severity: ${sanitizeDisplay(finding.severity)}`);
48049
49436
  lines.push(`- Status: ${sanitizeDisplay(finding.status)}`);
@@ -48102,6 +49489,18 @@ function failGetFinding(status, body) {
48102
49489
  );
48103
49490
  process.exit(1);
48104
49491
  }
49492
+ function readStaffFinding(organizationId, pentestId, pentestFindingId) {
49493
+ return authenticatedRequest(
49494
+ (client) => client.pentestFinding.getFinding({ params: { organizationId, pentestId, pentestFindingId } })
49495
+ );
49496
+ }
49497
+ function readCustomerFinding(organizationId, pentestId, pentestFindingId) {
49498
+ return authenticatedRequest(
49499
+ (client) => client.pentestFinding.getCustomerFinding({
49500
+ params: { organizationId, pentestId, pentestFindingId }
49501
+ })
49502
+ );
49503
+ }
48105
49504
  async function runGetFinding(options) {
48106
49505
  if (!isOutputFormat2(options.output)) {
48107
49506
  console.error(
@@ -48126,15 +49525,16 @@ async function runGetFinding(options) {
48126
49525
  process.exit(1);
48127
49526
  }
48128
49527
  const organizationId = await resolveOrganizationId();
48129
- const result = await authenticatedRequest(
48130
- (client) => client.pentestFinding.getCustomerFinding({
48131
- params: {
48132
- organizationId,
48133
- pentestId: options.pentest,
48134
- pentestFindingId: options.finding
48135
- }
48136
- })
48137
- );
49528
+ const me = requireAuth().kind === "user" ? await authenticatedRequestSafe((client) => client.user.getMyContext()) : null;
49529
+ const isStaff = me !== null && me.status === 200 && me.body.user.isStaff;
49530
+ let result;
49531
+ if (isStaff) {
49532
+ const pentestId = await resolvePentestId(organizationId, options.pentest);
49533
+ const pentestFindingId = await resolveFindingId(organizationId, pentestId, options.finding);
49534
+ result = await readStaffFinding(organizationId, pentestId, pentestFindingId);
49535
+ } else {
49536
+ result = await readCustomerFinding(organizationId, options.pentest, options.finding);
49537
+ }
48138
49538
  if (result.status !== 200) {
48139
49539
  failGetFinding(result.status, result.body);
48140
49540
  }
@@ -48163,6 +49563,185 @@ Examples:
48163
49563
  });
48164
49564
  }
48165
49565
 
49566
+ // src/pentest/publish-findings.ts
49567
+ var MAX_PUBLISH_BATCH = 200;
49568
+ function validatePublishSelection(options) {
49569
+ const all = options.all === true;
49570
+ const some = options.finding !== void 0 && options.finding.length > 0;
49571
+ if (all === some) {
49572
+ return { error: "Pass exactly one of --all or --finding <uuid> (repeatable)." };
49573
+ }
49574
+ return null;
49575
+ }
49576
+ function isPublishableDraft(finding) {
49577
+ return finding.publishedAt === null && finding.status !== "DISMISSED";
49578
+ }
49579
+ function formatCandidate(finding) {
49580
+ return ` [${sanitizeDisplay(finding.severity)}] ${sanitizeDisplay(finding.title)}`;
49581
+ }
49582
+ function draftsOutrankedBy(selected, all) {
49583
+ const rank = (severity) => SEVERITY_ORDER[severity] ?? Number.MAX_SAFE_INTEGER;
49584
+ const selectedIds = new Set(selected.map((f) => f.pentestFindingId));
49585
+ const leastSevereSelected = Math.max(...selected.map((f) => rank(f.severity)));
49586
+ return all.filter(
49587
+ (f) => !selectedIds.has(f.pentestFindingId) && isPublishableDraft(f) && // A numbered row that was unpublished keeps its number on republish, so it
49588
+ // cannot be pushed out of order by anything published before it.
49589
+ f.displayId === null && rank(f.severity) <= leastSevereSelected
49590
+ ).sort((a2, b) => rank(a2.severity) - rank(b.severity));
49591
+ }
49592
+ async function publishFindings(options) {
49593
+ const selection = validatePublishSelection(options);
49594
+ if (selection) {
49595
+ console.error(colorError(selection.error));
49596
+ process.exit(1);
49597
+ }
49598
+ const organizationId = await resolveOrganizationId();
49599
+ const pentestId = await resolvePentestId(organizationId, options.pentest);
49600
+ const pentestResult = await authenticatedRequest(
49601
+ (client) => client.pentest.getPentest({ params: { organizationId, pentestId } })
49602
+ );
49603
+ if (pentestResult.status !== SUCCESS_STATUS) {
49604
+ failStaffPentestWrite("read the pentest", pentestResult.status, pentestResult.body, {
49605
+ notFound: `Pentest '${sanitizeDisplay(options.pentest)}' not found. Verify the ID with \`tracecli pentest list\`.`
49606
+ });
49607
+ }
49608
+ const pentest = pentestResult.body;
49609
+ const label = `PT-${pentest.displayId}`;
49610
+ const listResult = await authenticatedRequest(
49611
+ (client) => client.pentestFinding.listFindings({ params: { organizationId, pentestId } })
49612
+ );
49613
+ if (listResult.status !== SUCCESS_STATUS) {
49614
+ failStaffPentestWrite("list the findings", listResult.status, listResult.body);
49615
+ }
49616
+ const findings = listResult.body;
49617
+ const byId = new Map(findings.map((f) => [f.pentestFindingId, f]));
49618
+ const drafts = findings.filter(isPublishableDraft);
49619
+ let candidates;
49620
+ if (options.all) {
49621
+ candidates = drafts;
49622
+ } else {
49623
+ const draftById = new Map(drafts.map((f) => [f.pentestFindingId, f]));
49624
+ candidates = (options.finding ?? []).map((id) => {
49625
+ const found = draftById.get(id.toLowerCase());
49626
+ if (!found) {
49627
+ const known = byId.get(id.toLowerCase());
49628
+ console.error(
49629
+ colorError(
49630
+ known ? `${sanitizeDisplay(id)} is ${known.publishedAt === null ? "dismissed" : "already published"} in ${label}, so it cannot be published.` : `${sanitizeDisplay(id)} is not an unpublished draft in ${label}. Drafts have no number yet; pass the UUID from \`tracecli pentest findings list --pentest ${label} --output json\`.`
49631
+ )
49632
+ );
49633
+ process.exit(1);
49634
+ }
49635
+ return found;
49636
+ });
49637
+ }
49638
+ if (candidates.length === 0) {
49639
+ if (options.output === "json") {
49640
+ printJson({ published: [] });
49641
+ } else {
49642
+ console.log(
49643
+ `Nothing to publish in ${label}: every non-dismissed finding is already published.`
49644
+ );
49645
+ }
49646
+ return;
49647
+ }
49648
+ if (candidates.length > MAX_PUBLISH_BATCH) {
49649
+ console.error(
49650
+ colorError(
49651
+ `${candidates.length} findings exceed the ${MAX_PUBLISH_BATCH} a single publish accepts. Publish from the staff pentest console for a set this large.`
49652
+ )
49653
+ );
49654
+ process.exit(1);
49655
+ }
49656
+ if (!options.all && options.outOfOrder !== true) {
49657
+ const leftBehind = draftsOutrankedBy(candidates, findings);
49658
+ if (leftBehind.length > 0) {
49659
+ console.error(
49660
+ colorError(
49661
+ `Publishing these now would number them ahead of ${leftBehind.length} unpublished finding${leftBehind.length === 1 ? "" : "s"} of equal or higher severity, and PTF numbers follow severity only when the set is published together:`
49662
+ )
49663
+ );
49664
+ for (const finding of leftBehind) {
49665
+ console.error(formatCandidate(finding));
49666
+ }
49667
+ console.error(
49668
+ colorError(
49669
+ "Use --all to publish them together, or --out-of-order to accept this numbering."
49670
+ )
49671
+ );
49672
+ process.exit(1);
49673
+ }
49674
+ }
49675
+ const hidden = "hiddenFromCustomerAt" in pentest && pentest.hiddenFromCustomerAt != null;
49676
+ if (options.output !== "json" || !options.yes) {
49677
+ console.log(
49678
+ `${candidates.length} finding${candidates.length === 1 ? "" : "s"} will be numbered and shown to the customer in ${label}:`
49679
+ );
49680
+ for (const finding of candidates) {
49681
+ console.log(formatCandidate(finding));
49682
+ }
49683
+ if (hidden) {
49684
+ console.log(
49685
+ colorWarning(
49686
+ `${label} is hidden from the customer, so nothing becomes visible until you run \`tracecli pentest visibility ${label} --visible\`.`
49687
+ )
49688
+ );
49689
+ }
49690
+ }
49691
+ const confirmed = await confirmAction("Publish to the customer?", {
49692
+ skip: options.yes === true,
49693
+ verb: "publish"
49694
+ });
49695
+ if (!confirmed) {
49696
+ console.log("Nothing published.");
49697
+ return;
49698
+ }
49699
+ const result = await authenticatedRequest(
49700
+ (client) => client.pentestFinding.publishAllFindings({
49701
+ params: { organizationId, pentestId },
49702
+ // Always the previewed ids, never the server's default set: a draft that lands
49703
+ // between the preview and the answer must not ride along unseen.
49704
+ body: { pentestFindingIds: candidates.map((f) => f.pentestFindingId) }
49705
+ })
49706
+ );
49707
+ if (result.status !== SUCCESS_STATUS) {
49708
+ failStaffPentestWrite("publish findings", result.status, result.body, {
49709
+ closed: "Closed pentests must be reopened before publishing findings."
49710
+ });
49711
+ }
49712
+ if (options.output === "json") {
49713
+ printJson(result.body);
49714
+ return;
49715
+ }
49716
+ const published = result.body.published;
49717
+ console.log(
49718
+ colorSuccess(
49719
+ `Published ${published.length} finding${published.length === 1 ? "" : "s"} in ${label}.`
49720
+ )
49721
+ );
49722
+ for (const entry of published) {
49723
+ const finding = byId.get(entry.pentestFindingId);
49724
+ console.log(
49725
+ ` PTF-${entry.displayId} ${finding ? sanitizeDisplay(finding.title) : entry.pentestFindingId}`
49726
+ );
49727
+ }
49728
+ if (hidden) {
49729
+ console.log(
49730
+ colorWarning(
49731
+ `${label} is still hidden from the customer. Run \`tracecli pentest visibility ${label} --visible\` when you are ready.`
49732
+ )
49733
+ );
49734
+ }
49735
+ }
49736
+ function createPublishFindingsCommand() {
49737
+ return new Command("publish").description("Number unpublished findings and show them to the customer (staff)").requiredOption("--pentest <id>", "Pentest display ID (e.g. PT-3) or UUID").option("--all", "Publish every unpublished, non-dismissed finding").option("--finding <uuid>", "Draft finding UUID to publish (repeatable)", collectRepeatable).option("-y, --yes", "Skip the confirmation prompt").option(
49738
+ "--out-of-order",
49739
+ "With --finding: accept numbering ahead of equal-or-higher-severity drafts left unpublished"
49740
+ ).option("-o, --output <format>", "Output format: text, json (default: text)", "text").action(async (options) => {
49741
+ await publishFindings(options);
49742
+ });
49743
+ }
49744
+
48166
49745
  // src/pentest/questionnaire.ts
48167
49746
  function buildUpsertQuestionnaireBody(answers) {
48168
49747
  if (answers === null) {
@@ -48274,6 +49853,56 @@ function surfaceSelectOptions() {
48274
49853
  }));
48275
49854
  }
48276
49855
 
49856
+ // src/pentest/visibility.ts
49857
+ function resolveVisibilityChoice(options) {
49858
+ const hidden = options.hidden === true;
49859
+ const visible = options.visible === true;
49860
+ if (hidden === visible) {
49861
+ return { error: "Pass exactly one of --hidden or --visible." };
49862
+ }
49863
+ return { visible };
49864
+ }
49865
+ async function setVisibility(pentestIdentifier, options) {
49866
+ const choice = resolveVisibilityChoice(options);
49867
+ if ("error" in choice) {
49868
+ console.error(colorError(choice.error));
49869
+ process.exit(1);
49870
+ }
49871
+ const organizationId = await resolveOrganizationId();
49872
+ const pentestId = await resolvePentestId(organizationId, pentestIdentifier);
49873
+ const result = await authenticatedRequest(
49874
+ (client) => client.staffPentests.setStaffOrganizationPentestCustomerVisibility({
49875
+ params: { organizationId, pentestId },
49876
+ body: { visible: choice.visible }
49877
+ })
49878
+ );
49879
+ if (result.status !== SUCCESS_STATUS) {
49880
+ failStaffPentestWrite("change pentest visibility", result.status, result.body, {
49881
+ notFound: `Pentest '${sanitizeDisplay(pentestIdentifier)}' not found. Verify the ID with \`tracecli pentest list\`.`
49882
+ });
49883
+ }
49884
+ if (options.output === "json") {
49885
+ printJson(result.body);
49886
+ return;
49887
+ }
49888
+ const label = `PT-${result.body.displayId}`;
49889
+ console.log(
49890
+ colorSuccess(
49891
+ result.body.hiddenFromCustomerAt === null ? `${label} is visible to the customer.` : `${label} is hidden from the customer.`
49892
+ )
49893
+ );
49894
+ if (result.body.hiddenFromCustomerAt === null) {
49895
+ console.log(" Published findings, reports, and attestations show in their dashboard again.");
49896
+ } else {
49897
+ console.log(" The customer no longer sees this pentest anywhere; staff views are unchanged.");
49898
+ }
49899
+ }
49900
+ function createVisibilityCommand() {
49901
+ return new Command("visibility").description("Hide a pentest from the customer or make it visible again (staff)").argument("<pentestId>", "Pentest display ID (e.g. PT-3) or UUID").option("--hidden", "Hide the pentest from the customer").option("--visible", "Make the pentest visible to the customer").option("-o, --output <format>", "Output format: text, json (default: text)", "text").action(async (pentestId, options) => {
49902
+ await setVisibility(pentestId, options);
49903
+ });
49904
+ }
49905
+
48277
49906
  // src/pentest/commands.ts
48278
49907
  var DEFAULT_LIMIT2 = 30;
48279
49908
  var MAX_LIMIT2 = 100;
@@ -48431,7 +50060,7 @@ function createFindingsListCommand() {
48431
50060
  (value) => value.toUpperCase()
48432
50061
  ).option(
48433
50062
  "--status <status>",
48434
- "Filter by status (OPEN, IN_PROGRESS, CONTESTED, REMEDIATED, VERIFIED_FIXED, VERIFIED_OPEN, ACCEPTED_WITH_CONTROLS, DISMISSED)",
50063
+ `Filter by status (${PentestFindingStatusValues.join(", ")})`,
48435
50064
  (value) => value.toUpperCase()
48436
50065
  ).option(
48437
50066
  "-L, --limit <n>",
@@ -48463,7 +50092,9 @@ Full detail, for feeding an agent that fixes the findings:
48463
50092
 
48464
50093
  Then mark work as it happens:
48465
50094
  tracecli pentest findings status-update --pentest PT-3 --finding 7 --status IN_PROGRESS
48466
- tracecli pentest findings status-update --pentest PT-3 --finding 7 --status REMEDIATED`
50095
+ tracecli pentest findings status-update --pentest PT-3 --finding 7 --status REMEDIATED
50096
+ tracecli pentest findings status-update --pentest PT-3 --finding 7 --status ACCEPTED \\
50097
+ --note "Internal-only endpoint behind SSO; residual risk accepted"`
48467
50098
  ).action(
48468
50099
  async (options) => {
48469
50100
  if (options.detail && options.output !== "json") {
@@ -48648,6 +50279,8 @@ function createFindingsCommand() {
48648
50279
  findings.addCommand(createStatusUpdateCommand());
48649
50280
  findings.addCommand(createAttachEvidenceCommand());
48650
50281
  findings.addCommand(createCreateFindingCommand(), { hidden: true });
50282
+ findings.addCommand(createCopyFindingsCommand(), { hidden: true });
50283
+ findings.addCommand(createPublishFindingsCommand(), { hidden: true });
48651
50284
  return findings;
48652
50285
  }
48653
50286
  var PRIORITY_SET = new Set(CustomerPriorityCategoryValues);
@@ -49131,6 +50764,8 @@ function createPentestCommand() {
49131
50764
  const pentest = new Command("pentest").description("Manage pentests");
49132
50765
  pentest.addCommand(createListCommand6());
49133
50766
  pentest.addCommand(createUpdateCommand4());
50767
+ pentest.addCommand(createCreatePentestCommand(), { hidden: true });
50768
+ pentest.addCommand(createVisibilityCommand(), { hidden: true });
49134
50769
  pentest.addCommand(createQuestionnaireCommand());
49135
50770
  pentest.addCommand(createFindingsCommand());
49136
50771
  pentest.addCommand(createReportCommand());
@@ -49138,8 +50773,251 @@ function createPentestCommand() {
49138
50773
  return pentest;
49139
50774
  }
49140
50775
 
50776
+ // src/test-integration/passkeys.ts
50777
+ import { readFileSync as readFileSync8, unlinkSync } from "fs";
50778
+
50779
+ // src/test-integration/resolve.ts
50780
+ async function resolveTestIntegrationId(label) {
50781
+ const result = await authenticatedRequest(
50782
+ (client) => client.testIntegration.listTestIntegrations()
50783
+ );
50784
+ if (result.status !== SUCCESS_STATUS) {
50785
+ failStaffPentestWrite("list test integrations", result.status, result.body);
50786
+ }
50787
+ const match = result.body.find((integration) => integration.label === label);
50788
+ if (match === void 0) {
50789
+ console.error(
50790
+ colorError(
50791
+ `No test integration named '${sanitizeDisplay(label)}'. Names are exact; the console's Test integrations page lists them.`
50792
+ )
50793
+ );
50794
+ process.exit(1);
50795
+ }
50796
+ return match.testIntegrationId;
50797
+ }
50798
+
50799
+ // src/test-integration/passkeys.ts
50800
+ function parseKeyFile(raw) {
50801
+ let parsed;
50802
+ try {
50803
+ parsed = JSON.parse(raw);
50804
+ } catch {
50805
+ return { error: "The key file is not JSON." };
50806
+ }
50807
+ if (Array.isArray(parsed)) {
50808
+ return {
50809
+ error: "The key file holds an array. Import takes one passkey per file: write the one entry you mean to import as a single object."
50810
+ };
50811
+ }
50812
+ const result = TestIntegrationPasskeyImportSchema.safeParse(parsed);
50813
+ if (!result.success) {
50814
+ const paths = result.error.issues.map((issue2) => issue2.path.map(String).join(".") || "(root)").filter((path, index, all) => all.indexOf(path) === index);
50815
+ return {
50816
+ error: `The key file is not a captured passkey. Problem fields: ${paths.join(", ")}.`
50817
+ };
50818
+ }
50819
+ return { passkey: result.data };
50820
+ }
50821
+ function describePasskey(passkey) {
50822
+ const user = passkey.userName ?? void 0;
50823
+ return user === void 0 || user.length === 0 ? `the ${sanitizeDisplay(passkey.rpId)} passkey` : `the ${sanitizeDisplay(passkey.rpId)} passkey for ${sanitizeDisplay(user)}`;
50824
+ }
50825
+ function importSuccessLine(input) {
50826
+ const head = `Imported ${describePasskey(input.passkey)} into ${sanitizeDisplay(input.label)} (id ${input.id}).`;
50827
+ return input.deleted ? `${head} Deleted the key file ${input.path}.` : `${head} Kept ${input.path}. It holds the private key, so delete it once every environment you need has it, or pass --delete-file on the last import.`;
50828
+ }
50829
+ function deleteSuccessLine(input) {
50830
+ return `Deleted ${describePasskey(input.passkey)} from ${sanitizeDisplay(input.label)}. The site still lists it on the account, so remove it there too, and do it now if the key may have leaked.`;
50831
+ }
50832
+ async function importPasskey(label, options) {
50833
+ let raw;
50834
+ try {
50835
+ raw = readFileSync8(options.file, "utf8");
50836
+ } catch {
50837
+ console.error(colorError(`Cannot read the key file ${options.file}.`));
50838
+ process.exit(1);
50839
+ }
50840
+ const parsed = parseKeyFile(raw);
50841
+ if ("error" in parsed) {
50842
+ console.error(colorError(parsed.error));
50843
+ process.exit(1);
50844
+ }
50845
+ const { passkey } = parsed;
50846
+ const testIntegrationId = await resolveTestIntegrationId(label);
50847
+ if (options.output !== "json") {
50848
+ console.log(`Integration ${sanitizeDisplay(label)}`);
50849
+ console.log(`Site ${sanitizeDisplay(passkey.rpId)}`);
50850
+ console.log(
50851
+ `User ${passkey.userName ? sanitizeDisplay(passkey.userName) : colorMuted("(none in file)")}`
50852
+ );
50853
+ console.log(`Counter ${passkey.signCount}`);
50854
+ }
50855
+ const confirmed = await confirmAction(`Import this passkey into ${sanitizeDisplay(label)}?`, {
50856
+ skip: options.yes === true,
50857
+ verb: "import"
50858
+ });
50859
+ if (!confirmed) {
50860
+ console.log("Nothing imported.");
50861
+ return;
50862
+ }
50863
+ const result = await authenticatedRequest(
50864
+ (client) => client.testIntegration.importTestIntegrationPasskey({
50865
+ params: { testIntegrationId },
50866
+ body: passkey
50867
+ })
50868
+ );
50869
+ if (result.status !== CREATED_STATUS) {
50870
+ if (result.status === 409) {
50871
+ const { message } = readErrorFields(result.body);
50872
+ console.error(colorError(message ?? "This passkey is already stored on the integration."));
50873
+ console.error(`The key file ${options.file} was kept.`);
50874
+ process.exit(1);
50875
+ }
50876
+ failStaffPentestWrite("import a passkey", result.status, result.body, {
50877
+ notFound: `No test integration named '${sanitizeDisplay(label)}'.`
50878
+ });
50879
+ }
50880
+ let deleted = false;
50881
+ if (options.deleteFile === true) {
50882
+ unlinkSync(options.file);
50883
+ deleted = true;
50884
+ }
50885
+ if (options.output === "json") {
50886
+ printJson({ ...result.body, keyFile: deleted ? null : options.file });
50887
+ return;
50888
+ }
50889
+ console.log(
50890
+ colorSuccess(
50891
+ importSuccessLine({
50892
+ passkey,
50893
+ label,
50894
+ id: result.body.testIntegrationPasskeyId,
50895
+ path: options.file,
50896
+ deleted
50897
+ })
50898
+ )
50899
+ );
50900
+ }
50901
+ async function listPasskeys(label, options) {
50902
+ const testIntegrationId = await resolveTestIntegrationId(label);
50903
+ const result = await authenticatedRequest(
50904
+ (client) => client.testIntegration.listTestIntegrationPasskeys({ params: { testIntegrationId } })
50905
+ );
50906
+ if (result.status !== SUCCESS_STATUS) {
50907
+ failStaffPentestWrite("list passkeys", result.status, result.body);
50908
+ }
50909
+ const rows = result.body;
50910
+ if (options.output === "json") {
50911
+ printJson(rows);
50912
+ return;
50913
+ }
50914
+ if (options.output === "tsv") {
50915
+ console.log("id site user imported lastUsed");
50916
+ for (const row of rows) {
50917
+ console.log(
50918
+ [
50919
+ row.testIntegrationPasskeyId,
50920
+ row.rpId,
50921
+ userColumn(row),
50922
+ toIso(row.createdAt),
50923
+ row.lastAssertedAt === null ? "" : toIso(row.lastAssertedAt)
50924
+ ].join(" ")
50925
+ );
50926
+ }
50927
+ return;
50928
+ }
50929
+ if (rows.length === 0) {
50930
+ console.log(`No passkeys stored for ${sanitizeDisplay(label)}.`);
50931
+ return;
50932
+ }
50933
+ for (const row of rows) {
50934
+ console.log(
50935
+ `${row.testIntegrationPasskeyId} ${sanitizeDisplay(row.rpId)} ${userColumn(row)} imported ${toIso(row.createdAt)} last used ${row.lastAssertedAt === null ? "never" : toIso(row.lastAssertedAt)}`
50936
+ );
50937
+ }
50938
+ }
50939
+ function userColumn(row) {
50940
+ if (row.unreadable) return "unreadable";
50941
+ return row.userName === null ? "" : sanitizeDisplay(row.userName);
50942
+ }
50943
+ async function deletePasskey(label, passkeyId, options) {
50944
+ const testIntegrationId = await resolveTestIntegrationId(label);
50945
+ const listed = await authenticatedRequest(
50946
+ (client) => client.testIntegration.listTestIntegrationPasskeys({ params: { testIntegrationId } })
50947
+ );
50948
+ if (listed.status !== SUCCESS_STATUS) {
50949
+ failStaffPentestWrite("list passkeys", listed.status, listed.body);
50950
+ }
50951
+ const row = listed.body.find(
50952
+ (r2) => r2.testIntegrationPasskeyId === passkeyId
50953
+ );
50954
+ if (row === void 0) {
50955
+ console.error(
50956
+ colorError(
50957
+ `No passkey with id ${sanitizeDisplay(passkeyId)} is stored on ${sanitizeDisplay(label)}. Run \`tracecli test-integration passkeys list ${sanitizeDisplay(label)}\`.`
50958
+ )
50959
+ );
50960
+ process.exit(1);
50961
+ }
50962
+ const confirmed = await confirmAction(
50963
+ `Delete ${describePasskey(row)} from ${sanitizeDisplay(label)}?`,
50964
+ { skip: options.yes === true, verb: "delete" }
50965
+ );
50966
+ if (!confirmed) {
50967
+ console.log("Nothing deleted.");
50968
+ return;
50969
+ }
50970
+ const result = await authenticatedRequest(
50971
+ (client) => client.testIntegration.deleteTestIntegrationPasskey({
50972
+ params: { testIntegrationId, testIntegrationPasskeyId: passkeyId }
50973
+ })
50974
+ );
50975
+ if (result.status !== NO_CONTENT_STATUS) {
50976
+ failStaffPentestWrite("delete a passkey", result.status, result.body, {
50977
+ notFound: `No passkey with id ${sanitizeDisplay(passkeyId)} is stored on ${sanitizeDisplay(label)}.`
50978
+ });
50979
+ }
50980
+ if (options.output === "json") {
50981
+ printJson({ deleted: passkeyId });
50982
+ return;
50983
+ }
50984
+ console.log(colorSuccess(deleteSuccessLine({ passkey: row, label })));
50985
+ }
50986
+ function createPasskeysCommand() {
50987
+ const passkeys = new Command("passkeys").description(
50988
+ "Passkeys stored on a test integration (staff)"
50989
+ );
50990
+ passkeys.addCommand(
50991
+ new Command("import").description("Store a passkey the capture script wrote (staff)").argument("<label>", "The test integration name, exactly as the console shows it").requiredOption("--file <path>", "The captured key file (one passkey)").option("--delete-file", "Delete the key file after a successful import").option("-y, --yes", "Skip the confirmation").option("-o, --output <format>", "Output format: text, json (default: text)", "text").action(async (label, options) => {
50992
+ await importPasskey(label, options);
50993
+ })
50994
+ );
50995
+ passkeys.addCommand(
50996
+ new Command("list").description("List a test integration's stored passkeys (staff)").argument("<label>", "The test integration name").option("-o, --output <format>", "Output format: text, json, tsv (default: text)", "text").action(async (label, options) => {
50997
+ await listPasskeys(label, options);
50998
+ })
50999
+ );
51000
+ passkeys.addCommand(
51001
+ new Command("delete").description("Remove one stored passkey from a test integration (staff)").argument("<label>", "The test integration name").argument("<passkeyId>", "The passkey id from `passkeys list`").option("-y, --yes", "Skip the confirmation").option("-o, --output <format>", "Output format: text, json (default: text)", "text").action(
51002
+ async (label, passkeyId, options) => {
51003
+ await deletePasskey(label, passkeyId, options);
51004
+ }
51005
+ )
51006
+ );
51007
+ return passkeys;
51008
+ }
51009
+
51010
+ // src/test-integration/commands.ts
51011
+ function createTestIntegrationCommand() {
51012
+ const group = new Command("test-integration").description(
51013
+ "Trace-owned test integrations (staff)"
51014
+ );
51015
+ group.addCommand(createPasskeysCommand());
51016
+ return group;
51017
+ }
51018
+
49141
51019
  // src/update/commands.ts
49142
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
51020
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
49143
51021
  import { dirname as dirname3, join as join7 } from "path";
49144
51022
  import { createInterface as createInterface4 } from "readline/promises";
49145
51023
  var UPDATE_CACHE_FILE = "update-check.json";
@@ -49157,7 +51035,7 @@ function readUpdateCheckCache() {
49157
51035
  return null;
49158
51036
  }
49159
51037
  try {
49160
- const rawCache = readFileSync8(cachePath, "utf-8");
51038
+ const rawCache = readFileSync9(cachePath, "utf-8");
49161
51039
  const parsed = JSON.parse(rawCache);
49162
51040
  if (typeof parsed !== "object" || parsed === null) {
49163
51041
  return null;
@@ -49948,13 +51826,13 @@ function buildUploadsTable(uploads) {
49948
51826
  size: formatArchiveBytes(upload.sizeBytes),
49949
51827
  id: upload.codeUploadId
49950
51828
  }));
49951
- const nameWidth = Math.max(4, ...cells.map((c62) => c62.name.length));
49952
- const kindWidth = Math.max(4, ...cells.map((c62) => c62.kind.length));
49953
- const statusWidth = Math.max(6, ...cells.map((c62) => c62.status.length));
49954
- const sizeWidth = Math.max(4, ...cells.map((c62) => c62.size.length));
51829
+ const nameWidth = Math.max(4, ...cells.map((c65) => c65.name.length));
51830
+ const kindWidth = Math.max(4, ...cells.map((c65) => c65.kind.length));
51831
+ const statusWidth = Math.max(6, ...cells.map((c65) => c65.status.length));
51832
+ const sizeWidth = Math.max(4, ...cells.map((c65) => c65.size.length));
49955
51833
  const header = `${"NAME".padEnd(nameWidth)} ${"KIND".padEnd(kindWidth)} ${"STATUS".padEnd(statusWidth)} ${"SIZE".padEnd(sizeWidth)} ID`;
49956
51834
  const rows = cells.map(
49957
- (c62) => `${c62.name.padEnd(nameWidth)} ${c62.kind.padEnd(kindWidth)} ${c62.status.padEnd(statusWidth)} ${c62.size.padEnd(sizeWidth)} ${c62.id}`
51835
+ (c65) => `${c65.name.padEnd(nameWidth)} ${c65.kind.padEnd(kindWidth)} ${c65.status.padEnd(statusWidth)} ${c65.size.padEnd(sizeWidth)} ${c65.id}`
49958
51836
  );
49959
51837
  return { header, rows };
49960
51838
  }
@@ -50312,7 +52190,7 @@ async function runList5(options) {
50312
52190
  const output = parseOutput(options.output);
50313
52191
  if (options.web) {
50314
52192
  const slug = await resolveOrganizationSlug();
50315
- const url2 = `${getAppBaseUrl()}/dashboard/${slug}/settings/integrations/sources`;
52193
+ const url2 = `${getAppBaseUrl()}/dashboard/${slug}/settings/integrations/source-code/upload`;
50316
52194
  const opened = await openBrowser(url2);
50317
52195
  if (!opened) console.log(url2);
50318
52196
  return;
@@ -50988,6 +52866,7 @@ program2.addCommand(createVulnerabilitiesCommand());
50988
52866
  program2.addCommand(createCodeSizeCommand());
50989
52867
  program2.addCommand(createUploadCommand());
50990
52868
  program2.addCommand(createPentestCommand());
52869
+ program2.addCommand(createTestIntegrationCommand(), { hidden: true });
50991
52870
  program2.addCommand(createCompletionCommand(program2));
50992
52871
  program2.addCommand(createUpgradeCommand());
50993
52872
  await program2.parseAsync();