@tokscale/cli 1.0.6 → 1.0.8

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/src/auth.ts ADDED
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Tokscale CLI Authentication Commands
3
+ * Handles login, logout, and whoami commands
4
+ */
5
+
6
+ import pc from "picocolors";
7
+ import * as os from "node:os";
8
+ import {
9
+ saveCredentials,
10
+ loadCredentials,
11
+ clearCredentials,
12
+ getApiBaseUrl,
13
+ } from "./credentials.js";
14
+
15
+ const MAX_POLL_ATTEMPTS = 180; // 15 minutes max
16
+
17
+ interface DeviceCodeResponse {
18
+ deviceCode: string;
19
+ userCode: string;
20
+ verificationUrl: string;
21
+ expiresIn: number;
22
+ interval: number;
23
+ }
24
+
25
+ interface PollResponse {
26
+ status: "pending" | "complete" | "expired";
27
+ token?: string;
28
+ user?: {
29
+ username: string;
30
+ avatarUrl?: string;
31
+ };
32
+ error?: string;
33
+ }
34
+
35
+ /**
36
+ * Login command - initiates device flow authentication
37
+ */
38
+ export async function login(): Promise<void> {
39
+ const credentials = loadCredentials();
40
+ if (credentials) {
41
+ console.log(pc.yellow(`\n Already logged in as ${pc.bold(credentials.username)}`));
42
+ console.log(pc.gray(" Run 'tokscale logout' to sign out first.\n"));
43
+ return;
44
+ }
45
+
46
+ const baseUrl = getApiBaseUrl();
47
+
48
+ console.log(pc.cyan("\n Tokscale - Login\n"));
49
+
50
+ // Step 1: Request device code
51
+ console.log(pc.gray(" Requesting authorization code..."));
52
+
53
+ let deviceCodeData: DeviceCodeResponse;
54
+ try {
55
+ const response = await fetch(`${baseUrl}/api/auth/device`, {
56
+ method: "POST",
57
+ headers: { "Content-Type": "application/json" },
58
+ body: JSON.stringify({ deviceName: getDeviceName() }),
59
+ });
60
+
61
+ if (!response.ok) {
62
+ throw new Error(`Server returned ${response.status}`);
63
+ }
64
+
65
+ deviceCodeData = await response.json();
66
+ } catch (error) {
67
+ console.error(pc.red(`\n Error: Failed to connect to server.`));
68
+ console.error(pc.gray(` ${(error as Error).message}\n`));
69
+ process.exit(1);
70
+ }
71
+
72
+ // Step 2: Display instructions
73
+ console.log();
74
+ console.log(pc.white(" Open this URL in your browser:"));
75
+ console.log(pc.cyan(` ${deviceCodeData.verificationUrl}\n`));
76
+ console.log(pc.white(" Enter this code:"));
77
+ console.log(pc.bold(pc.green(` ${deviceCodeData.userCode}\n`)));
78
+
79
+ // Try to open browser automatically
80
+ await openBrowser(deviceCodeData.verificationUrl);
81
+
82
+ console.log(pc.gray(" Waiting for authorization..."));
83
+
84
+ // Step 3: Poll for completion
85
+ let attempts = 0;
86
+ const pollInterval = (deviceCodeData.interval || 5) * 1000;
87
+
88
+ while (attempts < MAX_POLL_ATTEMPTS) {
89
+ await sleep(pollInterval);
90
+ attempts++;
91
+
92
+ try {
93
+ const response = await fetch(`${baseUrl}/api/auth/device/poll`, {
94
+ method: "POST",
95
+ headers: { "Content-Type": "application/json" },
96
+ body: JSON.stringify({ deviceCode: deviceCodeData.deviceCode }),
97
+ });
98
+
99
+ const data: PollResponse = await response.json();
100
+
101
+ if (data.status === "complete" && data.token && data.user) {
102
+ // Success!
103
+ saveCredentials({
104
+ token: data.token,
105
+ username: data.user.username,
106
+ avatarUrl: data.user.avatarUrl,
107
+ createdAt: new Date().toISOString(),
108
+ });
109
+
110
+ console.log(pc.green(`\n Success! Logged in as ${pc.bold(data.user.username)}`));
111
+ console.log(pc.gray(" You can now use 'tokscale submit' to share your usage.\n"));
112
+ return;
113
+ }
114
+
115
+ if (data.status === "expired") {
116
+ console.error(pc.red("\n Authorization code expired. Please try again.\n"));
117
+ process.exit(1);
118
+ }
119
+
120
+ // Still pending - show a dot to indicate progress
121
+ process.stdout.write(pc.gray("."));
122
+ } catch (error) {
123
+ // Network error - continue polling
124
+ process.stdout.write(pc.red("!"));
125
+ }
126
+ }
127
+
128
+ console.error(pc.red("\n\n Timeout: Authorization took too long. Please try again.\n"));
129
+ process.exit(1);
130
+ }
131
+
132
+ /**
133
+ * Logout command - clears stored credentials
134
+ */
135
+ export async function logout(): Promise<void> {
136
+ const credentials = loadCredentials();
137
+
138
+ if (!credentials) {
139
+ console.log(pc.yellow("\n Not logged in.\n"));
140
+ return;
141
+ }
142
+
143
+ const username = credentials.username;
144
+ const cleared = clearCredentials();
145
+
146
+ if (cleared) {
147
+ console.log(pc.green(`\n Logged out from ${pc.bold(username)}\n`));
148
+ } else {
149
+ console.error(pc.red("\n Failed to clear credentials.\n"));
150
+ process.exit(1);
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Whoami command - displays current user info
156
+ */
157
+ export async function whoami(): Promise<void> {
158
+ const credentials = loadCredentials();
159
+
160
+ if (!credentials) {
161
+ console.log(pc.yellow("\n Not logged in."));
162
+ console.log(pc.gray(" Run 'tokscale login' to authenticate.\n"));
163
+ return;
164
+ }
165
+
166
+ console.log(pc.cyan("\n Tokscale - Account Info\n"));
167
+ console.log(pc.white(` Username: ${pc.bold(credentials.username)}`));
168
+ console.log(pc.gray(` Logged in: ${new Date(credentials.createdAt).toLocaleDateString()}`));
169
+ console.log();
170
+ }
171
+
172
+ /**
173
+ * Get a device name for the token
174
+ */
175
+ function getDeviceName(): string {
176
+ return `CLI on ${os.hostname()}`;
177
+ }
178
+
179
+ /**
180
+ * Sleep helper
181
+ */
182
+ function sleep(ms: number): Promise<void> {
183
+ return new Promise((resolve) => setTimeout(resolve, ms));
184
+ }
185
+
186
+ /**
187
+ * Try to open browser automatically
188
+ */
189
+ async function openBrowser(url: string): Promise<void> {
190
+ try {
191
+ const { exec } = await import("node:child_process");
192
+ const platform = process.platform;
193
+
194
+ let command: string;
195
+ if (platform === "darwin") {
196
+ command = `open "${url}"`;
197
+ } else if (platform === "win32") {
198
+ command = `start "" "${url}"`;
199
+ } else {
200
+ command = `xdg-open "${url}"`;
201
+ }
202
+
203
+ exec(command, (error) => {
204
+ if (error) {
205
+ // Silent fail - user can still open manually
206
+ }
207
+ });
208
+ } catch {
209
+ // Silent fail
210
+ }
211
+ }