UNPKG

consortium

Version:

Remote control and session sharing CLI for AI coding agents

287 lines (282 loc) 10 kB
'use strict'; var axios = require('axios'); var node_crypto = require('node:crypto'); var os = require('node:os'); var tweetnacl = require('tweetnacl'); var chalk = require('chalk'); var React = require('react'); var ink = require('ink'); var persistence = require('./types-B_i6lpTn.cjs'); var index = require('./index-BMIckAk5.cjs'); var EmailInput = require('./EmailInput-94Ht3f-p.cjs'); require('fs'); require('node:fs'); require('node:path'); require('node:events'); require('socket.io-client'); require('zod'); require('child_process'); require('util'); require('fs/promises'); require('crypto'); require('path'); require('url'); require('os'); require('node:child_process'); require('node:fs/promises'); require('node:module'); require('node:util'); require('expo-server-sdk'); require('node:readline'); require('node:url'); require('ps-list'); require('cross-spawn'); require('tmp'); require('qrcode-terminal'); require('open'); require('fastify'); require('fastify-type-provider-zod'); require('http'); require('@modelcontextprotocol/sdk/client/index.js'); require('@modelcontextprotocol/sdk/client/streamableHttp.js'); require('readline'); require('@modelcontextprotocol/sdk/server/mcp.js'); require('node:http'); require('@modelcontextprotocol/sdk/server/streamableHttp.js'); const SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"]; const DeviceAuthScreen = ({ email, timeoutMs, onTimeout, onCancel }) => { const theme = index.useTheme(); const narrow = index.useIsNarrow(); const { stdout } = ink.useStdout(); const cols = stdout?.columns ?? 80; const rows = stdout?.rows ?? 24; const [frame, setFrame] = React.useState(0); const [elapsed, setElapsed] = React.useState(0); React.useEffect(() => { const id = setInterval(() => setFrame((f) => (f + 1) % SPINNER_FRAMES.length), 80); return () => clearInterval(id); }, []); React.useEffect(() => { const id = setInterval(() => { setElapsed((prev) => { const next = prev + 1; if (next * 1e3 >= timeoutMs) onTimeout(); return next; }); }, 1e3); return () => clearInterval(id); }, [timeoutMs, onTimeout]); ink.useInput((input, key) => { if (key.escape || input === "q" || input === "Q" || key.ctrl && input === "c") { onCancel(); } }); const remaining = Math.max(0, Math.ceil((timeoutMs - elapsed * 1e3) / 1e3)); const spinner = SPINNER_FRAMES[frame]; const showTimeoutWarning = remaining < 120; const infoBoxWidth = Math.min(cols - 4, narrow ? cols - 4 : 78); return /* @__PURE__ */ React.createElement( ink.Box, { flexDirection: "column", alignItems: "center", justifyContent: "center", width: cols, minHeight: rows }, /* @__PURE__ */ React.createElement(index.Constellation, null), /* @__PURE__ */ React.createElement(ink.Box, { height: 1 }), /* @__PURE__ */ React.createElement(index.Wordmark, null), /* @__PURE__ */ React.createElement(ink.Box, { height: 1 }), /* @__PURE__ */ React.createElement(ink.Text, { color: theme.dim }, "Build your Consortium"), /* @__PURE__ */ React.createElement(ink.Box, { height: 2 }), /* @__PURE__ */ React.createElement(ink.Text, { bold: true }, "Verify from Another Device"), /* @__PURE__ */ React.createElement(ink.Box, { height: 1 }), /* @__PURE__ */ React.createElement( ink.Box, { borderStyle: "round", borderColor: theme.chrome, paddingX: 2, paddingY: 1, width: infoBoxWidth, flexDirection: "column", alignItems: "center" }, /* @__PURE__ */ React.createElement(ink.Text, null, "Approval sent to ", /* @__PURE__ */ React.createElement(ink.Text, { color: "cyan" }, email)), /* @__PURE__ */ React.createElement(ink.Box, { height: 1 }), /* @__PURE__ */ React.createElement(ink.Text, { color: theme.dim }, "Open the Consortium app on a signed-in device and approve to continue.") ), /* @__PURE__ */ React.createElement(ink.Box, { height: 1 }), /* @__PURE__ */ React.createElement(ink.Box, null, /* @__PURE__ */ React.createElement(ink.Text, { color: theme.chrome }, spinner), /* @__PURE__ */ React.createElement(ink.Text, { color: theme.dim }, ` Waiting for approval\u2026 (${elapsed}s)`)), showTimeoutWarning && /* @__PURE__ */ React.createElement(ink.Text, { color: theme.dim }, `Times out in ${remaining}s`), /* @__PURE__ */ React.createElement(ink.Box, { height: 2 }), /* @__PURE__ */ React.createElement(ink.Text, { color: theme.dim }, "Esc or q to cancel") ); }; const TIMEOUT_MS = 6e5; function collectEmail() { return new Promise((resolve) => { let resolved = false; const onSubmit = (email) => { if (resolved) return; resolved = true; app.unmount(); resolve(email); }; const onCancel = () => { if (resolved) return; resolved = true; app.unmount(); resolve(null); }; const app = ink.render( React.createElement(EmailInput.EmailInput, { onSubmit, onCancel }), { exitOnCtrlC: false, patchConsole: false } ); }); } async function pollStatus(requestId) { const res = await axios.get( `${persistence.configuration.serverUrl}/v1/auth/device-approval/status/${requestId}`, { timeout: 1e4 } ); return res.data; } async function doDeviceAuth() { const email = await collectEmail(); if (!email) { console.log("\nAuthentication cancelled.\n"); return null; } const secret = new Uint8Array(node_crypto.randomBytes(32)); const keypair = tweetnacl.box.keyPair.fromSecretKey(secret); let requestId; try { const res = await axios.post( `${persistence.configuration.serverUrl}/v1/auth/device-approval/request`, { email, requestingPublicKey: persistence.encodeBase64(keypair.publicKey), requestingHost: os.hostname(), requestingPlatform: process.platform }, { timeout: 15e3 } ); requestId = res.data.requestId; } catch (err) { const message = extractErrorMessage(err); console.log(chalk.red(` Failed to send device-approval request: ${message}`)); return null; } let cancelled = false; let timedOut = false; const handleCancel = () => { cancelled = true; }; const handleTimeout = () => { timedOut = true; }; const handleInterrupt = () => { handleCancel(); }; process.on("SIGINT", handleInterrupt); const waitingApp = ink.render( React.createElement(DeviceAuthScreen, { email, timeoutMs: TIMEOUT_MS, onTimeout: handleTimeout, onCancel: handleCancel }), { exitOnCtrlC: false, patchConsole: false } ); let pollInterval = 1e3; let result = null; try { while (!cancelled && !timedOut) { try { const status = await pollStatus(requestId); if (status.status === "approved") { waitingApp.unmount(); const encryptedBundle = persistence.decodeBase64(status.response); const decrypted = index.decryptWithEphemeralKey(encryptedBundle, keypair.secretKey); if (!decrypted) { console.log(chalk.red("\nFailed to decrypt device-approval response. Please try again.")); return null; } const token = status.token; if (decrypted.length === 32) { const credentials = { secret: decrypted, token }; await persistence.writeCredentialsLegacy(credentials); console.log("\n\u2713 Authentication successful\n"); result = { encryption: { type: "legacy", secret: decrypted }, token }; } else if (decrypted[0] === 0) { const publicKey = decrypted.slice(1, 33); const machineKey = node_crypto.randomBytes(32); const credentials = { publicKey, machineKey, token }; await persistence.writeCredentialsDataKey(credentials); console.log("\n\u2713 Authentication successful\n"); result = { encryption: { type: "dataKey", publicKey, machineKey }, token }; } else { console.log(chalk.red("\nUnrecognised credential format in device-approval response.")); return null; } return result; } if (status.status === "denied") { waitingApp.unmount(); console.log(chalk.yellow("\nDevice-approval request was denied.")); return null; } if (status.status === "expired") { waitingApp.unmount(); console.log(chalk.yellow('\nDevice-approval request expired. Run "consortium auth login" to try again.')); return null; } } catch (err) { if (axios.isAxiosError(err) && err.response?.status === 404) { waitingApp.unmount(); console.log(chalk.red("\nDevice-approval request not found (server may have restarted). Please try again.")); return null; } if (process.env.DEBUG) { console.log(chalk.gray(` [device-auth transient error: ${err.message}]`)); } } await persistence.delay(pollInterval); pollInterval = Math.min(Math.round(pollInterval * 1.5), 5e3); } waitingApp.unmount(); if (cancelled) { console.log("\nAuthentication cancelled."); } else if (timedOut) { console.log(chalk.red('\nDevice-approval timed out. Run "consortium auth login" to try again.')); } } finally { process.off("SIGINT", handleInterrupt); waitingApp.unmount(); } return null; } function extractErrorMessage(err) { if (axios.isAxiosError(err)) { const data = err.response?.data; if (data?.error?.message) return data.error.message; if (err.message) return err.message; } if (err instanceof Error) return err.message; return "Unknown error"; } exports.doDeviceAuth = doDeviceAuth;