securewithtrace 0.17.1 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/dist/index.js +252 -18
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -73,6 +73,7 @@ tracecli auth logout
73
73
  ```bash
74
74
  tracecli org list
75
75
  tracecli org current
76
+ tracecli org switch
76
77
  tracecli org switch <organization-id-or-slug>
77
78
  ```
78
79
 
package/dist/index.js CHANGED
@@ -3352,6 +3352,9 @@ var {
3352
3352
  Help
3353
3353
  } = import_index.default;
3354
3354
 
3355
+ // src/auth/commands.ts
3356
+ import { createInterface } from "readline/promises";
3357
+
3355
3358
  // ../../node_modules/.bun/@ts-rest+core@3.53.0-rc.1+f9199aabcde8d6a0/node_modules/@ts-rest/core/index.esm.mjs
3356
3359
  var util;
3357
3360
  (function(util2) {
@@ -25253,6 +25256,65 @@ Use an organization ID or exact slug.`
25253
25256
  error: `Organization '${input}' not found. Run \`tracecli org list\` to see available organizations.`
25254
25257
  };
25255
25258
  }
25259
+ function resolveOrganizationSelectionInput(organizations, input) {
25260
+ const trimmedInput = input.trim();
25261
+ if (trimmedInput.length === 0) {
25262
+ return { error: "Organization selection cannot be empty." };
25263
+ }
25264
+ if (/^\d+$/.test(trimmedInput)) {
25265
+ const byNumericId = organizations.find(
25266
+ (organization) => normalizeForComparison(organization.organizationId) === normalizeForComparison(trimmedInput)
25267
+ );
25268
+ if (byNumericId) {
25269
+ return { organization: byNumericId };
25270
+ }
25271
+ const selectedIndex = Number.parseInt(trimmedInput, 10);
25272
+ if (selectedIndex < 1 || selectedIndex > organizations.length) {
25273
+ return {
25274
+ error: `Selection '${trimmedInput}' is out of range. Enter a number between 1 and ${organizations.length}.`
25275
+ };
25276
+ }
25277
+ const selectedOrganization = organizations[selectedIndex - 1];
25278
+ if (!selectedOrganization) {
25279
+ return {
25280
+ error: "Failed to resolve organization selection. Please try again with an organization ID or slug."
25281
+ };
25282
+ }
25283
+ return { organization: selectedOrganization };
25284
+ }
25285
+ return findOrganizationByInput(organizations, trimmedInput);
25286
+ }
25287
+ async function promptForOrganizationSelection(organizations, activeOrganizationId) {
25288
+ console.log("Select an organization:");
25289
+ for (const [index, organization] of organizations.entries()) {
25290
+ const marker = organization.organizationId === activeOrganizationId ? "*" : " ";
25291
+ console.log(
25292
+ `${marker} ${index + 1}) ${sanitizeForOutput(organization.name)} (${sanitizeForOutput(organization.slug)}) - ${organization.organizationId}`
25293
+ );
25294
+ }
25295
+ console.log("");
25296
+ const readline = createInterface({
25297
+ input: process.stdin,
25298
+ output: process.stdout
25299
+ });
25300
+ try {
25301
+ while (true) {
25302
+ const input = await readline.question(
25303
+ "Enter number, organization ID, slug, or name (press Enter to cancel): "
25304
+ );
25305
+ if (input.trim().length === 0) {
25306
+ return null;
25307
+ }
25308
+ const selection = resolveOrganizationSelectionInput(organizations, input);
25309
+ if ("organization" in selection) {
25310
+ return selection.organization;
25311
+ }
25312
+ console.error(selection.error);
25313
+ }
25314
+ } finally {
25315
+ readline.close();
25316
+ }
25317
+ }
25256
25318
  async function refreshAfterOrganizationSwitch(workosOrganizationId, refreshForOrganization = (organizationId) => refreshStoredAccessTokenForOrganization(organizationId)) {
25257
25319
  if (!workosOrganizationId || workosOrganizationId.trim().length === 0) {
25258
25320
  return false;
@@ -25500,33 +25562,57 @@ Examples:
25500
25562
  });
25501
25563
  }
