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