securewithtrace 0.18.0 → 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 (2) hide show
  1. package/dist/index.js +155 -7
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -26548,6 +26548,101 @@ function collectFilterExpression(value, previous) {
26548
26548
  previous.push(value);
26549
26549
  return previous;
26550
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
+ }
26551
26646
  function toRepositoryListFilterOptions(options) {
26552
26647
  return {
26553
26648
  ...options.platform ? { platform: options.platform } : {},
@@ -26764,24 +26859,66 @@ async function fetchCloneToken(repoUrl) {
26764
26859
  try {
26765
26860
  const organizationId = await resolveOrganizationIdSafe();
26766
26861
  if (!organizationId) {
26767
- return null;
26862
+ return { token: null, failure: resolveLocalCloneAuthFailure() };
26768
26863
  }
26769
- const result = await authenticatedRequestSafe(
26864
+ let result = await authenticatedRequestSafe(
26770
26865
  (client) => client.repository.getCloneToken({
26771
26866
  params: { organizationId },
26772
26867
  body: { repositoryUrl: repoUrl }
26773
26868
  })
26774
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
+ }
26775
26881
  if (!result) {
26776
- return null;
26882
+ return { token: null, failure: { kind: "request_failed" } };
26777
26883
  }
26778
26884
  if (result.status === 200) {
26779
26885
  const token = result.body.token.trim();
26780
- 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 } };
26781
26890
  }
26782
- 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
+ };
26783
26920
  } catch {
26784
- return null;
26921
+ return { token: null, failure: { kind: "request_failed" } };
26785
26922
  }
26786
26923
  }
26787
26924
  function createCloneCommand() {
@@ -26797,8 +26934,14 @@ Examples:
26797
26934
  const cloneUrl = resolveRepoUrl(repository);
26798
26935
  const targetDir = directory ?? extractRepoName(repository);
26799
26936
  let installationToken = null;
26937
+ let cloneTokenFailure = null;
26800
26938
  if (cloneUrl.startsWith("https://github.com/")) {
26801
- 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
+ }
26802
26945
  }
26803
26946
  const effectiveUrl = installationToken ? urlWithUsername(cloneUrl) : cloneUrl;
26804
26947
  let askPassScript;
@@ -26827,6 +26970,11 @@ Examples:
26827
26970
  });
26828
26971
  const exitCode = await proc.exited;
26829
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
+ }
26830
26978
  process.exit(1);
26831
26979
  }
26832
26980
  if (installationToken) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securewithtrace",
3
- "version": "0.18.0",
3
+ "version": "0.18.1",
4
4
  "description": "Official Trace CLI for vulnerability intelligence in your terminal",
5
5
  "type": "module",
6
6
  "bin": {