consortium
Version:
Remote control and session sharing CLI for AI coding agents
285 lines (281 loc) • 9.92 kB
JavaScript
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 };