@genesiscomputing/genesis-connect 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,62 @@
1
- # Temporary Holding Version
1
+ # genesis-connect
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Sign an MCP client in to Genesis with a code, on a machine with no browser — one you
4
+ reach over SSH, a dev box, a container. Like `gh auth login`: the command shows a code,
5
+ you approve it in any browser, and the sign-in finishes on this machine. No SSH tunnel
6
+ and no copy-pasting a redirect address.
7
+
8
+ ```bash
9
+ npx -y @genesiscomputing/genesis-connect https://<your-genesis-host>
10
+ ```
11
+
12
+ 1. It starts the client's own sign-in in the background and shows a code and a link,
13
+ `https://<your-genesis-host>/device`.
14
+ 2. Open the link in any browser (your laptop is fine), enter the code, and check what is
15
+ asking: the client, its server name, and the address it asked from. Choose
16
+ **Approve**, then sign in to Genesis as usual.
17
+ 3. The command hands the sign-in to the client on this machine and prints
18
+ `Signed in to Genesis`.
19
+
20
+ The code works once and expires after 10 minutes. Only approve a code you started
21
+ yourself: if someone sends you a link or a code to enter, choose **Deny**.
22
+
23
+ Works on macOS, Linux and Windows with Node 20 or later. On macOS and Linux,
24
+ `curl -fsSL https://<your-genesis-host>/mcp/connect | sh` does the same without Node.
25
+
26
+ ## Clients
27
+
28
+ | `--client` | What it runs |
29
+ |---|---|
30
+ | `genie` (Genie Code) | `genie mcp add <name> --url <host>/mcp`, or `genie mcp login <name>` if it is already set up for this Genesis |
31
+ | `codex` | the same with `codex` |
32
+ | `genesis-room` | `genesis-room login -genesis-url <host>` (needs a genesis-room newer than v0.1) |
33
+
34
+ With no `--client`, the first of those installed is used. `--name` sets the MCP server
35
+ name for genie and codex (default `genesis`).
36
+
37
+ Any other tool that signs in the same way — it prints a sign-in link for this Genesis's
38
+ `/authorize` and then waits for the browser on `127.0.0.1` — can be signed in by giving
39
+ its command after `--`:
40
+
41
+ ```bash
42
+ npx -y @genesiscomputing/genesis-connect https://<your-genesis-host> -- mytool login
43
+ ```
44
+
45
+ Claude Code, Claude Desktop and Cursor sign in from inside the app (`/mcp`), not with a
46
+ command, so they are not driven by this. Where they run without a browser, connect them
47
+ with a Genesis API key instead.
48
+
49
+ ## Development
50
+
51
+ ```bash
52
+ npm install
53
+ npm test
54
+ npm run build
55
+ node dist/cli.js --help
56
+ ```
57
+
58
+ Publishing (from an npm account with access to the `@genesiscomputing` scope):
59
+
60
+ ```bash
61
+ npm ci && npm test && npm publish
62
+ ```
package/dist/cli.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ export declare const CLI_VERSION = "0.1.0";
3
+ export interface CliArguments {
4
+ genesisUrl: string;
5
+ client?: string;
6
+ name?: string;
7
+ command?: string[];
8
+ }
9
+ /** The parsed command line, or the text to print for --help / --version. */
10
+ export declare function parseCommandLine(argv: string[], env?: NodeJS.ProcessEnv): CliArguments | {
11
+ print: string;
12
+ };
13
+ export declare function main(argv: string[], signal?: AbortSignal): Promise<number>;
package/dist/cli.js ADDED
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+ // genesis-connect <genesis-url> [--client genie|codex|genesis-room] [--name NAME] [-- command ...]
3
+ import { realpathSync } from "node:fs";
4
+ import { fileURLToPath } from "node:url";
5
+ import { parseArgs } from "node:util";
6
+ import { NAMED_CLIENTS, planLaunch } from "./clients.js";
7
+ import { connect, InterruptedError } from "./connect.js";
8
+ import { normalizeGenesisUrl, SignInError } from "./genesis.js";
9
+ export const CLI_VERSION = "0.1.0";
10
+ const USAGE = `Usage: genesis-connect <genesis-url> [--client ${NAMED_CLIENTS.join("|")}] [--name NAME]
11
+ genesis-connect <genesis-url> [--name NAME] -- <command> [args...]
12
+
13
+ Signs an MCP client on this machine in to Genesis with a code you approve in any
14
+ browser, so no browser or tunnel is needed here.
15
+
16
+ <genesis-url> Your Genesis, e.g. https://genesis.example.com (or set GENESIS_URL)
17
+ --client The client to sign in; default: the first of ${NAMED_CLIENTS.join(", ")} installed
18
+ --name Its MCP server name for this Genesis (genie, codex; default: genesis)
19
+ -- <command> Any other command that prints a Genesis sign-in link and waits on localhost
20
+ --version, --help`;
21
+ /** The parsed command line, or the text to print for --help / --version. */
22
+ export function parseCommandLine(argv, env = process.env) {
23
+ const separator = argv.indexOf("--");
24
+ const own = separator === -1 ? argv : argv.slice(0, separator);
25
+ const command = separator === -1 ? undefined : argv.slice(separator + 1);
26
+ let parsed;
27
+ try {
28
+ parsed = parseArgs({
29
+ args: own,
30
+ allowPositionals: true,
31
+ options: {
32
+ client: { type: "string" },
33
+ name: { type: "string" },
34
+ help: { type: "boolean", short: "h" },
35
+ version: { type: "boolean" },
36
+ },
37
+ });
38
+ }
39
+ catch (error) {
40
+ throw new SignInError(`${error.message}\n${USAGE}`);
41
+ }
42
+ if (parsed.values.help) {
43
+ return { print: USAGE };
44
+ }
45
+ if (parsed.values.version) {
46
+ return { print: CLI_VERSION };
47
+ }
48
+ if (parsed.positionals.length > 1) {
49
+ throw new SignInError(`unexpected argument: ${parsed.positionals[1]}\n${USAGE}`);
50
+ }
51
+ const rawUrl = parsed.positionals[0] ?? env.GENESIS_URL;
52
+ if (!rawUrl) {
53
+ throw new SignInError(`give your Genesis address.\n${USAGE}`);
54
+ }
55
+ if (command !== undefined && command.length === 0) {
56
+ throw new SignInError(`give a command after --.\n${USAGE}`);
57
+ }
58
+ return {
59
+ genesisUrl: normalizeGenesisUrl(rawUrl),
60
+ client: parsed.values.client,
61
+ name: parsed.values.name,
62
+ command,
63
+ };
64
+ }
65
+ /** Exit status for an interrupted sign-in, as for any command stopped by Ctrl-C. */
66
+ const INTERRUPTED_EXIT_STATUS = 130;
67
+ export async function main(argv, signal) {
68
+ try {
69
+ const parsed = parseCommandLine(argv);
70
+ if ("print" in parsed) {
71
+ console.log(parsed.print);
72
+ return 0;
73
+ }
74
+ const launch = await planLaunch(parsed, undefined, signal);
75
+ await connect({ genesisUrl: parsed.genesisUrl, launch, signal });
76
+ console.log("");
77
+ console.log(` Signed in to Genesis. ${launch.next}`);
78
+ return 0;
79
+ }
80
+ catch (error) {
81
+ if (error instanceof InterruptedError || signal?.aborted) {
82
+ console.error("genesis-connect: interrupted.");
83
+ return INTERRUPTED_EXIT_STATUS;
84
+ }
85
+ const message = error instanceof SignInError ? error.message : `unexpected error: ${error.message}`;
86
+ console.error(`genesis-connect: ${message}`);
87
+ return 1;
88
+ }
89
+ }
90
+ function invokedDirectly() {
91
+ try {
92
+ return realpathSync(process.argv[1] ?? "") === realpathSync(fileURLToPath(import.meta.url));
93
+ }
94
+ catch {
95
+ return false;
96
+ }
97
+ }
98
+ if (invokedDirectly()) {
99
+ // Ctrl-C cancels the sign-in, which stops the client it started, rather than exiting
100
+ // with that client still waiting on loopback.
101
+ const interrupt = new AbortController();
102
+ const onSignal = () => interrupt.abort();
103
+ process.once("SIGINT", onSignal);
104
+ process.once("SIGTERM", onSignal);
105
+ main(process.argv.slice(2), interrupt.signal).then((code) => {
106
+ process.off("SIGINT", onSignal);
107
+ process.off("SIGTERM", onSignal);
108
+ process.exitCode = code;
109
+ });
110
+ }
111
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,58 @@
1
+ import { type ChildProcess } from "node:child_process";
2
+ /** The `client` value Genesis knows; it shows a fixed name for each on its approval page. */
3
+ export type ClientId = "genie" | "codex" | "genesis-room" | "other";
4
+ export declare const NAMED_CLIENTS: readonly ["genie", "codex", "genesis-room"];
5
+ export declare const DEFAULT_SERVER_NAME = "genesis";
6
+ /** How to start one client's sign-in. */
7
+ export interface ClientLaunch {
8
+ client: ClientId;
9
+ /** What to call it in messages. */
10
+ label: string;
11
+ /** The executable, resolved on PATH. */
12
+ executable: string;
13
+ args: string[];
14
+ /** The name Genesis shows on its approval page. */
15
+ serverName: string;
16
+ /** What to do once signed in. */
17
+ next: string;
18
+ }
19
+ export interface LaunchRequest {
20
+ genesisUrl: string;
21
+ /** genie, codex or genesis-room; unset to pick the one installed. */
22
+ client?: string;
23
+ /** Any other command that prints a Genesis sign-in URL and waits on loopback. */
24
+ command?: string[];
25
+ name?: string;
26
+ }
27
+ /** Runs a short command to completion; swapped out in tests. */
28
+ export interface CommandRunner {
29
+ which(command: string): string | null;
30
+ run(executable: string, args: string[], signal?: AbortSignal): Promise<{
31
+ code: number | null;
32
+ output: string;
33
+ }>;
34
+ }
35
+ /**
36
+ * The launch for the requested client, after checking it is installed and, for genie
37
+ * and codex, that an MCP server of this name is not already set up for another Genesis.
38
+ * Runs at most one short command of the client's (`mcp get` or `-version`).
39
+ */
40
+ export declare function planLaunch(request: LaunchRequest, runner?: CommandRunner, signal?: AbortSignal): Promise<ClientLaunch>;
41
+ /** The real PATH lookup and process runner. */
42
+ export declare const systemRunner: CommandRunner;
43
+ /**
44
+ * Start a client process that this package owns: aborting `signal` stops it and every
45
+ * process under it (stopProcessTree). Windows cannot start a .cmd or .bat shim without a
46
+ * shell; every argument this package builds is a checked name or URL, so none carries a
47
+ * shell metacharacter.
48
+ */
49
+ export declare function startOwnedProcess(executable: string, args: string[], env: NodeJS.ProcessEnv, signal?: AbortSignal): ChildProcess;
50
+ /**
51
+ * Stop a started client and everything it started. On Windows a .cmd shim runs the
52
+ * client under cmd.exe, so killing the tracked process would stop only the shell and
53
+ * leave the client waiting on loopback: taskkill /T takes the whole tree. Runs taskkill
54
+ * to completion there (a short local command).
55
+ */
56
+ export declare function stopProcessTree(child: ChildProcess, platform?: NodeJS.Platform): void;
57
+ /** The executable PATH resolves `command` to, or null. Honours PATHEXT on Windows. */
58
+ export declare function findExecutable(command: string, pathVariable?: string, platform?: NodeJS.Platform, isExecutable?: (path: string) => boolean): string | null;
@@ -0,0 +1,197 @@
1
+ // Which MCP client to sign in, and the command that starts its own browser sign-in.
2
+ //
3
+ // Every client here signs in the same way: it prints a sign-in URL for Genesis's
4
+ // /authorize, then waits for the browser to come back to a 127.0.0.1 address on this
5
+ // machine. That wait is what a code sign-in finishes for it.
6
+ import { spawn, spawnSync } from "node:child_process";
7
+ import { accessSync, constants, statSync } from "node:fs";
8
+ import { isAbsolute, join } from "node:path";
9
+ import { SignInError } from "./genesis.js";
10
+ export const NAMED_CLIENTS = ["genie", "codex", "genesis-room"];
11
+ export const DEFAULT_SERVER_NAME = "genesis";
12
+ const SERVER_NAME_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
13
+ const PROBE_TIMEOUT_MS = 30_000;
14
+ /**
15
+ * The launch for the requested client, after checking it is installed and, for genie
16
+ * and codex, that an MCP server of this name is not already set up for another Genesis.
17
+ * Runs at most one short command of the client's (`mcp get` or `-version`).
18
+ */
19
+ export async function planLaunch(request, runner = systemRunner, signal) {
20
+ const name = request.name ?? DEFAULT_SERVER_NAME;
21
+ if (!SERVER_NAME_PATTERN.test(name)) {
22
+ throw new SignInError("--name may use only letters, digits, - and _ (at most 64).");
23
+ }
24
+ if (request.command && request.command.length > 0) {
25
+ if (request.client) {
26
+ throw new SignInError("give either --client or a command after --, not both.");
27
+ }
28
+ return planOther(request.command, name, runner);
29
+ }
30
+ const client = request.client ?? detectClient(runner);
31
+ switch (client) {
32
+ case "genie":
33
+ return planCodexFamily("genie", "Genie Code", name, request.genesisUrl, runner, signal);
34
+ case "codex":
35
+ return planCodexFamily("codex", "Codex", name, request.genesisUrl, runner, signal);
36
+ case "genesis-room":
37
+ return planGenesisRoom(request.genesisUrl, runner, signal);
38
+ default:
39
+ throw new SignInError(`unsupported client: ${client} (supported: ${NAMED_CLIENTS.join(", ")}, or a command after --)`);
40
+ }
41
+ }
42
+ function detectClient(runner) {
43
+ const installed = NAMED_CLIENTS.find((client) => runner.which(client) !== null);
44
+ if (!installed) {
45
+ throw new SignInError("no supported MCP client found. Install Genie Code (genie), Codex or genesis-room, or pass --client.");
46
+ }
47
+ return installed;
48
+ }
49
+ function requireInstalled(command, runner) {
50
+ const executable = runner.which(command);
51
+ if (!executable) {
52
+ throw new SignInError(`${command} is not installed or not on PATH.`);
53
+ }
54
+ return executable;
55
+ }
56
+ // Genie Code is Codex underneath: `mcp add` registers the server and signs in at once,
57
+ // `mcp login` signs an existing one in again.
58
+ async function planCodexFamily(client, label, name, genesisUrl, runner, signal) {
59
+ const executable = requireInstalled(client, runner);
60
+ const mcpUrl = `${genesisUrl}/mcp`;
61
+ const existing = await runner.run(executable, ["mcp", "get", name], signal);
62
+ let args = ["mcp", "add", name, "--url", mcpUrl];
63
+ if (existing.code === 0) {
64
+ const configured = /^\s*url:\s*(\S+)/m.exec(existing.output)?.[1]?.replace(/\/+$/, "");
65
+ if (configured !== mcpUrl) {
66
+ throw new SignInError(`${label} already has an MCP server named ${name} for ${configured ?? "another server"}. Pass --name with another name.`);
67
+ }
68
+ args = ["mcp", "login", name];
69
+ }
70
+ return {
71
+ client,
72
+ label,
73
+ executable,
74
+ args,
75
+ serverName: name,
76
+ next: `Start ${client} and type /mcp to see the ${name} tools.`,
77
+ };
78
+ }
79
+ async function planGenesisRoom(genesisUrl, runner, signal) {
80
+ const executable = requireInstalled("genesis-room", runner);
81
+ // v0.1 has no login command: given one, it starts its MCP server instead and waits on
82
+ // stdin for a client that never comes.
83
+ const version = await runner.run(executable, ["-version"], signal);
84
+ if (/v0\.1\.\d+\b/.test(version.output)) {
85
+ throw new SignInError(`this genesis-room (${version.output.trim()}) has no login command. Install a newer genesis-room and run this again.`);
86
+ }
87
+ return {
88
+ client: "genesis-room",
89
+ label: "genesis-room",
90
+ executable,
91
+ args: ["login", "-genesis-url", genesisUrl],
92
+ serverName: "genesis-room",
93
+ next: "Restart Claude Code (or the MCP client running genesis-room) to use the sign-in.",
94
+ };
95
+ }
96
+ function planOther(command, name, runner) {
97
+ const [program, ...args] = command;
98
+ const executable = requireInstalled(program, runner);
99
+ return {
100
+ client: "other",
101
+ label: program,
102
+ executable,
103
+ args,
104
+ serverName: name,
105
+ next: `${program} is signed in.`,
106
+ };
107
+ }
108
+ /** The real PATH lookup and process runner. */
109
+ export const systemRunner = {
110
+ which: findExecutable,
111
+ run: (executable, args, signal) => new Promise((resolve, reject) => {
112
+ const child = startOwnedProcess(executable, args, process.env, signal);
113
+ let output = "";
114
+ child.stdout?.on("data", (chunk) => (output += chunk.toString()));
115
+ child.stderr?.on("data", (chunk) => (output += chunk.toString()));
116
+ const timer = setTimeout(() => stopProcessTree(child), PROBE_TIMEOUT_MS);
117
+ child.on("error", (error) => {
118
+ clearTimeout(timer);
119
+ reject(new SignInError(`could not run ${executable}: ${error.message}`));
120
+ });
121
+ child.on("close", (code) => {
122
+ clearTimeout(timer);
123
+ resolve({ code, output });
124
+ });
125
+ }),
126
+ };
127
+ /**
128
+ * Start a client process that this package owns: aborting `signal` stops it and every
129
+ * process under it (stopProcessTree). Windows cannot start a .cmd or .bat shim without a
130
+ * shell; every argument this package builds is a checked name or URL, so none carries a
131
+ * shell metacharacter.
132
+ */
133
+ export function startOwnedProcess(executable, args, env, signal) {
134
+ const child = spawn(executable, args, {
135
+ env,
136
+ shell: process.platform === "win32" && /\.(cmd|bat)$/i.test(executable),
137
+ windowsHide: true,
138
+ stdio: ["pipe", "pipe", "pipe"],
139
+ });
140
+ if (signal) {
141
+ const stop = () => stopProcessTree(child);
142
+ if (signal.aborted) {
143
+ stop();
144
+ }
145
+ else {
146
+ signal.addEventListener("abort", stop, { once: true });
147
+ child.on("close", () => signal.removeEventListener("abort", stop));
148
+ }
149
+ }
150
+ return child;
151
+ }
152
+ /**
153
+ * Stop a started client and everything it started. On Windows a .cmd shim runs the
154
+ * client under cmd.exe, so killing the tracked process would stop only the shell and
155
+ * leave the client waiting on loopback: taskkill /T takes the whole tree. Runs taskkill
156
+ * to completion there (a short local command).
157
+ */
158
+ export function stopProcessTree(child, platform = process.platform) {
159
+ if (child.exitCode !== null || child.signalCode !== null || child.pid === undefined) {
160
+ return;
161
+ }
162
+ if (platform === "win32") {
163
+ spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
164
+ return;
165
+ }
166
+ child.kill();
167
+ }
168
+ /** The executable PATH resolves `command` to, or null. Honours PATHEXT on Windows. */
169
+ export function findExecutable(command, pathVariable = process.env.PATH ?? "", platform = process.platform, isExecutable = canExecute) {
170
+ // On Windows an extensionless file beside genie.cmd is a POSIX shim it cannot start.
171
+ const pathExtensions = (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";").filter(Boolean);
172
+ const extensions = platform !== "win32" ? [""] : /\.[A-Za-z0-9]+$/.test(command) ? [""] : pathExtensions;
173
+ const candidates = (base) => extensions.map((extension) => `${base}${extension}`);
174
+ if (isAbsolute(command) || command.includes("/") || command.includes("\\")) {
175
+ return candidates(command).find(isExecutable) ?? null;
176
+ }
177
+ for (const directory of pathVariable.split(platform === "win32" ? ";" : ":")) {
178
+ if (!directory) {
179
+ continue;
180
+ }
181
+ const found = candidates(join(directory, command)).find(isExecutable);
182
+ if (found) {
183
+ return found;
184
+ }
185
+ }
186
+ return null;
187
+ }
188
+ function canExecute(path) {
189
+ try {
190
+ accessSync(path, process.platform === "win32" ? constants.F_OK : constants.X_OK);
191
+ return statSync(path).isFile();
192
+ }
193
+ catch {
194
+ return false;
195
+ }
196
+ }
197
+ //# sourceMappingURL=clients.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clients.js","sourceRoot":"","sources":["../src/clients.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ import type { ChildProcess } from "node:child_process";
2
+ import { type ClientLaunch } from "./clients.js";
3
+ import { SignInError } from "./genesis.js";
4
+ export interface ConnectOptions {
5
+ genesisUrl: string;
6
+ launch: ClientLaunch;
7
+ /** Prints one line for the person. */
8
+ say?: (line: string) => void;
9
+ /** Waits, ending early when the signal aborts; swapped out in tests. */
10
+ sleep?: (ms: number, signal: AbortSignal) => Promise<void>;
11
+ start?: (executable: string, args: string[], signal: AbortSignal) => ChildProcess;
12
+ /** Aborting stops the client and rejects with InterruptedError. */
13
+ signal?: AbortSignal;
14
+ }
15
+ /** The sign-in was interrupted (Ctrl-C); the client has been stopped. */
16
+ export declare class InterruptedError extends SignInError {
17
+ constructor();
18
+ }
19
+ /**
20
+ * Run the whole code sign-in. Resolves once the client has saved its sign-in; rejects
21
+ * with a SignInError saying what to do otherwise. Network calls: one start, one poll
22
+ * every few seconds until approved (at most the code's lifetime, 10 minutes), and one
23
+ * loopback request.
24
+ */
25
+ export declare function connect(options: ConnectOptions): Promise<void>;
26
+ /**
27
+ * Wait `ms`, or less if `signal` aborts first. The timer is cleared either way, so an
28
+ * interrupted sign-in exits at once instead of sitting out the current poll interval.
29
+ */
30
+ export declare function abortableSleep(ms: number, signal: AbortSignal): Promise<void>;
@@ -0,0 +1,248 @@
1
+ // Sign a client in with a code: start its own sign-in, show a code for Genesis's
2
+ // /device page, and once the person has approved and signed in there, deliver the
3
+ // sign-in to the client on this machine, the one place its loopback callback reaches.
4
+ import { startOwnedProcess, stopProcessTree } from "./clients.js";
5
+ import { deliverCallback, findAuthorizeUrl, pollCodeSignIn, printedForeignAuthorizeUrl, SignInError, startCodeSignIn, } from "./genesis.js";
6
+ const AUTHORIZE_URL_WAIT_MS = 60_000;
7
+ const CLIENT_FINISH_WAIT_MS = 60_000;
8
+ const MAX_POLL_MISSES = 10;
9
+ const MAX_CLIENT_OUTPUT = 64 * 1024;
10
+ /** Backoff ceiling: Genesis asking to slow down never stretches a poll past this. */
11
+ const MAX_POLL_INTERVAL_SECONDS = 30;
12
+ /** The sign-in was interrupted (Ctrl-C); the client has been stopped. */
13
+ export class InterruptedError extends SignInError {
14
+ constructor() {
15
+ super("interrupted.");
16
+ }
17
+ }
18
+ /**
19
+ * Run the whole code sign-in. Resolves once the client has saved its sign-in; rejects
20
+ * with a SignInError saying what to do otherwise. Network calls: one start, one poll
21
+ * every few seconds until approved (at most the code's lifetime, 10 minutes), and one
22
+ * loopback request.
23
+ */
24
+ export async function connect(options) {
25
+ const { genesisUrl, launch } = options;
26
+ const say = options.say ?? ((line) => console.log(line));
27
+ const sleep = options.sleep ?? abortableSleep;
28
+ const signal = options.signal ?? new AbortController().signal;
29
+ const client = new RunningClient((options.start ?? startClient)(launch.executable, launch.args, signal), launch.label);
30
+ try {
31
+ const authorizeUrl = await client.waitForAuthorizeUrl(genesisUrl, sleep, signal);
32
+ if (authorizeUrl === null) {
33
+ // It finished without asking: its sign-in was still good.
34
+ return;
35
+ }
36
+ const started = await startCodeSignIn(genesisUrl, { authorizeUrl, client: launch.client, name: launch.serverName }, signal);
37
+ say("");
38
+ say(` To sign ${launch.label} in to Genesis, open this page in any browser:`);
39
+ say("");
40
+ say(` ${started.verificationUri}`);
41
+ say("");
42
+ say(` and enter the code ${started.userCode}`);
43
+ say("");
44
+ say(` Or open ${started.verificationUriComplete}`);
45
+ say("");
46
+ say(` Waiting for you to approve (the code expires in ${Math.round(started.expiresInSeconds / 60)} minutes)...`);
47
+ const callbackUrl = await waitForApproval(genesisUrl, started.deviceCode, started.intervalSeconds, client, say, sleep, signal);
48
+ if (callbackUrl === null) {
49
+ // Signed in on its own, through a browser on this machine.
50
+ return;
51
+ }
52
+ if (signal.aborted) {
53
+ throw new InterruptedError();
54
+ }
55
+ try {
56
+ await deliverCallback(callbackUrl, signal);
57
+ }
58
+ catch (error) {
59
+ if (error instanceof SignInError) {
60
+ throw error;
61
+ }
62
+ throw new SignInError(`could not hand the sign-in to ${launch.label} on this machine.`);
63
+ }
64
+ await client.waitForSuccess(signal);
65
+ }
66
+ catch (error) {
67
+ if (signal.aborted) {
68
+ throw new InterruptedError();
69
+ }
70
+ throw error;
71
+ }
72
+ finally {
73
+ client.stop();
74
+ }
75
+ }
76
+ async function waitForApproval(genesisUrl, deviceCode, intervalSeconds, client, say, sleep, signal) {
77
+ let interval = intervalSeconds;
78
+ let misses = 0;
79
+ let announced = false;
80
+ for (;;) {
81
+ await sleep(interval * 1000, signal);
82
+ if (signal.aborted) {
83
+ throw new InterruptedError();
84
+ }
85
+ if (client.exitCode !== null) {
86
+ if (client.exitCode === 0) {
87
+ return null;
88
+ }
89
+ throw client.failure("stopped waiting for the sign-in. Run the command again.");
90
+ }
91
+ const polled = await pollCodeSignIn(genesisUrl, deviceCode, signal);
92
+ if (polled === null) {
93
+ misses += 1;
94
+ if (misses > MAX_POLL_MISSES) {
95
+ throw new SignInError(`lost contact with ${genesisUrl}.`);
96
+ }
97
+ continue;
98
+ }
99
+ misses = 0;
100
+ switch (polled.status) {
101
+ case "authorization_pending":
102
+ break;
103
+ case "slow_down":
104
+ interval = Math.min(interval + 2, MAX_POLL_INTERVAL_SECONDS);
105
+ break;
106
+ case "rate_limited":
107
+ interval = Math.min(Math.max(interval * 2, 1), MAX_POLL_INTERVAL_SECONDS);
108
+ break;
109
+ case "signing_in":
110
+ if (!announced) {
111
+ say(" Approved. Finish signing in to Genesis in the browser...");
112
+ announced = true;
113
+ }
114
+ break;
115
+ case "complete":
116
+ return polled.callbackUrl;
117
+ case "denied":
118
+ throw new SignInError("the sign-in was denied in the browser.");
119
+ case "expired":
120
+ throw new SignInError("the code expired before it was approved. Run the command again.");
121
+ case "failed":
122
+ throw new SignInError(`the sign-in did not complete: ${polled.failure}`);
123
+ }
124
+ }
125
+ }
126
+ /**
127
+ * Wait `ms`, or less if `signal` aborts first. The timer is cleared either way, so an
128
+ * interrupted sign-in exits at once instead of sitting out the current poll interval.
129
+ */
130
+ export function abortableSleep(ms, signal) {
131
+ return new Promise((resolve) => {
132
+ if (signal.aborted) {
133
+ resolve();
134
+ return;
135
+ }
136
+ const onAbort = () => {
137
+ clearTimeout(timer);
138
+ resolve();
139
+ };
140
+ const timer = setTimeout(() => {
141
+ signal.removeEventListener("abort", onAbort);
142
+ resolve();
143
+ }, ms);
144
+ signal.addEventListener("abort", onAbort, { once: true });
145
+ });
146
+ }
147
+ function startClient(executable, args, signal) {
148
+ // BROWSER=true stops genie and codex opening a browser on this machine (macOS and
149
+ // Linux); the person signs in from whichever browser they open the code in.
150
+ return startOwnedProcess(executable, args, { ...process.env, BROWSER: "true" }, signal);
151
+ }
152
+ /** The client's process: its output so far and how it ended. */
153
+ class RunningClient {
154
+ child;
155
+ label;
156
+ output = "";
157
+ exitCode = null;
158
+ exited;
159
+ startError = null;
160
+ constructor(child, label) {
161
+ this.child = child;
162
+ this.label = label;
163
+ const collect = (chunk) => {
164
+ if (this.output.length < MAX_CLIENT_OUTPUT) {
165
+ this.output += chunk.toString();
166
+ }
167
+ };
168
+ child.stdout?.on("data", collect);
169
+ child.stderr?.on("data", collect);
170
+ // Kept open and never written: genesis-room login reads a pasted address from stdin,
171
+ // and stops waiting if it closes.
172
+ child.stdin?.on("error", () => undefined);
173
+ this.exited = new Promise((resolve) => {
174
+ child.on("error", (error) => {
175
+ this.startError = error;
176
+ this.exitCode = -1;
177
+ resolve();
178
+ });
179
+ child.on("close", (code) => {
180
+ this.exitCode = code ?? -1;
181
+ resolve();
182
+ });
183
+ });
184
+ }
185
+ /** The sign-in URL it printed, or null if it finished at once without needing one. */
186
+ async waitForAuthorizeUrl(genesisUrl, sleep, signal) {
187
+ const deadline = Date.now() + AUTHORIZE_URL_WAIT_MS;
188
+ for (;;) {
189
+ if (signal.aborted) {
190
+ throw new InterruptedError();
191
+ }
192
+ const url = findAuthorizeUrl(this.output, genesisUrl);
193
+ if (url !== null) {
194
+ return url;
195
+ }
196
+ if (printedForeignAuthorizeUrl(this.output)) {
197
+ throw new SignInError(`${this.label} is signing in somewhere other than ${genesisUrl}.`);
198
+ }
199
+ if (this.exitCode !== null) {
200
+ if (this.exitCode === 0) {
201
+ return null;
202
+ }
203
+ throw this.failure("stopped before it started signing in.");
204
+ }
205
+ if (Date.now() > deadline) {
206
+ throw this.failure("printed no sign-in link within a minute.");
207
+ }
208
+ await sleep(200, signal);
209
+ }
210
+ }
211
+ async waitForSuccess(signal) {
212
+ // An owned timer, cleared however the wait ends: a timer left running would keep the
213
+ // command alive for a minute after it printed "Signed in".
214
+ let timer;
215
+ const timedOut = new Promise((resolve) => {
216
+ timer = setTimeout(() => resolve(false), CLIENT_FINISH_WAIT_MS);
217
+ });
218
+ const finished = await Promise.race([this.exited.then(() => true), timedOut]);
219
+ clearTimeout(timer);
220
+ if (signal.aborted) {
221
+ throw new InterruptedError();
222
+ }
223
+ if (!finished) {
224
+ throw this.failure("did not finish signing in.");
225
+ }
226
+ if (this.exitCode !== 0) {
227
+ throw this.failure("could not finish signing in.");
228
+ }
229
+ }
230
+ failure(what) {
231
+ if (this.startError) {
232
+ return new SignInError(`could not start ${this.label}: ${this.startError.message}`);
233
+ }
234
+ const shown = this.output
235
+ .replace(/[^\x20-\x7e\n]/g, "")
236
+ .trim()
237
+ .split("\n")
238
+ .map((line) => ` ${line}`)
239
+ .join("\n");
240
+ return new SignInError(`${this.label} ${what}${shown ? `\n${shown}` : ""}`);
241
+ }
242
+ stop() {
243
+ if (this.exitCode === null) {
244
+ stopProcessTree(this.child);
245
+ }
246
+ }
247
+ }
248
+ //# sourceMappingURL=connect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connect.js","sourceRoot":"","sources":["../src/connect.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,kFAAkF;AAClF,sFAAsF;AAItF,OAAO,EAAqB,iBAAiB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACrF,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,0BAA0B,EAC1B,WAAW,EACX,eAAe,GAChB,MAAM,cAAc,CAAC;AAEtB,MAAM,qBAAqB,GAAG,MAAM,CAAC;AACrC,MAAM,qBAAqB,GAAG,MAAM,CAAC;AACrC,MAAM,eAAe,GAAG,EAAE,CAAC;AAC3B,MAAM,iBAAiB,GAAG,EAAE,GAAG,IAAI,CAAC;AACpC,qFAAqF;AACrF,MAAM,yBAAyB,GAAG,EAAE,CAAC;AAcrC,yEAAyE;AACzE,MAAM,OAAO,gBAAiB,SAAQ,WAAW;IAC/C;QACE,KAAK,CAAC,cAAc,CAAC,CAAC;IACxB,CAAC;CACF;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,OAAuB;IACnD,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC;IACvC,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC,IAAY,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;IACjE,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,cAAc,CAAC;IAC9C,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,IAAI,eAAe,EAAE,CAAC,MAAM,CAAC;IAC9D,MAAM,MAAM,GAAG,IAAI,aAAa,CAC9B,CAAC,OAAO,CAAC,KAAK,IAAI,WAAW,CAAC,CAAC,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,EACtE,MAAM,CAAC,KAAK,CACb,CAAC;IACF,IAAI,CAAC;QACH,MAAM,YAAY,GAAG,MAAM,MAAM,CAAC,mBAAmB,CAAC,UAAU,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;QACjF,IAAI,YAAY,KAAK,IAAI,EAAE,CAAC;YAC1B,0DAA0D;YAC1D,OAAO;QACT,CAAC;QACD,MAAM,OAAO,GAAG,MAAM,eAAe,CACnC,UAAU,EACV,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,UAAU,EAAE,EAChE,MAAM,CACP,CAAC;QACF,GAAG,CAAC,EAAE,CAAC,CAAC;QACR,GAAG,CAAC,aAAa,MAAM,CAAC,KAAK,gDAAgD,CAAC,CAAC;QAC/E,GAAG,CAAC,EAAE,CAAC,CAAC;QACR,GAAG,CAAC,SAAS,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;QACxC,GAAG,CAAC,EAAE,CAAC,CAAC;QACR,GAAG,CAAC,0BAA0B,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;QAClD,GAAG,CAAC,EAAE,CAAC,CAAC;QACR,GAAG,CAAC,aAAa,OAAO,CAAC,uBAAuB,EAAE,CAAC,CAAC;QACpD,GAAG,CAAC,EAAE,CAAC,CAAC;QACR,GAAG,CAAC,qDAAqD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,gBAAgB,GAAG,EAAE,CAAC,cAAc,CAAC,CAAC;QAElH,MAAM,WAAW,GAAG,MAAM,eAAe,CAAC,UAAU,EAAE,OAAO,CAAC,UAAU,EAAE,OAAO,CAAC,eAAe,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;QAC/H,IAAI,WAAW,KAAK,IAAI,EAAE,CAAC;YACzB,2DAA2D;YAC3D,OAAO;QACT,CAAC;QACD,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACnB,MAAM,IAAI,gBAAgB,EAAE,CAAC;QAC/B,CAAC;QACD,IAAI,CAAC;YACH,MAAM,eAAe,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;QAC7C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,KAAK,YAAY,WAAW,EAAE,CAAC;gBACjC,MAAM,KAAK,CAAC;YACd,CAAC;YACD,MAAM,IAAI,WAAW,CAAC,iCAAiC,MAAM,CAAC,KAAK,mBAAmB,CAAC,CAAC;QAC1F,CAAC;QACD,MAAM,MAAM,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;IACtC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACnB,MAAM,IAAI,gBAAgB,EAAE,CAAC;QAC/B,CAAC;QACD,MAAM,KAAK,CAAC;IACd,CAAC;YAAS,CAAC;QACT,MAAM,CAAC,IAAI,EAAE,CAAC;IAChB,CAAC;AACH,CAAC;AAED,KAAK,UAAU,eAAe,CAC5B,UAAkB,EAClB,UAAkB,EAClB,eAAuB,EACvB,MAAqB,EACrB,GAA2B,EAC3B,KAAyD,EACzD,MAAmB;IAEnB,IAAI,QAAQ,GAAG,eAAe,CAAC;IAC/B,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,IAAI,SAAS,GAAG,KAAK,CAAC;IACtB,SAAS,CAAC;QACR,MAAM,KAAK,CAAC,QAAQ,GAAG,IAAI,EAAE,MAAM,CAAC,CAAC;QACrC,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACnB,MAAM,IAAI,gBAAgB,EAAE,CAAC;QAC/B,CAAC;QACD,IAAI,MAAM,CAAC,QAAQ,KAAK,IAAI,EAAE,CAAC;YAC7B,IAAI,MAAM,CAAC,QAAQ,KAAK,CAAC,EAAE,CAAC;gBAC1B,OAAO,IAAI,CAAC;YACd,CAAC;YACD,MAAM,MAAM,CAAC,OAAO,CAAC,yDAAyD,CAAC,CAAC;QAClF,CAAC;QACD,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC,UAAU,EAAE,UAAU,EAAE,MAAM,CAAC,CAAC;QACpE,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;YACpB,MAAM,IAAI,CAAC,CAAC;YACZ,IAAI,MAAM,GAAG,eAAe,EAAE,CAAC;gBAC7B,MAAM,IAAI,WAAW,CAAC,qBAAqB,UAAU,GAAG,CAAC,CAAC;YAC5D,CAAC;YACD,SAAS;QACX,CAAC;QACD,MAAM,GAAG,CAAC,CAAC;QACX,QAAQ,MAAM,CAAC,MAAM,EAAE,CAAC;YACtB,KAAK,uBAAuB;gBAC1B,MAAM;YACR,KAAK,WAAW;gBACd,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,GAAG,CAAC,EAAE,yBAAyB,CAAC,CAAC;gBAC7D,MAAM;YACR,KAAK,cAAc;gBACjB,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,GAAG,CAAC,EAAE,CAAC,CAAC,EAAE,yBAAyB,CAAC,CAAC;gBAC1E,MAAM;YACR,KAAK,YAAY;gBACf,IAAI,CAAC,SAAS,EAAE,CAAC;oBACf,GAAG,CAAC,4DAA4D,CAAC,CAAC;oBAClE,SAAS,GAAG,IAAI,CAAC;gBACnB,CAAC;gBACD,MAAM;YACR,KAAK,UAAU;gBACb,OAAO,MAAM,CAAC,WAAW,CAAC;YAC5B,KAAK,QAAQ;gBACX,MAAM,IAAI,WAAW,CAAC,wCAAwC,CAAC,CAAC;YAClE,KAAK,SAAS;gBACZ,MAAM,IAAI,WAAW,CAAC,iEAAiE,CAAC,CAAC;YAC3F,KAAK,QAAQ;gBACX,MAAM,IAAI,WAAW,CAAC,iCAAiC,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;QAC7E,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,EAAU,EAAE,MAAmB;IAC5D,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACnB,OAAO,EAAE,CAAC;YACV,OAAO;QACT,CAAC;QACD,MAAM,OAAO,GAAG,GAAG,EAAE;YACnB,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,OAAO,EAAE,CAAC;QACZ,CAAC,CAAC;QACF,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YAC5B,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAC7C,OAAO,EAAE,CAAC;QACZ,CAAC,EAAE,EAAE,CAAC,CAAC;QACP,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;IAC5D,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,WAAW,CAAC,UAAkB,EAAE,IAAc,EAAE,MAAmB;IAC1E,kFAAkF;IAClF,4EAA4E;IAC5E,OAAO,iBAAiB,CAAC,UAAU,EAAE,IAAI,EAAE,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,MAAM,CAAC,CAAC;AAC1F,CAAC;AAED,gEAAgE;AAChE,MAAM,aAAa;IAOE;IACA;IAPnB,MAAM,GAAG,EAAE,CAAC;IACZ,QAAQ,GAAkB,IAAI,CAAC;IACvB,MAAM,CAAgB;IACtB,UAAU,GAAiB,IAAI,CAAC;IAExC,YACmB,KAAmB,EACnB,KAAa;QADb,UAAK,GAAL,KAAK,CAAc;QACnB,UAAK,GAAL,KAAK,CAAQ;QAE9B,MAAM,OAAO,GAAG,CAAC,KAAa,EAAE,EAAE;YAChC,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,iBAAiB,EAAE,CAAC;gBAC3C,IAAI,CAAC,MAAM,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;YAClC,CAAC;QACH,CAAC,CAAC;QACF,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAClC,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAClC,qFAAqF;QACrF,kCAAkC;QAClC,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QAC1C,IAAI,CAAC,MAAM,GAAG,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;YACpC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE;gBAC1B,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC;gBACxB,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC;gBACnB,OAAO,EAAE,CAAC;YACZ,CAAC,CAAC,CAAC;YACH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE;gBACzB,IAAI,CAAC,QAAQ,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC;gBAC3B,OAAO,EAAE,CAAC;YACZ,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC;IAED,sFAAsF;IACtF,KAAK,CAAC,mBAAmB,CACvB,UAAkB,EAClB,KAAyD,EACzD,MAAmB;QAEnB,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,qBAAqB,CAAC;QACpD,SAAS,CAAC;YACR,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;gBACnB,MAAM,IAAI,gBAAgB,EAAE,CAAC;YAC/B,CAAC;YACD,MAAM,GAAG,GAAG,gBAAgB,CAAC,IAAI,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;YACtD,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;gBACjB,OAAO,GAAG,CAAC;YACb,CAAC;YACD,IAAI,0BAA0B,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC5C,MAAM,IAAI,WAAW,CAAC,GAAG,IAAI,CAAC,KAAK,uCAAuC,UAAU,GAAG,CAAC,CAAC;YAC3F,CAAC;YACD,IAAI,IAAI,CAAC,QAAQ,KAAK,IAAI,EAAE,CAAC;gBAC3B,IAAI,IAAI,CAAC,QAAQ,KAAK,CAAC,EAAE,CAAC;oBACxB,OAAO,IAAI,CAAC;gBACd,CAAC;gBACD,MAAM,IAAI,CAAC,OAAO,CAAC,uCAAuC,CAAC,CAAC;YAC9D,CAAC;YACD,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,EAAE,CAAC;gBAC1B,MAAM,IAAI,CAAC,OAAO,CAAC,0CAA0C,CAAC,CAAC;YACjE,CAAC;YACD,MAAM,KAAK,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QAC3B,CAAC;IACH,CAAC;IAED,KAAK,CAAC,cAAc,CAAC,MAAmB;QACtC,qFAAqF;QACrF,2DAA2D;QAC3D,IAAI,KAAiC,CAAC;QACtC,MAAM,QAAQ,GAAG,IAAI,OAAO,CAAU,CAAC,OAAO,EAAE,EAAE;YAChD,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,qBAAqB,CAAC,CAAC;QAClE,CAAC,CAAC,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC;QAC9E,YAAY,CAAC,KAAK,CAAC,CAAC;QACpB,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACnB,MAAM,IAAI,gBAAgB,EAAE,CAAC;QAC/B,CAAC;QACD,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,MAAM,IAAI,CAAC,OAAO,CAAC,4BAA4B,CAAC,CAAC;QACnD,CAAC;QACD,IAAI,IAAI,CAAC,QAAQ,KAAK,CAAC,EAAE,CAAC;YACxB,MAAM,IAAI,CAAC,OAAO,CAAC,8BAA8B,CAAC,CAAC;QACrD,CAAC;IACH,CAAC;IAED,OAAO,CAAC,IAAY;QAClB,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACpB,OAAO,IAAI,WAAW,CAAC,mBAAmB,IAAI,CAAC,KAAK,KAAK,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC,CAAC;QACtF,CAAC;QACD,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM;aACtB,OAAO,CAAC,iBAAiB,EAAE,EAAE,CAAC;aAC9B,IAAI,EAAE;aACN,KAAK,CAAC,IAAI,CAAC;aACX,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC;aAC1B,IAAI,CAAC,IAAI,CAAC,CAAC;QACd,OAAO,IAAI,WAAW,CAAC,GAAG,IAAI,CAAC,KAAK,IAAI,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAC9E,CAAC;IAED,IAAI;QACF,IAAI,IAAI,CAAC,QAAQ,KAAK,IAAI,EAAE,CAAC;YAC3B,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC9B,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,56 @@
1
+ export declare const DEVICE_START_PATH = "/mcp/device/start";
2
+ export declare const DEVICE_POLL_PATH = "/mcp/device/poll";
3
+ /** What /mcp/device/start answers: the code to show and the secret to poll with. */
4
+ export interface StartedSignIn {
5
+ deviceCode: string;
6
+ userCode: string;
7
+ verificationUri: string;
8
+ verificationUriComplete: string;
9
+ intervalSeconds: number;
10
+ expiresInSeconds: number;
11
+ }
12
+ /** Where a code sign-in stands, from /mcp/device/poll. */
13
+ export type PollResult = {
14
+ status: "authorization_pending" | "slow_down" | "rate_limited" | "signing_in";
15
+ } | {
16
+ status: "complete";
17
+ callbackUrl: string;
18
+ } | {
19
+ status: "denied" | "expired";
20
+ } | {
21
+ status: "failed";
22
+ failure: string;
23
+ };
24
+ export declare class SignInError extends Error {
25
+ }
26
+ /**
27
+ * The Genesis base URL (scheme and host) from what a person typed: the base itself, the
28
+ * MCP URL ending in /mcp, or either with a trailing slash.
29
+ */
30
+ export declare function normalizeGenesisUrl(raw: string): string;
31
+ /**
32
+ * The first sign-in URL for this Genesis in what a client printed, or null.
33
+ *
34
+ * Only this Genesis's own /authorize is accepted: a client signing in anywhere else is
35
+ * not one a Genesis code can finish.
36
+ */
37
+ export declare function findAuthorizeUrl(output: string, genesisUrl: string): string | null;
38
+ /** True if the client printed a sign-in URL for some other server. */
39
+ export declare function printedForeignAuthorizeUrl(output: string): boolean;
40
+ /** Open a code sign-in for the authorize URL a client printed. One HTTP request. */
41
+ export declare function startCodeSignIn(genesisUrl: string, form: {
42
+ authorizeUrl: string;
43
+ client: string;
44
+ name: string;
45
+ }, signal?: AbortSignal): Promise<StartedSignIn>;
46
+ /**
47
+ * Where the sign-in stands. One HTTP request; null when Genesis could not be reached or
48
+ * answered with an error status, which the caller retries a few times. Aborting
49
+ * `signal` cancels the request and rejects, never counting as a miss.
50
+ */
51
+ export declare function pollCodeSignIn(genesisUrl: string, deviceCode: string, signal?: AbortSignal): Promise<PollResult | null>;
52
+ /**
53
+ * Hand the sign-in to the client listening on this machine. One HTTP request, to
54
+ * loopback only, never through a proxy and never following a redirect.
55
+ */
56
+ export declare function deliverCallback(callbackUrl: string, signal?: AbortSignal): Promise<void>;
@@ -0,0 +1,164 @@
1
+ // The Genesis side of a code sign-in: /mcp/device/start, /mcp/device/poll, and the
2
+ // loopback callback Genesis hands back once the person has approved and signed in.
3
+ // Same protocol as the `curl <host>/mcp/connect | sh` script Genesis serves.
4
+ export const DEVICE_START_PATH = "/mcp/device/start";
5
+ export const DEVICE_POLL_PATH = "/mcp/device/poll";
6
+ export class SignInError extends Error {
7
+ }
8
+ const DEFAULT_INTERVAL_SECONDS = 3;
9
+ const DEFAULT_EXPIRES_IN_SECONDS = 600;
10
+ const DEVICE_CODE_PATTERN = /^[A-Za-z0-9_-]+$/;
11
+ const USER_CODE_PATTERN = /^[A-Z-]+$/;
12
+ const CALLBACK_CHARACTERS = /^[A-Za-z0-9:/?=&%._~+-]+$/;
13
+ const CALLBACK_DELIVERY_TIMEOUT_MS = 30_000;
14
+ /**
15
+ * The Genesis base URL (scheme and host) from what a person typed: the base itself, the
16
+ * MCP URL ending in /mcp, or either with a trailing slash.
17
+ */
18
+ export function normalizeGenesisUrl(raw) {
19
+ let parsed;
20
+ try {
21
+ parsed = new URL(raw.trim());
22
+ }
23
+ catch {
24
+ throw new SignInError(`not a URL: ${raw}`);
25
+ }
26
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
27
+ throw new SignInError(`the Genesis URL must start with https:// (got ${raw})`);
28
+ }
29
+ if (parsed.username || parsed.password || parsed.search || parsed.hash) {
30
+ throw new SignInError(`give the Genesis address only, e.g. https://genesis.example.com (got ${raw})`);
31
+ }
32
+ const path = parsed.pathname.replace(/\/+$/, "");
33
+ if (path !== "" && path !== "/mcp") {
34
+ throw new SignInError(`give the Genesis address only, e.g. https://genesis.example.com (got ${raw})`);
35
+ }
36
+ return parsed.origin;
37
+ }
38
+ /**
39
+ * The first sign-in URL for this Genesis in what a client printed, or null.
40
+ *
41
+ * Only this Genesis's own /authorize is accepted: a client signing in anywhere else is
42
+ * not one a Genesis code can finish.
43
+ */
44
+ export function findAuthorizeUrl(output, genesisUrl) {
45
+ for (const match of output.matchAll(/https?:\/\/[^\s"'<>`]+\/authorize\?[^\s"'<>`]+/g)) {
46
+ const url = match[0].replace(/[).,;]+$/, "");
47
+ if (url.startsWith(`${genesisUrl}/authorize?`)) {
48
+ return url;
49
+ }
50
+ }
51
+ return null;
52
+ }
53
+ /** True if the client printed a sign-in URL for some other server. */
54
+ export function printedForeignAuthorizeUrl(output) {
55
+ return /https?:\/\/[^\s"'<>`]+\/authorize\?/.test(output);
56
+ }
57
+ /** Open a code sign-in for the authorize URL a client printed. One HTTP request. */
58
+ export async function startCodeSignIn(genesisUrl, form, signal) {
59
+ const answer = await postForm(`${genesisUrl}${DEVICE_START_PATH}`, { authorize_url: form.authorizeUrl, client: form.client, name: form.name }, signal);
60
+ if (answer.status !== 200) {
61
+ throw new SignInError(`Genesis refused the sign-in (${answer.status}): ${errorText(answer.body)}`);
62
+ }
63
+ const body = answer.body;
64
+ const deviceCode = stringField(body, "device_code");
65
+ const userCode = stringField(body, "user_code");
66
+ if (!DEVICE_CODE_PATTERN.test(deviceCode) || !USER_CODE_PATTERN.test(userCode)) {
67
+ throw new SignInError("Genesis sent an unreadable answer.");
68
+ }
69
+ return {
70
+ deviceCode,
71
+ userCode,
72
+ verificationUri: `${genesisUrl}/device`,
73
+ verificationUriComplete: `${genesisUrl}/device?code=${userCode}`,
74
+ intervalSeconds: numberField(body, "interval") ?? DEFAULT_INTERVAL_SECONDS,
75
+ expiresInSeconds: numberField(body, "expires_in") ?? DEFAULT_EXPIRES_IN_SECONDS,
76
+ };
77
+ }
78
+ /**
79
+ * Where the sign-in stands. One HTTP request; null when Genesis could not be reached or
80
+ * answered with an error status, which the caller retries a few times. Aborting
81
+ * `signal` cancels the request and rejects, never counting as a miss.
82
+ */
83
+ export async function pollCodeSignIn(genesisUrl, deviceCode, signal) {
84
+ let answer;
85
+ try {
86
+ answer = await postForm(`${genesisUrl}${DEVICE_POLL_PATH}`, { device_code: deviceCode }, signal);
87
+ }
88
+ catch (error) {
89
+ if (signal?.aborted) {
90
+ throw error;
91
+ }
92
+ return null;
93
+ }
94
+ if (answer.status !== 200) {
95
+ return null;
96
+ }
97
+ const status = stringField(answer.body, "status");
98
+ switch (status) {
99
+ case "authorization_pending":
100
+ case "slow_down":
101
+ case "rate_limited":
102
+ case "signing_in":
103
+ case "denied":
104
+ case "expired":
105
+ return { status };
106
+ case "complete":
107
+ return { status, callbackUrl: stringField(answer.body, "callback_url") };
108
+ case "failed":
109
+ return { status, failure: printable(stringField(answer.body, "failure")) };
110
+ default:
111
+ throw new SignInError("Genesis sent an unreadable answer.");
112
+ }
113
+ }
114
+ /**
115
+ * Hand the sign-in to the client listening on this machine. One HTTP request, to
116
+ * loopback only, never through a proxy and never following a redirect.
117
+ */
118
+ export async function deliverCallback(callbackUrl, signal) {
119
+ if (!/^http:\/\/(127\.0\.0\.1|localhost):\d+\//.test(callbackUrl)) {
120
+ throw new SignInError("Genesis sent a callback that is not on this machine.");
121
+ }
122
+ if (!CALLBACK_CHARACTERS.test(callbackUrl)) {
123
+ throw new SignInError("Genesis sent an unreadable callback.");
124
+ }
125
+ // Node's fetch reads no proxy variables, so this request stays on this machine.
126
+ await fetch(callbackUrl, {
127
+ redirect: "manual",
128
+ signal: signal
129
+ ? AbortSignal.any([signal, AbortSignal.timeout(CALLBACK_DELIVERY_TIMEOUT_MS)])
130
+ : AbortSignal.timeout(CALLBACK_DELIVERY_TIMEOUT_MS),
131
+ });
132
+ }
133
+ async function postForm(url, fields, signal) {
134
+ const response = await fetch(url, {
135
+ method: "POST",
136
+ signal,
137
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
138
+ body: new URLSearchParams(fields).toString(),
139
+ redirect: "error",
140
+ });
141
+ let body = null;
142
+ try {
143
+ body = await response.json();
144
+ }
145
+ catch {
146
+ body = null;
147
+ }
148
+ return { status: response.status, body };
149
+ }
150
+ function stringField(body, key) {
151
+ const value = body?.[key];
152
+ return typeof value === "string" ? value : "";
153
+ }
154
+ function numberField(body, key) {
155
+ const value = body?.[key];
156
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
157
+ }
158
+ function errorText(body) {
159
+ return printable(stringField(body, "error")) || "no reason given";
160
+ }
161
+ function printable(text) {
162
+ return text.replace(/[^\x20-\x7e]/g, "");
163
+ }
164
+ //# sourceMappingURL=genesis.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"genesis.js","sourceRoot":"","sources":["../src/genesis.ts"],"names":[],"mappings":"AAAA,mFAAmF;AACnF,mFAAmF;AACnF,6EAA6E;AAE7E,MAAM,CAAC,MAAM,iBAAiB,GAAG,mBAAmB,CAAC;AACrD,MAAM,CAAC,MAAM,gBAAgB,GAAG,kBAAkB,CAAC;AAmBnD,MAAM,OAAO,WAAY,SAAQ,KAAK;CAAG;AAEzC,MAAM,wBAAwB,GAAG,CAAC,CAAC;AACnC,MAAM,0BAA0B,GAAG,GAAG,CAAC;AACvC,MAAM,mBAAmB,GAAG,kBAAkB,CAAC;AAC/C,MAAM,iBAAiB,GAAG,WAAW,CAAC;AACtC,MAAM,mBAAmB,GAAG,2BAA2B,CAAC;AACxD,MAAM,4BAA4B,GAAG,MAAM,CAAC;AAE5C;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CAAC,GAAW;IAC7C,IAAI,MAAW,CAAC;IAChB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,WAAW,CAAC,cAAc,GAAG,EAAE,CAAC,CAAC;IAC7C,CAAC;IACD,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QAChE,MAAM,IAAI,WAAW,CAAC,iDAAiD,GAAG,GAAG,CAAC,CAAC;IACjF,CAAC;IACD,IAAI,MAAM,CAAC,QAAQ,IAAI,MAAM,CAAC,QAAQ,IAAI,MAAM,CAAC,MAAM,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;QACvE,MAAM,IAAI,WAAW,CAAC,wEAAwE,GAAG,GAAG,CAAC,CAAC;IACxG,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACjD,IAAI,IAAI,KAAK,EAAE,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;QACnC,MAAM,IAAI,WAAW,CAAC,wEAAwE,GAAG,GAAG,CAAC,CAAC;IACxG,CAAC;IACD,OAAO,MAAM,CAAC,MAAM,CAAC;AACvB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAAc,EAAE,UAAkB;IACjE,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,QAAQ,CAAC,iDAAiD,CAAC,EAAE,CAAC;QACvF,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;QAC7C,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,UAAU,aAAa,CAAC,EAAE,CAAC;YAC/C,OAAO,GAAG,CAAC;QACb,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,0BAA0B,CAAC,MAAc;IACvD,OAAO,qCAAqC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAC5D,CAAC;AAED,oFAAoF;AACpF,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,UAAkB,EAClB,IAA4D,EAC5D,MAAoB;IAEpB,MAAM,MAAM,GAAG,MAAM,QAAQ,CAC3B,GAAG,UAAU,GAAG,iBAAiB,EAAE,EACnC,EAAE,aAAa,EAAE,IAAI,CAAC,YAAY,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,EAC1E,MAAM,CACP,CAAC;IACF,IAAI,MAAM,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC1B,MAAM,IAAI,WAAW,CAAC,gCAAgC,MAAM,CAAC,MAAM,MAAM,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACrG,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;IACzB,MAAM,UAAU,GAAG,WAAW,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC;IACpD,MAAM,QAAQ,GAAG,WAAW,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IAChD,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC/E,MAAM,IAAI,WAAW,CAAC,oCAAoC,CAAC,CAAC;IAC9D,CAAC;IACD,OAAO;QACL,UAAU;QACV,QAAQ;QACR,eAAe,EAAE,GAAG,UAAU,SAAS;QACvC,uBAAuB,EAAE,GAAG,UAAU,gBAAgB,QAAQ,EAAE;QAChE,eAAe,EAAE,WAAW,CAAC,IAAI,EAAE,UAAU,CAAC,IAAI,wBAAwB;QAC1E,gBAAgB,EAAE,WAAW,CAAC,IAAI,EAAE,YAAY,CAAC,IAAI,0BAA0B;KAChF,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,UAAkB,EAClB,UAAkB,EAClB,MAAoB;IAEpB,IAAI,MAAyC,CAAC;IAC9C,IAAI,CAAC;QACH,MAAM,GAAG,MAAM,QAAQ,CAAC,GAAG,UAAU,GAAG,gBAAgB,EAAE,EAAE,EAAE,WAAW,EAAE,UAAU,EAAE,EAAE,MAAM,CAAC,CAAC;IACnG,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;YACpB,MAAM,KAAK,CAAC;QACd,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,MAAM,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC1B,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,MAAM,GAAG,WAAW,CAAC,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IAClD,QAAQ,MAAM,EAAE,CAAC;QACf,KAAK,uBAAuB,CAAC;QAC7B,KAAK,WAAW,CAAC;QACjB,KAAK,cAAc,CAAC;QACpB,KAAK,YAAY,CAAC;QAClB,KAAK,QAAQ,CAAC;QACd,KAAK,SAAS;YACZ,OAAO,EAAE,MAAM,EAAE,CAAC;QACpB,KAAK,UAAU;YACb,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,WAAW,CAAC,MAAM,CAAC,IAAI,EAAE,cAAc,CAAC,EAAE,CAAC;QAC3E,KAAK,QAAQ;YACX,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,CAAC,WAAW,CAAC,MAAM,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC,EAAE,CAAC;QAC7E;YACE,MAAM,IAAI,WAAW,CAAC,oCAAoC,CAAC,CAAC;IAChE,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,WAAmB,EAAE,MAAoB;IAC7E,IAAI,CAAC,0CAA0C,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC;QAClE,MAAM,IAAI,WAAW,CAAC,sDAAsD,CAAC,CAAC;IAChF,CAAC;IACD,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC;QAC3C,MAAM,IAAI,WAAW,CAAC,sCAAsC,CAAC,CAAC;IAChE,CAAC;IACD,gFAAgF;IAChF,MAAM,KAAK,CAAC,WAAW,EAAE;QACvB,QAAQ,EAAE,QAAQ;QAClB,MAAM,EAAE,MAAM;YACZ,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,4BAA4B,CAAC,CAAC,CAAC;YAC9E,CAAC,CAAC,WAAW,CAAC,OAAO,CAAC,4BAA4B,CAAC;KACtD,CAAC,CAAC;AACL,CAAC;AAED,KAAK,UAAU,QAAQ,CACrB,GAAW,EACX,MAA8B,EAC9B,MAAoB;IAEpB,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;QAChC,MAAM,EAAE,MAAM;QACd,MAAM;QACN,OAAO,EAAE,EAAE,cAAc,EAAE,mCAAmC,EAAE;QAChE,IAAI,EAAE,IAAI,eAAe,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE;QAC5C,QAAQ,EAAE,OAAO;KAClB,CAAC,CAAC;IACH,IAAI,IAAI,GAAY,IAAI,CAAC;IACzB,IAAI,CAAC;QACH,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,IAAI,GAAG,IAAI,CAAC;IACd,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC;AAC3C,CAAC;AAED,SAAS,WAAW,CAAC,IAAa,EAAE,GAAW;IAC7C,MAAM,KAAK,GAAI,IAAuC,EAAE,CAAC,GAAG,CAAC,CAAC;IAC9D,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;AAChD,CAAC;AAED,SAAS,WAAW,CAAC,IAAa,EAAE,GAAW;IAC7C,MAAM,KAAK,GAAI,IAAuC,EAAE,CAAC,GAAG,CAAC,CAAC;IAC9D,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;AAC1F,CAAC;AAED,SAAS,SAAS,CAAC,IAAa;IAC9B,OAAO,SAAS,CAAC,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,IAAI,iBAAiB,CAAC;AACpE,CAAC;AAED,SAAS,SAAS,CAAC,IAAY;IAC7B,OAAO,IAAI,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC;AAC3C,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { type ClientLaunch, type LaunchRequest, planLaunch } from "./clients.js";
2
+ export { connect, type ConnectOptions, InterruptedError } from "./connect.js";
3
+ export { deliverCallback, normalizeGenesisUrl, pollCodeSignIn, type PollResult, SignInError, startCodeSignIn, type StartedSignIn, } from "./genesis.js";
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { planLaunch } from "./clients.js";
2
+ export { connect, InterruptedError } from "./connect.js";
3
+ export { deliverCallback, normalizeGenesisUrl, pollCodeSignIn, SignInError, startCodeSignIn, } from "./genesis.js";
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAyC,UAAU,EAAE,MAAM,cAAc,CAAC;AACjF,OAAO,EAAE,OAAO,EAAuB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAC9E,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,cAAc,EAEd,WAAW,EACX,eAAe,GAEhB,MAAM,cAAc,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,41 @@
1
1
  {
2
2
  "name": "@genesiscomputing/genesis-connect",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Sign Genie Code, Codex or genesis-room in to Genesis with a code: no browser or tunnel needed on the machine they run on.",
5
+ "type": "module",
6
+ "keywords": [
7
+ "genesis",
8
+ "mcp",
9
+ "oauth",
10
+ "device-code",
11
+ "genie",
12
+ "codex"
13
+ ],
14
+ "bin": {
15
+ "genesis-connect": "dist/cli.js"
16
+ },
17
+ "main": "dist/index.js",
18
+ "types": "dist/index.d.ts",
19
+ "files": [
20
+ "dist",
21
+ "README.md"
22
+ ],
23
+ "scripts": {
24
+ "build": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.json",
25
+ "test": "vitest run",
26
+ "typecheck": "tsc -p tsconfig.json --noEmit",
27
+ "prepack": "npm run build"
28
+ },
29
+ "engines": {
30
+ "node": ">=20.3"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "license": "UNLICENSED",
36
+ "devDependencies": {
37
+ "@types/node": "^25.6.2",
38
+ "typescript": "^6.0.3",
39
+ "vitest": "^4.1.5"
40
+ }
41
+ }