@cargo-ai/cli 1.0.44 → 1.0.46
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 +50 -10
- package/build/api.d.ts +2 -1
- package/build/api.d.ts.map +1 -1
- package/build/api.js +5 -1
- package/build/commands/auth/auth0.d.ts +33 -0
- package/build/commands/auth/auth0.d.ts.map +1 -0
- package/build/commands/auth/auth0.js +90 -0
- package/build/commands/auth/email.d.ts +16 -0
- package/build/commands/auth/email.d.ts.map +1 -0
- package/build/commands/auth/email.js +93 -0
- package/build/commands/auth/index.d.ts +19 -1
- package/build/commands/auth/index.d.ts.map +1 -1
- package/build/commands/auth/index.js +121 -59
- package/build/commands/auth/loginChannel.d.ts +21 -0
- package/build/commands/auth/loginChannel.d.ts.map +1 -0
- package/build/commands/auth/loginChannel.js +42 -0
- package/build/commands/auth/oauth.d.ts +2 -6
- package/build/commands/auth/oauth.d.ts.map +1 -1
- package/build/commands/auth/oauth.js +54 -91
- package/build/commands/auth/passwordless.d.ts +22 -0
- package/build/commands/auth/passwordless.d.ts.map +1 -0
- package/build/commands/auth/passwordless.js +70 -0
- package/build/commands/auth/session.d.ts +21 -0
- package/build/commands/auth/session.d.ts.map +1 -0
- package/build/commands/auth/session.js +55 -0
- package/build/commands/auth/workspace.d.ts +9 -1
- package/build/commands/auth/workspace.d.ts.map +1 -1
- package/build/commands/auth/workspace.js +42 -69
- package/build/commands/billing/addPaymentMethod.d.ts +26 -0
- package/build/commands/billing/addPaymentMethod.d.ts.map +1 -0
- package/build/commands/billing/addPaymentMethod.js +57 -0
- package/build/commands/billing/subscription.d.ts.map +1 -1
- package/build/commands/billing/subscription.js +28 -1
- package/build/commands/doctor.js +3 -3
- package/build/commands/mcp.d.ts.map +1 -1
- package/build/commands/mcp.js +35 -8
- package/build/commands/orchestration/action.d.ts.map +1 -1
- package/build/commands/orchestration/action.js +11 -1
- package/build/commands/orchestration/index.d.ts.map +1 -1
- package/build/commands/orchestration/index.js +6 -1
- package/build/commands/orchestration/node.d.ts.map +1 -1
- package/build/commands/orchestration/node.js +41 -1
- package/build/commands/runHandler.d.ts +6 -0
- package/build/commands/runHandler.d.ts.map +1 -1
- package/build/commands/runHandler.js +34 -4
- package/build/config.d.ts +22 -1
- package/build/config.d.ts.map +1 -1
- package/build/config.js +44 -3
- package/build/credentials.d.ts +1 -10
- package/build/credentials.d.ts.map +1 -1
- package/build/credentials.js +4 -34
- package/build/index.js +14 -7
- package/build/oauthConfig.d.ts +1 -6
- package/build/oauthConfig.d.ts.map +1 -1
- package/build/oauthConfig.js +9 -14
- package/build/utils/openBrowser.d.ts +7 -0
- package/build/utils/openBrowser.d.ts.map +1 -0
- package/build/utils/openBrowser.js +29 -0
- package/build/utils/prompt.d.ts +12 -0
- package/build/utils/prompt.d.ts.map +1 -0
- package/build/utils/prompt.js +38 -0
- package/package.json +4 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loginChannel.d.ts","sourceRoot":"","sources":["../../../src/commands/auth/loginChannel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEpD,MAAM,MAAM,YAAY,GAAG,iBAAiB,GAAG;IAC7C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAC7B;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,GACpB;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,GACpB;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACnC;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtB;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,YAAY,GAAG,qBAAqB,CAmD5E"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Picks the sign-in channel and rejects flag combinations whose extra options
|
|
3
|
+
* would otherwise be accepted and quietly ignored.
|
|
4
|
+
*/
|
|
5
|
+
export function selectLoginChannel(opts) {
|
|
6
|
+
const wantsEmail = opts.email !== undefined;
|
|
7
|
+
const wantsOAuth = opts.oauth === true;
|
|
8
|
+
const wantsToken = opts.token !== undefined;
|
|
9
|
+
const selectedCount = [wantsEmail, wantsOAuth, wantsToken].filter((selected) => {
|
|
10
|
+
return selected === true;
|
|
11
|
+
}).length;
|
|
12
|
+
if (selectedCount > 1) {
|
|
13
|
+
return { error: "Pass only one of --email, --oauth or --token." };
|
|
14
|
+
}
|
|
15
|
+
if (selectedCount === 0) {
|
|
16
|
+
return {
|
|
17
|
+
error: "Must pass --email <email> (emailed code), --oauth (browser sign-in) or --token <token> (existing API token).",
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
if (opts.code !== undefined && wantsEmail === false) {
|
|
21
|
+
return { error: "--code only applies to --email <email>." };
|
|
22
|
+
}
|
|
23
|
+
if (opts.workspaceName !== undefined && opts.workspaceUuid !== undefined) {
|
|
24
|
+
return {
|
|
25
|
+
error: "Pass only one of --workspace-name and --workspace-uuid; they select different workspaces.",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
// An API token already carries its workspace, so creating one for it cannot
|
|
29
|
+
// be honoured and must not look like it was.
|
|
30
|
+
if (wantsToken === true && opts.workspaceName !== undefined) {
|
|
31
|
+
return {
|
|
32
|
+
error: "--workspace-name does not apply to --token; an API token is already scoped to a workspace.",
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
if (opts.email !== undefined) {
|
|
36
|
+
return { channel: "email" };
|
|
37
|
+
}
|
|
38
|
+
if (opts.token !== undefined) {
|
|
39
|
+
return { channel: "token", token: opts.token };
|
|
40
|
+
}
|
|
41
|
+
return { channel: "oauth" };
|
|
42
|
+
}
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
export
|
|
3
|
-
accessToken: string;
|
|
4
|
-
expiresIn: number;
|
|
5
|
-
};
|
|
6
|
-
export declare function runOAuthDeviceFlow(overrides: OAuthConfigOverrides): Promise<OAuthLoginResult>;
|
|
1
|
+
import { type IdentityTokens } from "./auth0.js";
|
|
2
|
+
export declare function runOAuthDeviceFlow(): Promise<IdentityTokens>;
|
|
7
3
|
//# sourceMappingURL=oauth.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../../../src/commands/auth/oauth.ts"],"names":[],"mappings":"AAGA,OAAO,
|
|
1
|
+
{"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../../../src/commands/auth/oauth.ts"],"names":[],"mappings":"AAGA,OAAO,EAIL,KAAK,cAAc,EAIpB,MAAM,YAAY,CAAC;AAepB,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,cAAc,CAAC,CAmBlE"}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { getOAuthConfig } from "../../oauthConfig.js";
|
|
2
|
+
import { openBrowser } from "../../utils/openBrowser.js";
|
|
3
|
+
import { failWith, info, sleep } from "../runHandler.js";
|
|
4
|
+
import { describeError, errorCodeOf, parseTokenResponse, postToAuth0, readErrorBody, } from "./auth0.js";
|
|
5
5
|
const DEFAULT_POLL_INTERVAL_SECONDS = 5;
|
|
6
6
|
const SLOW_DOWN_BACKOFF_SECONDS = 5;
|
|
7
7
|
const DEVICE_CODE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
|
8
|
-
export async function runOAuthDeviceFlow(
|
|
9
|
-
const
|
|
10
|
-
const deviceCode = await requestDeviceCode(oauthConfig);
|
|
8
|
+
export async function runOAuthDeviceFlow() {
|
|
9
|
+
const deviceCode = await requestDeviceCode();
|
|
11
10
|
info([
|
|
12
11
|
"",
|
|
13
12
|
"To complete sign-in, open this URL in a browser:",
|
|
@@ -19,27 +18,22 @@ export async function runOAuthDeviceFlow(overrides) {
|
|
|
19
18
|
"",
|
|
20
19
|
].join("\n"));
|
|
21
20
|
openBrowser(deviceCode.verification_uri_complete);
|
|
22
|
-
|
|
23
|
-
return {
|
|
24
|
-
accessToken: token.access_token,
|
|
25
|
-
expiresIn: token.expires_in,
|
|
26
|
-
};
|
|
21
|
+
return await pollForToken(deviceCode);
|
|
27
22
|
}
|
|
28
|
-
async function requestDeviceCode(
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
body: body.toString(),
|
|
23
|
+
async function requestDeviceCode() {
|
|
24
|
+
const config = getOAuthConfig();
|
|
25
|
+
const response = await postToAuth0({
|
|
26
|
+
path: "/oauth/device/code",
|
|
27
|
+
body: new URLSearchParams({
|
|
28
|
+
client_id: config.clientId,
|
|
29
|
+
scope: config.scope,
|
|
30
|
+
audience: config.audience,
|
|
31
|
+
screen_hint: "signup",
|
|
32
|
+
}),
|
|
39
33
|
});
|
|
40
34
|
if (response.ok === false) {
|
|
41
|
-
const
|
|
42
|
-
failWith(`Failed to start device authorization (HTTP ${String(response.status)}): ${
|
|
35
|
+
const body = await readErrorBody(response);
|
|
36
|
+
failWith(`Failed to start device authorization (HTTP ${String(response.status)}): ${describeError(body)}`, { extra: { oauthError: body } });
|
|
43
37
|
}
|
|
44
38
|
const json = (await response.json());
|
|
45
39
|
if (typeof json.device_code !== "string" ||
|
|
@@ -51,32 +45,36 @@ async function requestDeviceCode(config) {
|
|
|
51
45
|
}
|
|
52
46
|
return json;
|
|
53
47
|
}
|
|
54
|
-
async function pollForToken(
|
|
55
|
-
const
|
|
56
|
-
const expiresAtMs =
|
|
48
|
+
async function pollForToken(deviceCode) {
|
|
49
|
+
const config = getOAuthConfig();
|
|
50
|
+
const expiresAtMs = Date.now() + deviceCode.expires_in * 1000;
|
|
57
51
|
let intervalSeconds = deviceCode.interval > 0
|
|
58
52
|
? deviceCode.interval
|
|
59
53
|
: DEFAULT_POLL_INTERVAL_SECONDS;
|
|
60
54
|
while (Date.now() < expiresAtMs) {
|
|
61
55
|
await sleep(intervalSeconds * 1000);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
56
|
+
let response;
|
|
57
|
+
try {
|
|
58
|
+
response = await postToAuth0({
|
|
59
|
+
path: "/oauth/token",
|
|
60
|
+
body: new URLSearchParams({
|
|
61
|
+
grant_type: DEVICE_CODE_GRANT,
|
|
62
|
+
device_code: deviceCode.device_code,
|
|
63
|
+
client_id: config.clientId,
|
|
64
|
+
}),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// The user may already be mid-way through approving in the browser, so a
|
|
69
|
+
// dropped connection is not worth making them start over. Keep polling
|
|
70
|
+
// until the device code itself expires.
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
71
73
|
if (response.ok === true) {
|
|
72
|
-
|
|
73
|
-
if (typeof token.access_token !== "string") {
|
|
74
|
-
failWith(`Unexpected token response from OAuth provider: ${JSON.stringify(token)}`);
|
|
75
|
-
}
|
|
76
|
-
return token;
|
|
74
|
+
return parseTokenResponse(await response.json());
|
|
77
75
|
}
|
|
78
|
-
const errorBody =
|
|
79
|
-
const errorCode = errorBody
|
|
76
|
+
const errorBody = await readErrorBody(response);
|
|
77
|
+
const errorCode = errorCodeOf(errorBody);
|
|
80
78
|
if (errorCode === "authorization_pending") {
|
|
81
79
|
continue;
|
|
82
80
|
}
|
|
@@ -84,57 +82,22 @@ async function pollForToken(config, deviceCode) {
|
|
|
84
82
|
intervalSeconds = intervalSeconds + SLOW_DOWN_BACKOFF_SECONDS;
|
|
85
83
|
continue;
|
|
86
84
|
}
|
|
87
|
-
|
|
88
|
-
errorCode === "access_denied" ||
|
|
89
|
-
errorCode === "invalid_grant") {
|
|
90
|
-
failWith(`Authorization failed: ${errorCode}${errorBody !== undefined && errorBody.error_description !== undefined
|
|
91
|
-
? ` - ${errorBody.error_description}`
|
|
92
|
-
: ""}`, { extra: { oauthError: errorBody } });
|
|
93
|
-
}
|
|
94
|
-
failWith(`Token polling failed (HTTP ${String(response.status)}): ${errorBody !== undefined ? JSON.stringify(errorBody) : "no body"}`, { extra: { oauthError: errorBody } });
|
|
85
|
+
failIfTerminal(response.status, errorCode, errorBody);
|
|
95
86
|
}
|
|
96
87
|
failWith("Authorization timed out before the user completed the flow.");
|
|
97
88
|
}
|
|
98
|
-
function
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
stdio: "ignore",
|
|
89
|
+
function failIfTerminal(status, errorCode, errorBody) {
|
|
90
|
+
if (errorCode === "expired_token" ||
|
|
91
|
+
errorCode === "access_denied" ||
|
|
92
|
+
errorCode === "invalid_grant") {
|
|
93
|
+
failWith(`Authorization failed: ${describeError(errorBody)}`, {
|
|
94
|
+
extra: { oauthError: errorBody },
|
|
105
95
|
});
|
|
106
|
-
child.on("error", () => {
|
|
107
|
-
// Browser launch failed silently; user can still use the printed URL.
|
|
108
|
-
});
|
|
109
|
-
child.unref();
|
|
110
|
-
}
|
|
111
|
-
catch {
|
|
112
|
-
// Ignore; user can still use the printed URL.
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
function pickOpenCommand() {
|
|
116
|
-
if (platform === "darwin")
|
|
117
|
-
return "open";
|
|
118
|
-
if (platform === "win32")
|
|
119
|
-
return "cmd";
|
|
120
|
-
return "xdg-open";
|
|
121
|
-
}
|
|
122
|
-
function sleep(ms) {
|
|
123
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
124
|
-
}
|
|
125
|
-
async function safeReadText(response) {
|
|
126
|
-
try {
|
|
127
|
-
return await response.text();
|
|
128
|
-
}
|
|
129
|
-
catch {
|
|
130
|
-
return "<unreadable body>";
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
async function safeReadJson(response) {
|
|
134
|
-
try {
|
|
135
|
-
return await response.json();
|
|
136
96
|
}
|
|
137
|
-
|
|
138
|
-
|
|
97
|
+
// Rate limits and provider hiccups are transient: the browser half of the
|
|
98
|
+
// flow is still valid, so only give up once the device code expires.
|
|
99
|
+
if (status === 429 || status >= 500) {
|
|
100
|
+
return;
|
|
139
101
|
}
|
|
102
|
+
failWith(`Token polling failed (HTTP ${String(status)}): ${describeError(errorBody)}`, { extra: { oauthError: errorBody } });
|
|
140
103
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type IdentityTokens } from "./auth0.js";
|
|
2
|
+
/**
|
|
3
|
+
* Asks the identity provider to email a one-time code. This is the only step of
|
|
4
|
+
* the sign-in flow that needs no credential, which is what makes the flow
|
|
5
|
+
* usable from a terminal with no browser.
|
|
6
|
+
*/
|
|
7
|
+
export declare function startEmailPasswordless(opts: {
|
|
8
|
+
email: string;
|
|
9
|
+
}): Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* Trades the emailed code for an access token. `offline_access` is requested so
|
|
12
|
+
* the caller can refresh once a workspace exists: the workspace list is baked
|
|
13
|
+
* into the token's claims and a freshly created workspace is not in them yet.
|
|
14
|
+
*/
|
|
15
|
+
export declare function exchangeEmailOtp(opts: {
|
|
16
|
+
email: string;
|
|
17
|
+
otp: string;
|
|
18
|
+
}): Promise<IdentityTokens>;
|
|
19
|
+
export declare function refreshAccessToken(opts: {
|
|
20
|
+
refreshToken: string;
|
|
21
|
+
}): Promise<IdentityTokens>;
|
|
22
|
+
//# sourceMappingURL=passwordless.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"passwordless.d.ts","sourceRoot":"","sources":["../../../src/commands/auth/passwordless.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,cAAc,EAIpB,MAAM,YAAY,CAAC;AAMpB;;;;GAIG;AACH,wBAAsB,sBAAsB,CAAC,IAAI,EAAE;IACjD,KAAK,EAAE,MAAM,CAAC;CACf,GAAG,OAAO,CAAC,IAAI,CAAC,CAoBhB;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb,GAAG,OAAO,CAAC,cAAc,CAAC,CAiC1B;AAED,wBAAsB,kBAAkB,CAAC,IAAI,EAAE;IAC7C,YAAY,EAAE,MAAM,CAAC;CACtB,GAAG,OAAO,CAAC,cAAc,CAAC,CAqB1B"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { getOAuthConfig } from "../../oauthConfig.js";
|
|
2
|
+
import { failWith } from "../runHandler.js";
|
|
3
|
+
import { describeError, errorCodeOf, parseTokenResponse, postToAuth0, readErrorBody, } from "./auth0.js";
|
|
4
|
+
const PASSWORDLESS_OTP_GRANT = "http://auth0.com/oauth/grant-type/passwordless/otp";
|
|
5
|
+
const EMAIL_REALM = "email";
|
|
6
|
+
/**
|
|
7
|
+
* Asks the identity provider to email a one-time code. This is the only step of
|
|
8
|
+
* the sign-in flow that needs no credential, which is what makes the flow
|
|
9
|
+
* usable from a terminal with no browser.
|
|
10
|
+
*/
|
|
11
|
+
export async function startEmailPasswordless(opts) {
|
|
12
|
+
const config = getOAuthConfig();
|
|
13
|
+
const response = await postToAuth0({
|
|
14
|
+
path: "/passwordless/start",
|
|
15
|
+
body: {
|
|
16
|
+
client_id: config.clientId,
|
|
17
|
+
connection: EMAIL_REALM,
|
|
18
|
+
email: opts.email,
|
|
19
|
+
send: "code",
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
if (response.ok === false) {
|
|
23
|
+
const body = await readErrorBody(response);
|
|
24
|
+
failWith(`Failed to send the verification code to ${opts.email} (HTTP ${String(response.status)}): ${describeError(body)}`, { extra: { oauthError: body } });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Trades the emailed code for an access token. `offline_access` is requested so
|
|
29
|
+
* the caller can refresh once a workspace exists: the workspace list is baked
|
|
30
|
+
* into the token's claims and a freshly created workspace is not in them yet.
|
|
31
|
+
*/
|
|
32
|
+
export async function exchangeEmailOtp(opts) {
|
|
33
|
+
const config = getOAuthConfig();
|
|
34
|
+
const response = await postToAuth0({
|
|
35
|
+
path: "/oauth/token",
|
|
36
|
+
body: new URLSearchParams({
|
|
37
|
+
grant_type: PASSWORDLESS_OTP_GRANT,
|
|
38
|
+
client_id: config.clientId,
|
|
39
|
+
username: opts.email,
|
|
40
|
+
otp: opts.otp,
|
|
41
|
+
realm: EMAIL_REALM,
|
|
42
|
+
audience: config.audience,
|
|
43
|
+
scope: config.scope,
|
|
44
|
+
}),
|
|
45
|
+
});
|
|
46
|
+
if (response.ok === false) {
|
|
47
|
+
const body = await readErrorBody(response);
|
|
48
|
+
if (errorCodeOf(body) === "invalid_grant") {
|
|
49
|
+
failWith("That code is not valid or has expired. Codes are single-use and short-lived — request a new one by re-running without --code.", { extra: { oauthError: body } });
|
|
50
|
+
}
|
|
51
|
+
failWith(`Failed to verify the code (HTTP ${String(response.status)}): ${describeError(body)}`, { extra: { oauthError: body } });
|
|
52
|
+
}
|
|
53
|
+
return parseTokenResponse(await response.json());
|
|
54
|
+
}
|
|
55
|
+
export async function refreshAccessToken(opts) {
|
|
56
|
+
const config = getOAuthConfig();
|
|
57
|
+
const response = await postToAuth0({
|
|
58
|
+
path: "/oauth/token",
|
|
59
|
+
body: new URLSearchParams({
|
|
60
|
+
grant_type: "refresh_token",
|
|
61
|
+
client_id: config.clientId,
|
|
62
|
+
refresh_token: opts.refreshToken,
|
|
63
|
+
}),
|
|
64
|
+
});
|
|
65
|
+
if (response.ok === false) {
|
|
66
|
+
const body = await readErrorBody(response);
|
|
67
|
+
failWith(`Failed to refresh the access token (HTTP ${String(response.status)}): ${describeError(body)}`, { extra: { oauthError: body } });
|
|
68
|
+
}
|
|
69
|
+
return parseTokenResponse(await response.json());
|
|
70
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type IdentityTokens } from "./auth0.js";
|
|
2
|
+
import { type ResolvedWorkspace } from "./workspace.js";
|
|
3
|
+
export declare const DEFAULT_BASE_URL = "https://api.getcargo.io";
|
|
4
|
+
export declare function resolveBaseUrl(override: string | undefined): string;
|
|
5
|
+
/**
|
|
6
|
+
* Picks the workspace to work in and stores the signed-in session. Every
|
|
7
|
+
* sign-in channel ends here, so the credentials file looks the same however
|
|
8
|
+
* the user signed in.
|
|
9
|
+
*
|
|
10
|
+
* The access token is short-lived and deliberately not treated as the
|
|
11
|
+
* credential: the refresh token beside it is what keeps the CLI signed in, and
|
|
12
|
+
* it is renewed on demand rather than by minting an API token per machine.
|
|
13
|
+
*/
|
|
14
|
+
export declare function establishSession(opts: {
|
|
15
|
+
tokens: IdentityTokens;
|
|
16
|
+
/** Raw --base-url, so the default is not written to the credentials file. */
|
|
17
|
+
baseUrlOverride: string | undefined;
|
|
18
|
+
workspaceUuid: string | undefined;
|
|
19
|
+
workspaceName: string | undefined;
|
|
20
|
+
}): Promise<ResolvedWorkspace>;
|
|
21
|
+
//# sourceMappingURL=session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../../src/commands/auth/session.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,OAAO,EAAE,KAAK,iBAAiB,EAAoB,MAAM,gBAAgB,CAAC;AAE1E,eAAO,MAAM,gBAAgB,4BAA4B,CAAC;AAE1D,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAMnE;AAED;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE;IAC3C,MAAM,EAAE,cAAc,CAAC;IACvB,6EAA6E;IAC7E,eAAe,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;CACnC,GAAG,OAAO,CAAC,iBAAiB,CAAC,CA4C7B"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { saveCredentials } from "../../credentials.js";
|
|
2
|
+
import { getOAuthConfig } from "../../oauthConfig.js";
|
|
3
|
+
import { failWith } from "../runHandler.js";
|
|
4
|
+
import { refreshAccessToken } from "./passwordless.js";
|
|
5
|
+
import { resolveWorkspace } from "./workspace.js";
|
|
6
|
+
export const DEFAULT_BASE_URL = "https://api.getcargo.io";
|
|
7
|
+
export function resolveBaseUrl(override) {
|
|
8
|
+
if (override !== undefined && override.length > 0) {
|
|
9
|
+
return override;
|
|
10
|
+
}
|
|
11
|
+
return DEFAULT_BASE_URL;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Picks the workspace to work in and stores the signed-in session. Every
|
|
15
|
+
* sign-in channel ends here, so the credentials file looks the same however
|
|
16
|
+
* the user signed in.
|
|
17
|
+
*
|
|
18
|
+
* The access token is short-lived and deliberately not treated as the
|
|
19
|
+
* credential: the refresh token beside it is what keeps the CLI signed in, and
|
|
20
|
+
* it is renewed on demand rather than by minting an API token per machine.
|
|
21
|
+
*/
|
|
22
|
+
export async function establishSession(opts) {
|
|
23
|
+
const baseUrl = resolveBaseUrl(opts.baseUrlOverride);
|
|
24
|
+
const { workspace, wasCreated } = await resolveWorkspace({
|
|
25
|
+
baseUrl,
|
|
26
|
+
accessToken: opts.tokens.accessToken,
|
|
27
|
+
explicitUuid: opts.workspaceUuid,
|
|
28
|
+
explicitName: opts.workspaceName,
|
|
29
|
+
});
|
|
30
|
+
let { accessToken, refreshToken, expiresAt } = opts.tokens;
|
|
31
|
+
if (refreshToken === undefined) {
|
|
32
|
+
failWith("The identity provider issued no refresh token, so the CLI cannot keep this session alive. Enable the Refresh Token grant for the CLI application in Auth0, then sign in again.");
|
|
33
|
+
}
|
|
34
|
+
if (wasCreated === true) {
|
|
35
|
+
// The workspace list is a claim on the access token, so a workspace created
|
|
36
|
+
// moments ago is absent from the one we are holding and every request for
|
|
37
|
+
// it would be rejected. Only reissuing the token picks it up.
|
|
38
|
+
const refreshed = await refreshAccessToken({ refreshToken });
|
|
39
|
+
accessToken = refreshed.accessToken;
|
|
40
|
+
expiresAt = refreshed.expiresAt;
|
|
41
|
+
// Only a tenant that rotates refresh tokens returns a new one; keep the
|
|
42
|
+
// existing one otherwise, since it is still the valid one.
|
|
43
|
+
if (refreshed.refreshToken !== undefined) {
|
|
44
|
+
refreshToken = refreshed.refreshToken;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const { domain, clientId } = getOAuthConfig();
|
|
48
|
+
saveCredentials({
|
|
49
|
+
accessToken,
|
|
50
|
+
workspaceUuid: workspace.uuid,
|
|
51
|
+
baseUrl: opts.baseUrlOverride,
|
|
52
|
+
session: { refreshToken, expiresAt, domain, clientId },
|
|
53
|
+
});
|
|
54
|
+
return workspace;
|
|
55
|
+
}
|
|
@@ -1,10 +1,18 @@
|
|
|
1
|
+
import { type WorkspaceManagement } from "@cargo-ai/api";
|
|
1
2
|
export type ResolvedWorkspace = {
|
|
2
3
|
uuid: string;
|
|
3
4
|
name: string;
|
|
4
5
|
};
|
|
6
|
+
export type WorkspaceResolution = {
|
|
7
|
+
workspace: ResolvedWorkspace;
|
|
8
|
+
/** Callers need this: a new workspace is absent from the caller's token claims. */
|
|
9
|
+
wasCreated: boolean;
|
|
10
|
+
};
|
|
5
11
|
export declare function resolveWorkspace(opts: {
|
|
6
12
|
baseUrl: string;
|
|
7
13
|
accessToken: string;
|
|
8
14
|
explicitUuid: string | undefined;
|
|
9
|
-
|
|
15
|
+
explicitName: string | undefined;
|
|
16
|
+
}): Promise<WorkspaceResolution>;
|
|
17
|
+
export declare function findWorkspaceByName(workspaces: readonly WorkspaceManagement.Workspace[], name: string): ResolvedWorkspace | undefined;
|
|
10
18
|
//# sourceMappingURL=workspace.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../../../src/commands/auth/workspace.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../../../src/commands/auth/workspace.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAYzD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,SAAS,EAAE,iBAAiB,CAAC;IAC7B,mFAAmF;IACnF,UAAU,EAAE,OAAO,CAAC;CACrB,CAAC;AAEF,wBAAsB,gBAAgB,CAAC,IAAI,EAAE;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;CAClC,GAAG,OAAO,CAAC,mBAAmB,CAAC,CA2H/B;AAcD,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,SAAS,mBAAmB,CAAC,SAAS,EAAE,EACpD,IAAI,EAAE,MAAM,GACX,iBAAiB,GAAG,SAAS,CAY/B"}
|
|
@@ -1,41 +1,26 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
1
|
import * as readline from "node:readline/promises";
|
|
3
|
-
import * as tty from "node:tty";
|
|
4
2
|
import { createApi } from "../../api.js";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
// installers and agent-driven shells pipe stdin while the controlling
|
|
8
|
-
// terminal stays reachable via /dev/tty. Prefer stdin when it is a TTY,
|
|
9
|
-
// otherwise fall back to opening /dev/tty directly.
|
|
10
|
-
function openPromptInput() {
|
|
11
|
-
if (process.stdin.isTTY === true) {
|
|
12
|
-
return { stream: process.stdin, close: () => undefined };
|
|
13
|
-
}
|
|
14
|
-
try {
|
|
15
|
-
const fd = fs.openSync("/dev/tty", "r");
|
|
16
|
-
const stream = new tty.ReadStream(fd);
|
|
17
|
-
return { stream, close: () => stream.destroy() };
|
|
18
|
-
}
|
|
19
|
-
catch {
|
|
20
|
-
return undefined;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
3
|
+
import { askQuestion, openPromptInput } from "../../utils/prompt.js";
|
|
4
|
+
import { ExitCodes, failWith, handleApiCall, info, success, } from "../runHandler.js";
|
|
23
5
|
export async function resolveWorkspace(opts) {
|
|
6
|
+
const accessToken = opts.accessToken;
|
|
24
7
|
const api = createApi({
|
|
25
8
|
baseUrl: opts.baseUrl,
|
|
26
|
-
|
|
9
|
+
getAccessToken: () => Promise.resolve(accessToken),
|
|
27
10
|
workspaceUuid: undefined,
|
|
28
11
|
});
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
12
|
+
const { workspaces } = await handleApiCall(() => api.workspaceManagement.workspace.all({ refresh: false }), { spinner: "Loading workspaces..." });
|
|
13
|
+
if (opts.explicitName !== undefined && opts.explicitName.length > 0) {
|
|
14
|
+
// Reuse a same-named workspace rather than creating a duplicate, so that
|
|
15
|
+
// re-running a non-interactive sign-in is idempotent as documented.
|
|
16
|
+
const existingWorkspace = findWorkspaceByName(workspaces, opts.explicitName);
|
|
17
|
+
if (existingWorkspace !== undefined) {
|
|
18
|
+
return { workspace: existingWorkspace, wasCreated: false };
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
workspace: await createWorkspace(api, opts.explicitName),
|
|
22
|
+
wasCreated: true,
|
|
23
|
+
};
|
|
39
24
|
}
|
|
40
25
|
if (opts.explicitUuid !== undefined) {
|
|
41
26
|
const matchedWorkspace = workspaces.find((workspace) => {
|
|
@@ -44,16 +29,20 @@ export async function resolveWorkspace(opts) {
|
|
|
44
29
|
if (matchedWorkspace === undefined) {
|
|
45
30
|
failWith(`Workspace ${opts.explicitUuid} is not in your accessible workspaces.`, { code: ExitCodes.NotFound });
|
|
46
31
|
}
|
|
47
|
-
return {
|
|
32
|
+
return {
|
|
33
|
+
workspace: { uuid: matchedWorkspace.uuid, name: matchedWorkspace.name },
|
|
34
|
+
wasCreated: false,
|
|
35
|
+
};
|
|
48
36
|
}
|
|
49
37
|
if (workspaces.length === 0) {
|
|
50
38
|
const promptInput = openPromptInput();
|
|
51
39
|
if (promptInput === undefined) {
|
|
52
|
-
failWith("No workspaces found.", { code: ExitCodes.NotFound });
|
|
40
|
+
failWith("No workspaces found. Re-run with --workspace-name <name> to create one.", { code: ExitCodes.NotFound });
|
|
53
41
|
}
|
|
54
42
|
let name;
|
|
55
43
|
try {
|
|
56
|
-
|
|
44
|
+
info("You don't have any workspaces yet. Let's create one.");
|
|
45
|
+
name = await askQuestion(promptInput.stream, "Workspace name (or press Enter to skip): ");
|
|
57
46
|
}
|
|
58
47
|
finally {
|
|
59
48
|
promptInput.close();
|
|
@@ -63,31 +52,19 @@ export async function resolveWorkspace(opts) {
|
|
|
63
52
|
code: ExitCodes.InvalidUsage,
|
|
64
53
|
});
|
|
65
54
|
}
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
createdWorkspace = await api.workspaceManagement.workspace.create({
|
|
70
|
-
name,
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
catch (err) {
|
|
74
|
-
spinner.stop();
|
|
75
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
76
|
-
failWith(`Failed to create workspace: ${message}`);
|
|
77
|
-
}
|
|
78
|
-
spinner.stop();
|
|
79
|
-
success(`Created workspace: ${createdWorkspace.workspace.name} (${createdWorkspace.workspace.uuid})`);
|
|
80
|
-
return {
|
|
81
|
-
uuid: createdWorkspace.workspace.uuid,
|
|
82
|
-
name: createdWorkspace.workspace.name,
|
|
83
|
-
};
|
|
55
|
+
const workspace = await createWorkspace(api, name);
|
|
56
|
+
success(`Created workspace: ${workspace.name} (${workspace.uuid})`);
|
|
57
|
+
return { workspace, wasCreated: true };
|
|
84
58
|
}
|
|
85
59
|
if (workspaces.length === 1) {
|
|
86
60
|
const [firstWorkspace] = workspaces;
|
|
87
61
|
if (firstWorkspace === undefined) {
|
|
88
62
|
failWith("Unexpected empty workspace list after length check.");
|
|
89
63
|
}
|
|
90
|
-
return {
|
|
64
|
+
return {
|
|
65
|
+
workspace: { uuid: firstWorkspace.uuid, name: firstWorkspace.name },
|
|
66
|
+
wasCreated: false,
|
|
67
|
+
};
|
|
91
68
|
}
|
|
92
69
|
const promptInput = openPromptInput();
|
|
93
70
|
if (promptInput === undefined) {
|
|
@@ -103,25 +80,21 @@ export async function resolveWorkspace(opts) {
|
|
|
103
80
|
if (selectedWorkspace === undefined) {
|
|
104
81
|
failWith("No workspace selected.", { code: ExitCodes.InvalidUsage });
|
|
105
82
|
}
|
|
106
|
-
return selectedWorkspace;
|
|
83
|
+
return { workspace: selectedWorkspace, wasCreated: false };
|
|
107
84
|
}
|
|
108
|
-
async function
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
85
|
+
async function createWorkspace(api, name) {
|
|
86
|
+
const result = await handleApiCall(() => api.workspaceManagement.workspace.create({ name }), { spinner: "Creating workspace..." });
|
|
87
|
+
return { uuid: result.workspace.uuid, name: result.workspace.name };
|
|
88
|
+
}
|
|
89
|
+
export function findWorkspaceByName(workspaces, name) {
|
|
90
|
+
const wanted = name.trim().toLowerCase();
|
|
91
|
+
const matched = workspaces.find((workspace) => {
|
|
92
|
+
return workspace.name.trim().toLowerCase() === wanted;
|
|
112
93
|
});
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const answer = await rl.question("Workspace name (or press Enter to skip): ");
|
|
116
|
-
const trimmed = answer.trim();
|
|
117
|
-
if (trimmed === "") {
|
|
118
|
-
return undefined;
|
|
119
|
-
}
|
|
120
|
-
return trimmed;
|
|
121
|
-
}
|
|
122
|
-
finally {
|
|
123
|
-
rl.close();
|
|
94
|
+
if (matched === undefined) {
|
|
95
|
+
return undefined;
|
|
124
96
|
}
|
|
97
|
+
return { uuid: matched.uuid, name: matched.name };
|
|
125
98
|
}
|
|
126
99
|
async function promptWorkspaceSelection(workspaces, input) {
|
|
127
100
|
const rl = readline.createInterface({
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type Billing } from "@cargo-ai/api";
|
|
2
|
+
import type { Api } from "../../api.js";
|
|
3
|
+
export declare const DEFAULT_TIMEOUT_SECONDS = 300;
|
|
4
|
+
export declare const DEFAULT_POLL_INTERVAL_SECONDS = 3;
|
|
5
|
+
export type AddPaymentMethodOptions = {
|
|
6
|
+
timeout: number;
|
|
7
|
+
pollInterval: number;
|
|
8
|
+
open: boolean;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Hands card entry off to Stripe's hosted billing portal and waits for the card
|
|
12
|
+
* on file to change.
|
|
13
|
+
*
|
|
14
|
+
* The card never touches this process: the CLI only ever sees the last four
|
|
15
|
+
* digits that the API already exposes. Polling is what makes the handoff usable
|
|
16
|
+
* from a script — the caller gets a definite answer instead of having to guess
|
|
17
|
+
* whether the human finished.
|
|
18
|
+
*/
|
|
19
|
+
export declare function runAddPaymentMethod(api: Api, opts: AddPaymentMethodOptions): Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* Stripe never exposes a raw card number here, so a change is detected from the
|
|
22
|
+
* fields the API does return. Replacing a card with the very same one is
|
|
23
|
+
* therefore indistinguishable from doing nothing, and the command times out.
|
|
24
|
+
*/
|
|
25
|
+
export declare function fingerprintCard(card: Billing.CreditCard | undefined): string;
|
|
26
|
+
//# sourceMappingURL=addPaymentMethod.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"addPaymentMethod.d.ts","sourceRoot":"","sources":["../../../src/commands/billing/addPaymentMethod.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,eAAe,CAAC;AAE7C,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAWxC,eAAO,MAAM,uBAAuB,MAAM,CAAC;AAC3C,eAAO,MAAM,6BAA6B,IAAI,CAAC;AAE/C,MAAM,MAAM,uBAAuB,GAAG;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,OAAO,CAAC;CACf,CAAC;AAEF;;;;;;;;GAQG;AACH,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,GAAG,EACR,IAAI,EAAE,uBAAuB,GAC5B,OAAO,CAAC,IAAI,CAAC,CAgDf;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,OAAO,CAAC,UAAU,GAAG,SAAS,GAAG,MAAM,CAW5E"}
|