securewithtrace 0.49.0 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +1567 -241
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -6283,7 +6283,7 @@ var $ZodBase64 = /* @__PURE__ */ $constructor("$ZodBase64", (inst, def) => {
6283
6283
  function isValidBase64URL(data) {
6284
6284
  if (!base64url.test(data))
6285
6285
  return false;
6286
- const base644 = data.replace(/[-_]/g, (c83) => c83 === "-" ? "+" : "/");
6286
+ const base644 = data.replace(/[-_]/g, (c84) => c84 === "-" ? "+" : "/");
6287
6287
  const padded = base644.padEnd(Math.ceil(base644.length / 4) * 4, "=");
6288
6288
  return isValidBase64(padded);
6289
6289
  }
@@ -16327,9 +16327,9 @@ var ZodDate = /* @__PURE__ */ $constructor("ZodDate", (inst, def) => {
16327
16327
  inst._zod.processJSONSchema = (ctx, json2, params3) => dateProcessor(inst, ctx, json2, params3);
16328
16328
  inst.min = (value, params3) => inst.check(_gte(value, params3));
16329
16329
  inst.max = (value, params3) => inst.check(_lte(value, params3));
16330
- const c83 = inst._zod.bag;
16331
- inst.minDate = c83.minimum ? new Date(c83.minimum) : null;
16332
- inst.maxDate = c83.maximum ? new Date(c83.maximum) : null;
16330
+ const c84 = inst._zod.bag;
16331
+ inst.minDate = c84.minimum ? new Date(c84.minimum) : null;
16332
+ inst.maxDate = c84.maximum ? new Date(c84.maximum) : null;
16333
16333
  });
16334
16334
  function date3(params3) {
16335
16335
  return _date(ZodDate, params3);
@@ -18329,6 +18329,8 @@ var STAFF_NOTIFICATION_TOOLSET_VERSIONS = /* @__PURE__ */ new Set([
18329
18329
  // API_COVERAGE_TOOLSET_VERSION and its predecessor, written out because they are declared below.
18330
18330
  "api-coverage-tools-v1",
18331
18331
  "api-coverage-tools-v2",
18332
+ // SETUP_TOOLSET_VERSION, written out for the same reason.
18333
+ "setup-tools-v1",
18332
18334
  ...STAFF_NOTIFICATION_OOB_PROFILE_SUCCESSIONS.flatMap(({ successor, predecessor }) => [
18333
18335
  predecessor.toolsetVersion,
18334
18336
  successor.toolsetVersion
@@ -18654,6 +18656,21 @@ var PENTEST_CUSTOMER_INTAKE_QUESTIONS = [
18654
18656
  }
18655
18657
  ];
18656
18658
  var PENTEST_CUSTOMER_INTAKE_QUESTION_COUNT = PENTEST_CUSTOMER_INTAKE_QUESTIONS.length;
18659
+ function isIntakeAnswerPresent(value) {
18660
+ if (value === void 0 || value === null) return false;
18661
+ if (typeof value === "string") return value.trim().length > 0;
18662
+ if (Array.isArray(value)) return value.length > 0;
18663
+ return true;
18664
+ }
18665
+ function countAnsweredIntakeQuestions(answers) {
18666
+ if (!answers) return 0;
18667
+ let answered = 0;
18668
+ for (const question of PENTEST_CUSTOMER_INTAKE_QUESTIONS) {
18669
+ const keys = question.kind === "multiSelect" ? [question.key, question.notesKey] : [question.key];
18670
+ if (keys.some((key) => isIntakeAnswerPresent(answers[key]))) answered += 1;
18671
+ }
18672
+ return answered;
18673
+ }
18657
18674
  var INTAKE_ISSUE_CLASS_LABELS = {
18658
18675
  AUTHN_BYPASS: "Authn bypass",
18659
18676
  AUTHZ_IDOR: "Broken access control",
@@ -21924,23 +21941,23 @@ var ZodEffects = class extends ZodType2 {
21924
21941
  }
21925
21942
  if (effect.type === "transform") {
21926
21943
  if (ctx.common.async === false) {
21927
- const base2 = this._def.schema._parseSync({
21944
+ const base3 = this._def.schema._parseSync({
21928
21945
  data: ctx.data,
21929
21946
  path: ctx.path,
21930
21947
  parent: ctx
21931
21948
  });
21932
- if (!isValid(base2))
21933
- return base2;
21934
- const result = effect.transform(base2.value, checkCtx);
21949
+ if (!isValid(base3))
21950
+ return base3;
21951
+ const result = effect.transform(base3.value, checkCtx);
21935
21952
  if (result instanceof Promise) {
21936
21953
  throw new Error(`Asynchronous transform encountered during synchronous parse operation. Use .parseAsync instead.`);
21937
21954
  }
21938
21955
  return { status: status.value, value: result };
21939
21956
  } else {
21940
- return this._def.schema._parseAsync({ data: ctx.data, path: ctx.path, parent: ctx }).then((base2) => {
21941
- if (!isValid(base2))
21942
- return base2;
21943
- return Promise.resolve(effect.transform(base2.value, checkCtx)).then((result) => ({ status: status.value, value: result }));
21957
+ return this._def.schema._parseAsync({ data: ctx.data, path: ctx.path, parent: ctx }).then((base3) => {
21958
+ if (!isValid(base3))
21959
+ return base3;
21960
+ return Promise.resolve(effect.transform(base3.value, checkCtx)).then((result) => ({ status: status.value, value: result }));
21944
21961
  });
21945
21962
  }
21946
21963
  }
@@ -22598,8 +22615,8 @@ var initContract = () => {
22598
22615
  };
22599
22616
  };
22600
22617
  var insertParamsIntoPath = ({ path: path3, params: params3 }) => {
22601
- const pathParams2 = params3;
22602
- return path3.replace(/\/?:([^/?]+)\??/g, (matched, p) => pathParams2[p] ? `${matched.startsWith("/") ? "/" : ""}${pathParams2[p]}` : "");
22618
+ const pathParams3 = params3;
22619
+ return path3.replace(/\/?:([^/?]+)\??/g, (matched, p) => pathParams3[p] ? `${matched.startsWith("/") ? "/" : ""}${pathParams3[p]}` : "");
22603
22620
  };
22604
22621
  var convertQueryParamsToUrlString = (query, json2 = false) => {
22605
22622
  const queryString = json2 ? encodeQueryParamsJson(query) : encodeQueryParams(query);
@@ -23172,9 +23189,16 @@ var AgentSessionTypeValues = [
23172
23189
  "ASSESSMENT",
23173
23190
  "COORDINATOR",
23174
23191
  "REMEDIATION",
23175
- "API_COVERAGE"
23192
+ "API_COVERAGE",
23193
+ "SETUP"
23176
23194
  ];
23177
23195
  var AgentSessionType = external_exports.enum(AgentSessionTypeValues);
23196
+ var SetupTenantSchema = external_exports.object({
23197
+ admin: external_exports.boolean(),
23198
+ member: external_exports.boolean(),
23199
+ apiKey: external_exports.boolean()
23200
+ }).strict();
23201
+ var MAX_SETUP_TENANTS = 4;
23178
23202
  var AgentSessionStatusValues = [
23179
23203
  "LAUNCHING",
23180
23204
  "RUNNING",
@@ -23337,6 +23361,17 @@ var CreateAgentSessionRequestSchema = external_exports.discriminatedUnion("type"
23337
23361
  CreateAgentSessionCommonSchema.extend({
23338
23362
  type: external_exports.literal("API_COVERAGE"),
23339
23363
  focus: external_exports.string().min(1).max(MAX_AGENT_SESSION_FOCUS_CHARS).optional()
23364
+ }),
23365
+ // SETUP: sets up a PoC engagement's application, environment, exclusions and tenant
23366
+ // accounts. `focus` carries the staff notes (which environment, rate limits, what is
23367
+ // off-limits); `tenants` the accounts to aim for; `shareLink` the customer's logins.
23368
+ CreateAgentSessionCommonSchema.extend({
23369
+ type: external_exports.literal("SETUP"),
23370
+ focus: external_exports.string().min(1).max(MAX_AGENT_SESSION_FOCUS_CHARS).optional(),
23371
+ /** Absent = the default target, two tenants of admin + member + API key. */
23372
+ tenants: external_exports.array(SetupTenantSchema).min(1).max(MAX_SETUP_TENANTS).optional(),
23373
+ /** A credential share link. Stored encrypted and never returned by any read route. */
23374
+ shareLink: external_exports.url({ protocol: /^https$/ }).max(2048).optional()
23340
23375
  })
23341
23376
  ]);
23342
23377
  var SendAgentSessionMessageRequestSchema = external_exports.object({
@@ -27462,7 +27497,17 @@ var CustomerPentestFindingRepositorySchema = PentestFindingRepositorySchema.omit
27462
27497
  repositoryId: true
27463
27498
  });
27464
27499
  var CustomerFindingPendingRetestSchema = external_exports.discriminatedUnion("state", [
27465
- external_exports.object({ state: external_exports.literal("QUEUED"), since: external_exports.coerce.date() }),
27500
+ external_exports.object({
27501
+ state: external_exports.literal("QUEUED"),
27502
+ since: external_exports.coerce.date(),
27503
+ /**
27504
+ * Present only while the retest waits on a completed ticket's delay: when
27505
+ * it becomes due. A field on QUEUED rather than its own state because prod
27506
+ * promotes web after the API and web parses these responses, so a new
27507
+ * variant would fail the parse in the web still serving.
27508
+ */
27509
+ startsAt: external_exports.coerce.date().optional()
27510
+ }),
27466
27511
  external_exports.object({ state: external_exports.literal("RUNNING"), since: external_exports.coerce.date() }),
27467
27512
  /**
27468
27513
  * A mobile-only engagement whose finding has no usable build newer than its
@@ -28816,13 +28861,13 @@ var StorageStateSchema = external_exports.object({
28816
28861
  } catch {
28817
28862
  return;
28818
28863
  }
28819
- s.cookies.forEach((c83, i2) => {
28820
- const d = c83.domain.replace(/^\./, "").toLowerCase();
28864
+ s.cookies.forEach((c84, i2) => {
28865
+ const d = c84.domain.replace(/^\./, "").toLowerCase();
28821
28866
  if (d !== host && !host.endsWith(`.${d}`)) {
28822
28867
  ctx.addIssue({
28823
28868
  code: "custom",
28824
28869
  path: ["cookies", i2, "domain"],
28825
- message: `cookie domain "${c83.domain}" is not the capture origin host "${host}" or a parent domain of it`
28870
+ message: `cookie domain "${c84.domain}" is not the capture origin host "${host}" or a parent domain of it`
28826
28871
  });
28827
28872
  }
28828
28873
  });
@@ -30156,7 +30201,27 @@ var AgentSessionCreateTestEmailResponseSchema = external_exports.object({
30156
30201
  */
30157
30202
  reused: external_exports.boolean()
30158
30203
  });
30159
- var AgentSessionSaveTestAccountRequestSchema = external_exports.object({
30204
+ var ProvidedCredentialType = external_exports.enum(CredentialTypeValues).extract(["USERNAME_PASSWORD", "API_KEY", "BEARER_TOKEN"]);
30205
+ var AgentSessionSaveProvidedAccountRequestSchema = external_exports.object({
30206
+ runVersion: external_exports.number().int().nonnegative(),
30207
+ provided: external_exports.literal(true),
30208
+ /**
30209
+ * `customer`: the customer handed it over (a share link). `created`: the agent made it, such as an
30210
+ * API key from an admin account it signed up; it is labelled like any account the agent mints, so
30211
+ * a black-box engagement does not read it as customer-supplied.
30212
+ */
30213
+ source: external_exports.enum(["customer", "created"]),
30214
+ label: external_exports.string().min(1).max(200),
30215
+ credentialType: ProvidedCredentialType,
30216
+ /** USERNAME_PASSWORD only. */
30217
+ username: external_exports.string().min(1).max(320).optional(),
30218
+ /** The password, key or token. */
30219
+ secret: external_exports.string().min(1).max(4096),
30220
+ /** API_KEY only: the header that carries the key. */
30221
+ headerName: external_exports.string().min(1).max(100).optional(),
30222
+ targetUrl: external_exports.string().min(1).max(2048).optional()
30223
+ });
30224
+ var AgentSessionSaveMintedAccountRequestSchema = external_exports.strictObject({
30160
30225
  runVersion: external_exports.number().int().nonnegative(),
30161
30226
  /** The `label` returned by `create_test_email`. */
30162
30227
  label: external_exports.string().min(1).max(200),
@@ -30184,11 +30249,18 @@ var AgentSessionSaveTestAccountRequestSchema = external_exports.object({
30184
30249
  */
30185
30250
  targetUrl: external_exports.string().min(1).max(2048).optional()
30186
30251
  });
30252
+ var AgentSessionSaveTestAccountRequestSchema = external_exports.union([
30253
+ AgentSessionSaveProvidedAccountRequestSchema,
30254
+ AgentSessionSaveMintedAccountRequestSchema
30255
+ ]);
30187
30256
  var AgentSessionSaveTestAccountResponseSchema = external_exports.object({
30188
30257
  /** Echoed so the agent names the account the same way afterwards. */
30189
30258
  label: external_exports.string().max(200),
30190
- /** `USERNAME_PASSWORD` when a password was supplied, else `MAGIC_AUTH`. */
30191
- credentialType: external_exports.enum(["USERNAME_PASSWORD", "MAGIC_AUTH"]),
30259
+ /**
30260
+ * Minted accounts: `USERNAME_PASSWORD` when a password was supplied, else `MAGIC_AUTH`.
30261
+ * Provided logins: the type the agent named.
30262
+ */
30263
+ credentialType: external_exports.enum(["USERNAME_PASSWORD", "MAGIC_AUTH", "API_KEY", "BEARER_TOKEN"]),
30192
30264
  /**
30193
30265
  * The environment the credential landed on. Authoritative, not an echo: it is
30194
30266
  * how the agent learns that its `targetUrl` matched nothing and the server
@@ -30579,7 +30651,7 @@ var ParkAgentSessionRequestSchema = external_exports.object({
30579
30651
  * idle-lingered out (the host's only natural-completion signal); INTERRUPTED =
30580
30652
  * SIGTERM / stream-close / api-outage give-up / wall-clock. Meaningful only when
30581
30653
  * outcome === 'IDLE'. OPTIONAL for rolling-deploy tolerance: an old host omits
30582
- * it, and the api treats absent as non-natural (no VALIDATION auto-advance).
30654
+ * it, and the api treats absent as non-natural (the validation run stays RUNNING).
30583
30655
  */
30584
30656
  idleReason: external_exports.enum(["COMPLETED", "INTERRUPTED"]).optional(),
30585
30657
  waitingReason: AgentSessionWaitingReason.optional(),
@@ -31460,7 +31532,7 @@ var agentSessionInternalContract = c4.router({
31460
31532
  201: RecordValidationResultResponseSchema,
31461
31533
  400: ErrorResponseSchema,
31462
31534
  401: ErrorResponseSchema,
31463
- // runVersion/status guard, or pentest not in VALIDATION_RUNNING.
31535
+ // runVersion/status guard, an ended engagement, or no validation run for the session.
31464
31536
  409: ErrorResponseSchema,
31465
31537
  500: ErrorResponseSchema
31466
31538
  }
@@ -36719,6 +36791,7 @@ var LinearIntegrationStatusValues = [
36719
36791
  "DISCONNECTED"
36720
36792
  ];
36721
36793
  var linearEntityId2 = external_exports.string().min(1).max(64);
36794
+ var LINEAR_RETEST_DELAY_MINUTES_OPTIONS = [0, 60, 180, 720, 1440, 4320];
36722
36795
  var LinearIntegrationSummarySchema = external_exports.object({
36723
36796
  linearIntegrationId: external_exports.uuid(),
36724
36797
  status: external_exports.enum(LinearIntegrationStatusValues),
@@ -36728,6 +36801,12 @@ var LinearIntegrationSummarySchema = external_exports.object({
36728
36801
  defaultTeamId: external_exports.string().nullable(),
36729
36802
  defaultProjectId: external_exports.string().nullable(),
36730
36803
  defaultLabelIds: external_exports.array(external_exports.string()),
36804
+ /**
36805
+ * Optional on the wire only: prod promotes web after the API, so a web bundle
36806
+ * can read an API that predates the field. Consumers default it. Not limited
36807
+ * to the options so retiring one never makes a stored value unparseable.
36808
+ */
36809
+ retestDelayMinutes: external_exports.number().int().nonnegative().optional(),
36731
36810
  connectedAt: external_exports.iso.datetime(),
36732
36811
  connectedByName: external_exports.string().nullable(),
36733
36812
  /** Non-null → disconnect committed but Linear-side revocation didn't confirm; UI shows the confirm-removal banner. */
@@ -36759,6 +36838,9 @@ var UpdateLinearDefaultsRequestSchema = external_exports.object({
36759
36838
  defaultProjectId: linearEntityId2.nullable().optional(),
36760
36839
  defaultLabelIds: external_exports.array(linearEntityId2).max(10).optional()
36761
36840
  });
36841
+ var UpdateLinearRetestDelayRequestSchema = external_exports.object({
36842
+ retestDelayMinutes: external_exports.literal(LINEAR_RETEST_DELAY_MINUTES_OPTIONS)
36843
+ });
36762
36844
  var LinearTeamSchema = external_exports.object({
36763
36845
  id: external_exports.string(),
36764
36846
  name: external_exports.string(),
@@ -36845,6 +36927,27 @@ var linearIntegrationContract = c26.router({
36845
36927
  503: ErrorResponseSchema
36846
36928
  }
36847
36929
  },
36930
+ /**
36931
+ * Separate from the defaults PATCH, which revalidates the saved team against
36932
+ * Linear on every save: this setting is ours alone and must save while Linear
36933
+ * is unreachable or the team was archived.
36934
+ */
36935
+ updateLinearRetestDelay: {
36936
+ method: "PATCH",
36937
+ path: "/organizations/:organizationId/integrations/linear/retest-delay",
36938
+ pathParams: OrgIdParamsSchema,
36939
+ body: UpdateLinearRetestDelayRequestSchema,
36940
+ responses: {
36941
+ 200: LinearIntegrationResponseSchema,
36942
+ 400: ErrorResponseSchema,
36943
+ 401: ErrorResponseSchema,
36944
+ 403: ErrorResponseSchema,
36945
+ 404: ErrorResponseSchema,
36946
+ 409: ErrorResponseSchema,
36947
+ 500: ErrorResponseSchema,
36948
+ 503: ErrorResponseSchema
36949
+ }
36950
+ },
36848
36951
  getLinearMetadata: {
36849
36952
  method: "GET",
36850
36953
  path: "/organizations/:organizationId/integrations/linear/metadata",
@@ -41541,6 +41644,23 @@ var pentestFindingContract = c45.router({
41541
41644
  500: ErrorResponseSchema
41542
41645
  }
41543
41646
  },
41647
+ // Customer says the fix is already live for a finding whose retest waits on its
41648
+ // ticket's delay: the retest becomes due now. No status change.
41649
+ startScheduledRetest: {
41650
+ method: "POST",
41651
+ path: "/organizations/:organizationId/customer-pentests/:pentestId/findings/:pentestFindingId/start-retest",
41652
+ pathParams: CustomerPentestFindingIdParamsSchema,
41653
+ body: c45.noBody(),
41654
+ responses: {
41655
+ 200: CustomerPentestFindingResponseSchema,
41656
+ 401: ErrorResponseSchema,
41657
+ 403: ErrorResponseSchema,
41658
+ 404: ErrorResponseSchema,
41659
+ // RETEST_NOT_SCHEDULED: not REMEDIATED with a retest still waiting.
41660
+ 409: ErrorResponseSchema,
41661
+ 500: ErrorResponseSchema
41662
+ }
41663
+ },
41544
41664
  // Customer accepts the finding → ACCEPTED_WITH_CONTROLS without a dispute or a
41545
41665
  // staff decision. Human principals here; an API key sends the same transition
41546
41666
  // through `automationStatusUpdate` with status ACCEPTED_WITH_CONTROLS.
@@ -41935,6 +42055,57 @@ var PentestIntakeDocumentTypeValues = [
41935
42055
  "OTHER"
41936
42056
  ];
41937
42057
  var PentestIntakeDocumentTypeSchema = external_exports.enum(PentestIntakeDocumentTypeValues);
42058
+ var PENTEST_INTAKE_DOCUMENTS_QUESTION = "Do you have any relevant documents to share?";
42059
+ var PENTEST_INTAKE_DOCUMENT_TYPE_LABELS = {
42060
+ PREVIOUS_PENTEST: "Previous pentest report",
42061
+ ARCHITECTURE_DIAGRAM: "Architecture diagram",
42062
+ THREAT_MODEL: "Threat model",
42063
+ OTHER: "Other"
42064
+ };
42065
+ function guessPentestIntakeDocumentType(fileName) {
42066
+ const name = fileName.toLowerCase();
42067
+ if (/threat[\s_-]*model|stride|attack[\s_-]*tree|dfd/.test(name)) return "THREAT_MODEL";
42068
+ if (/pen[\s_-]*test|pentest|penetration|security[\s_-]*assessment|vapt|findings/.test(name)) {
42069
+ return "PREVIOUS_PENTEST";
42070
+ }
42071
+ if (/arch|diagram|topology|infra|system[\s_-]*design|c4|\.drawio/.test(name)) {
42072
+ return "ARCHITECTURE_DIAGRAM";
42073
+ }
42074
+ return "OTHER";
42075
+ }
42076
+ var INTAKE_DOCUMENT_MIME_BY_EXTENSION = {
42077
+ pdf: "application/pdf",
42078
+ doc: "application/msword",
42079
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
42080
+ xls: "application/vnd.ms-excel",
42081
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
42082
+ ppt: "application/vnd.ms-powerpoint",
42083
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
42084
+ txt: "text/plain",
42085
+ md: "text/markdown",
42086
+ csv: "text/csv",
42087
+ json: "application/json",
42088
+ xml: "application/xml",
42089
+ // text/yaml rather than application/yaml: readers that only accept text/* still take it.
42090
+ yaml: "text/yaml",
42091
+ yml: "text/yaml",
42092
+ html: "text/html",
42093
+ htm: "text/html",
42094
+ png: "image/png",
42095
+ jpg: "image/jpeg",
42096
+ jpeg: "image/jpeg",
42097
+ gif: "image/gif",
42098
+ svg: "image/svg+xml",
42099
+ webp: "image/webp",
42100
+ zip: "application/zip",
42101
+ drawio: "application/xml",
42102
+ vsdx: "application/vnd.ms-visio.drawing"
42103
+ };
42104
+ function pentestIntakeDocumentMimeType(fileName) {
42105
+ const dot = fileName.lastIndexOf(".");
42106
+ const extension = dot === -1 ? "" : fileName.slice(dot + 1).toLowerCase();
42107
+ return INTAKE_DOCUMENT_MIME_BY_EXTENSION[extension] ?? "application/octet-stream";
42108
+ }
41938
42109
  var MAX_PENTEST_INTAKE_DOCUMENT_BYTES = 50 * 1024 * 1024;
41939
42110
  var PentestIntakeDocumentIdParamsSchema = CustomerPentestIdParamsSchema.extend({
41940
42111
  documentId: external_exports.uuid()
@@ -46227,16 +46398,16 @@ var COVERAGE_CLASSES = [
46227
46398
  },
46228
46399
  { taxonomy: "RECON", id: "RECON-15", name: "Deployment-platform config", shortLabel: "Deploy" }
46229
46400
  ];
46230
- var BY_ID = new Map(COVERAGE_CLASSES.map((c83) => [c83.id, c83]));
46401
+ var BY_ID = new Map(COVERAGE_CLASSES.map((c84) => [c84.id, c84]));
46231
46402
  function idsWhere(keep) {
46232
- return [OTHER_CLASS_ID, ...COVERAGE_CLASSES.filter(keep).map((c83) => c83.id)];
46403
+ return [OTHER_CLASS_ID, ...COVERAGE_CLASSES.filter(keep).map((c84) => c84.id)];
46233
46404
  }
46234
- var HUNTING_CLASS_IDS = idsWhere((c83) => c83.taxonomy !== "RECON");
46235
- var RECON_CLASS_IDS = idsWhere((c83) => c83.taxonomy === "RECON");
46405
+ var HUNTING_CLASS_IDS = idsWhere((c84) => c84.taxonomy !== "RECON");
46406
+ var RECON_CLASS_IDS = idsWhere((c84) => c84.taxonomy === "RECON");
46236
46407
  var HUNTING_ID_SET = new Set(HUNTING_CLASS_IDS);
46237
46408
  var RECON_ID_SET = new Set(RECON_CLASS_IDS);
46238
46409
  var TAXONOMY_ORDER = new Map(CoverageTaxonomyValues.map((t2, i2) => [t2, i2]));
46239
- var CLASS_ORDER = new Map(COVERAGE_CLASSES.map((c83, i2) => [c83.id, i2]));
46410
+ var CLASS_ORDER = new Map(COVERAGE_CLASSES.map((c84, i2) => [c84.id, i2]));
46240
46411
 
46241
46412
  // ../../packages/api-contracts/src/contracts/v1/api-coverage-internal.ts
46242
46413
  var c80 = initContract();
@@ -46309,6 +46480,135 @@ var apiCoverageInternalContract = c80.router({
46309
46480
  }
46310
46481
  });
46311
46482
 
46483
+ // ../../packages/api-contracts/src/schemas/api/v1/setup-session-internal.api.ts
46484
+ var RunVersionSchema2 = external_exports.number().int().nonnegative();
46485
+ var SetupTargetType = external_exports.enum(ApplicationTypeValues).extract(["WEB", "API_SERVICE"]);
46486
+ var ReasonSchema = external_exports.string().min(1).max(500);
46487
+ var SetupVerificationStatusValues = [
46488
+ "UNVERIFIED",
46489
+ "PENDING",
46490
+ "VERIFIED",
46491
+ "FAILED",
46492
+ "INCONCLUSIVE"
46493
+ ];
46494
+ var SetupVerificationStatus = external_exports.enum(SetupVerificationStatusValues);
46495
+ var SetupAddTargetRequestSchema = external_exports.object({
46496
+ runVersion: RunVersionSchema2,
46497
+ reason: ReasonSchema,
46498
+ /** The application's name in Trace. An existing org application with this name is reused. */
46499
+ name: external_exports.string().min(1).max(200),
46500
+ type: SetupTargetType,
46501
+ /** The environment's login or base URL. */
46502
+ url: external_exports.url({ protocol: /^https?$/ }).max(2048),
46503
+ environmentName: external_exports.string().min(1).max(100),
46504
+ isProduction: external_exports.boolean()
46505
+ });
46506
+ var SetupAddTargetResponseSchema = external_exports.object({
46507
+ applicationName: external_exports.string(),
46508
+ environmentName: external_exports.string(),
46509
+ created: external_exports.object({
46510
+ application: external_exports.boolean(),
46511
+ environment: external_exports.boolean(),
46512
+ binding: external_exports.boolean(),
46513
+ selection: external_exports.boolean()
46514
+ }),
46515
+ /** The URL's registrable domain differs from the org website's: list it in the summary. */
46516
+ offHomepageDomain: external_exports.boolean()
46517
+ });
46518
+ var SetupAddExclusionRequestSchema = external_exports.object({
46519
+ runVersion: RunVersionSchema2,
46520
+ reason: ReasonSchema,
46521
+ expression: external_exports.string().min(1).max(500)
46522
+ });
46523
+ var SetupAddExclusionResponseSchema = external_exports.object({
46524
+ /** false when the same exclusion was already on the engagement. */
46525
+ created: external_exports.boolean()
46526
+ });
46527
+ var SetupUpdateEngagementRequestSchema = external_exports.object({
46528
+ runVersion: RunVersionSchema2,
46529
+ reason: ReasonSchema,
46530
+ testType: PentestTestType.optional(),
46531
+ // The same ranges the staff engagement form enforces: a rate outside them turns API coverage's
46532
+ // sender off at launch. There is no description field: it is shown to the customer. Not
46533
+ // nullable: setup only adds, so it can set a limit but never clear one.
46534
+ requestRatePerSec: external_exports.number().min(REQUEST_RATE_PER_SEC_RANGE.min).max(REQUEST_RATE_PER_SEC_RANGE.max).optional(),
46535
+ requestBurst: external_exports.number().int().min(REQUEST_BURST_RANGE.min).max(REQUEST_BURST_RANGE.max).optional()
46536
+ });
46537
+ var SetupVerifyRequestSchema = external_exports.object({
46538
+ runVersion: RunVersionSchema2,
46539
+ action: external_exports.enum(["start", "status"])
46540
+ });
46541
+ var SetupVerifyResponseSchema = external_exports.object({
46542
+ applications: external_exports.array(
46543
+ external_exports.object({
46544
+ applicationName: external_exports.string(),
46545
+ /** What `start` did for this application; null on `status`. */
46546
+ launch: external_exports.enum(["STARTED", "IN_FLIGHT", "NOTHING_TO_CHECK"]).nullable(),
46547
+ environments: external_exports.array(external_exports.object({ name: external_exports.string(), status: SetupVerificationStatus })),
46548
+ credentials: external_exports.array(
46549
+ external_exports.object({
46550
+ label: external_exports.string(),
46551
+ environmentName: external_exports.string(),
46552
+ status: SetupVerificationStatus
46553
+ })
46554
+ )
46555
+ })
46556
+ )
46557
+ });
46558
+ var SetupShareLinkRequestSchema = external_exports.object({ runVersion: RunVersionSchema2 });
46559
+ var SetupShareLinkResponseSchema = external_exports.object({ shareLink: external_exports.string().nullable() });
46560
+
46561
+ // ../../packages/api-contracts/src/contracts/v1/setup-session-internal.ts
46562
+ var c81 = initContract();
46563
+ var pathParams2 = external_exports.strictObject({ agentSessionId: external_exports.uuid() });
46564
+ var errors6 = {
46565
+ 400: ErrorResponseSchema,
46566
+ 401: ErrorResponseSchema,
46567
+ 403: ErrorResponseSchema,
46568
+ 404: ErrorResponseSchema,
46569
+ 409: ErrorResponseSchema,
46570
+ 500: ErrorResponseSchema,
46571
+ 503: ErrorResponseSchema
46572
+ };
46573
+ var base2 = "/internal/v1/agent-sessions/:agentSessionId/setup";
46574
+ var setupSessionInternalContract = c81.router({
46575
+ addSetupTarget: {
46576
+ method: "POST",
46577
+ path: `${base2}/target`,
46578
+ pathParams: pathParams2,
46579
+ body: SetupAddTargetRequestSchema,
46580
+ responses: { 200: SetupAddTargetResponseSchema, ...errors6 }
46581
+ },
46582
+ addSetupExclusion: {
46583
+ method: "POST",
46584
+ path: `${base2}/exclusion`,
46585
+ pathParams: pathParams2,
46586
+ body: SetupAddExclusionRequestSchema,
46587
+ responses: { 200: SetupAddExclusionResponseSchema, ...errors6 }
46588
+ },
46589
+ updateSetupEngagement: {
46590
+ method: "POST",
46591
+ path: `${base2}/engagement`,
46592
+ pathParams: pathParams2,
46593
+ body: SetupUpdateEngagementRequestSchema,
46594
+ responses: { 200: OperatorPentestContextSchema, ...errors6 }
46595
+ },
46596
+ verifySetup: {
46597
+ method: "POST",
46598
+ path: `${base2}/verify`,
46599
+ pathParams: pathParams2,
46600
+ body: SetupVerifyRequestSchema,
46601
+ responses: { 200: SetupVerifyResponseSchema, ...errors6 }
46602
+ },
46603
+ getSetupShareLink: {
46604
+ method: "POST",
46605
+ path: `${base2}/share-link`,
46606
+ pathParams: pathParams2,
46607
+ body: SetupShareLinkRequestSchema,
46608
+ responses: { 200: SetupShareLinkResponseSchema, ...errors6 }
46609
+ }
46610
+ });
46611
+
46312
46612
  // ../../packages/api-contracts/src/schemas/api/v1/premise-check.api.ts
46313
46613
  var PREMISE_VERDICTS = ["supported", "contradicted", "cant_tell"];
46314
46614
  var EXTRACTED_PREMISE_KINDS = [
@@ -46398,8 +46698,8 @@ var ListReviewPremiseChecksResponseSchema = external_exports.object({
46398
46698
  });
46399
46699
 
46400
46700
  // ../../packages/api-contracts/src/contracts/v1/premise-check-internal.ts
46401
- var c81 = initContract();
46402
- var premiseCheckInternalContract = c81.router({
46701
+ var c82 = initContract();
46702
+ var premiseCheckInternalContract = c82.router({
46403
46703
  listReviewPremiseChecks: {
46404
46704
  method: "POST",
46405
46705
  path: "/internal/v1/agent-sessions/:agentSessionId/premise-checks/list",
@@ -48261,11 +48561,11 @@ function buildCloneTokenFailureWarning(cloneUrl, failure) {
48261
48561
  return `Trace returned an unexpected response while generating clone credentials for ${cloneUrl}${quotedMessage ? ` ${quotedMessage}` : ""}. ${fallbackSuffix}`;
48262
48562
  }
48263
48563
  }
48264
- function withOptionalMessage(base2, message) {
48564
+ function withOptionalMessage(base3, message) {
48265
48565
  if (typeof message === "string") {
48266
- return { ...base2, message };
48566
+ return { ...base3, message };
48267
48567
  }
48268
- return base2;
48568
+ return base3;
48269
48569
  }
48270
48570
  async function listOrganizationsSafe() {
48271
48571
  const result = await authenticatedRequestSafe(
@@ -49083,6 +49383,13 @@ function parseOutput(value) {
49083
49383
  console.error(colorError(`Invalid --output '${value}'. Use table, json, or tsv.`));
49084
49384
  process.exit(1);
49085
49385
  }
49386
+ function parseTextOutput(value) {
49387
+ if (value === "text" || value === "json") {
49388
+ return value;
49389
+ }
49390
+ console.error(colorError(`Invalid --output '${sanitizeDisplay(value)}'. Use text or json.`));
49391
+ process.exit(1);
49392
+ }
49086
49393
  var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
49087
49394
  function isUuid(value) {
49088
49395
  return UUID_PATTERN.test(value);
@@ -49316,12 +49623,12 @@ function buildApplicationsTable(apps) {
49316
49623
  repos: String(a2.repositories.length),
49317
49624
  url: a2.applicationUrl ? sanitizeDisplay(a2.applicationUrl) : "\u2014"
49318
49625
  }));
49319
- const idWidth = Math.max(2, ...cells.map((c83) => c83.id.length));
49320
- const nameWidth = Math.max(4, ...cells.map((c83) => c83.name.length));
49321
- const typeWidth = Math.max(4, ...cells.map((c83) => c83.type.length));
49626
+ const idWidth = Math.max(2, ...cells.map((c84) => c84.id.length));
49627
+ const nameWidth = Math.max(4, ...cells.map((c84) => c84.name.length));
49628
+ const typeWidth = Math.max(4, ...cells.map((c84) => c84.type.length));
49322
49629
  const header = `${"ID".padEnd(idWidth)} ${"NAME".padEnd(nameWidth)} ${"TYPE".padEnd(typeWidth)} REPOS URL`;
49323
49630
  const rows = cells.map(
49324
- (c83) => `${c83.id.padEnd(idWidth)} ${c83.name.padEnd(nameWidth)} ${c83.type.padEnd(typeWidth)} ${c83.repos.padStart(5)} ${c83.url}`
49631
+ (c84) => `${c84.id.padEnd(idWidth)} ${c84.name.padEnd(nameWidth)} ${c84.type.padEnd(typeWidth)} ${c84.repos.padStart(5)} ${c84.url}`
49325
49632
  );
49326
49633
  return { header, rows };
49327
49634
  }
@@ -49341,12 +49648,12 @@ function buildApplicationRepositoriesTable(links) {
49341
49648
  path: l2.repositoryPath ? sanitizeDisplay(l2.repositoryPath) : "\u2014",
49342
49649
  status: formatStatus(l2.repositoryStatus)
49343
49650
  }));
49344
- const nameWidth = Math.max(10, ...cells.map((c83) => c83.name.length));
49345
- const roleWidth = Math.max(4, ...cells.map((c83) => c83.role.length));
49346
- const pathWidth = Math.max(4, ...cells.map((c83) => c83.path.length));
49651
+ const nameWidth = Math.max(10, ...cells.map((c84) => c84.name.length));
49652
+ const roleWidth = Math.max(4, ...cells.map((c84) => c84.role.length));
49653
+ const pathWidth = Math.max(4, ...cells.map((c84) => c84.path.length));
49347
49654
  const header = `${"REPOSITORY".padEnd(nameWidth)} SCAN ${"ROLE".padEnd(roleWidth)} ${"PATH".padEnd(pathWidth)} STATUS`;
49348
49655
  const rows = cells.map(
49349
- (c83) => `${c83.name.padEnd(nameWidth)} ${c83.scan.padEnd(4)} ${c83.role.padEnd(roleWidth)} ${c83.path.padEnd(pathWidth)} ${c83.status}`
49656
+ (c84) => `${c84.name.padEnd(nameWidth)} ${c84.scan.padEnd(4)} ${c84.role.padEnd(roleWidth)} ${c84.path.padEnd(pathWidth)} ${c84.status}`
49350
49657
  );
49351
49658
  return { header, rows };
49352
49659
  }
@@ -50131,8 +50438,8 @@ function wrapTextWithPrefix(e, r2, o2, t2 = o2, s = o2, n3) {
50131
50438
  hard: true,
50132
50439
  trim: false
50133
50440
  }).split(`
50134
- `).map((c83, i2, m2) => {
50135
- const d = n3 ? n3(c83, i2) : c83;
50441
+ `).map((c84, i2, m2) => {
50442
+ const d = n3 ? n3(c84, i2) : c84;
50136
50443
  return i2 === 0 ? `${t2}${d}` : i2 === m2.length - 1 ? `${s}${d}` : `${o2}${d}`;
50137
50444
  }).join(`
50138
50445
  `);
@@ -50630,14 +50937,14 @@ var limitOptions = ({
50630
50937
  Math.min(l2 - a2 + 3, e.length - a2),
50631
50938
  0
50632
50939
  ));
50633
- let d = a2 < e.length && f > 0, c83 = a2 < e.length && f + a2 < e.length;
50940
+ let d = a2 < e.length && f > 0, c84 = a2 < e.length && f + a2 < e.length;
50634
50941
  const W = Math.min(
50635
50942
  f + a2,
50636
50943
  e.length
50637
50944
  ), s = [];
50638
50945
  let g = 0;
50639
- d && g++, c83 && g++;
50640
- const T = f + (d ? 1 : 0), y = W - (c83 ? 1 : 0);
50946
+ d && g++, c84 && g++;
50947
+ const T = f + (d ? 1 : 0), y = W - (c84 ? 1 : 0);
50641
50948
  for (let t2 = T; t2 < y; t2++) {
50642
50949
  const n3 = e[t2], o2 = n3 ? w(n3, t2 === l2) : "", h2 = wrapAnsi(o2, i2, {
50643
50950
  hard: true,
@@ -50658,14 +50965,14 @@ var limitOptions = ({
50658
50965
  u4,
50659
50966
  true
50660
50967
  );
50661
- d ? ({ lineCount: o2, removals: t2 } = L(), o2 > u4 && (c83 || (u4 -= 1), { lineCount: o2, removals: n3 } = E())) : (c83 || (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 && (c83 = true, s.splice(s.length - n3, n3));
50968
+ d ? ({ lineCount: o2, removals: t2 } = L(), o2 > u4 && (c84 || (u4 -= 1), { lineCount: o2, removals: n3 } = E())) : (c84 || (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 && (c84 = true, s.splice(s.length - n3, n3));
50662
50969
  }
50663
50970
  const x = [];
50664
50971
  d && x.push(M);
50665
50972
  for (const t2 of s)
50666
50973
  for (const n3 of t2)
50667
50974
  x.push(n3);
50668
- return c83 && x.push(M), x;
50975
+ return c84 && x.push(M), x;
50669
50976
  };
50670
50977
  var confirm = (i2) => {
50671
50978
  const a2 = i2.active ?? "Yes", s = i2.inactive ?? "No";
@@ -50684,15 +50991,15 @@ var confirm = (i2) => {
50684
50991
  u4
50685
50992
  ), o2 = `${e ? `${styleText2("gray", S_BAR)}
50686
50993
  ` : ""}${f}
50687
- `, c83 = this.value ? a2 : s;
50994
+ `, c84 = this.value ? a2 : s;
50688
50995
  switch (this.state) {
50689
50996
  case "submit": {
50690
50997
  const r2 = e ? `${styleText2("gray", S_BAR)} ` : "";
50691
- return `${o2}${r2}${styleText2("dim", c83)}`;
50998
+ return `${o2}${r2}${styleText2("dim", c84)}`;
50692
50999
  }
50693
51000
  case "cancel": {
50694
51001
  const r2 = e ? `${styleText2("gray", S_BAR)} ` : "";
50695
- return `${o2}${r2}${styleText2(["strikethrough", "dim"], c83)}${e ? `
51002
+ return `${o2}${r2}${styleText2(["strikethrough", "dim"], c84)}${e ? `
50696
51003
  ${styleText2("gray", S_BAR)}` : ""}`;
50697
51004
  }
50698
51005
  default: {
@@ -50840,13 +51147,13 @@ var multiline = (e) => new h({
50840
51147
  `, m2 = e.placeholder && e.placeholder.length > 0 ? (
50841
51148
  // biome-ignore lint/style/noNonNullAssertion: guarded by placeholder.length > 0
50842
51149
  styleText2("inverse", e.placeholder[0]) + styleText2("dim", e.placeholder.slice(1))
50843
- ) : styleText2(["inverse", "hidden"], "_"), a2 = this.userInput ? this.userInputWithCursor : m2, l2 = this.value ?? "", c83 = e.showSubmit ? `
51150
+ ) : styleText2(["inverse", "hidden"], "_"), a2 = this.userInput ? this.userInputWithCursor : m2, l2 = this.value ?? "", c84 = e.showSubmit ? `
50844
51151
  ${styleText2(this.focused === "submit" ? "cyan" : "dim", "[ submit ]")}` : "";
50845
51152
  switch (this.state) {
50846
51153
  case "error": {
50847
51154
  const n3 = `${styleText2("yellow", S_BAR)} `, r2 = i2 ? wrapTextWithPrefix(e.output, a2, n3, void 0) : a2, u4 = styleText2("yellow", S_BAR_END);
50848
51155
  return `${o2}${r2}
50849
- ${u4} ${styleText2("yellow", this.error)}${c83}
51156
+ ${u4} ${styleText2("yellow", this.error)}${c84}
50850
51157
  `;
50851
51158
  }
50852
51159
  case "submit": {
@@ -50874,7 +51181,7 @@ ${u4} ${styleText2("yellow", this.error)}${c83}
50874
51181
  default: {
50875
51182
  const n3 = i2 ? `${styleText2("cyan", S_BAR)} ` : "", r2 = i2 ? styleText2("cyan", S_BAR_END) : "", u4 = i2 ? wrapTextWithPrefix(e.output, a2, n3) : a2;
50876
51183
  return `${o2}${u4}
50877
- ${r2}${c83}
51184
+ ${r2}${c84}
50878
51185
  `;
50879
51186
  }
50880
51187
  }
@@ -50889,7 +51196,7 @@ var password = (r2) => new u$1({
50889
51196
  render() {
50890
51197
  const e = r2.withGuide ?? settings.withGuide, o2 = `${e ? `${styleText2("gray", S_BAR)}
50891
51198
  ` : ""}${symbol2(this.state)} ${r2.message}
50892
- `, c83 = this.userInputWithCursor, i2 = this.masked;
51199
+ `, c84 = this.userInputWithCursor, i2 = this.masked;
50893
51200
  switch (this.state) {
50894
51201
  case "error": {
50895
51202
  const s = e ? `${styleText2("yellow", S_BAR)} ` : "", n3 = e ? `${styleText2("yellow", S_BAR_END)} ` : "", l2 = i2 ?? "";
@@ -50909,7 +51216,7 @@ ${styleText2("gray", S_BAR)}` : ""}`;
50909
51216
  }
50910
51217
  default: {
50911
51218
  const s = e ? `${styleText2("cyan", S_BAR)} ` : "", n3 = e ? styleText2("cyan", S_BAR_END) : "";
50912
- return `${o2}${s}${c83}
51219
+ return `${o2}${s}${c84}
50913
51220
  ${n3}
50914
51221
  `;
50915
51222
  }
@@ -50925,7 +51232,7 @@ var SELECT_INSTRUCTIONS = [
50925
51232
  `${styleText2("dim", "\u2191/\u2193")} to navigate`,
50926
51233
  `${styleText2("dim", "Enter:")} confirm`
50927
51234
  ];
50928
- var c82 = (t2, o2) => t2.includes(`
51235
+ var c83 = (t2, o2) => t2.includes(`
50929
51236
  `) ? t2.split(`
50930
51237
  `).map((d) => o2(d)).join(`
50931
51238
  `) : o2(t2);
@@ -50936,15 +51243,15 @@ var select = (t2) => {
50936
51243
  const s = n3.label ?? String(n3.value);
50937
51244
  switch (m2) {
50938
51245
  case "disabled":
50939
- return `${styleText2("gray", S_RADIO_INACTIVE)} ${c82(s, (i2) => styleText2("gray", i2))}${n3.hint ? ` ${styleText2("dim", `(${n3.hint ?? "disabled"})`)}` : ""}`;
51246
+ return `${styleText2("gray", S_RADIO_INACTIVE)} ${c83(s, (i2) => styleText2("gray", i2))}${n3.hint ? ` ${styleText2("dim", `(${n3.hint ?? "disabled"})`)}` : ""}`;
50940
51247
  case "selected":
50941
- return `${c82(s, (i2) => styleText2("dim", i2))}`;
51248
+ return `${c83(s, (i2) => styleText2("dim", i2))}`;
50942
51249
  case "active":
50943
51250
  return `${styleText2("green", S_RADIO_ACTIVE)} ${s}${n3.hint ? ` ${styleText2("dim", `(${n3.hint})`)}` : ""}`;
50944
51251
  case "cancelled":
50945
- return `${c82(s, (i2) => styleText2(["strikethrough", "dim"], i2))}`;
51252
+ return `${c83(s, (i2) => styleText2(["strikethrough", "dim"], i2))}`;
50946
51253
  default:
50947
- return `${styleText2("dim", S_RADIO_INACTIVE)} ${c82(s, (i2) => styleText2("dim", i2))}`;
51254
+ return `${styleText2("dim", S_RADIO_INACTIVE)} ${c83(s, (i2) => styleText2("dim", i2))}`;
50948
51255
  }
50949
51256
  }, d = t2.showInstructions ?? true;
50950
51257
  return new n$1({
@@ -51817,7 +52124,7 @@ function buildAuthStatusInfo(credential, refreshToken, selectedOrganizationId) {
51817
52124
  };
51818
52125
  }
51819
52126
  const token = credential.token;
51820
- const base2 = {
52127
+ const base3 = {
51821
52128
  loggedIn: true,
51822
52129
  authMethod: "user",
51823
52130
  expired: false,
@@ -51828,20 +52135,20 @@ function buildAuthStatusInfo(credential, refreshToken, selectedOrganizationId) {
51828
52135
  const parts = token.split(".");
51829
52136
  const payload = parts[1];
51830
52137
  if (!payload) {
51831
- return base2;
52138
+ return base3;
51832
52139
  }
51833
52140
  const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf-8"));
51834
52141
  const expiry = decoded.exp ? new Date(decoded.exp * 1e3) : null;
51835
52142
  const isExpired = expiry ? expiry < /* @__PURE__ */ new Date() : false;
51836
52143
  return {
51837
- ...base2,
52144
+ ...base3,
51838
52145
  ...decoded.sub ? { userId: decoded.sub } : {},
51839
52146
  ...decoded.org_id ? { orgId: decoded.org_id } : {},
51840
52147
  ...expiry ? { expires: expiry.toISOString() } : {},
51841
52148
  expired: isExpired
51842
52149
  };
51843
52150
  } catch {
51844
- return base2;
52151
+ return base3;
51845
52152
  }
51846
52153
  }
51847
52154
  function createStatusCommand() {
@@ -52441,7 +52748,7 @@ function extractCommandTree(cmd) {
52441
52748
  description: o2.description,
52442
52749
  takesValue: o2.required || o2.optional
52443
52750
  }));
52444
- const subcommands = cmd.commands.filter((c83) => !c83._hidden).map((c83) => extractCommandTree(c83));
52751
+ const subcommands = cmd.commands.filter((c84) => !c84._hidden).map((c84) => extractCommandTree(c84));
52445
52752
  return {
52446
52753
  name: cmd.name(),
52447
52754
  description: cmd.description(),
@@ -52507,7 +52814,7 @@ function generateBashCompletion(tree, binName) {
52507
52814
  }
52508
52815
  function collectBashCases(tree, prefix) {
52509
52816
  const cases = /* @__PURE__ */ new Map();
52510
- const subcommandNames = tree.subcommands.map((c83) => c83.name);
52817
+ const subcommandNames = tree.subcommands.map((c84) => c84.name);
52511
52818
  const optionNames = tree.options.flatMap((o2) => o2.short ? [o2.long, o2.short] : [o2.long]);
52512
52819
  const extraOptions = prefix === "" ? ["--help", "--version"] : ["--help"];
52513
52820
  const allOptions = [.../* @__PURE__ */ new Set([...optionNames, ...extraOptions])];
@@ -52922,9 +53229,9 @@ function buildInboxesTable(inboxes) {
52922
53229
  address: sanitizeDisplay(inbox.address),
52923
53230
  label: sanitizeDisplay(inbox.label)
52924
53231
  }));
52925
- const addressWidth = Math.max(7, ...cells.map((c83) => c83.address.length));
53232
+ const addressWidth = Math.max(7, ...cells.map((c84) => c84.address.length));
52926
53233
  const header = `${"ADDRESS".padEnd(addressWidth)} LABEL`;
52927
- const rows = cells.map((c83) => `${c83.address.padEnd(addressWidth)} ${c83.label}`);
53234
+ const rows = cells.map((c84) => `${c84.address.padEnd(addressWidth)} ${c84.label}`);
52928
53235
  return { header, rows };
52929
53236
  }
52930
53237
  function formatInboxQuota(quota) {
@@ -52948,14 +53255,14 @@ function buildInboxEmailsTable(messages) {
52948
53255
  subject: sanitizeDisplay(message.subject ?? ""),
52949
53256
  unread: message.unread ? "yes" : "no"
52950
53257
  }));
52951
- const idWidth = Math.max(2, ...cells.map((c83) => c83.id.length));
52952
- const receivedWidth = Math.max(8, ...cells.map((c83) => c83.received.length));
52953
- const fromWidth = Math.max(4, ...cells.map((c83) => c83.from.length));
52954
- const toWidth = Math.max(2, ...cells.map((c83) => c83.to.length));
52955
- const subjectWidth = Math.max(7, ...cells.map((c83) => c83.subject.length));
53258
+ const idWidth = Math.max(2, ...cells.map((c84) => c84.id.length));
53259
+ const receivedWidth = Math.max(8, ...cells.map((c84) => c84.received.length));
53260
+ const fromWidth = Math.max(4, ...cells.map((c84) => c84.from.length));
53261
+ const toWidth = Math.max(2, ...cells.map((c84) => c84.to.length));
53262
+ const subjectWidth = Math.max(7, ...cells.map((c84) => c84.subject.length));
52956
53263
  const header = `${"ID".padEnd(idWidth)} ${"RECEIVED".padEnd(receivedWidth)} ${"FROM".padEnd(fromWidth)} ${"TO".padEnd(toWidth)} ${"SUBJECT".padEnd(subjectWidth)} UNREAD`;
52957
53264
  const rows = cells.map(
52958
- (c83) => `${c83.id.padEnd(idWidth)} ${c83.received.padEnd(receivedWidth)} ${c83.from.padEnd(fromWidth)} ${c83.to.padEnd(toWidth)} ${c83.subject.padEnd(subjectWidth)} ${c83.unread}`
53265
+ (c84) => `${c84.id.padEnd(idWidth)} ${c84.received.padEnd(receivedWidth)} ${c84.from.padEnd(fromWidth)} ${c84.to.padEnd(toWidth)} ${c84.subject.padEnd(subjectWidth)} ${c84.unread}`
52959
53266
  );
52960
53267
  return { header, rows };
52961
53268
  }
@@ -53698,6 +54005,13 @@ function mfaDisplay(mfa) {
53698
54005
  if (mfa.algorithm !== "SHA1") parts.push(mfa.algorithm);
53699
54006
  return parts.join(" \xB7 ");
53700
54007
  }
54008
+ function sourceDisplay(place) {
54009
+ return `${sanitizeDisplay(place.applicationName)} / ${sanitizeDisplay(place.environmentName)}`;
54010
+ }
54011
+ function linkedIntoDisplay(linkedInto) {
54012
+ if (!linkedInto || linkedInto.length === 0) return "(none)";
54013
+ return linkedInto.map(sourceDisplay).join(", ");
54014
+ }
53701
54015
  function renderCredentialDetail(cred) {
53702
54016
  const pad = 13;
53703
54017
  return [
@@ -53707,6 +54021,7 @@ function renderCredentialDetail(cred) {
53707
54021
  `${"Environment:".padEnd(pad)} ${cred.environmentId}`,
53708
54022
  `${"Auth config:".padEnd(pad)} ${authConfigDisplay(cred.authConfig)}`,
53709
54023
  `${"MFA:".padEnd(pad)} ${mfaDisplay(cred.mfa)}`,
54024
+ `${"Linked into:".padEnd(pad)} ${linkedIntoDisplay(cred.linkedInto)}`,
53710
54025
  `${"Created:".padEnd(pad)} ${formatDate(cred.createdAt)}`
53711
54026
  ].join("\n");
53712
54027
  }
@@ -53730,16 +54045,16 @@ function printCredentialsTsv(creds) {
53730
54045
  }
53731
54046
  }
53732
54047
  function buildCredentialsTable(creds) {
53733
- const cells = creds.map((c83) => ({
53734
- id: c83.credentialId,
53735
- label: sanitizeDisplay(c83.label),
53736
- type: c83.credentialType
54048
+ const cells = creds.map((c84) => ({
54049
+ id: c84.credentialId,
54050
+ label: sanitizeDisplay(c84.label),
54051
+ type: c84.credentialType
53737
54052
  }));
53738
- const idWidth = Math.max(2, ...cells.map((c83) => c83.id.length));
53739
- const labelWidth = Math.max(5, ...cells.map((c83) => c83.label.length));
54053
+ const idWidth = Math.max(2, ...cells.map((c84) => c84.id.length));
54054
+ const labelWidth = Math.max(5, ...cells.map((c84) => c84.label.length));
53740
54055
  const header = `${"ID".padEnd(idWidth)} ${"LABEL".padEnd(labelWidth)} TYPE`;
53741
54056
  const rows = cells.map(
53742
- (c83) => `${c83.id.padEnd(idWidth)} ${c83.label.padEnd(labelWidth)} ${c83.type}`
54057
+ (c84) => `${c84.id.padEnd(idWidth)} ${c84.label.padEnd(labelWidth)} ${c84.type}`
53743
54058
  );
53744
54059
  return { header, rows };
53745
54060
  }
@@ -54174,6 +54489,512 @@ async function runDelete3(credentialId, options) {
54174
54489
  }
54175
54490
  console.log(colorSuccess(`Deleted credential ${credentialId}`));
54176
54491
  }
54492
+ function failLinkOp(op, status, body, notFoundLabel) {
54493
+ const { message, errorCode } = readErrorFields(body);
54494
+ if (status === UNAUTHORIZED_STATUS2) {
54495
+ exitAuthFailed();
54496
+ }
54497
+ if (status === 403) {
54498
+ const verb = op.startsWith("un") ? "unlink" : "link";
54499
+ console.error(
54500
+ colorError(
54501
+ `Only an organization admin or Trace staff can ${verb} credentials, and API keys can't.`
54502
+ )
54503
+ );
54504
+ if (message !== void 0) console.error(message);
54505
+ process.exit(1);
54506
+ }
54507
+ if (status === 404) {
54508
+ console.error(colorError(message ?? `Not found: ${notFoundLabel}.`));
54509
+ process.exit(1);
54510
+ }
54511
+ if (status === 400) {
54512
+ console.error(colorError(message ?? "Invalid request (status 400)."));
54513
+ process.exit(1);
54514
+ }
54515
+ if (status === 409) {
54516
+ if (errorCode === "CREDENTIAL_LINK_SELF") {
54517
+ console.error(
54518
+ colorError(message ?? "That already belongs to this environment; nothing to link.")
54519
+ );
54520
+ } else if (errorCode === "CREDENTIAL_ALREADY_LINKED") {
54521
+ console.error(colorError(message ?? "That is already linked into this environment."));
54522
+ } else if (errorCode === "CREDENTIAL_LABEL_TAKEN") {
54523
+ console.error(
54524
+ colorError(
54525
+ message ?? "This environment already shows a credential with that label. Rename one of them first."
54526
+ )
54527
+ );
54528
+ } else if (errorCode === "CREDENTIAL_IN_USE") {
54529
+ console.error(
54530
+ colorError(message ?? "You can't unlink while a pentest using this environment is running.")
54531
+ );
54532
+ } else {
54533
+ console.error(colorError(message ?? "Conflict (status 409)."));
54534
+ }
54535
+ process.exit(1);
54536
+ }
54537
+ console.error(colorError(`Failed to ${op} (status ${status})${message ? `: ${message}` : "."}`));
54538
+ process.exit(1);
54539
+ }
54540
+ function requireLinkId(credentialLinkId) {
54541
+ if (!isUuid(credentialLinkId)) {
54542
+ console.error(colorError(`Invalid credential link id '${credentialLinkId}'. Expected a UUID.`));
54543
+ process.exit(1);
54544
+ }
54545
+ }
54546
+ function renderCredentialLinkDetail(link) {
54547
+ const pad = 13;
54548
+ return [
54549
+ `${"Link ID:".padEnd(pad)} ${link.credentialLinkId}`,
54550
+ `${"Credential:".padEnd(pad)} ${link.credential.credentialId}`,
54551
+ `${"Label:".padEnd(pad)} ${sanitizeDisplay(link.credential.label)}`,
54552
+ `${"Type:".padEnd(pad)} ${link.credential.credentialType}`,
54553
+ `${"Source:".padEnd(pad)} ${sourceDisplay(link.source)} (${link.source.environmentId})`,
54554
+ `${"Linked into:".padEnd(pad)} ${link.environmentId}`,
54555
+ `${"MFA:".padEnd(pad)} ${mfaDisplay(link.credential.mfa)}`,
54556
+ `${"Linked:".padEnd(pad)} ${formatDate(link.createdAt)}`
54557
+ ].join("\n");
54558
+ }
54559
+ function printCredentialLinksTsv(links) {
54560
+ console.log(
54561
+ [
54562
+ "credentialLinkId",
54563
+ "credentialId",
54564
+ "label",
54565
+ "credentialType",
54566
+ "sourceApplication",
54567
+ "sourceEnvironment",
54568
+ "sourceEnvironmentId",
54569
+ "environmentCredentialSourceId",
54570
+ "mfa",
54571
+ "createdAt"
54572
+ ].join(" ")
54573
+ );
54574
+ for (const link of links) {
54575
+ console.log(
54576
+ [
54577
+ tsvSafe(link.credentialLinkId),
54578
+ tsvSafe(link.credential.credentialId),
54579
+ tsvSafe(sanitizeDisplay(link.credential.label)),
54580
+ tsvSafe(link.credential.credentialType),
54581
+ tsvSafe(sanitizeDisplay(link.source.applicationName)),
54582
+ tsvSafe(sanitizeDisplay(link.source.environmentName)),
54583
+ tsvSafe(link.source.environmentId),
54584
+ tsvSafe(link.environmentCredentialSourceId ?? ""),
54585
+ tsvSafe(link.credential.mfa ? link.credential.mfa.kind : ""),
54586
+ tsvSafe(toIso(link.createdAt))
54587
+ ].join(" ")
54588
+ );
54589
+ }
54590
+ }
54591
+ function buildCredentialLinksTable(links) {
54592
+ const cells = links.map((link) => ({
54593
+ id: link.credentialLinkId,
54594
+ label: sanitizeDisplay(link.credential.label),
54595
+ type: link.credential.credentialType,
54596
+ source: sourceDisplay(link.source),
54597
+ via: link.environmentCredentialSourceId ? "link-all" : ""
54598
+ }));
54599
+ const idWidth = Math.max("LINK ID".length, ...cells.map((c84) => c84.id.length));
54600
+ const labelWidth = Math.max("LABEL".length, ...cells.map((c84) => c84.label.length));
54601
+ const typeWidth = Math.max("TYPE".length, ...cells.map((c84) => c84.type.length));
54602
+ const sourceWidth = Math.max("SOURCE".length, ...cells.map((c84) => c84.source.length));
54603
+ const header = `${"LINK ID".padEnd(idWidth)} ${"LABEL".padEnd(labelWidth)} ${"TYPE".padEnd(typeWidth)} ${"SOURCE".padEnd(sourceWidth)} VIA`;
54604
+ const rows = cells.map(
54605
+ (c84) => `${c84.id.padEnd(idWidth)} ${c84.label.padEnd(labelWidth)} ${c84.type.padEnd(typeWidth)} ${c84.source.padEnd(sourceWidth)} ${c84.via}`.trimEnd()
54606
+ );
54607
+ return { header, rows };
54608
+ }
54609
+ function outputCredentialLink(link, output) {
54610
+ if (output === "json") {
54611
+ printJson(link);
54612
+ return;
54613
+ }
54614
+ if (output === "tsv") {
54615
+ printCredentialLinksTsv([link]);
54616
+ return;
54617
+ }
54618
+ console.log(renderCredentialLinkDetail(link));
54619
+ }
54620
+ function printLinkableCredentialsTsv(creds) {
54621
+ console.log(
54622
+ [
54623
+ "credentialId",
54624
+ "label",
54625
+ "credentialType",
54626
+ "application",
54627
+ "environment",
54628
+ "environmentId",
54629
+ "hasMfa",
54630
+ "alreadyLinked"
54631
+ ].join(" ")
54632
+ );
54633
+ for (const cred of creds) {
54634
+ console.log(
54635
+ [
54636
+ tsvSafe(cred.credentialId),
54637
+ tsvSafe(sanitizeDisplay(cred.label)),
54638
+ tsvSafe(cred.credentialType),
54639
+ tsvSafe(sanitizeDisplay(cred.application.name)),
54640
+ tsvSafe(sanitizeDisplay(cred.environment.name)),
54641
+ tsvSafe(cred.environment.environmentId),
54642
+ tsvSafe(String(cred.hasMfa)),
54643
+ tsvSafe(String(cred.alreadyLinked))
54644
+ ].join(" ")
54645
+ );
54646
+ }
54647
+ }
54648
+ function buildLinkableCredentialsTable(creds) {
54649
+ const cells = creds.map((cred) => ({
54650
+ id: cred.credentialId,
54651
+ label: sanitizeDisplay(cred.label),
54652
+ type: cred.credentialType,
54653
+ source: sourceDisplay({
54654
+ applicationName: cred.application.name,
54655
+ environmentName: cred.environment.name
54656
+ }),
54657
+ linked: cred.alreadyLinked ? "linked" : ""
54658
+ }));
54659
+ const idWidth = Math.max("ID".length, ...cells.map((c84) => c84.id.length));
54660
+ const labelWidth = Math.max("LABEL".length, ...cells.map((c84) => c84.label.length));
54661
+ const typeWidth = Math.max("TYPE".length, ...cells.map((c84) => c84.type.length));
54662
+ const sourceWidth = Math.max("SOURCE".length, ...cells.map((c84) => c84.source.length));
54663
+ const header = `${"ID".padEnd(idWidth)} ${"LABEL".padEnd(labelWidth)} ${"TYPE".padEnd(typeWidth)} ${"SOURCE".padEnd(sourceWidth)} STATUS`;
54664
+ const rows = cells.map(
54665
+ (c84) => `${c84.id.padEnd(idWidth)} ${c84.label.padEnd(labelWidth)} ${c84.type.padEnd(typeWidth)} ${c84.source.padEnd(sourceWidth)} ${c84.linked}`.trimEnd()
54666
+ );
54667
+ return { header, rows };
54668
+ }
54669
+ async function runLink(credentialId, options) {
54670
+ const output = parseOutput(options.output);
54671
+ requireEnvId(options.env);
54672
+ requireCredId(credentialId);
54673
+ const organizationId = await resolveOrganizationId();
54674
+ const result = await authenticatedRequest(
54675
+ (client) => client.credentialLink.createCredentialLink({
54676
+ params: { organizationId, environmentId: options.env },
54677
+ body: { credentialId }
54678
+ })
54679
+ );
54680
+ if (result.status !== CREATED_STATUS) {
54681
+ failLinkOp("link", result.status, result.body, credentialId);
54682
+ }
54683
+ if (output === "table") {
54684
+ console.log(
54685
+ colorSuccess(
54686
+ `Linked credential ${sanitizeDisplay(result.body.credential.label)} from ${sourceDisplay(result.body.source)}`
54687
+ )
54688
+ );
54689
+ }
54690
+ outputCredentialLink(result.body, output);
54691
+ }
54692
+ async function runUnlink(credentialLinkId, options) {
54693
+ requireEnvId(options.env);
54694
+ requireLinkId(credentialLinkId);
54695
+ const confirmed = await confirmAction(
54696
+ `Unlink credential link ${credentialLinkId} (the source credential is kept)?`,
54697
+ { skip: options.yes, verb: "unlink" }
54698
+ );
54699
+ if (!confirmed) {
54700
+ console.log("Aborted.");
54701
+ return;
54702
+ }
54703
+ const organizationId = await resolveOrganizationId();
54704
+ const result = await authenticatedRequest(
54705
+ (client) => client.credentialLink.deleteCredentialLink({
54706
+ params: { organizationId, environmentId: options.env, credentialLinkId }
54707
+ })
54708
+ );
54709
+ if (result.status !== NO_CONTENT_STATUS) {
54710
+ if (result.status === 409 && readErrorFields(result.body).errorCode === "CREDENTIAL_LINKED_BY_RULE") {
54711
+ await failLinkedByRule(organizationId, options.env, result.body);
54712
+ }
54713
+ failLinkOp("unlink", result.status, result.body, credentialLinkId);
54714
+ }
54715
+ console.log(colorSuccess(`Unlinked credential link ${credentialLinkId}`));
54716
+ }
54717
+ function coveringRuleId(body) {
54718
+ if (typeof body !== "object" || body === null || !("details" in body)) return null;
54719
+ const { details } = body;
54720
+ if (typeof details !== "object" || details === null) return null;
54721
+ if (!("environmentCredentialSourceId" in details)) return null;
54722
+ const id = details.environmentCredentialSourceId;
54723
+ return typeof id === "string" && isUuid(id) ? id : null;
54724
+ }
54725
+ async function failLinkedByRule(organizationId, environmentId, body) {
54726
+ const { message } = readErrorFields(body);
54727
+ console.error(
54728
+ colorError(
54729
+ message ?? "All of the environment this credential comes from is linked here, so it stays."
54730
+ )
54731
+ );
54732
+ const ruleId = coveringRuleId(body);
54733
+ const rules = ruleId === null ? null : await fetchCredentialSources(organizationId, environmentId);
54734
+ const rule = rules?.find((r2) => r2.environmentCredentialSourceId === ruleId);
54735
+ console.error(
54736
+ rule ? `Unlink all of ${sourceDisplay(rule.source)} with: tracecli credential unlink-all ${rule.source.environmentId} --env ${environmentId}` : `See which environment with: tracecli credential links --env ${environmentId}`
54737
+ );
54738
+ process.exit(1);
54739
+ }
54740
+ async function fetchCredentialSources(organizationId, environmentId) {
54741
+ const result = await authenticatedRequestSafe(
54742
+ (client) => client.credentialLink.listCredentialSources({
54743
+ params: { organizationId, environmentId }
54744
+ })
54745
+ );
54746
+ if (result === null) return null;
54747
+ if (result.status === SUCCESS_STATUS) return result.body;
54748
+ if (result.status === 404) return [];
54749
+ return null;
54750
+ }
54751
+ function credentialSourceLines(rules) {
54752
+ return rules.map(
54753
+ (rule) => `All of ${sourceDisplay(rule.source)} is linked here, including credentials added there later.`
54754
+ );
54755
+ }
54756
+ async function runLinks(options) {
54757
+ const output = parseOutput(options.output);
54758
+ requireEnvId(options.env);
54759
+ const organizationId = await resolveOrganizationId();
54760
+ const result = await authenticatedRequest(
54761
+ (client) => client.credentialLink.listCredentialLinks({
54762
+ params: { organizationId, environmentId: options.env }
54763
+ })
54764
+ );
54765
+ if (result.status !== SUCCESS_STATUS) {
54766
+ failReadOp3("list credential links", result.status, result.body, options.env);
54767
+ }
54768
+ const links = result.body;
54769
+ if (output === "json") {
54770
+ printJson(links);
54771
+ return;
54772
+ }
54773
+ if (output === "tsv") {
54774
+ printCredentialLinksTsv(links);
54775
+ return;
54776
+ }
54777
+ const rules = await fetchCredentialSources(organizationId, options.env);
54778
+ if (rules === null) {
54779
+ console.error(
54780
+ colorError("Could not read which environments are linked whole; showing links only.")
54781
+ );
54782
+ }
54783
+ const ruleLines = credentialSourceLines(rules ?? []);
54784
+ if (links.length === 0) {
54785
+ if (ruleLines.length > 0) {
54786
+ for (const line of ruleLines) console.log(line);
54787
+ console.log("No credentials from there yet.");
54788
+ return;
54789
+ }
54790
+ console.log(
54791
+ `No linked credentials. See what you can link with: tracecli credential linkable --env ${options.env}`
54792
+ );
54793
+ return;
54794
+ }
54795
+ for (const line of ruleLines) console.log(line);
54796
+ if (ruleLines.length > 0) console.log("");
54797
+ const { header, rows } = buildCredentialLinksTable(links);
54798
+ await runInteractivePager({
54799
+ items: links,
54800
+ header,
54801
+ renderItem: (_link, i2) => [rows[i2] ?? ""],
54802
+ disabled: options.pager === false
54803
+ });
54804
+ }
54805
+ async function runLinkable(options) {
54806
+ const output = parseOutput(options.output);
54807
+ requireEnvId(options.env);
54808
+ const organizationId = await resolveOrganizationId();
54809
+ const result = await authenticatedRequest(
54810
+ (client) => client.credentialLink.listLinkableCredentials({
54811
+ params: { organizationId, environmentId: options.env }
54812
+ })
54813
+ );
54814
+ if (result.status !== SUCCESS_STATUS) {
54815
+ failReadOp3("list linkable credentials", result.status, result.body, options.env);
54816
+ }
54817
+ const creds = result.body;
54818
+ if (output === "json") {
54819
+ printJson(creds);
54820
+ return;
54821
+ }
54822
+ if (output === "tsv") {
54823
+ printLinkableCredentialsTsv(creds);
54824
+ return;
54825
+ }
54826
+ if (creds.length === 0) {
54827
+ console.log("No credentials elsewhere in this organization to link.");
54828
+ return;
54829
+ }
54830
+ const { header, rows } = buildLinkableCredentialsTable(creds);
54831
+ await runInteractivePager({
54832
+ items: creds,
54833
+ header,
54834
+ renderItem: (_cred, i2) => [rows[i2] ?? ""],
54835
+ disabled: options.pager === false
54836
+ });
54837
+ }
54838
+ function printLinkableEnvironmentsTsv(envs) {
54839
+ console.log(
54840
+ ["environmentId", "application", "environment", "credentialCount", "alreadyLinked"].join(" ")
54841
+ );
54842
+ for (const env2 of envs) {
54843
+ console.log(
54844
+ [
54845
+ tsvSafe(env2.environmentId),
54846
+ tsvSafe(sanitizeDisplay(env2.application.name)),
54847
+ tsvSafe(sanitizeDisplay(env2.name)),
54848
+ tsvSafe(String(env2.credentialCount)),
54849
+ tsvSafe(String(env2.alreadyLinked))
54850
+ ].join(" ")
54851
+ );
54852
+ }
54853
+ }
54854
+ function buildLinkableEnvironmentsTable(envs) {
54855
+ const cells = envs.map((env2) => ({
54856
+ id: env2.environmentId,
54857
+ name: sourceDisplay({ applicationName: env2.application.name, environmentName: env2.name }),
54858
+ count: String(env2.credentialCount),
54859
+ linked: env2.alreadyLinked ? "linked" : ""
54860
+ }));
54861
+ const idWidth = Math.max("ENVIRONMENT ID".length, ...cells.map((c84) => c84.id.length));
54862
+ const nameWidth = Math.max("ENVIRONMENT".length, ...cells.map((c84) => c84.name.length));
54863
+ const countWidth = "CREDENTIALS".length;
54864
+ const header = `${"ENVIRONMENT ID".padEnd(idWidth)} ${"ENVIRONMENT".padEnd(nameWidth)} ${"CREDENTIALS".padEnd(countWidth)} STATUS`;
54865
+ const rows = cells.map(
54866
+ (c84) => `${c84.id.padEnd(idWidth)} ${c84.name.padEnd(nameWidth)} ${c84.count.padEnd(countWidth)} ${c84.linked}`.trimEnd()
54867
+ );
54868
+ return { header, rows };
54869
+ }
54870
+ async function runLinkableEnvironments(options) {
54871
+ const output = parseOutput(options.output);
54872
+ requireEnvId(options.env);
54873
+ const organizationId = await resolveOrganizationId();
54874
+ const result = await authenticatedRequest(
54875
+ (client) => client.credentialLink.listLinkableEnvironments({
54876
+ params: { organizationId, environmentId: options.env }
54877
+ })
54878
+ );
54879
+ if (result.status !== SUCCESS_STATUS) {
54880
+ failReadOp3("list linkable environments", result.status, result.body, options.env);
54881
+ }
54882
+ const envs = result.body;
54883
+ if (output === "json") {
54884
+ printJson(envs);
54885
+ return;
54886
+ }
54887
+ if (output === "tsv") {
54888
+ printLinkableEnvironmentsTsv(envs);
54889
+ return;
54890
+ }
54891
+ if (envs.length === 0) {
54892
+ console.log("No other environments in this organization to link.");
54893
+ return;
54894
+ }
54895
+ const { header, rows } = buildLinkableEnvironmentsTable(envs);
54896
+ await runInteractivePager({
54897
+ items: envs,
54898
+ header,
54899
+ renderItem: (_env, i2) => [rows[i2] ?? ""],
54900
+ disabled: options.pager === false
54901
+ });
54902
+ }
54903
+ function requireSourceEnvId(sourceEnvironmentId) {
54904
+ if (!isUuid(sourceEnvironmentId)) {
54905
+ console.error(
54906
+ colorError(
54907
+ `Invalid source environment id '${sourceEnvironmentId}'. Expected an environment UUID.`
54908
+ )
54909
+ );
54910
+ process.exit(1);
54911
+ }
54912
+ }
54913
+ async function runLinkAll(sourceEnvironmentId, options) {
54914
+ const output = parseOutput(options.output);
54915
+ requireEnvId(options.env);
54916
+ requireSourceEnvId(sourceEnvironmentId);
54917
+ const organizationId = await resolveOrganizationId();
54918
+ const result = await authenticatedRequest(
54919
+ (client) => client.credentialLink.createCredentialSource({
54920
+ params: { organizationId, environmentId: options.env },
54921
+ body: { sourceEnvironmentId }
54922
+ })
54923
+ );
54924
+ if (result.status !== CREATED_STATUS) {
54925
+ failLinkOp("link all", result.status, result.body, sourceEnvironmentId);
54926
+ }
54927
+ const rule = result.body;
54928
+ if (output === "json") {
54929
+ printJson(rule);
54930
+ return;
54931
+ }
54932
+ if (output === "tsv") {
54933
+ printCredentialLinksTsv(rule.links);
54934
+ return;
54935
+ }
54936
+ const pad = 13;
54937
+ console.log(
54938
+ colorSuccess(
54939
+ `Linked all of ${sourceDisplay(rule.source)}. Credentials added there later show up here too.`
54940
+ )
54941
+ );
54942
+ console.log(
54943
+ [
54944
+ `${"Source:".padEnd(pad)} ${sourceDisplay(rule.source)} (${rule.source.environmentId})`,
54945
+ `${"Linked into:".padEnd(pad)} ${rule.environmentId}`,
54946
+ `${"New links:".padEnd(pad)} ${rule.links.length}`
54947
+ ].join("\n")
54948
+ );
54949
+ }
54950
+ async function runUnlinkAll(sourceEnvironmentId, options) {
54951
+ requireEnvId(options.env);
54952
+ requireSourceEnvId(sourceEnvironmentId);
54953
+ const organizationId = await resolveOrganizationId();
54954
+ const listed = await authenticatedRequest(
54955
+ (client) => client.credentialLink.listCredentialSources({
54956
+ params: { organizationId, environmentId: options.env }
54957
+ })
54958
+ );
54959
+ if (listed.status !== SUCCESS_STATUS) {
54960
+ failReadOp3("look up the link-all", listed.status, listed.body, options.env);
54961
+ }
54962
+ const wanted = sourceEnvironmentId.toLowerCase();
54963
+ const rule = listed.body.find((r2) => r2.source.environmentId.toLowerCase() === wanted);
54964
+ if (!rule) {
54965
+ console.error(
54966
+ colorError(
54967
+ `Nothing from ${sourceEnvironmentId} is linked whole into this environment. See what is with: tracecli credential linkable --env ${options.env} --environments`
54968
+ )
54969
+ );
54970
+ process.exit(1);
54971
+ }
54972
+ const confirmed = await confirmAction(
54973
+ `Unlink all of ${sourceDisplay(rule.source)} from this environment (credentials linked on their own stay)?`,
54974
+ { skip: options.yes, verb: "unlink" }
54975
+ );
54976
+ if (!confirmed) {
54977
+ console.log("Aborted.");
54978
+ return;
54979
+ }
54980
+ const result = await authenticatedRequest(
54981
+ (client) => client.credentialLink.deleteCredentialSource({
54982
+ params: {
54983
+ organizationId,
54984
+ environmentId: options.env,
54985
+ environmentCredentialSourceId: rule.environmentCredentialSourceId
54986
+ }
54987
+ })
54988
+ );
54989
+ if (result.status !== NO_CONTENT_STATUS) {
54990
+ failLinkOp("unlink all", result.status, result.body, rule.environmentCredentialSourceId);
54991
+ }
54992
+ console.log(
54993
+ colorSuccess(
54994
+ `Unlinked all of ${sourceDisplay(rule.source)}. Credentials linked on their own stay.`
54995
+ )
54996
+ );
54997
+ }
54177
54998
  function addSecretInputOptions(command) {
54178
54999
  return command.option("--secret-stdin", "Read the secret value from stdin").option("--secret-file <path>", "Read the secret value from a file").option("--secret-env <name>", "Read the secret value from an env var").option("--username-stdin", "Read the username from stdin").option("--username-file <path>", "Read the username from a file").option("--username-env <name>", "Read the username from an env var").option("--password-stdin", "Read the password from stdin").option("--password-file <path>", "Read the password from a file").option("--password-env <name>", "Read the password from an env var");
54179
55000
  }
@@ -54284,6 +55105,89 @@ left \u2014 if the current window is nearly over, the API waits for the next one
54284
55105
  await runMfaCode(credentialId, options);
54285
55106
  });
54286
55107
  }
55108
+ function createLinkCommand() {
55109
+ return new Command("link").description("Link another application's credential into an environment").argument("<credentialId>", "UUID of the credential to link (owned by another environment)").requiredOption("--env <environmentId>", "Environment UUID to link it into").option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").addHelpText(
55110
+ "after",
55111
+ `
55112
+ A link holds no secret: the linked environment sees the source credential as it
55113
+ is now, so a rotation, MFA change, or relabel on the source shows up here too.
55114
+ Edit the credential where it lives (its own --env). The label must be free in
55115
+ the target environment, and the credential must not already belong to it.
55116
+
55117
+ Find candidates with: tracecli credential linkable --env <envId>
55118
+ To link every credential of an environment, now and as they are added, use
55119
+ link-all instead.
55120
+
55121
+ Examples:
55122
+ tracecli credential linkable --env <envId>
55123
+ tracecli credential link <credentialId> --env <envId>`
55124
+ ).action(async (credentialId, options) => {
55125
+ await runLink(credentialId, options);
55126
+ });
55127
+ }
55128
+ function createUnlinkCommand() {
55129
+ return new Command("unlink").description("Remove a credential link from an environment (the source credential is kept)").argument("<credentialLinkId>", "Credential link UUID (the LINK ID from `credential links`)").requiredOption("--env <environmentId>", "Environment UUID the credential is linked into").option("-y, --yes", "Skip the confirmation prompt (required in non-interactive shells)").addHelpText(
55130
+ "after",
55131
+ `
55132
+ Blocked while a pentest using this environment is running, like deleting one of
55133
+ the environment's own credentials. A credential whose whole environment is
55134
+ linked here (VIA link-all in \`credential links\`) stays until that is removed
55135
+ with \`credential unlink-all\`.`
55136
+ ).action(async (credentialLinkId, options) => {
55137
+ await runUnlink(credentialLinkId, options);
55138
+ });
55139
+ }
55140
+ function createLinksCommand() {
55141
+ return new Command("links").description(
55142
+ "List the credentials linked into an environment from elsewhere (secrets are never shown)"
55143
+ ).requiredOption("--env <environmentId>", "Environment UUID").option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").option("--no-pager", "Disable the interactive pager for table output").action(async (options) => {
55144
+ await runLinks(options);
55145
+ });
55146
+ }
55147
+ function createLinkableCommand() {
55148
+ return new Command("linkable").description("List credentials elsewhere in the organization that an environment could link").requiredOption("--env <environmentId>", "Environment UUID to link into").option("--environments", "List whole environments to link-all instead of credentials").option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").option("--no-pager", "Disable the interactive pager for table output").addHelpText(
55149
+ "after",
55150
+ `
55151
+ Rows already linked into this environment are marked "linked" (table) or
55152
+ alreadyLinked=true (tsv). With --environments, "linked" means all of that
55153
+ environment is linked here already.`
55154
+ ).action(async (options) => {
55155
+ if (options.environments) {
55156
+ await runLinkableEnvironments(options);
55157
+ return;
55158
+ }
55159
+ await runLinkable(options);
55160
+ });
55161
+ }
55162
+ function createLinkAllCommand() {
55163
+ return new Command("link-all").description("Link every credential of another environment, including ones added there later").argument("<sourceEnvironmentId>", "UUID of the environment whose credentials to link").requiredOption("--env <environmentId>", "Environment UUID to link them into").option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").addHelpText(
55164
+ "after",
55165
+ `
55166
+ Each credential shows here as a link, like \`credential link\`, and a credential
55167
+ added to the source later shows up here too. Edit them where they live. Every
55168
+ label must be free in this environment. Credentials you already linked on their
55169
+ own stay as they are.
55170
+
55171
+ Find environments with: tracecli credential linkable --env <envId> --environments
55172
+
55173
+ Examples:
55174
+ tracecli credential link-all <sourceEnvId> --env <envId>
55175
+ tracecli credential unlink-all <sourceEnvId> --env <envId>`
55176
+ ).action(async (sourceEnvironmentId, options) => {
55177
+ await runLinkAll(sourceEnvironmentId, options);
55178
+ });
55179
+ }
55180
+ function createUnlinkAllCommand() {
55181
+ return new Command("unlink-all").description("Stop linking every credential of another environment into this one").argument("<sourceEnvironmentId>", "UUID of the environment passed to link-all").requiredOption("--env <environmentId>", "Environment UUID they are linked into").option("-y, --yes", "Skip the confirmation prompt (required in non-interactive shells)").addHelpText(
55182
+ "after",
55183
+ `
55184
+ Removes the links link-all made; the credentials stay in their own environment.
55185
+ Credentials you linked on their own stay linked. Blocked while a pentest using
55186
+ this environment is running.`
55187
+ ).action(async (sourceEnvironmentId, options) => {
55188
+ await runUnlinkAll(sourceEnvironmentId, options);
55189
+ });
55190
+ }
54287
55191
  function createCredentialCommand() {
54288
55192
  const credential = new Command("credential").alias("cred").description("Manage environment credentials");
54289
55193
  credential.addCommand(createCreateCommand3());
@@ -54294,6 +55198,12 @@ function createCredentialCommand() {
54294
55198
  credential.addCommand(createSetMfaCommand());
54295
55199
  credential.addCommand(createClearMfaCommand());
54296
55200
  credential.addCommand(createMfaCodeCommand());
55201
+ credential.addCommand(createLinkCommand());
55202
+ credential.addCommand(createUnlinkCommand());
55203
+ credential.addCommand(createLinkAllCommand());
55204
+ credential.addCommand(createUnlinkAllCommand());
55205
+ credential.addCommand(createLinksCommand());
55206
+ credential.addCommand(createLinkableCommand());
54297
55207
  credential.addCommand(createInboxCommand(), { hidden: true });
54298
55208
  return credential;
54299
55209
  }
@@ -54509,14 +55419,14 @@ function buildEnvironmentTable(envs, leading) {
54509
55419
  url: e.primaryUrl != null ? sanitizeDisplay(e.primaryUrl) : "\u2014"
54510
55420
  }));
54511
55421
  const leadingWidths = leading.map(
54512
- (column, i2) => Math.max(column.header.length, ...cells.map((c83) => c83.leading[i2]?.length ?? 0))
55422
+ (column, i2) => Math.max(column.header.length, ...cells.map((c84) => c84.leading[i2]?.length ?? 0))
54513
55423
  );
54514
- const nameWidth = Math.max(4, ...cells.map((c83) => c83.name.length));
55424
+ const nameWidth = Math.max(4, ...cells.map((c84) => c84.name.length));
54515
55425
  const padLeading = (values) => values.map((v, i2) => v.padEnd(leadingWidths[i2] ?? 0)).join(" ");
54516
55426
  return {
54517
- header: `${padLeading(leading.map((c83) => c83.header))} ${"NAME".padEnd(nameWidth)} PROD URL`,
55427
+ header: `${padLeading(leading.map((c84) => c84.header))} ${"NAME".padEnd(nameWidth)} PROD URL`,
54518
55428
  rows: cells.map(
54519
- (c83) => `${padLeading(c83.leading)} ${c83.name.padEnd(nameWidth)} ${c83.prod.padEnd(4)} ${c83.url}`
55429
+ (c84) => `${padLeading(c84.leading)} ${c84.name.padEnd(nameWidth)} ${c84.prod.padEnd(4)} ${c84.url}`
54520
55430
  )
54521
55431
  };
54522
55432
  }
@@ -54898,7 +55808,6 @@ var CLI_VERSION = packageMetadata.version;
54898
55808
  import { readFileSync as readFileSync7 } from "fs";
54899
55809
 
54900
55810
  // src/pentest/evidence.ts
54901
- import { readFileSync as readFileSync5, statSync as statSync2 } from "fs";
54902
55811
  import { basename as basename2 } from "path";
54903
55812
 
54904
55813
  // src/pentest/resolve.ts
@@ -54973,6 +55882,67 @@ Run \`tracecli pentest list\` to see available pentests.`
54973
55882
  return pentest.pentestId;
54974
55883
  }
54975
55884
 
55885
+ // src/pentest/upload-file.ts
55886
+ import { readFileSync as readFileSync5, statSync as statSync2 } from "fs";
55887
+ function formatBytes2(bytes) {
55888
+ if (bytes < 1024) return `${bytes} B`;
55889
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
55890
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
55891
+ }
55892
+ function statUploadFile(path3, limit) {
55893
+ let sizeBytes;
55894
+ try {
55895
+ const stats = statSync2(path3);
55896
+ if (!stats.isFile()) {
55897
+ console.error(colorError(`--file '${sanitizeDisplay(path3)}' is not a regular file.`));
55898
+ process.exit(1);
55899
+ }
55900
+ sizeBytes = stats.size;
55901
+ } catch {
55902
+ console.error(colorError(`Cannot read --file '${sanitizeDisplay(path3)}'.`));
55903
+ process.exit(1);
55904
+ }
55905
+ if (sizeBytes === 0) {
55906
+ console.error(colorError(`--file '${sanitizeDisplay(path3)}' is empty.`));
55907
+ process.exit(1);
55908
+ }
55909
+ if (sizeBytes > limit.bytes) {
55910
+ console.error(
55911
+ colorError(
55912
+ `File is ${formatBytes2(sizeBytes)}; the limit for one ${limit.noun} is ${formatBytes2(limit.bytes)}.`
55913
+ )
55914
+ );
55915
+ process.exit(1);
55916
+ }
55917
+ return sizeBytes;
55918
+ }
55919
+ async function putFileToPresignedUrl(uploadUrl, path3, options = {}) {
55920
+ let put;
55921
+ try {
55922
+ put = await fetch(uploadUrl, {
55923
+ method: "PUT",
55924
+ body: new Uint8Array(readFileSync5(path3)),
55925
+ ...options.contentType === void 0 ? {} : { headers: { "Content-Type": options.contentType } }
55926
+ });
55927
+ } catch (error48) {
55928
+ const reason = error48 instanceof Error ? error48.message : "unknown error";
55929
+ console.error(
55930
+ colorError(
55931
+ `Upload to storage failed: could not reach storage (${sanitizeDisplay(reason)}). Check the connection and re-run the command.`
55932
+ )
55933
+ );
55934
+ process.exit(1);
55935
+ }
55936
+ if (!put.ok) {
55937
+ console.error(
55938
+ colorError(
55939
+ `Upload to storage failed (status ${put.status}). The upload link is short-lived; re-run the command.`
55940
+ )
55941
+ );
55942
+ process.exit(1);
55943
+ }
55944
+ }
55945
+
54976
55946
  // src/pentest/evidence.ts
54977
55947
  var SECTION_LIST = ValidationEvidenceSectionValues.join(", ");
54978
55948
  var KIND_LIST = PentestExploitArtifactKindValues.join(", ");
@@ -54982,11 +55952,6 @@ function isSection(value) {
54982
55952
  function isKind(value) {
54983
55953
  return PentestExploitArtifactKindValues.includes(value);
54984
55954
  }
54985
- function formatBytes2(bytes) {
54986
- if (bytes < 1024) return `${bytes} B`;
54987
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
54988
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
54989
- }
54990
55955
  async function resolveFindingId(organizationId, pentestId, identifier) {
54991
55956
  if (isUuid(identifier)) {
54992
55957
  return identifier;
@@ -55033,30 +55998,10 @@ async function runAttachEvidence(options) {
55033
55998
  console.error(`Valid kinds: ${KIND_LIST}`);
55034
55999
  process.exit(1);
55035
56000
  }
55036
- let sizeBytes;
55037
- try {
55038
- const stats = statSync2(options.file);
55039
- if (!stats.isFile()) {
55040
- console.error(colorError(`--file '${sanitizeDisplay(options.file)}' is not a regular file.`));
55041
- process.exit(1);
55042
- }
55043
- sizeBytes = stats.size;
55044
- } catch {
55045
- console.error(colorError(`Cannot read --file '${sanitizeDisplay(options.file)}'.`));
55046
- process.exit(1);
55047
- }
55048
- if (sizeBytes === 0) {
55049
- console.error(colorError(`--file '${sanitizeDisplay(options.file)}' is empty.`));
55050
- process.exit(1);
55051
- }
55052
- if (sizeBytes > MAX_STAFF_EVIDENCE_UPLOAD_BYTES) {
55053
- console.error(
55054
- colorError(
55055
- `File is ${formatBytes2(sizeBytes)}; the limit for one exhibit is ${formatBytes2(MAX_STAFF_EVIDENCE_UPLOAD_BYTES)}.`
55056
- )
55057
- );
55058
- process.exit(1);
55059
- }
56001
+ const sizeBytes = statUploadFile(options.file, {
56002
+ bytes: MAX_STAFF_EVIDENCE_UPLOAD_BYTES,
56003
+ noun: "exhibit"
56004
+ });
55060
56005
  const organizationId = await resolveOrganizationId();
55061
56006
  const pentestId = await resolvePentestId(organizationId, options.pentest);
55062
56007
  const pentestFindingId = await resolveFindingId(organizationId, pentestId, options.finding);
@@ -55078,16 +56023,7 @@ async function runAttachEvidence(options) {
55078
56023
  );
55079
56024
  process.exit(1);
55080
56025
  }
55081
- const body = readFileSync5(options.file);
55082
- const put = await fetch(slot.body.uploadUrl, { method: "PUT", body: new Uint8Array(body) });
55083
- if (!put.ok) {
55084
- console.error(
55085
- colorError(
55086
- `Upload to storage failed (status ${put.status}). The upload link is short-lived \u2014 re-run the command.`
55087
- )
55088
- );
55089
- process.exit(1);
55090
- }
56026
+ await putFileToPresignedUrl(slot.body.uploadUrl, options.file);
55091
56027
  const attached = await authenticatedRequest(
55092
56028
  (client) => client.pentestFinding.attachFindingEvidence({
55093
56029
  params: params3,
@@ -55896,8 +56832,8 @@ function requiredExtensionForKind(kind) {
55896
56832
  return ".zip";
55897
56833
  }
55898
56834
  function defaultDisplayName(originalFilename) {
55899
- const base2 = originalFilename.replace(/\.(zip|apk|ipa)$/i, "").trim();
55900
- return base2.slice(0, 200) || "source";
56835
+ const base3 = originalFilename.replace(/\.(zip|apk|ipa)$/i, "").trim();
56836
+ return base3.slice(0, 200) || "source";
55901
56837
  }
55902
56838
  function formatArchiveBytes(bytes) {
55903
56839
  if (bytes < 1024) return `${bytes} B`;
@@ -56812,6 +57748,258 @@ function createAttestationCommand() {
56812
57748
  return attestation;
56813
57749
  }
56814
57750
 
57751
+ // src/pentest/documents.ts
57752
+ import { basename as basename5 } from "path";
57753
+
57754
+ // src/pentest/intake-errors.ts
57755
+ var WRITE_FORBIDDEN_MESSAGE = {
57756
+ questionnaire: "Questionnaire writes require an organization admin (or Trace staff) signed in with a browser session. API keys are rejected.",
57757
+ "questionnaire documents": "Questionnaire document writes require an organization admin (or Trace staff) signed in with a browser session. API keys are rejected."
57758
+ };
57759
+ function failIntakeRequest(status, body, options) {
57760
+ const { message } = readErrorFields(body);
57761
+ if (status === UNAUTHORIZED_STATUS2) {
57762
+ exitAuthFailed();
57763
+ }
57764
+ if (status === 403) {
57765
+ console.error(
57766
+ colorError(
57767
+ options.action === "write" ? WRITE_FORBIDDEN_MESSAGE[options.subject] : message ?? `You do not have permission to view this ${options.subject}.`
57768
+ )
57769
+ );
57770
+ process.exit(1);
57771
+ }
57772
+ if (status === 404) {
57773
+ console.error(
57774
+ colorError(
57775
+ options.notFound === void 0 ? message ?? "Not found." : `${options.notFound.what} not found: ${sanitizeDisplay(options.notFound.label)}.`
57776
+ )
57777
+ );
57778
+ process.exit(1);
57779
+ }
57780
+ if (status === 400) {
57781
+ console.error(colorError(message ?? `Invalid ${options.subject} request (status 400).`));
57782
+ process.exit(1);
57783
+ }
57784
+ if (status === 409) {
57785
+ console.error(colorError(message ?? QUESTIONNAIRE_LOCKED_MESSAGE));
57786
+ process.exit(1);
57787
+ }
57788
+ console.error(
57789
+ colorError(
57790
+ `Failed to ${options.action === "write" ? "save" : "load"} ${options.subject} (status ${status})${message ? `: ${message}` : "."}`
57791
+ )
57792
+ );
57793
+ process.exit(1);
57794
+ }
57795
+
57796
+ // src/pentest/documents.ts
57797
+ var TYPE_LIST = PentestIntakeDocumentTypeValues.join(", ");
57798
+ function isDocumentType(value) {
57799
+ return PentestIntakeDocumentTypeValues.includes(value);
57800
+ }
57801
+ function renderDocumentLines(doc) {
57802
+ const uploader = doc.uploadedByName ? ` by ${sanitizeDisplay(doc.uploadedByName)}` : "";
57803
+ return [
57804
+ ` ${sanitizeDisplay(doc.fileName)}`,
57805
+ ` ${PENTEST_INTAKE_DOCUMENT_TYPE_LABELS[doc.documentType]} | ${formatBytes2(doc.fileSizeBytes)} | uploaded${uploader} on ${formatDate(doc.createdAt)}`,
57806
+ ` id ${doc.documentId}`
57807
+ ];
57808
+ }
57809
+ var TSV_HEADER = "documentId documentType fileName fileType fileSizeBytes uploadedByName createdAt";
57810
+ async function runList5(options) {
57811
+ const output = parseOutput(options.output);
57812
+ const organizationId = await resolveOrganizationId();
57813
+ const pentestId = await resolvePentestId(organizationId, options.pentest);
57814
+ const result = await authenticatedRequest(
57815
+ (client) => client.pentestIntakeDocument.listDocuments({ params: { organizationId, pentestId } })
57816
+ );
57817
+ if (result.status !== SUCCESS_STATUS) {
57818
+ failIntakeRequest(result.status, result.body, {
57819
+ subject: "questionnaire documents",
57820
+ action: "read",
57821
+ notFound: { what: "Pentest", label: options.pentest }
57822
+ });
57823
+ }
57824
+ const { editable, documents } = result.body;
57825
+ if (output === "json") {
57826
+ printJson({ editable, documents });
57827
+ return;
57828
+ }
57829
+ if (output === "tsv") {
57830
+ console.log(TSV_HEADER);
57831
+ for (const doc of documents) {
57832
+ console.log(
57833
+ [
57834
+ doc.documentId,
57835
+ doc.documentType,
57836
+ tsvSafe(sanitizeDisplay(doc.fileName)),
57837
+ tsvSafe(sanitizeDisplay(doc.fileType)),
57838
+ String(doc.fileSizeBytes),
57839
+ tsvSafe(doc.uploadedByName === null ? null : sanitizeDisplay(doc.uploadedByName)),
57840
+ toIso(doc.createdAt)
57841
+ ].join(" ")
57842
+ );
57843
+ }
57844
+ return;
57845
+ }
57846
+ console.log(`Editable: ${editable ? "yes" : "no"}`);
57847
+ if (documents.length === 0) {
57848
+ console.log("No documents attached.");
57849
+ return;
57850
+ }
57851
+ console.log(`${documents.length} document${documents.length === 1 ? "" : "s"}:`);
57852
+ for (const doc of documents) {
57853
+ for (const line of renderDocumentLines(doc)) {
57854
+ console.log(line);
57855
+ }
57856
+ }
57857
+ }
57858
+ async function runAttach(options) {
57859
+ const output = parseTextOutput(options.output);
57860
+ if (options.type !== void 0 && !isDocumentType(options.type)) {
57861
+ console.error(colorError(`Invalid --type '${sanitizeDisplay(options.type)}'.`));
57862
+ console.error(`Valid types: ${TYPE_LIST}`);
57863
+ process.exit(1);
57864
+ }
57865
+ const sizeBytes = statUploadFile(options.file, {
57866
+ bytes: MAX_PENTEST_INTAKE_DOCUMENT_BYTES,
57867
+ noun: "document"
57868
+ });
57869
+ const fileName = basename5(options.file);
57870
+ const documentType = options.type !== void 0 && isDocumentType(options.type) ? options.type : guessPentestIntakeDocumentType(fileName);
57871
+ const organizationId = await resolveOrganizationId();
57872
+ const pentestId = await resolvePentestId(organizationId, options.pentest);
57873
+ const params3 = { organizationId, pentestId };
57874
+ const slot = await authenticatedRequest(
57875
+ (client) => client.pentestIntakeDocument.createUpload({
57876
+ params: params3,
57877
+ body: { fileName, fileSizeBytes: sizeBytes }
57878
+ })
57879
+ );
57880
+ if (slot.status !== CREATED_STATUS) {
57881
+ failIntakeRequest(slot.status, slot.body, {
57882
+ subject: "questionnaire documents",
57883
+ action: "write",
57884
+ notFound: { what: "Pentest", label: options.pentest }
57885
+ });
57886
+ }
57887
+ const fileType = pentestIntakeDocumentMimeType(fileName);
57888
+ await putFileToPresignedUrl(slot.body.uploadUrl, options.file, { contentType: fileType });
57889
+ const attached = await authenticatedRequest(
57890
+ (client) => client.pentestIntakeDocument.attachDocument({
57891
+ params: params3,
57892
+ body: {
57893
+ uploadKey: slot.body.uploadKey,
57894
+ documentType,
57895
+ fileName,
57896
+ fileType
57897
+ }
57898
+ })
57899
+ );
57900
+ if (attached.status !== CREATED_STATUS) {
57901
+ failIntakeRequest(attached.status, attached.body, {
57902
+ subject: "questionnaire documents",
57903
+ action: "write",
57904
+ notFound: { what: "Pentest", label: options.pentest }
57905
+ });
57906
+ }
57907
+ if (output === "json") {
57908
+ printJson(attached.body);
57909
+ return;
57910
+ }
57911
+ console.log(
57912
+ colorSuccess(
57913
+ `Attached ${sanitizeDisplay(attached.body.fileName)} (${PENTEST_INTAKE_DOCUMENT_TYPE_LABELS[attached.body.documentType]}, ${formatBytes2(attached.body.fileSizeBytes)})`
57914
+ )
57915
+ );
57916
+ console.log(` id ${attached.body.documentId}`);
57917
+ }
57918
+ async function runRemove(documentId, options) {
57919
+ if (!isUuid(documentId)) {
57920
+ console.error(
57921
+ colorError(
57922
+ `Invalid document ID '${sanitizeDisplay(documentId)}'. Pass the UUID from \`tracecli pentest documents list\`.`
57923
+ )
57924
+ );
57925
+ process.exit(1);
57926
+ }
57927
+ const organizationId = await resolveOrganizationId();
57928
+ const pentestId = await resolvePentestId(organizationId, options.pentest);
57929
+ const confirmed = await confirmDeletion(`document ${documentId}`, options.yes === true);
57930
+ if (!confirmed) {
57931
+ console.log("Cancelled.");
57932
+ return;
57933
+ }
57934
+ const result = await authenticatedRequest(
57935
+ (client) => client.pentestIntakeDocument.deleteDocument({
57936
+ params: { organizationId, pentestId, documentId }
57937
+ })
57938
+ );
57939
+ if (result.status !== NO_CONTENT_STATUS) {
57940
+ failIntakeRequest(result.status, result.body, {
57941
+ subject: "questionnaire documents",
57942
+ action: "write",
57943
+ notFound: { what: "Document", label: documentId }
57944
+ });
57945
+ }
57946
+ console.log(colorSuccess(`Removed document ${documentId}.`));
57947
+ }
57948
+ function createListCommand6() {
57949
+ return new Command("list").description("List documents attached to the intake questionnaire").requiredOption("--pentest <id>", "Pentest display ID (e.g. PT-3) or UUID").option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").addHelpText(
57950
+ "after",
57951
+ `
57952
+ Examples:
57953
+ tracecli pentest documents list --pentest PT-1
57954
+ tracecli pentest documents list --pentest PT-1 --output json`
57955
+ ).action(async (options) => {
57956
+ await runList5(options);
57957
+ });
57958
+ }
57959
+ function createAttachCommand() {
57960
+ return new Command("attach").description(
57961
+ "Attach a document (prior report, architecture diagram, threat model) to the questionnaire"
57962
+ ).requiredOption("--pentest <id>", "Pentest display ID (e.g. PT-3) or UUID").requiredOption("--file <path>", "Path to the document").option(
57963
+ "--type <type>",
57964
+ `Document type (${TYPE_LIST}); guessed from the file name when omitted`
57965
+ ).option("-o, --output <format>", "Output format: text, json (default: text)", "text").addHelpText(
57966
+ "after",
57967
+ `
57968
+ Answers the questionnaire's "documents to share" question. Same edit window as
57969
+ the answers: allowed until testing starts. Every documents command needs a
57970
+ browser session from \`tracecli auth login\`; API keys are rejected. Attach and
57971
+ remove also need an organization admin (or Trace staff).
57972
+ Up to ${formatBytes2(MAX_PENTEST_INTAKE_DOCUMENT_BYTES)} per file.
57973
+
57974
+ Examples:
57975
+ tracecli pentest documents attach --pentest PT-1 --file ./threat-model.pdf
57976
+ tracecli pentest documents attach --pentest PT-1 --file ./q3-report.pdf --type PREVIOUS_PENTEST
57977
+ tracecli pentest documents attach --pentest PT-1 --file ./arch.png --output json`
57978
+ ).action(async (options) => {
57979
+ await runAttach(options);
57980
+ });
57981
+ }
57982
+ function createRemoveCommand() {
57983
+ return new Command("remove").description("Remove a document from the intake questionnaire").argument("<documentId>", "Document UUID (from `documents list`)").requiredOption("--pentest <id>", "Pentest display ID (e.g. PT-3) or UUID").option("-y, --yes", "Skip the confirmation prompt").addHelpText(
57984
+ "after",
57985
+ `
57986
+ Examples:
57987
+ tracecli pentest documents remove 3f1c... --pentest PT-1
57988
+ tracecli pentest documents remove 3f1c... --pentest PT-1 --yes`
57989
+ ).action(async (documentId, options) => {
57990
+ await runRemove(documentId, options);
57991
+ });
57992
+ }
57993
+ function createDocumentsCommand() {
57994
+ const documents = new Command("documents").description(
57995
+ "Documents attached to the pentest intake questionnaire"
57996
+ );
57997
+ documents.addCommand(createListCommand6());
57998
+ documents.addCommand(createAttachCommand());
57999
+ documents.addCommand(createRemoveCommand());
58000
+ return documents;
58001
+ }
58002
+
56815
58003
  // src/pentest/get-finding.ts
56816
58004
  var OUTPUT_FORMATS2 = ["text", "json"];
56817
58005
  function isOutputFormat2(value) {
@@ -57261,7 +58449,10 @@ function parseQuestionnaireAnswersFile(raw, sourceLabel = "file") {
57261
58449
  error: `${sourceLabel} must contain the answers object directly, not an { answers } wrapper. The CLI wraps it for the API.`
57262
58450
  };
57263
58451
  }
57264
- const result = PentestCustomerIntakeAnswersSchema.safeParse(parsed);
58452
+ const withoutNulls = Object.fromEntries(
58453
+ Object.entries(parsed).filter(([, value]) => value !== null)
58454
+ );
58455
+ const result = PentestCustomerIntakeAnswersSchema.safeParse(withoutNulls);
57265
58456
  if (!result.success) {
57266
58457
  const issue2 = result.error.issues[0];
57267
58458
  const path3 = issue2?.path.length ? issue2.path.join(".") : "answers";
@@ -57286,6 +58477,12 @@ function applyMultiSelectIntakeAnswer(current, key, selected) {
57286
58477
  [key]: Array.from(new Set(selected)).sort()
57287
58478
  };
57288
58479
  }
58480
+ function mergeIntakeAnswers(base3, patch) {
58481
+ return normalizePentestCustomerIntakeAnswers({
58482
+ ...base3 ?? {},
58483
+ ...patch
58484
+ });
58485
+ }
57289
58486
  function formatIssueClasses(values) {
57290
58487
  return values.map((v) => INTAKE_ISSUE_CLASS_LABELS[v]).join(", ");
57291
58488
  }
@@ -57317,22 +58514,113 @@ function formatAnswerValue(key, answers) {
57317
58514
  }
57318
58515
  }
57319
58516
  }
58517
+ var NOT_ANSWERED = "(not answered)";
57320
58518
  function formatQuestionnaireShow(input) {
57321
58519
  const submitted = input.submitted === true;
57322
58520
  const editableLine = input.editable ? "yes" : input.lockedReason === "ENGAGEMENT_ENDED" ? "no (engagement has ended)" : input.lockedReason === "TESTING_STARTED" ? "no (testing has started)" : "no";
57323
- const lines = [`Editable: ${editableLine}`, `Submitted: ${submitted ? "yes" : "no"}`];
57324
- if (input.answers === null || Object.keys(input.answers).length === 0) {
57325
- lines.push("Answers: (none)");
57326
- return lines.join("\n");
57327
- }
57328
- lines.push("Answers:");
58521
+ const answers = input.answers ?? {};
58522
+ const lines = [
58523
+ `Editable: ${editableLine}`,
58524
+ `Submitted: ${submitted ? "yes" : "no"}`,
58525
+ `Answers: ${countAnsweredIntakeQuestions(answers)} of ${PENTEST_CUSTOMER_INTAKE_QUESTION_COUNT} questions filled`
58526
+ ];
57329
58527
  for (const key of PENTEST_CUSTOMER_INTAKE_ANSWER_KEYS) {
57330
- const value = formatAnswerValue(key, input.answers);
57331
- if (value === null) {
57332
- continue;
58528
+ const value = formatAnswerValue(key, answers) ?? NOT_ANSWERED;
58529
+ lines.push(`${PENTEST_CUSTOMER_INTAKE_LABELS[key]} (${key}): ${value}`);
58530
+ }
58531
+ return lines.join("\n");
58532
+ }
58533
+ function buildQuestionnaireExport(answers) {
58534
+ const n3 = normalizePentestCustomerIntakeAnswers(answers ?? {});
58535
+ const text = (value) => value === void 0 || value.trim() === "" ? null : value;
58536
+ const list = (value) => value === void 0 || value.length === 0 ? null : [...value];
58537
+ return {
58538
+ issueClasses: list(n3.issueClasses),
58539
+ issueClassesNotes: text(n3.issueClassesNotes),
58540
+ focusAreas: text(n3.focusAreas),
58541
+ rolesAndPermissions: text(n3.rolesAndPermissions),
58542
+ authenticationSetup: text(n3.authenticationSetup),
58543
+ tenantIsolationExpectation: text(n3.tenantIsolationExpectation),
58544
+ surfaces: list(n3.surfaces),
58545
+ surfacesNotes: text(n3.surfacesNotes),
58546
+ thirdParties: text(n3.thirdParties),
58547
+ sensitiveData: text(n3.sensitiveData),
58548
+ neverTouch: text(n3.neverTouch),
58549
+ knownRisks: text(n3.knownRisks),
58550
+ tribalKnowledge: text(n3.tribalKnowledge)
58551
+ };
58552
+ }
58553
+ function buildQuestionnaireRecordJson(input) {
58554
+ const toIsoString = (value) => typeof value === "string" ? value : value.toISOString();
58555
+ return {
58556
+ pentestId: input.pentestId,
58557
+ editable: input.editable,
58558
+ lockedReason: input.lockedReason ?? null,
58559
+ submitted: input.submittedAt != null,
58560
+ submittedAt: input.submittedAt == null ? null : toIsoString(input.submittedAt),
58561
+ updatedAt: toIsoString(input.updatedAt),
58562
+ answers: buildQuestionnaireExport(input.answers)
58563
+ };
58564
+ }
58565
+ var DOCUMENTS_ATTACH_COMMAND = "tracecli pentest documents attach --pentest <pentestId> --file <path>";
58566
+ function buildQuestionnaireCatalog() {
58567
+ const questions = PENTEST_CUSTOMER_INTAKE_QUESTIONS.map(
58568
+ (entry, index) => {
58569
+ if (entry.kind === "text") {
58570
+ return {
58571
+ number: index + 1,
58572
+ key: entry.key,
58573
+ kind: "text",
58574
+ question: entry.question,
58575
+ helper: entry.helper ?? null,
58576
+ options: null,
58577
+ notes: null
58578
+ };
58579
+ }
58580
+ return {
58581
+ number: index + 1,
58582
+ key: entry.key,
58583
+ kind: "multiSelect",
58584
+ question: entry.question,
58585
+ helper: entry.helper ?? null,
58586
+ options: entry.key === "issueClasses" ? issueClassSelectOptions() : surfaceSelectOptions(),
58587
+ notes: { key: entry.notesKey, question: entry.notesQuestion }
58588
+ };
58589
+ }
58590
+ );
58591
+ return {
58592
+ questions,
58593
+ documents: {
58594
+ question: PENTEST_INTAKE_DOCUMENTS_QUESTION,
58595
+ command: DOCUMENTS_ATTACH_COMMAND,
58596
+ types: PentestIntakeDocumentTypeValues.map((value) => ({
58597
+ value,
58598
+ label: PENTEST_INTAKE_DOCUMENT_TYPE_LABELS[value]
58599
+ }))
58600
+ }
58601
+ };
58602
+ }
58603
+ function formatQuestionnaireCatalog(catalog) {
58604
+ const lines = [];
58605
+ for (const entry of catalog.questions) {
58606
+ lines.push(`${entry.number}. ${entry.question}`);
58607
+ if (entry.helper !== null) {
58608
+ lines.push(` ${entry.helper}`);
57333
58609
  }
57334
- lines.push(`${PENTEST_CUSTOMER_INTAKE_LABELS[key]}: ${value}`);
58610
+ lines.push(` key: ${entry.key} (${entry.kind === "text" ? "text" : "multi-select"})`);
58611
+ if (entry.options !== null) {
58612
+ lines.push(` options: ${entry.options.map((o2) => `${o2.value} (${o2.label})`).join(", ")}`);
58613
+ }
58614
+ if (entry.notes !== null) {
58615
+ lines.push(` notes key: ${entry.notes.key} (text): ${entry.notes.question}`);
58616
+ }
58617
+ lines.push("");
57335
58618
  }
58619
+ lines.push(`${catalog.questions.length + 1}. ${catalog.documents.question}`);
58620
+ lines.push(` Files, not an answers key. Attach with: ${catalog.documents.command}`);
58621
+ lines.push(
58622
+ ` types: ${catalog.documents.types.map((t2) => `${t2.value} (${t2.label})`).join(", ")}`
58623
+ );
57336
58624
  return lines.join("\n");
57337
58625
  }
57338
58626
  function issueClassSelectOptions() {
@@ -57431,7 +58719,7 @@ function formatFindingSeverity(severity) {
57431
58719
  return severity;
57432
58720
  }
57433
58721
  }
57434
- function createListCommand6() {
58722
+ function createListCommand7() {
57435
58723
  return new Command("list").description("List pentests for your organization").option(
57436
58724
  "--status <status>",
57437
58725
  "Filter by status (SCOPING, IN_PROGRESS, REMEDIATION, CLOSED)",
@@ -57994,46 +59282,6 @@ Examples:
57994
59282
  await runUpdate5(pentestId, options);
57995
59283
  });
57996
59284
  }
57997
- function failQuestionnaire(status, body, action, notFoundLabel) {
57998
- const { message } = readErrorFields(body);
57999
- if (status === UNAUTHORIZED_STATUS2) {
58000
- exitAuthFailed();
58001
- }
58002
- if (status === 403) {
58003
- if (action === "write") {
58004
- console.error(
58005
- colorError(
58006
- "Questionnaire writes require an organization admin (or Trace staff) signed in with a browser session. API keys are rejected."
58007
- )
58008
- );
58009
- } else {
58010
- console.error(
58011
- colorError(message ?? "You do not have permission to view this questionnaire.")
58012
- );
58013
- }
58014
- process.exit(1);
58015
- }
58016
- if (status === 404) {
58017
- console.error(
58018
- colorError(notFoundLabel ? `Pentest not found: ${notFoundLabel}.` : message ?? "Not found.")
58019
- );
58020
- process.exit(1);
58021
- }
58022
- if (status === 400) {
58023
- console.error(colorError(message ?? "Invalid questionnaire answers (status 400)."));
58024
- process.exit(1);
58025
- }
58026
- if (status === 409) {
58027
- console.error(colorError(message ?? QUESTIONNAIRE_LOCKED_MESSAGE));
58028
- process.exit(1);
58029
- }
58030
- console.error(
58031
- colorError(
58032
- `Failed to ${action === "write" ? "save" : "load"} questionnaire (status ${status})${message ? `: ${message}` : "."}`
58033
- )
58034
- );
58035
- process.exit(1);
58036
- }
58037
59285
  async function fetchQuestionnaire(organizationId, pentestId, notFoundLabel) {
58038
59286
  const result = await authenticatedRequest(
58039
59287
  (client) => client.pentest.getCustomerPentestQuestionnaire({
@@ -58041,7 +59289,11 @@ async function fetchQuestionnaire(organizationId, pentestId, notFoundLabel) {
58041
59289
  })
58042
59290
  );
58043
59291
  if (result.status !== SUCCESS_STATUS) {
58044
- failQuestionnaire(result.status, result.body, "read", notFoundLabel);
59292
+ failIntakeRequest(result.status, result.body, {
59293
+ subject: "questionnaire",
59294
+ action: "read",
59295
+ notFound: { what: "Pentest", label: notFoundLabel }
59296
+ });
58045
59297
  }
58046
59298
  return toQuestionnaireGetBody(result.body);
58047
59299
  }
@@ -58054,7 +59306,11 @@ async function putQuestionnaire(organizationId, pentestId, answers, notFoundLabe
58054
59306
  })
58055
59307
  );
58056
59308
  if (result.status !== SUCCESS_STATUS) {
58057
- failQuestionnaire(result.status, result.body, "write", notFoundLabel);
59309
+ failIntakeRequest(result.status, result.body, {
59310
+ subject: "questionnaire",
59311
+ action: "write",
59312
+ notFound: { what: "Pentest", label: notFoundLabel }
59313
+ });
58058
59314
  }
58059
59315
  return toQuestionnaireGetBody(result.body);
58060
59316
  }
@@ -58066,7 +59322,11 @@ async function submitQuestionnaire(organizationId, pentestId, answers, notFoundL
58066
59322
  })
58067
59323
  );
58068
59324
  if (result.status !== SUCCESS_STATUS) {
58069
- failQuestionnaire(result.status, result.body, "write", notFoundLabel);
59325
+ failIntakeRequest(result.status, result.body, {
59326
+ subject: "questionnaire",
59327
+ action: "write",
59328
+ notFound: { what: "Pentest", label: notFoundLabel }
59329
+ });
58070
59330
  }
58071
59331
  return toQuestionnaireGetBody(result.body);
58072
59332
  }
@@ -58149,35 +59409,85 @@ async function promptQuestionnaireAnswers(existing) {
58149
59409
  }
58150
59410
  return working;
58151
59411
  }
58152
- async function runQuestionnaire(pentestIdentifier, options) {
58153
- const show = options.show === true;
58154
- const filePath = options.file;
58155
- const submit = options.submit === true;
58156
- if (show && filePath !== void 0) {
58157
- console.error(colorError("Pass only one of --show / --file."));
59412
+ function resolveQuestionnaireMode(options) {
59413
+ const exclusive = [
59414
+ { flag: "--show", set: options.show === true },
59415
+ { flag: "--questions", set: options.questions === true },
59416
+ { flag: "--export", set: options.export === true },
59417
+ { flag: "--file", set: options.file !== void 0 }
59418
+ ];
59419
+ const chosen = exclusive.filter((entry) => entry.set);
59420
+ if (chosen.length > 1) {
59421
+ console.error(colorError(`Pass only one of ${chosen.map((c84) => c84.flag).join(" / ")}.`));
59422
+ process.exit(1);
59423
+ }
59424
+ const only = chosen[0];
59425
+ if (only !== void 0 && only.flag !== "--file" && options.submit === true) {
59426
+ console.error(colorError(`Pass only one of ${only.flag} / --submit.`));
58158
59427
  process.exit(1);
58159
59428
  }
58160
- if (show && submit) {
58161
- console.error(colorError("Pass only one of --show / --submit."));
59429
+ if (options.merge === true && options.file === void 0) {
59430
+ console.error(colorError("--merge needs --file <path>."));
58162
59431
  process.exit(1);
58163
59432
  }
59433
+ if (options.show) return { kind: "show" };
59434
+ if (options.questions) return { kind: "questions" };
59435
+ if (options.export) return { kind: "export" };
59436
+ if (options.file !== void 0) {
59437
+ return { kind: "file", path: options.file, merge: options.merge === true };
59438
+ }
59439
+ if (options.submit) return { kind: "submit" };
59440
+ return { kind: "interactive" };
59441
+ }
59442
+ function printQuestionnaireRecord(body, output, successLine) {
59443
+ if (output === "json") {
59444
+ printJson(buildQuestionnaireRecordJson(body));
59445
+ return;
59446
+ }
59447
+ if (successLine !== void 0) {
59448
+ console.log(colorSuccess(successLine));
59449
+ }
59450
+ console.log(formatQuestionnaireShow(questionnaireShowInput(body)));
59451
+ }
59452
+ function submittedLine(record2) {
59453
+ return record2.editable ? "Submitted questionnaire. You can edit until testing starts." : "Submitted questionnaire.";
59454
+ }
59455
+ async function runQuestionnaire(pentestIdentifier, options) {
59456
+ const mode = resolveQuestionnaireMode(options);
59457
+ const output = parseTextOutput(options.output);
59458
+ const submit = options.submit === true;
59459
+ if (mode.kind === "questions") {
59460
+ const catalog = buildQuestionnaireCatalog();
59461
+ if (output === "json") {
59462
+ printJson(catalog);
59463
+ } else {
59464
+ console.log(formatQuestionnaireCatalog(catalog));
59465
+ }
59466
+ return;
59467
+ }
58164
59468
  const isInteractiveTty = Boolean(process.stdin.isTTY && process.stdout.isTTY);
58165
- if (!show && filePath === void 0 && !submit && !isInteractiveTty) {
59469
+ if (mode.kind === "interactive" && !isInteractiveTty) {
58166
59470
  console.error(
58167
59471
  colorError(
58168
- "Interactive questionnaire requires a TTY. Use --show to print answers, --file <path> to upload an answers JSON object, or --submit to mark the current answers done."
59472
+ "Interactive questionnaire requires a TTY. Without one: --questions lists every question with its answer key, --export prints the current answers as JSON (blanks as null), --file <path> --merge uploads only the keys the file sets, --show prints the saved state, --submit marks it done. --file without --merge replaces every saved answer, so keys missing or null in the file are cleared."
58169
59473
  )
58170
59474
  );
58171
59475
  process.exit(2);
58172
59476
  }
58173
59477
  const organizationId = await resolveOrganizationId();
58174
59478
  const pentestId = await resolvePentestId(organizationId, pentestIdentifier);
58175
- if (show) {
59479
+ if (mode.kind === "show") {
59480
+ const current2 = await fetchQuestionnaire(organizationId, pentestId, pentestIdentifier);
59481
+ printQuestionnaireRecord(current2, output);
59482
+ return;
59483
+ }
59484
+ if (mode.kind === "export") {
58176
59485
  const current2 = await fetchQuestionnaire(organizationId, pentestId, pentestIdentifier);
58177
- console.log(formatQuestionnaireShow(questionnaireShowInput(current2)));
59486
+ printJson(buildQuestionnaireExport(current2.answers));
58178
59487
  return;
58179
59488
  }
58180
- if (filePath !== void 0) {
59489
+ if (mode.kind === "file") {
59490
+ const filePath = mode.path;
58181
59491
  let raw;
58182
59492
  try {
58183
59493
  raw = readFileSync7(filePath, "utf8");
@@ -58190,28 +59500,27 @@ async function runQuestionnaire(pentestIdentifier, options) {
58190
59500
  console.error(colorError(parsed.error));
58191
59501
  process.exit(1);
58192
59502
  }
58193
- const saved2 = submit ? await submitQuestionnaire(organizationId, pentestId, parsed.answers, pentestIdentifier) : await putQuestionnaire(organizationId, pentestId, parsed.answers, pentestIdentifier);
58194
- console.log(
58195
- colorSuccess(
58196
- submit ? "Submitted questionnaire. You can edit until testing starts." : "Saved questionnaire answers."
58197
- )
59503
+ let answers = parsed.answers;
59504
+ if (mode.merge) {
59505
+ const current2 = await fetchQuestionnaire(organizationId, pentestId, pentestIdentifier);
59506
+ answers = mergeIntakeAnswers(current2.answers, parsed.answers);
59507
+ }
59508
+ const saved2 = submit ? await submitQuestionnaire(organizationId, pentestId, answers, pentestIdentifier) : await putQuestionnaire(organizationId, pentestId, answers, pentestIdentifier);
59509
+ printQuestionnaireRecord(
59510
+ saved2,
59511
+ output,
59512
+ submit ? submittedLine(saved2) : "Saved questionnaire answers."
58198
59513
  );
58199
- console.log(formatQuestionnaireShow(questionnaireShowInput(saved2)));
58200
59514
  return;
58201
59515
  }
58202
- if (submit) {
59516
+ if (mode.kind === "submit") {
58203
59517
  const submitted = await submitQuestionnaire(
58204
59518
  organizationId,
58205
59519
  pentestId,
58206
59520
  void 0,
58207
59521
  pentestIdentifier
58208
59522
  );
58209
- console.log(
58210
- colorSuccess(
58211
- submitted.editable ? "Submitted questionnaire. You can edit until testing starts." : "Submitted questionnaire."
58212
- )
58213
- );
58214
- console.log(formatQuestionnaireShow(questionnaireShowInput(submitted)));
59523
+ printQuestionnaireRecord(submitted, output, submittedLine(submitted));
58215
59524
  return;
58216
59525
  }
58217
59526
  const current = await fetchQuestionnaire(organizationId, pentestId, pentestIdentifier);
@@ -58225,32 +59534,48 @@ async function runQuestionnaire(pentestIdentifier, options) {
58225
59534
  }
58226
59535
  const merged = await promptQuestionnaireAnswers(current.answers);
58227
59536
  const saved = await putQuestionnaire(organizationId, pentestId, merged, pentestIdentifier);
58228
- console.log(colorSuccess("Saved questionnaire answers."));
58229
- console.log(formatQuestionnaireShow(questionnaireShowInput(saved)));
59537
+ printQuestionnaireRecord(saved, output, "Saved questionnaire answers.");
58230
59538
  }
58231
59539
  function createQuestionnaireCommand() {
58232
- return new Command("questionnaire").description("View or fill the customer pentest intake questionnaire").argument("<pentestId>", "Pentest display ID (e.g. PT-3) or UUID").option("--show", "Print current answers using shared labels (GET)").option(
59540
+ return new Command("questionnaire").description("View or fill the customer pentest intake questionnaire").argument("<pentestId>", "Pentest display ID (e.g. PT-3) or UUID").option("--show", "Print every question with its answer key and current answer (GET)").option(
59541
+ "--questions",
59542
+ "Print the question catalog: keys, kinds, option values (no auth needed)"
59543
+ ).option("--export", "Print the current answers as JSON, every key present, blanks as null").option(
58233
59544
  "--file <path>",
58234
- "Upload answers from a JSON file (answers object only \u2014 not { answers }; whole-object replace)"
58235
- ).option("--submit", "Mark the questionnaire as done (still editable until testing starts)").addHelpText(
59545
+ "Upload answers from a JSON file (the answers object itself). Replaces every saved answer: keys missing or null in the file are cleared unless you add --merge"
59546
+ ).option(
59547
+ "--merge",
59548
+ "With --file: change only the keys the file sets; keep every other saved answer"
59549
+ ).option("--submit", "Mark the questionnaire as done (still editable until testing starts)").option("-o, --output <format>", "Output format: text, json (default: text)", "text").addHelpText(
58236
59550
  "after",
58237
59551
  `
58238
- Optional scoping questionnaire. Editable until testing starts. Reads accept a
58239
- browser session or API key;
58240
- writes require an organization admin (or Trace staff) signed in via \`tracecli
58241
- auth login\` \u2014 API keys are rejected.
59552
+ Optional scoping questionnaire, editable until testing starts.
59553
+ Every mode but --questions needs a browser session from \`tracecli auth login\`;
59554
+ API keys are rejected. Writes (--file, --submit, the TTY fill) also need an
59555
+ organization admin (or Trace staff).
58242
59556
 
58243
59557
  Modes:
58244
- --show GET and print key: value with shared customer-facing labels
58245
- --file <path> PUT the file's answers object (CLI wraps { answers } for the API)
59558
+ --questions The question catalog with answer keys and option values
59559
+ --show GET and print every question with its key and current answer
59560
+ --export GET and print the answers object as JSON (all keys, null when blank)
59561
+ --file <path> PUT the file's answers object, replacing every saved answer;
59562
+ add --merge to change only the keys the file sets
58246
59563
  --submit Mark as done (combine with --file or TTY fill, or alone)
58247
59564
  (TTY default) load-merge-write: GET, prompt each question, confirm, PUT
58248
59565
 
58249
- Non-interactive shells must pass --show, --file, or --submit (exit 2 otherwise).
59566
+ Every mode takes -o json. Non-interactive shells must pass one of the flags
59567
+ above (exit 2 otherwise).
59568
+
59569
+ Scripted fill (what a coding agent runs):
59570
+ tracecli pentest questionnaire PT-1 --questions
59571
+ tracecli pentest questionnaire PT-1 --export > answers.json
59572
+ # fill in the null values the code can answer, then
59573
+ tracecli pentest questionnaire PT-1 --file answers.json --merge
59574
+ tracecli pentest documents attach --pentest PT-1 --file ./threat-model.pdf
58250
59575
 
58251
59576
  Examples:
58252
59577
  tracecli pentest questionnaire PT-1 --show
58253
- tracecli pentest questionnaire PT-1 --file answers.json
59578
+ tracecli pentest questionnaire PT-1 --show -o json
58254
59579
  tracecli pentest questionnaire PT-1 --file answers.json --submit
58255
59580
  tracecli pentest questionnaire PT-1 --submit
58256
59581
  tracecli pentest questionnaire PT-1`
@@ -58260,11 +59585,12 @@ Examples:
58260
59585
  }
58261
59586
  function createPentestCommand() {
58262
59587
  const pentest = new Command("pentest").description("Manage pentests");
58263
- pentest.addCommand(createListCommand6());
59588
+ pentest.addCommand(createListCommand7());
58264
59589
  pentest.addCommand(createUpdateCommand4());
58265
59590
  pentest.addCommand(createCreatePentestCommand(), { hidden: true });
58266
59591
  pentest.addCommand(createVisibilityCommand(), { hidden: true });
58267
59592
  pentest.addCommand(createQuestionnaireCommand());
59593
+ pentest.addCommand(createDocumentsCommand());
58268
59594
  pentest.addCommand(createFindingsCommand());
58269
59595
  pentest.addCommand(createHypothesesCommand(), { hidden: true });
58270
59596
  pentest.addCommand(createReportCommand());
@@ -58901,7 +60227,7 @@ Examples:
58901
60227
  // src/upload/download.ts
58902
60228
  import { createHash as createHash2 } from "crypto";
58903
60229
  import { open as open3, rename as rename2, rm as rm2, stat as stat2 } from "fs/promises";
58904
- import { basename as basename5, join as join8, resolve as resolve2 } from "path";
60230
+ import { basename as basename6, join as join8, resolve as resolve2 } from "path";
58905
60231
  var CodeUploadDownloadError = class extends Error {
58906
60232
  constructor(message) {
58907
60233
  super(message);
@@ -58911,7 +60237,7 @@ var CodeUploadDownloadError = class extends Error {
58911
60237
  function safeDownloadFilename(upload) {
58912
60238
  const extension = requiredExtensionForKind(upload.artifactKind);
58913
60239
  const fallback = `app${extension}`;
58914
- const stripped = Array.from(basename5(upload.originalFilename.replace(/\\/g, "/")), (char) => {
60240
+ const stripped = Array.from(basename6(upload.originalFilename.replace(/\\/g, "/")), (char) => {
58915
60241
  const code = char.codePointAt(0) ?? 0;
58916
60242
  return code <= 31 || code === 127 ? "_" : char;
58917
60243
  }).join("").trim();
@@ -59326,13 +60652,13 @@ function buildUploadsTable(uploads) {
59326
60652
  size: formatArchiveBytes(upload.sizeBytes),
59327
60653
  id: upload.codeUploadId
59328
60654
  }));
59329
- const nameWidth = Math.max(4, ...cells.map((c83) => c83.name.length));
59330
- const kindWidth = Math.max(4, ...cells.map((c83) => c83.kind.length));
59331
- const statusWidth = Math.max(6, ...cells.map((c83) => c83.status.length));
59332
- const sizeWidth = Math.max(4, ...cells.map((c83) => c83.size.length));
60655
+ const nameWidth = Math.max(4, ...cells.map((c84) => c84.name.length));
60656
+ const kindWidth = Math.max(4, ...cells.map((c84) => c84.kind.length));
60657
+ const statusWidth = Math.max(6, ...cells.map((c84) => c84.status.length));
60658
+ const sizeWidth = Math.max(4, ...cells.map((c84) => c84.size.length));
59333
60659
  const header = `${"NAME".padEnd(nameWidth)} ${"KIND".padEnd(kindWidth)} ${"STATUS".padEnd(statusWidth)} ${"SIZE".padEnd(sizeWidth)} ID`;
59334
60660
  const rows = cells.map(
59335
- (c83) => `${c83.name.padEnd(nameWidth)} ${c83.kind.padEnd(kindWidth)} ${c83.status.padEnd(statusWidth)} ${c83.size.padEnd(sizeWidth)} ${c83.id}`
60661
+ (c84) => `${c84.name.padEnd(nameWidth)} ${c84.kind.padEnd(kindWidth)} ${c84.status.padEnd(statusWidth)} ${c84.size.padEnd(sizeWidth)} ${c84.id}`
59336
60662
  );
59337
60663
  return { header, rows };
59338
60664
  }
@@ -59686,7 +61012,7 @@ async function runResume(codeUploadIdArg, filePath, options, runtime) {
59686
61012
  }
59687
61013
  outputUpload(upload, output);
59688
61014
  }
59689
- async function runList5(options) {
61015
+ async function runList6(options) {
59690
61016
  const output = parseOutput(options.output);
59691
61017
  if (options.web) {
59692
61018
  const slug = await resolveOrganizationSlug();
@@ -59819,7 +61145,7 @@ async function runDelete5(codeUploadIdArg, options) {
59819
61145
  }
59820
61146
  console.log(colorSuccess(`Deleted upload ${codeUploadId}`));
59821
61147
  }
59822
- async function runLink(options) {
61148
+ async function runLink2(options) {
59823
61149
  const output = parseOutput(options.output);
59824
61150
  const applicationId = requireUuid(options.app, "--app");
59825
61151
  const codeUploadId = requireUuid(options.upload, "--upload");
@@ -59901,9 +61227,9 @@ Examples:
59901
61227
  await runBuild(file2, options, runtime);
59902
61228
  });
59903
61229
  }
59904
- function createListCommand7() {
61230
+ function createListCommand8() {
59905
61231
  return new Command("list").description("List source and build uploads in the active organization").option("--kind <kind>", "Filter: source (code ZIP), apk, or ipa").option("--app <applicationId>", "Filter build uploads for one application").option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").option("--no-pager", "Disable the interactive pager for table output").option("-w, --web", "Open the source uploads page in a browser").action(async (options) => {
59906
- await runList5(options);
61232
+ await runList6(options);
59907
61233
  });
59908
61234
  }
59909
61235
  function createGetCommand4() {
@@ -59951,7 +61277,7 @@ function createDeleteCommand5() {
59951
61277
  await runDelete5(codeUploadId, options);
59952
61278
  });
59953
61279
  }
59954
- function createLinkCommand() {
61280
+ function createLinkCommand2() {
59955
61281
  return new Command("link").description("Link a ready source upload to an application").requiredOption("--app <applicationId>", "Application UUID").requiredOption("--upload <codeUploadId>", "Upload UUID (must be a ready source ZIP)").option("--path <repositoryPath>", "Path within the archive").option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").addHelpText(
59956
61282
  "after",
59957
61283
  `
@@ -59961,7 +61287,7 @@ APK and IPA builds attach to an application with \`upload build --app\`.
59961
61287
  Examples:
59962
61288
  tracecli upload link --app <applicationId> --upload <codeUploadId>`
59963
61289
  ).action(async (options) => {
59964
- await runLink(options);
61290
+ await runLink2(options);
59965
61291
  });
59966
61292
  }
59967
61293
  function createUploadCommand(runtime = {}) {
@@ -59969,13 +61295,13 @@ function createUploadCommand(runtime = {}) {
59969
61295
  const upload = new Command("upload").alias("code-upload").description("Upload source archives and mobile builds for pentest; download builds back");
59970
61296
  upload.addCommand(createCreateCommand5(resolved));
59971
61297
  upload.addCommand(createBuildCommand(resolved));
59972
- upload.addCommand(createListCommand7());
61298
+ upload.addCommand(createListCommand8());
59973
61299
  upload.addCommand(createGetCommand4());
59974
61300
  upload.addCommand(createDownloadCommand(resolved));
59975
61301
  upload.addCommand(createResumeCommand(resolved));
59976
61302
  upload.addCommand(createWaitCommand(resolved));
59977
61303
  upload.addCommand(createDeleteCommand5());
59978
- upload.addCommand(createLinkCommand());
61304
+ upload.addCommand(createLinkCommand2());
59979
61305
  return upload;
59980
61306
  }
59981
61307
 
@@ -60214,7 +61540,7 @@ Run \`tracecli vulnerabilities list\` to see available vulnerabilities.`
60214
61540
  printVulnerabilityDetail(result.body);
60215
61541
  });
60216
61542
  }
60217
- function createListCommand8() {
61543
+ function createListCommand9() {
60218
61544
  return new Command("list").description("List vulnerabilities for your organization").option("--repo <repo>", "Filter by repository name or owner/repo").option(
60219
61545
  "--severity <severity>",
60220
61546
  "Filter by severity (CRITICAL, HIGH, MEDIUM, LOW)",
@@ -60348,7 +61674,7 @@ Examples:
60348
61674
  function createVulnerabilitiesCommand() {
60349
61675
  const vulnerabilities = new Command("vulnerabilities").description("Manage vulnerabilities");
60350
61676
  vulnerabilities.addCommand(createGetCommand5());
60351
- vulnerabilities.addCommand(createListCommand8());
61677
+ vulnerabilities.addCommand(createListCommand9());
60352
61678
  return vulnerabilities;
60353
61679
  }
60354
61680