25502
25564
  function createOrgSwitchCommand() {
25503
- return new Command("switch").description("Switch the active organization for subsequent CLI commands").argument("<organization>", "Organization ID, slug, or name").addHelpText(
25565
+ return new Command("switch").description("Switch the active organization for subsequent CLI commands").argument("[organization]", "Organization ID, slug, name, or menu selection index").addHelpText(
25504
25566
  "after",
25505
25567
  `
25506
25568
  Examples:
25569
+ tracecli org switch
25507
25570
  tracecli org switch engineering
25508
25571
  tracecli org switch org_01KJTJFFG497Z1C54Z07AP6NA3`
25509
25572
  ).action(async (organization) => {
25510
- const organizationContext = await fetchOrganizationContext();
25511
- const match = findOrganizationByInput(organizationContext.organizations, organization);
25512
- if ("error" in match) {
25513
- console.error(match.error);
25573
+ const normalizedOrganizationInput = organization?.trim() ?? "";
25574
+ if (normalizedOrganizationInput.length === 0 && (!process.stdin.isTTY || !process.stdout.isTTY)) {
25575
+ console.error(
25576
+ "Missing required argument 'organization'. Run `tracecli org switch <organization>` in non-interactive environments."
25577
+ );
25514
25578
  process.exit(1);
25515
25579
  }
25516
- if (!isSafeOrganizationId(match.organization.organizationId)) {
25580
+ const organizationContext = await fetchOrganizationContext();
25581
+ let selectedOrganization = null;
25582
+ if (normalizedOrganizationInput.length > 0) {
25583
+ const selection = resolveOrganizationSelectionInput(
25584
+ organizationContext.organizations,
25585
+ normalizedOrganizationInput
25586
+ );
25587
+ if ("error" in selection) {
25588
+ console.error(selection.error);
25589
+ process.exit(1);
25590
+ }
25591
+ selectedOrganization = selection.organization;
25592
+ } else {
25593
+ selectedOrganization = await promptForOrganizationSelection(
25594
+ organizationContext.organizations,
25595
+ organizationContext.activeOrganization.organizationId
25596
+ );
25597
+ if (!selectedOrganization) {
25598
+ console.error("Organization switch cancelled.");
25599
+ process.exit(1);
25600
+ }
25601
+ }
25602
+ if (!isSafeOrganizationId(selectedOrganization.organizationId)) {
25517
25603
  console.error(
25518
- `Cannot switch to organization '${match.organization.organizationId}': invalid organization ID format.`
25604
+ `Cannot switch to organization '${selectedOrganization.organizationId}': invalid organization ID format.`
25519
25605
  );
25520
25606
  process.exit(1);
25521
25607
  }
25522
- writeSelectedOrganizationId(match.organization.organizationId);
25608
+ writeSelectedOrganizationId(selectedOrganization.organizationId);
25523
25609
  const refreshed = await refreshAfterOrganizationSwitch(
25524
- match.organization.workosOrganizationId
25610
+ selectedOrganization.workosOrganizationId
25525
25611
  );
25526
25612
  console.log(
25527
- `Active organization set to ${sanitizeForOutput(match.organization.name)} (${sanitizeForOutput(match.organization.slug)})`
25613
+ `Active organization set to ${sanitizeForOutput(selectedOrganization.name)} (${sanitizeForOutput(selectedOrganization.slug)})`
25528
25614
  );
25529
- console.log(` Org ID: ${match.organization.organizationId}`);
25615
+ console.log(` Org ID: ${selectedOrganization.organizationId}`);
25530
25616
  if (!refreshed) {
25531
25617
  console.warn(
25532
25618
  "Token refresh for the selected organization was not completed. If repository commands fail, run `tracecli auth login` again."
@@ -26462,6 +26548,101 @@ function collectFilterExpression(value, previous) {
26462
26548
  previous.push(value);
26463
26549
  return previous;
26464
26550
  }
26551
+ function extractErrorMessage(body) {
26552
+ if (typeof body === "object" && body !== null && "message" in body && typeof body.message === "string") {
26553
+ const message = body.message.trim();
26554
+ return message.length > 0 ? message : null;
26555
+ }
26556
+ return null;
26557
+ }
26558
+ function decodeJwtExpiration(token) {
26559
+ if (!token) {
26560
+ return null;
26561
+ }
26562
+ const parts = token.split(".");
26563
+ const payload = parts[1];
26564
+ if (!payload) {
26565
+ return null;
26566
+ }
26567
+ try {
26568
+ const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf-8"));
26569
+ if (typeof decoded.exp !== "number" || !Number.isFinite(decoded.exp)) {
26570
+ return null;
26571
+ }
26572
+ return new Date(decoded.exp * 1e3);
26573
+ } catch {
26574
+ return null;
26575
+ }
26576
+ }
26577
+ function resolveLocalCloneAuthFailure() {
26578
+ const token = readTokenSafe();
26579
+ if (!token) {
26580
+ return { kind: "not_logged_in", hasRefreshToken: readRefreshToken() !== null };
26581
+ }
26582
+ const expiresAt = decodeJwtExpiration(token);
26583
+ if (expiresAt && expiresAt.getTime() <= Date.now()) {
26584
+ return { kind: "expired_access_token", hasRefreshToken: readRefreshToken() !== null };
26585
+ }
26586
+ return { kind: "organization_unresolved" };
26587
+ }
26588
+ function buildCloneTokenFailureWarning(cloneUrl, failure) {
26589
+ const fallbackSuffix = "Falling back to standard git authentication for this clone attempt.";
26590
+ const quotedMessage = failure.message ? ` (${failure.message})` : "";
26591
+ switch (failure.kind) {
26592
+ case "not_logged_in":
26593
+ return `Trace clone credentials are unavailable for ${cloneUrl}: you are not logged in. Run \`tracecli auth login\`. ${fallbackSuffix}`;
26594
+ case "expired_access_token":
26595
+ return `Trace clone credentials are unavailable for ${cloneUrl}: your CLI access token is expired${failure.hasRefreshToken ? ", and refresh did not complete" : ""}. Run \`tracecli auth logout && tracecli auth login\`, then retry. ${fallbackSuffix}`;
26596
+ case "organization_unresolved":
26597
+ return `Trace clone credentials are unavailable for ${cloneUrl}: the active organization could not be resolved from your session. Run \`tracecli org current\` and \`tracecli org switch <organization>\`, or re-authenticate with \`tracecli auth logout && tracecli auth login\`. ${fallbackSuffix}`;
26598
+ case "request_failed":
26599
+ return `Trace clone credentials are temporarily unavailable for ${cloneUrl}: the request to the Trace API failed. Check connectivity, then retry. ${fallbackSuffix}`;
26600
+ case "unauthorized":
26601
+ return `Trace rejected clone credential generation for ${cloneUrl} (401${quotedMessage}). Re-authenticate with \`tracecli auth logout && tracecli auth login\`, then retry. ${fallbackSuffix}`;
26602
+ case "forbidden":
26603
+ return `Trace denied clone credential generation for ${cloneUrl} (403${quotedMessage}). Confirm you can access this organization and repository in Trace. ${fallbackSuffix}`;
26604
+ case "not_found":
26605
+ return `Trace could not generate clone credentials for ${cloneUrl} (404${quotedMessage}). Ensure the repository is connected in the active organization and the GitHub App installation is active. ${fallbackSuffix}`;
26606
+ case "server_error":
26607
+ return `Trace failed to generate clone credentials for ${cloneUrl} due to a server error${quotedMessage}. Retry shortly. ${fallbackSuffix}`;
26608
+ case "unexpected_status":
26609
+ return `Trace returned an unexpected response while generating clone credentials for ${cloneUrl}${quotedMessage ? ` ${quotedMessage}` : ""}. ${fallbackSuffix}`;
26610
+ }
26611
+ }
26612
+ function withOptionalMessage(base, message) {
26613
+ if (typeof message === "string") {
26614
+ return { ...base, message };
26615
+ }
26616
+ return base;
26617
+ }
26618
+ async function listOrganizationsSafe() {
26619
+ const result = await authenticatedRequestSafe(
26620
+ (client) => client.organization.listMyOrganizations()
26621
+ );
26622
+ if (!result || result.status !== 200) {
26623
+ return null;
26624
+ }
26625
+ return result.body;
26626
+ }
26627
+ function resolveWorkosOrganizationId(organizations, organizationId) {
26628
+ const organization = organizations.find((item) => item.organizationId === organizationId);
26629
+ const workosOrganizationId = organization?.workosOrganizationId;
26630
+ if (!workosOrganizationId || workosOrganizationId.trim().length === 0) {
26631
+ return null;
26632
+ }
26633
+ return workosOrganizationId;
26634
+ }
26635
+ async function maybeRefreshCloneTokenSessionForOrganization(organizationId, listOrganizations = listOrganizationsSafe, refreshForOrganization = (workosOrganizationId) => refreshStoredAccessTokenForOrganization(workosOrganizationId)) {
26636
+ const organizations = await listOrganizations();
26637
+ if (!organizations) {
26638
+ return false;
26639
+ }
26640
+ const workosOrganizationId = resolveWorkosOrganizationId(organizations, organizationId);
26641
+ if (!workosOrganizationId) {
26642
+ return false;
26643
+ }
26644
+ return refreshForOrganization(workosOrganizationId);
26645
+ }
26465
26646
  function toRepositoryListFilterOptions(options) {
26466
26647
  return {
26467
26648
  ...options.platform ? { platform: options.platform } : {},
@@ -26678,24 +26859,66 @@ async function fetchCloneToken(repoUrl) {
26678
26859
  try {
26679
26860
  const organizationId = await resolveOrganizationIdSafe();
26680
26861
  if (!organizationId) {
26681
- return null;
26862
+ return { token: null, failure: resolveLocalCloneAuthFailure() };
26682
26863
  }
26683
- const result = await authenticatedRequestSafe(
26864
+ let result = await authenticatedRequestSafe(
26684
26865
  (client) => client.repository.getCloneToken({
26685
26866
  params: { organizationId },
26686
26867
  body: { repositoryUrl: repoUrl }
26687
26868
  })
26688
26869
  );
26870
+ if (result && (result.status === 401 || result.status === 403)) {
26871
+ const refreshed = await maybeRefreshCloneTokenSessionForOrganization(organizationId);
26872
+ if (refreshed) {
26873
+ result = await authenticatedRequestSafe(
26874
+ (client) => client.repository.getCloneToken({
26875
+ params: { organizationId },
26876
+ body: { repositoryUrl: repoUrl }
26877
+ })
26878
+ );
26879
+ }
26880
+ }
26689
26881
  if (!result) {
26690
- return null;
26882
+ return { token: null, failure: { kind: "request_failed" } };
26691
26883
  }
26692
26884
  if (result.status === 200) {
26693
26885
  const token = result.body.token.trim();
26694
- return token.length > 0 ? token : null;
26886
+ if (token.length > 0) {
26887
+ return { token, failure: null };
26888
+ }
26889
+ return { token: null, failure: { kind: "unexpected_status", status: 200 } };
26695
26890
  }
26696
- return null;
26891
+ const message = extractErrorMessage(result.body) ?? void 0;
26892
+ if (result.status === 401) {
26893
+ return {
26894
+ token: null,
26895
+ failure: withOptionalMessage({ kind: "unauthorized", status: 401 }, message)
26896
+ };
26897
+ }
26898
+ if (result.status === 403) {
26899
+ return {
26900
+ token: null,
26901
+ failure: withOptionalMessage({ kind: "forbidden", status: 403 }, message)
26902
+ };
26903
+ }
26904
+ if (result.status === 404) {
26905
+ return {
26906
+ token: null,
26907
+ failure: withOptionalMessage({ kind: "not_found", status: 404 }, message)
26908
+ };
26909
+ }
26910
+ if (result.status >= 500) {
26911
+ return {
26912
+ token: null,
26913
+ failure: withOptionalMessage({ kind: "server_error", status: result.status }, message)
26914
+ };
26915
+ }
26916
+ return {
26917
+ token: null,
26918
+ failure: withOptionalMessage({ kind: "unexpected_status", status: result.status }, message)
26919
+ };
26697
26920
  } catch {
26698
- return null;
26921
+ return { token: null, failure: { kind: "request_failed" } };
26699
26922
  }
26700
26923
  }
26701
26924
  function createCloneCommand() {
@@ -26711,8 +26934,14 @@ Examples:
26711
26934
  const cloneUrl = resolveRepoUrl(repository);
26712
26935
  const targetDir = directory ?? extractRepoName(repository);
26713
26936
  let installationToken = null;
26937
+ let cloneTokenFailure = null;
26714
26938
  if (cloneUrl.startsWith("https://github.com/")) {
26715
- installationToken = await fetchCloneToken(cloneUrl);
26939
+ const cloneTokenResult = await fetchCloneToken(cloneUrl);
26940
+ installationToken = cloneTokenResult.token;
26941
+ cloneTokenFailure = cloneTokenResult.failure;
26942
+ if (cloneTokenFailure) {
26943
+ console.warn(buildCloneTokenFailureWarning(cloneUrl, cloneTokenFailure));
26944
+ }
26716
26945
  }
26717
26946
  const effectiveUrl = installationToken ? urlWithUsername(cloneUrl) : cloneUrl;
26718
26947
  let askPassScript;
@@ -26741,6 +26970,11 @@ Examples:
26741
26970
  });
26742
26971
  const exitCode = await proc.exited;
26743
26972
  if (exitCode !== 0) {
26973
+ if (!installationToken && cloneTokenFailure) {
26974
+ console.error(
26975
+ "Clone failed while Trace clone credentials were unavailable. Resolve the warning above and retry."
26976
+ );
26977
+ }
26744
26978
  process.exit(1);
26745
26979
  }
26746
26980
  if (installationToken) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securewithtrace",
3
- "version": "0.17.1",
3
+ "version": "0.18.1",
4
4
  "description": "Official Trace CLI for vulnerability intelligence in your terminal",
5
5
  "type": "module",
6
6
  "bin": {