UNPKG

consortium

Version:

Remote control and session sharing CLI for AI coding agents

285 lines (281 loc) 9.92 kB
import axios from 'axios'; import { randomBytes } from 'node:crypto'; import os__default from 'node:os'; import tweetnacl from 'tweetnacl'; import chalk from 'chalk'; import React, { useState, useEffect } from 'react'; import { useStdout, useInput, Box, Text, render } from 'ink'; import { c as configuration, e as encodeBase64, d as decodeBase64, w as writeCredentialsLegacy, f as writeCredentialsDataKey, g as delay } from './types-DETLaopx.mjs'; import { u as useIsNarrow, C as Constellation, W as Wordmark, c as useTheme, d as decryptWithEphemeralKey } from './index-DiNLHtkZ.mjs'; import { E as EmailInput } from './EmailInput-DNuvoMjN.mjs'; import 'fs'; import 'node:fs'; import 'node:path'; import 'node:events'; import 'socket.io-client'; import 'zod'; import 'child_process'; import 'util'; import 'fs/promises'; import 'crypto'; import 'path'; import 'url'; import 'os'; import 'node:child_process'; import 'node:fs/promises'; import 'node:module'; import 'node:util'; import 'expo-server-sdk'; import 'node:readline'; import 'node:url'; import 'ps-list'; import 'cross-spawn'; import 'tmp'; import 'qrcode-terminal'; import 'open'; import 'fastify'; import 'fastify-type-provider-zod'; import 'http'; import '@modelcontextprotocol/sdk/client/index.js'; import '@modelcontextprotocol/sdk/client/streamableHttp.js'; import 'readline'; import '@modelcontextprotocol/sdk/server/mcp.js'; import 'node:http'; import '@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 = useTheme(); const narrow = useIsNarrow(); const { stdout } = useStdout(); const cols = stdout?.columns ?? 80; const rows = stdout?.rows ?? 24; const [frame, setFrame] = useState(0); const [elapsed, setElapsed] = useState(0); useEffect(() => { const id = setInterval(() => setFrame((f) => (f + 1) % SPINNER_FRAMES.length), 80); return () => clearInterval(id); }, []); useEffect(() => { const id = setInterval(() => { setElapsed((prev) => { const next = prev + 1; if (next * 1e3 >= timeoutMs) onTimeout(); return next; }); }, 1e3); return () => clearInterval(id); }, [timeoutMs, onTimeout]); 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( Box, { flexDirection: "column", alignItems: "center", justifyContent: "center", width: cols, minHeight: rows }, /* @__PURE__ */ React.createElement(Constellation, null), /* @__PURE__ */ React.createElement(Box, { height: 1 }), /* @__PURE__ */ React.createElement(Wordmark, null), /* @__PURE__ */ React.createElement(Box, { height: 1 }), /* @__PURE__ */ React.createElement(Text, { color: theme.dim }, "Build your Consortium"), /* @__PURE__ */ React.createElement(Box, { height: 2 }), /* @__PURE__ */ React.createElement(Text, { bold: true }, "Verify from Another Device"), /* @__PURE__ */ React.createElement(Box, { height: 1 }), /* @__PURE__ */ React.createElement( Box, { borderStyle: "round", borderColor: theme.chrome, paddingX: 2, paddingY: 1, width: infoBoxWidth, flexDirection: "column", alignItems: "center" }, /* @__PURE__ */ React.createElement(Text, null, "Approval sent to ", /* @__PURE__ */ React.createElement(Text, { color: "cyan" }, email)), /* @__PURE__ */ React.createElement(Box, { height: 1 }), /* @__PURE__ */ React.createElement(Text, { color: theme.dim }, "Open the Consortium app on a signed-in device and approve to continue.") ), /* @__PURE__ */ React.createElement(Box, { height: 1 }), /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { color: theme.chrome }, spinner), /* @__PURE__ */ React.createElement(Text, { color: theme.dim }, ` Waiting for approval\u2026 (${elapsed}s)`)), showTimeoutWarning && /* @__PURE__ */ React.createElement(Text, { color: theme.dim }, `Times out in ${remaining}s`), /* @__PURE__ */ React.createElement(Box, { height: 2 }), /* @__PURE__ */ React.createElement(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 = render( React.createElement(EmailInput, { onSubmit, onCancel }), { exitOnCtrlC: false, patchConsole: false } ); }); } async function pollStatus(requestId) { const res = await axios.get( `${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(randomBytes(32)); const keypair = tweetnacl.box.keyPair.fromSecretKey(secret); let requestId; try { const res = await axios.post( `${configuration.serverUrl}/v1/auth/device-approval/request`, { email, requestingPublicKey: encodeBase64(keypair.publicKey), requestingHost: os__default.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 = 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 = decodeBase64(status.response); const decrypted = 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 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 = randomBytes(32); const credentials = { publicKey, machineKey, token }; await 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 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"; } export { doDeviceAuth };