// Generated by npm run chatgpt-helper:build. Source: packages/cli/src/chatgpt-helper.ts // packages/cli/src/chatgpt.ts import { createHash, randomBytes } from "node:crypto"; import { createServer } from "node:http"; import { setTimeout as delay } from "node:timers/promises"; // packages/cli/src/errors.ts var CliError = class extends Error { code; hint; constructor(code, message, hint) { super(message); this.name = "CliError"; this.code = code; this.hint = hint; } }; // packages/cli/src/origin.ts var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]); var MCP_RESOURCE_PATH = "/mcp"; var MCP_WORKSPACE_SCOPE = "chickpea:workspace"; var MCP_OAUTH_SCOPE = `${MCP_WORKSPACE_SCOPE} offline_access`; function isLoopbackHost(hostname) { if (LOOPBACK_HOSTS.has(hostname)) return true; const octets = hostname.split("."); return octets.length === 4 && octets[0] === "127" && octets.every((octet) => /^\d+$/.test(octet) && Number(octet) <= 255); } function normalizeDeploymentOrigin(input) { const trimmed = input.trim(); let parsed; try { parsed = new URL(/^[a-z]+:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`); } catch { throw new CliError("INVALID_URL", `"${trimmed}" is not a deployment URL`, "Pass the origin, for example https://chickpea.example.com"); } if (parsed.username || parsed.password) { throw new CliError("INVALID_URL", "The deployment URL must not contain credentials", "Pass the bare origin, for example https://chickpea.example.com"); } if (parsed.search || parsed.hash) { throw new CliError("INVALID_URL", "The deployment URL must not contain a query or fragment", "Pass the bare origin, for example https://chickpea.example.com"); } const path = parsed.pathname.replace(/\/+$/, ""); if (path !== "" && path !== MCP_RESOURCE_PATH) { throw new CliError("INVALID_URL", `The deployment URL must not contain a path (got ${parsed.pathname})`, "Pass the bare origin, for example https://chickpea.example.com"); } if (parsed.protocol !== "https:" && !(parsed.protocol === "http:" && isLoopbackHost(parsed.hostname))) { throw new CliError("INVALID_URL", "The deployment URL must use HTTPS (plain HTTP is allowed only for loopback development)", "Use https:// or http://127.0.0.1:"); } return parsed.origin; } // packages/cli/src/chatgpt-page.ts function chatgptReturnPage(deployment) { const origin = normalizeDeploymentOrigin(deployment).replace(/&/g, "&").replace(/"/g, """).replace(/ Finish connecting ChatGPT \xB7 Chickpea
Chickpea
\u2713 Sign inConfirm account

One last step.

Head back to Chickpea and confirm your ChatGPT account in Model providers to finish connecting.

Continue to Chickpea

Already have Chickpea open? You can switch back to that tab to confirm your account.

