@tonbo/cli 0.0.1 → 0.0.3
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/dist/src/app.js +4 -0
- package/dist/src/auth.d.ts +9 -2
- package/dist/src/auth.js +67 -18
- package/dist/src/commands.d.ts +4 -0
- package/dist/src/commands.js +50 -11
- package/dist/src/progress.d.ts +25 -0
- package/dist/src/progress.js +60 -0
- package/package.json +1 -1
package/dist/src/app.js
CHANGED
|
@@ -5,6 +5,8 @@ import { AuthClient } from "./auth.js";
|
|
|
5
5
|
import { deployCommand, runCommand, loginCommand, projectCreateCommand, projectUseCommand, sshCommand, sshKeyAddCommand, sshKeyRemoveCommand, secretListCommand, secretRemoveCommand, secretSetCommand, } from "./commands.js";
|
|
6
6
|
import { FileConfigStore } from "./config.js";
|
|
7
7
|
import { FileCredentialStore } from "./credentials.js";
|
|
8
|
+
import { silentProgress, TerminalProgress } from "./progress.js";
|
|
9
|
+
import { readDefaultSshPublicKeys } from "./ssh-key.js";
|
|
8
10
|
const packageVersion = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
|
|
9
11
|
export function createDependencies(json = false) {
|
|
10
12
|
const accountOrigin = process.env.TONBO_ACCOUNT_ORIGIN || "https://tonbo.dev";
|
|
@@ -14,12 +16,14 @@ export function createDependencies(json = false) {
|
|
|
14
16
|
auth: new AuthClient(new FileCredentialStore(), fetch, accountOrigin),
|
|
15
17
|
config: new FileConfigStore(),
|
|
16
18
|
cwd: () => process.cwd(),
|
|
19
|
+
defaultSshPublicKeys: readDefaultSshPublicKeys,
|
|
17
20
|
output: (value) => {
|
|
18
21
|
if (json)
|
|
19
22
|
console.log(JSON.stringify(value));
|
|
20
23
|
else
|
|
21
24
|
console.log(value.message ?? value);
|
|
22
25
|
},
|
|
26
|
+
progress: json ? silentProgress : new TerminalProgress(process.stderr),
|
|
23
27
|
secretValue: async (name, fromEnvironment) => {
|
|
24
28
|
const environmentName = fromEnvironment ?? name;
|
|
25
29
|
const value = process.env[environmentName];
|
package/dist/src/auth.d.ts
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import type { CredentialStore } from "./credentials.js";
|
|
2
2
|
import type { OAuthTokenSet } from "./types.js";
|
|
3
|
+
export type LoginStep = "account-config" | "callback-server" | "browser" | "browser-authorization" | "callback-close" | "token-exchange" | "credential-store";
|
|
4
|
+
export interface LoginProgressEvent {
|
|
5
|
+
status: "completed" | "started";
|
|
6
|
+
step: LoginStep;
|
|
7
|
+
}
|
|
8
|
+
export type LoginProgress = (event: LoginProgressEvent) => void;
|
|
3
9
|
export declare class AuthClient {
|
|
4
10
|
private readonly credentials;
|
|
5
11
|
private readonly fetcher;
|
|
6
12
|
private readonly accountOrigin;
|
|
7
|
-
|
|
13
|
+
private readonly browserOpener;
|
|
14
|
+
constructor(credentials: CredentialStore, fetcher: typeof fetch, accountOrigin?: string, browserOpener?: (url: string) => Promise<void>);
|
|
8
15
|
accessToken(): Promise<string>;
|
|
9
|
-
login(): Promise<OAuthTokenSet>;
|
|
16
|
+
login(progress?: LoginProgress): Promise<OAuthTokenSet>;
|
|
10
17
|
private config;
|
|
11
18
|
private exchange;
|
|
12
19
|
private listenForCode;
|
package/dist/src/auth.js
CHANGED
|
@@ -21,10 +21,12 @@ export class AuthClient {
|
|
|
21
21
|
credentials;
|
|
22
22
|
fetcher;
|
|
23
23
|
accountOrigin;
|
|
24
|
-
|
|
24
|
+
browserOpener;
|
|
25
|
+
constructor(credentials, fetcher, accountOrigin = "https://tonbo.dev", browserOpener = openBrowser) {
|
|
25
26
|
this.credentials = credentials;
|
|
26
27
|
this.fetcher = fetcher;
|
|
27
28
|
this.accountOrigin = accountOrigin;
|
|
29
|
+
this.browserOpener = browserOpener;
|
|
28
30
|
}
|
|
29
31
|
async accessToken() {
|
|
30
32
|
const injected = process.env.TONBO_ACCESS_TOKEN?.trim();
|
|
@@ -53,8 +55,10 @@ export class AuthClient {
|
|
|
53
55
|
await this.credentials.save(refreshed);
|
|
54
56
|
return refreshed.access_token;
|
|
55
57
|
}
|
|
56
|
-
async login() {
|
|
58
|
+
async login(progress = () => { }) {
|
|
59
|
+
progress({ status: "started", step: "account-config" });
|
|
57
60
|
const config = await this.config();
|
|
61
|
+
progress({ status: "completed", step: "account-config" });
|
|
58
62
|
if (config.redirect_uri !== LOOPBACK_REDIRECT)
|
|
59
63
|
throw new Error(`Unsupported OAuth redirect URI: ${config.redirect_uri}`);
|
|
60
64
|
const verifier = base64url(randomBytes(48));
|
|
@@ -70,7 +74,8 @@ export class AuthClient {
|
|
|
70
74
|
scope: "openid profile email",
|
|
71
75
|
state,
|
|
72
76
|
}).toString();
|
|
73
|
-
const code = await this.listenForCode(state, () =>
|
|
77
|
+
const code = await this.listenForCode(state, () => this.browserOpener(authorization.toString()), progress);
|
|
78
|
+
progress({ status: "started", step: "token-exchange" });
|
|
74
79
|
const tokens = withAbsoluteExpiry(await this.exchange(config.token_endpoint, new URLSearchParams({
|
|
75
80
|
client_id: config.client_id,
|
|
76
81
|
code,
|
|
@@ -78,7 +83,10 @@ export class AuthClient {
|
|
|
78
83
|
grant_type: "authorization_code",
|
|
79
84
|
redirect_uri: config.redirect_uri,
|
|
80
85
|
})));
|
|
86
|
+
progress({ status: "completed", step: "token-exchange" });
|
|
87
|
+
progress({ status: "started", step: "credential-store" });
|
|
81
88
|
await this.credentials.save(tokens);
|
|
89
|
+
progress({ status: "completed", step: "credential-store" });
|
|
82
90
|
return tokens;
|
|
83
91
|
}
|
|
84
92
|
config() {
|
|
@@ -91,19 +99,45 @@ export class AuthClient {
|
|
|
91
99
|
body,
|
|
92
100
|
});
|
|
93
101
|
}
|
|
94
|
-
listenForCode(expectedState, ready) {
|
|
102
|
+
listenForCode(expectedState, ready, progress) {
|
|
95
103
|
return new Promise((resolve, reject) => {
|
|
96
104
|
let settled = false;
|
|
97
|
-
const
|
|
105
|
+
const sockets = new Set();
|
|
106
|
+
let authorizationStarted = false;
|
|
107
|
+
const startAuthorization = () => {
|
|
108
|
+
if (authorizationStarted)
|
|
109
|
+
return;
|
|
110
|
+
authorizationStarted = true;
|
|
111
|
+
progress({ status: "completed", step: "browser" });
|
|
112
|
+
progress({ status: "started", step: "browser-authorization" });
|
|
113
|
+
};
|
|
114
|
+
const finish = (result, completedResponseSocket) => {
|
|
98
115
|
if (settled)
|
|
99
116
|
return;
|
|
100
117
|
settled = true;
|
|
101
118
|
clearTimeout(timeout);
|
|
102
|
-
|
|
119
|
+
const complete = (closeError) => {
|
|
120
|
+
if (closeError)
|
|
121
|
+
reject(closeError);
|
|
122
|
+
else if ("code" in result) {
|
|
123
|
+
progress({ status: "completed", step: "callback-close" });
|
|
124
|
+
resolve(result.code);
|
|
125
|
+
}
|
|
126
|
+
else
|
|
127
|
+
reject(result.error);
|
|
128
|
+
};
|
|
103
129
|
if ("code" in result)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
130
|
+
progress({ status: "started", step: "callback-close" });
|
|
131
|
+
if (!server.listening) {
|
|
132
|
+
complete();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
server.close(complete);
|
|
136
|
+
for (const socket of sockets) {
|
|
137
|
+
if (socket !== completedResponseSocket)
|
|
138
|
+
socket.destroy();
|
|
139
|
+
}
|
|
140
|
+
server.closeIdleConnections();
|
|
107
141
|
};
|
|
108
142
|
const timeout = setTimeout(() => {
|
|
109
143
|
finish({ error: new Error("Timed out waiting for browser login.") });
|
|
@@ -113,8 +147,8 @@ export class AuthClient {
|
|
|
113
147
|
const code = url.searchParams.get("code");
|
|
114
148
|
const oauthError = url.searchParams.get("error_description") ?? url.searchParams.get("error");
|
|
115
149
|
if (oauthError) {
|
|
116
|
-
|
|
117
|
-
finish({ error: new Error(oauthError) });
|
|
150
|
+
const responseSocket = response.socket;
|
|
151
|
+
respond(response, "denied", () => finish({ error: new Error(oauthError) }, responseSocket));
|
|
118
152
|
return;
|
|
119
153
|
}
|
|
120
154
|
if (url.pathname !== "/callback" ||
|
|
@@ -123,19 +157,34 @@ export class AuthClient {
|
|
|
123
157
|
respond(response, "invalid");
|
|
124
158
|
return;
|
|
125
159
|
}
|
|
126
|
-
|
|
127
|
-
|
|
160
|
+
startAuthorization();
|
|
161
|
+
progress({ status: "completed", step: "browser-authorization" });
|
|
162
|
+
const responseSocket = response.socket;
|
|
163
|
+
respond(response, "complete", () => finish({ code }, responseSocket));
|
|
164
|
+
});
|
|
165
|
+
server.on("connection", (socket) => {
|
|
166
|
+
sockets.add(socket);
|
|
167
|
+
socket.once("close", () => sockets.delete(socket));
|
|
128
168
|
});
|
|
129
169
|
server.once("error", (error) => finish({ error }));
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
170
|
+
progress({ status: "started", step: "callback-server" });
|
|
171
|
+
server.listen(17655, "localhost", () => {
|
|
172
|
+
progress({ status: "completed", step: "callback-server" });
|
|
173
|
+
progress({ status: "started", step: "browser" });
|
|
174
|
+
void ready()
|
|
175
|
+
.then(startAuthorization)
|
|
176
|
+
.catch((error) => finish({
|
|
177
|
+
error: error instanceof Error ? error : new Error("Could not open the login browser."),
|
|
178
|
+
}));
|
|
179
|
+
});
|
|
133
180
|
});
|
|
134
181
|
}
|
|
135
182
|
}
|
|
136
|
-
function respond(response, outcome) {
|
|
183
|
+
function respond(response, outcome, finished) {
|
|
137
184
|
const page = callbackResponse(outcome);
|
|
138
|
-
|
|
185
|
+
if (finished)
|
|
186
|
+
response.once("finish", finished);
|
|
187
|
+
response.writeHead(page.status, { ...page.headers, connection: "close" });
|
|
139
188
|
response.end(page.body);
|
|
140
189
|
}
|
|
141
190
|
async function openBrowser(url) {
|
package/dist/src/commands.d.ts
CHANGED
|
@@ -2,12 +2,16 @@ import type { AuthClient } from "./auth.js";
|
|
|
2
2
|
import type { TonboApi } from "./api.js";
|
|
3
3
|
import type { ConfigStore } from "./config.js";
|
|
4
4
|
import type { ProjectSummary } from "./types.js";
|
|
5
|
+
import { readDefaultSshPublicKeys } from "./ssh-key.js";
|
|
6
|
+
import type { ProgressReporter } from "./progress.js";
|
|
5
7
|
export interface CommandDependencies {
|
|
6
8
|
api: TonboApi;
|
|
7
9
|
auth: AuthClient;
|
|
8
10
|
config: ConfigStore;
|
|
9
11
|
cwd: () => string;
|
|
12
|
+
defaultSshPublicKeys: typeof readDefaultSshPublicKeys;
|
|
10
13
|
output: (value: unknown) => void;
|
|
14
|
+
progress: ProgressReporter;
|
|
11
15
|
executable?: () => string;
|
|
12
16
|
secretValue: (name: string, fromEnvironment?: string) => Promise<string>;
|
|
13
17
|
}
|
package/dist/src/commands.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { buildRevision, loadDeclaration } from "./declaration.js";
|
|
2
2
|
import { buildSourceBundle, findDeclarationRoot } from "./source.js";
|
|
3
|
-
import {
|
|
3
|
+
import { readSshPublicKey } from "./ssh-key.js";
|
|
4
4
|
import { launchProjectSsh } from "./ssh.js";
|
|
5
5
|
export async function resolveProject(deps, selector) {
|
|
6
6
|
const oauthToken = await deps.auth.accessToken();
|
|
@@ -37,16 +37,55 @@ export function selectProject(projects, selector) {
|
|
|
37
37
|
return matches[0];
|
|
38
38
|
}
|
|
39
39
|
export async function loginCommand(deps) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
await deps.api.registerSshKey(
|
|
45
|
-
|
|
46
|
-
message:
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
40
|
+
try {
|
|
41
|
+
const tokens = await deps.auth.login((event) => reportLoginProgress(deps.progress, event));
|
|
42
|
+
deps.progress.start("Preparing SSH access");
|
|
43
|
+
const keys = await deps.defaultSshPublicKeys();
|
|
44
|
+
await Promise.all(keys.map((key) => deps.api.registerSshKey(tokens.access_token, key)));
|
|
45
|
+
deps.progress.succeed("SSH access ready");
|
|
46
|
+
deps.output({ message: "Logged in to Tonbo." });
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
deps.progress.fail();
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const LOGIN_PROGRESS_MESSAGES = {
|
|
54
|
+
"account-config": {
|
|
55
|
+
started: "Connecting to Tonbo",
|
|
56
|
+
completed: "Connected to Tonbo",
|
|
57
|
+
},
|
|
58
|
+
"callback-server": {
|
|
59
|
+
started: "Starting local browser callback",
|
|
60
|
+
completed: "Local browser callback ready",
|
|
61
|
+
},
|
|
62
|
+
browser: {
|
|
63
|
+
started: "Opening browser",
|
|
64
|
+
completed: "Browser opened",
|
|
65
|
+
},
|
|
66
|
+
"browser-authorization": {
|
|
67
|
+
started: "Waiting for browser authorization",
|
|
68
|
+
completed: "Browser authorization received",
|
|
69
|
+
},
|
|
70
|
+
"callback-close": {
|
|
71
|
+
started: "Closing local browser callback",
|
|
72
|
+
completed: "Local browser callback closed",
|
|
73
|
+
},
|
|
74
|
+
"token-exchange": {
|
|
75
|
+
started: "Exchanging authorization code",
|
|
76
|
+
completed: "Authorization code exchanged",
|
|
77
|
+
},
|
|
78
|
+
"credential-store": {
|
|
79
|
+
started: "Saving login session",
|
|
80
|
+
completed: "Login session saved",
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
function reportLoginProgress(progress, event) {
|
|
84
|
+
const messages = LOGIN_PROGRESS_MESSAGES[event.step];
|
|
85
|
+
if (event.status === "started")
|
|
86
|
+
progress.start(messages.started);
|
|
87
|
+
else
|
|
88
|
+
progress.succeed(messages.completed);
|
|
50
89
|
}
|
|
51
90
|
export async function sshKeyAddCommand(deps, path) {
|
|
52
91
|
const key = await readSshPublicKey(path);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface ProgressReporter {
|
|
2
|
+
start(message: string): void;
|
|
3
|
+
succeed(message?: string): void;
|
|
4
|
+
fail(message?: string): void;
|
|
5
|
+
}
|
|
6
|
+
interface ProgressStream {
|
|
7
|
+
isTTY?: boolean;
|
|
8
|
+
write(value: string): unknown;
|
|
9
|
+
}
|
|
10
|
+
export declare class TerminalProgress implements ProgressReporter {
|
|
11
|
+
private readonly stream;
|
|
12
|
+
private readonly intervalMs;
|
|
13
|
+
private activeMessage;
|
|
14
|
+
private frame;
|
|
15
|
+
private lastWidth;
|
|
16
|
+
private timer;
|
|
17
|
+
constructor(stream: ProgressStream, intervalMs?: number);
|
|
18
|
+
start(message: string): void;
|
|
19
|
+
succeed(message?: string | undefined): void;
|
|
20
|
+
fail(message?: string | undefined): void;
|
|
21
|
+
private finish;
|
|
22
|
+
private render;
|
|
23
|
+
}
|
|
24
|
+
export declare const silentProgress: ProgressReporter;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const FRAMES = ["|", "/", "-", "\\"];
|
|
2
|
+
export class TerminalProgress {
|
|
3
|
+
stream;
|
|
4
|
+
intervalMs;
|
|
5
|
+
activeMessage;
|
|
6
|
+
frame = 0;
|
|
7
|
+
lastWidth = 0;
|
|
8
|
+
timer;
|
|
9
|
+
constructor(stream, intervalMs = 80) {
|
|
10
|
+
this.stream = stream;
|
|
11
|
+
this.intervalMs = intervalMs;
|
|
12
|
+
}
|
|
13
|
+
start(message) {
|
|
14
|
+
if (this.activeMessage)
|
|
15
|
+
this.succeed();
|
|
16
|
+
this.activeMessage = message;
|
|
17
|
+
this.frame = 0;
|
|
18
|
+
if (!this.stream.isTTY) {
|
|
19
|
+
this.stream.write(`[..] ${message}\n`);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
this.render(`[${FRAMES[this.frame]}] ${message}`);
|
|
23
|
+
this.timer = setInterval(() => {
|
|
24
|
+
this.frame = (this.frame + 1) % FRAMES.length;
|
|
25
|
+
this.render(`[${FRAMES[this.frame]}] ${this.activeMessage}`);
|
|
26
|
+
}, this.intervalMs);
|
|
27
|
+
this.timer.unref();
|
|
28
|
+
}
|
|
29
|
+
succeed(message = this.activeMessage) {
|
|
30
|
+
this.finish("ok", message);
|
|
31
|
+
}
|
|
32
|
+
fail(message = this.activeMessage) {
|
|
33
|
+
this.finish("!!", message);
|
|
34
|
+
}
|
|
35
|
+
finish(marker, message) {
|
|
36
|
+
if (this.timer)
|
|
37
|
+
clearInterval(this.timer);
|
|
38
|
+
this.timer = undefined;
|
|
39
|
+
this.activeMessage = undefined;
|
|
40
|
+
if (!message)
|
|
41
|
+
return;
|
|
42
|
+
const line = `[${marker}] ${message}`;
|
|
43
|
+
if (this.stream.isTTY) {
|
|
44
|
+
this.render(line);
|
|
45
|
+
this.stream.write("\n");
|
|
46
|
+
this.lastWidth = 0;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
this.stream.write(`${line}\n`);
|
|
50
|
+
}
|
|
51
|
+
render(value) {
|
|
52
|
+
this.stream.write(`\r${value.padEnd(this.lastWidth)}`);
|
|
53
|
+
this.lastWidth = value.length;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export const silentProgress = {
|
|
57
|
+
start: () => { },
|
|
58
|
+
succeed: () => { },
|
|
59
|
+
fail: () => { },
|
|
60
|
+
};
|