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.
- package/README.md +1 -0
- package/dist/index.js +252 -18
- package/package.json +1 -1
package/README.md
CHANGED
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("
|
|
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
|
|
25511
|
-
|
|
25512
|
-
|
|
25513
|
-
|
|
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
|
-
|
|
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 '${
|
|
25604
|
+
`Cannot switch to organization '${selectedOrganization.organizationId}': invalid organization ID format.`
|
|
25519
25605
|
);
|
|
25520
25606
|
process.exit(1);
|
|
25521
25607
|
}
|
|
25522
|
-
writeSelectedOrganizationId(
|
|
25608
|
+
writeSelectedOrganizationId(selectedOrganization.organizationId);
|
|
25523
25609
|
const refreshed = await refreshAfterOrganizationSwitch(
|
|
25524
|
-
|
|
25610
|
+
selectedOrganization.workosOrganizationId
|
|
25525
25611
|
);
|
|
25526
25612
|
console.log(
|
|
25527
|
-
`Active organization set to ${sanitizeForOutput(
|
|
25613
|
+
`Active organization set to ${sanitizeForOutput(selectedOrganization.name)} (${sanitizeForOutput(selectedOrganization.slug)})`
|
|
25528
25614
|
);
|
|
25529
|
-
console.log(` Org ID: ${
|
|
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
|
-
|
|
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
|
-
|
|
26886
|
+
if (token.length > 0) {
|
|
26887
|
+
return { token, failure: null };
|
|
26888
|
+
}
|
|
26889
|
+
return { token: null, failure: { kind: "unexpected_status", status: 200 } };
|
|
26695
26890
|
}
|
|
26696
|
-
|
|
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
|
-
|
|
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) {
|