`; } // packages/cli/src/chatgpt.ts async function connectChatgpt(origin, deps) { const verifier = randomBytes(32).toString("base64url"); const state = randomBytes(32).toString("base64url"); const nonce = randomBytes(32).toString("base64url"); const challenge = createHash("sha256").update(verifier).digest("base64url"); const signal = AbortSignal.timeout(deps.timeoutMs ?? 15 * 6e4); let expectedClient; let ready = false; let callbackStarted = false; let callbackError = false; let authorized = false; const post = async (body) => { const response = await deps.fetch(`${origin}/auth/chatgpt-plan/handoff`, { method: "POST", redirect: "manual", signal: AbortSignal.any([signal, AbortSignal.timeout(6e4)]), headers: { "content-type": "application/json" }, body: JSON.stringify({ verifier, ...body }) }); if (!response.ok) { await response.body?.cancel(); throw new CliError("CHATGPT_CONNECTION_FAILED", "Chickpea could not finish ChatGPT sign-in. Return to Model providers to start again."); } return await response.json(); }; const server = createServer((req, res) => { res.setHeader("Cache-Control", "no-store"); res.setHeader("Referrer-Policy", "no-referrer"); res.setHeader("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; frame-ancestors 'none'"); if (req.method !== "GET" || req.headers.host !== new URL(redirectUri).host) { res.writeHead(400).end(); return; } const url = new URL(req.url ?? "/", redirectUri); if (url.pathname === "/complete") { res.setHeader("Content-Security-Policy", `default-src 'none'; img-src ${origin}; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'`); res.setHeader("content-type", "text/html; charset=utf-8"); res.end(chatgptReturnPage(origin)); return; } if (url.pathname !== "/auth/callback" || !ready || callbackStarted || url.searchParams.get("state") !== state) { res.writeHead(400).end("This sign-in callback is not valid."); return; } callbackStarted = true; const code = url.searchParams.get("code"); const clientId = url.searchParams.get("client_id") ?? expectedClient; if (url.searchParams.has("error") || !code || !clientId || !/^oaiapp_[A-Za-z0-9_-]{1,200}$/.test(clientId) || expectedClient && clientId !== expectedClient) { callbackError = true; res.writeHead(400).end("Sign-in was not completed. Return to Chickpea and try again."); return; } res.writeHead(303, { location: "/complete" }).end(); void post({ code, clientId }).then((result) => { authorized = result.state === "awaiting_confirmation"; callbackError = !authorized; if (authorized) deps.note("Sign-in received. Confirm the account in Chickpea Model providers."); }).catch(() => { callbackError = true; }); }); let redirectUri = ""; try { await new Promise((resolve, reject) => { server.once("error", reject); server.listen(0, "127.0.0.1", resolve); }); const address = server.address(); if (!address || typeof address === "string") throw new Error("Loopback listener unavailable"); redirectUri = `http://127.0.0.1:${address.port}/auth/callback`; const descriptor = Buffer.from(JSON.stringify({ challenge, state, nonce, redirectUri })).toString("base64url"); const adminUrl = `${origin}/admin/settings/providers?chatgpt_connect=${descriptor}`; deps.note(`Opening ${origin}. Approve ChatGPT sign-in in Model providers.`); deps.note(`If the browser does not open, visit: ${adminUrl}`); await deps.openBrowser(adminUrl); while (!signal.aborted) { if (callbackError) throw new CliError("CHATGPT_SIGN_IN_FAILED", "ChatGPT sign-in did not complete. Return to Chickpea to start again."); const result = await post({}); if (result.state === "connected") { deps.note("ChatGPT is connected to this Chickpea installation."); return; } if (result.state === "ready" && !ready) { const url = new URL(String(result.authorizationUrl)); if (url.origin !== "https://auth.openai.com" || url.pathname !== "/api/accounts/authorize" || url.searchParams.get("state") !== state || url.searchParams.get("nonce") !== nonce || url.searchParams.get("code_challenge") !== challenge || url.searchParams.get("redirect_uri") !== redirectUri) throw new CliError("CHATGPT_SIGN_IN_FAILED", "Chickpea returned an invalid sign-in request."); expectedClient = typeof result.clientId === "string" ? result.clientId : void 0; ready = true; deps.note("Opening OpenAI. Sign in with the account you want this installation to use."); await deps.openBrowser(url.href); } await delay(1500, void 0, { signal }); } throw new CliError("CHATGPT_SIGN_IN_EXPIRED", "Sign-in expired. Run the command again."); } finally { server.closeAllConnections(); await new Promise((resolve) => server.close(() => resolve())); } } // packages/cli/src/browser.ts import { spawn } from "node:child_process"; async function openBrowserDetached(url) { const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url.replace(/&/g, "^&")]] : ["xdg-open", [url]]; await new Promise((resolve) => { try { const child = spawn(command, args, { detached: true, stdio: "ignore" }); child.once("error", () => resolve()); child.once("spawn", () => { child.unref(); resolve(); }); } catch { resolve(); } }); } // packages/cli/src/chatgpt-helper.ts try { if (!process.argv[2]) throw new Error("Pass your Chickpea deployment URL."); await connectChatgpt(normalizeDeploymentOrigin(process.argv[2]), { fetch, openBrowser: openBrowserDetached, note: (text) => process.stderr.write(`${text} `) }); } catch { process.stderr.write("ChatGPT connection did not finish. Return to Chickpea Model providers, cancel sign-in, and run the helper again.\n"); process.exitCode = 1; }