kubeagent 0.1.7 → 0.1.9
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/auth.d.ts +1 -0
- package/dist/auth.js +76 -14
- package/dist/cli.js +6 -3
- package/package.json +1 -1
package/dist/auth.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export declare function loadAuth(): AuthState | null;
|
|
|
10
10
|
export declare function saveAuth(auth: AuthState): void;
|
|
11
11
|
export declare function clearAuth(): void;
|
|
12
12
|
export declare function loginBrowser(serverUrl: string, appUrl: string): Promise<AuthState>;
|
|
13
|
+
export declare function loginDevice(serverUrl: string, appUrl: string): Promise<AuthState>;
|
|
13
14
|
export declare function createApiKey(auth: AuthState, name: string): Promise<{
|
|
14
15
|
key: string;
|
|
15
16
|
prefix: string;
|
package/dist/auth.js
CHANGED
|
@@ -53,13 +53,19 @@ export async function loginBrowser(serverUrl, appUrl) {
|
|
|
53
53
|
res.end();
|
|
54
54
|
return;
|
|
55
55
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
56
|
+
// Parse credentials from POST body (hidden form submit) to avoid
|
|
57
|
+
// leaking the JWT in URL query params, browser history, and Referer headers.
|
|
58
|
+
const chunks = [];
|
|
59
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
60
|
+
req.on("end", () => {
|
|
61
|
+
const body = Buffer.concat(chunks).toString();
|
|
62
|
+
const params = new URLSearchParams(body);
|
|
63
|
+
const receivedState = params.get("state");
|
|
64
|
+
const token = params.get("token");
|
|
65
|
+
const email = params.get("email") ?? "";
|
|
66
|
+
const name = params.get("name") ?? "";
|
|
67
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
68
|
+
res.end(`<!DOCTYPE html>
|
|
63
69
|
<html lang="en">
|
|
64
70
|
<head>
|
|
65
71
|
<meta charset="UTF-8" />
|
|
@@ -89,13 +95,14 @@ export async function loginBrowser(serverUrl, appUrl) {
|
|
|
89
95
|
</div>
|
|
90
96
|
</body>
|
|
91
97
|
</html>`);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
98
|
+
server.close();
|
|
99
|
+
if (receivedState !== state || !token) {
|
|
100
|
+
reject(new Error("Invalid callback — state mismatch or missing token"));
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
resolve({ token, email, name });
|
|
104
|
+
}
|
|
105
|
+
});
|
|
99
106
|
});
|
|
100
107
|
server.listen(0, "127.0.0.1", async () => {
|
|
101
108
|
const port = server.address().port;
|
|
@@ -126,6 +133,61 @@ export async function loginBrowser(serverUrl, appUrl) {
|
|
|
126
133
|
saveAuth(auth);
|
|
127
134
|
return auth;
|
|
128
135
|
}
|
|
136
|
+
// Device code flow — for headless/remote machines (RFC 8628 style):
|
|
137
|
+
// 1. POST /auth/device → get device_code + user_code
|
|
138
|
+
// 2. Print user_code and verification URL
|
|
139
|
+
// 3. Poll POST /auth/device/token until approved, expired, or timed out
|
|
140
|
+
export async function loginDevice(serverUrl, appUrl) {
|
|
141
|
+
// Step 1: request a device code
|
|
142
|
+
let initRes;
|
|
143
|
+
try {
|
|
144
|
+
initRes = await fetch(`${serverUrl}/auth/device`, {
|
|
145
|
+
method: "POST",
|
|
146
|
+
headers: { "Content-Type": "application/json", Origin: appUrl },
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
throw new Error(`Could not reach server at ${serverUrl} — is it running?\n (${err.message})`);
|
|
151
|
+
}
|
|
152
|
+
if (!initRes.ok) {
|
|
153
|
+
throw new Error(`Failed to start device login (${initRes.status})`);
|
|
154
|
+
}
|
|
155
|
+
const { device_code, user_code, verification_uri, interval } = await initRes.json();
|
|
156
|
+
// Step 2: show the code
|
|
157
|
+
console.log(`\n Visit: ${verification_uri}`);
|
|
158
|
+
console.log(` Code: ${user_code}\n`);
|
|
159
|
+
console.log("Waiting for authorization...");
|
|
160
|
+
// Step 3: poll
|
|
161
|
+
const pollInterval = (interval ?? 5) * 1000;
|
|
162
|
+
const deadline = Date.now() + 15 * 60 * 1000;
|
|
163
|
+
while (Date.now() < deadline) {
|
|
164
|
+
await new Promise((r) => setTimeout(r, pollInterval));
|
|
165
|
+
let pollRes;
|
|
166
|
+
try {
|
|
167
|
+
pollRes = await fetch(`${serverUrl}/auth/device/token`, {
|
|
168
|
+
method: "POST",
|
|
169
|
+
headers: { "Content-Type": "application/json" },
|
|
170
|
+
body: JSON.stringify({ device_code }),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
continue; // network blip — keep polling
|
|
175
|
+
}
|
|
176
|
+
if (pollRes.status === 202)
|
|
177
|
+
continue; // still pending
|
|
178
|
+
if (!pollRes.ok) {
|
|
179
|
+
const body = await pollRes.json().catch(() => ({}));
|
|
180
|
+
if (body.error === "expired_token")
|
|
181
|
+
throw new Error("Device code expired. Run `kubeagent login` again.");
|
|
182
|
+
throw new Error(body.error ?? `Unexpected response (${pollRes.status})`);
|
|
183
|
+
}
|
|
184
|
+
const { token, email, name } = await pollRes.json();
|
|
185
|
+
const auth = { serverUrl, appUrl, token, email, name };
|
|
186
|
+
saveAuth(auth);
|
|
187
|
+
return auth;
|
|
188
|
+
}
|
|
189
|
+
throw new Error("Login timed out (15 minutes).");
|
|
190
|
+
}
|
|
129
191
|
export async function createApiKey(auth, name) {
|
|
130
192
|
const url = `${auth.serverUrl}/keys`;
|
|
131
193
|
let res;
|
package/dist/cli.js
CHANGED
|
@@ -18,7 +18,7 @@ import { sendResolve } from "./notify/index.js";
|
|
|
18
18
|
import { diagnose } from "./diagnoser/index.js";
|
|
19
19
|
import { buildSystemPrompt } from "./kb/loader.js";
|
|
20
20
|
import { join } from "node:path";
|
|
21
|
-
import { loadAuth, loginBrowser, createApiKey, showAccount, clearAuth } from "./auth.js";
|
|
21
|
+
import { loadAuth, loginBrowser, loginDevice, createApiKey, showAccount, clearAuth } from "./auth.js";
|
|
22
22
|
import { fetchSlackWebhook } from "./proxy-client.js";
|
|
23
23
|
import { createInterface } from "node:readline";
|
|
24
24
|
import { scanProjectDirectory, matchProjectsToWorkloads, bestMatches } from "./onboard/project-matcher.js";
|
|
@@ -383,12 +383,15 @@ const DEFAULT_SERVER = "https://api.kubeagent.net";
|
|
|
383
383
|
const DEFAULT_APP_URL = "https://app.kubeagent.net";
|
|
384
384
|
program
|
|
385
385
|
.command("login")
|
|
386
|
-
.description("Log in to KubeAgent (opens browser)")
|
|
386
|
+
.description("Log in to KubeAgent (opens browser, or use --device for headless machines)")
|
|
387
387
|
.option("-s, --server <url>", "API server URL", DEFAULT_SERVER)
|
|
388
388
|
.option("--app-url <url>", "Dashboard URL (for browser login)", DEFAULT_APP_URL)
|
|
389
|
+
.option("--device", "Use device code flow (for remote/headless machines — no browser required)")
|
|
389
390
|
.action(async (opts) => {
|
|
390
391
|
try {
|
|
391
|
-
const auth =
|
|
392
|
+
const auth = opts.device
|
|
393
|
+
? await loginDevice(opts.server, opts.appUrl)
|
|
394
|
+
: await loginBrowser(opts.server, opts.appUrl);
|
|
392
395
|
console.log(chalk.green(`Logged in as ${auth.email ?? auth.name ?? "unknown"}`));
|
|
393
396
|
const { prefix } = await createApiKey(auth, "cli-default");
|
|
394
397
|
console.log(chalk.dim(`API key created: ${prefix}...`));
|