gensx
Version:
`GenSX command line tools.
131 lines • 6.41 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { Buffer } from "node:buffer";
import { createHash, getRandomValues } from "node:crypto";
import { hostname } from "node:os";
import { Box, Text, useApp, useInput } from "ink";
import Spinner from "ink-spinner";
import { useCallback, useState } from "react";
import { ErrorMessage } from "../components/ErrorMessage.js";
import { LoadingSpinner } from "../components/LoadingSpinner.js";
import { resolveApiBaseUrl, resolveAppBaseUrl, saveAuth, saveState, } from "../utils/config.js";
import { USER_AGENT } from "../utils/user-agent.js";
function generateVerificationCode() {
return Buffer.from(getRandomValues(new Uint8Array(32))).toString("base64url");
}
function createCodeHash(code) {
return createHash("sha256").update(code).digest("base64url");
}
async function createLoginRequest(verificationCode) {
const apiBaseUrl = await resolveApiBaseUrl();
const url = new URL("/auth/device/request", apiBaseUrl);
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"User-Agent": USER_AGENT,
},
body: JSON.stringify({
clientId: hostname(),
codeChallenge: createCodeHash(verificationCode),
codeChallengeMethod: "S256",
}),
});
if (!response.ok) {
throw new Error(`Failed to create login request: ${response.statusText}`);
}
const body = (await response.json());
if (!body.requestId || !body.expiresAt) {
throw new Error("Invalid response from server");
}
return body;
}
async function pollLoginStatus(requestId, verificationCode) {
const apiBaseUrl = await resolveApiBaseUrl();
const url = new URL(`/auth/device/request/${requestId}`, apiBaseUrl);
url.searchParams.set("code_verifier", verificationCode);
const response = await fetch(url, {
headers: {
"User-Agent": USER_AGENT,
},
});
if (!response.ok) {
throw new Error(`Failed to check login status: ${response.statusText}`);
}
const body = (await response.json());
if (body.status === "pending") {
return { status: "pending" };
}
if (body.status === "expired") {
throw new Error("Login expired");
}
return body;
}
export function LoginUI() {
const { exit } = useApp();
const [phase, setPhase] = useState("initial");
const [error, setError] = useState(null);
const [authUrl, setAuthUrl] = useState(null);
const startLogin = useCallback(async () => {
try {
setPhase("creating");
const verificationCode = generateVerificationCode();
const request = await createLoginRequest(verificationCode);
setPhase("opening");
const appBaseUrl = await resolveAppBaseUrl();
const url = new URL(`/auth/device/${request.requestId}`, appBaseUrl);
url.searchParams.set("code_verifier", verificationCode);
setAuthUrl(url.toString());
// Open the browser
await import("open").then((open) => open.default(url.toString()));
setPhase("waiting");
// Poll until we get a completed status
let status;
do {
status = await pollLoginStatus(request.requestId, verificationCode);
if (status.status === "completed") {
await saveAuth({
token: status.token,
org: status.orgSlug,
});
await saveState({
hasCompletedFirstTimeSetup: true,
lastLoginAt: new Date().toISOString(),
});
setPhase("done");
setTimeout(() => {
exit();
}, 100);
return;
}
// Wait 1 second before polling again
await new Promise((resolve) => setTimeout(resolve, 1000));
} while (status.status === "pending");
}
catch (err) {
setError(err.message);
setPhase("error");
setTimeout(() => {
exit();
}, 100);
}
}, [exit]);
// Handle input
useInput((input, key) => {
if (key.escape) {
exit();
return;
}
if (phase === "initial" &&
(input === "\r" || input === "\n" || key.return)) {
void startLogin();
}
});
if (error) {
return _jsx(ErrorMessage, { message: error });
}
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [phase === "initial" && (_jsx(Text, { children: _jsx(Text, { color: "yellow", children: "Press Enter to open your browser and log in to GenSX Cloud (or press Escape to skip)" }) })), phase === "creating" && (_jsx(LoadingSpinner, { message: "Creating login request..." })), phase === "opening" && authUrl && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { color: "green", bold: true, children: "\u2714" }), " ", "Login request created"] }), _jsxs(Text, { children: [_jsx(Spinner, { type: "dots" }), " Opening ", _jsx(Text, { color: "cyan", children: authUrl })] })] })), phase === "waiting" && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { color: "green", bold: true, children: "\u2714" }), " ", "Login request created"] }), _jsxs(Text, { children: [_jsx(Text, { color: "green", bold: true, children: "\u2714" }), " ", "Browser opened ", _jsx(Text, { color: "cyan", children: authUrl })] }), _jsx(LoadingSpinner, { message: "Waiting for authentication..." })] })), phase === "done" && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { color: "green", bold: true, children: "\u2714" }), " ", "Login request created"] }), _jsxs(Text, { children: [_jsx(Text, { color: "green", bold: true, children: "\u2714" }), " ", "Browser opened ", _jsx(Text, { color: "cyan", children: authUrl })] }), _jsxs(Text, { children: [_jsx(Text, { color: "green", bold: true, children: "\u2714" }), " ", "Successfully logged into GenSX"] })] }))] }));
}
export function login() {
return { skipped: false };
}
//# sourceMappingURL=login.js.map