crosscheck-cli 0.0.6

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.
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Heartbeat scheduler. Runs every 24h from a long-lived `crosscheck serve`
3
+ * process; tolerates up to 72h of network failure before locking the CLI
4
+ * out (matches the server's grace window).
5
+ *
6
+ * On `status: "revoked"` the CLI must refuse to make further calls and
7
+ * surface the reason ("your subscription was canceled", "this machine was
8
+ * deactivated from the dashboard", etc.) — heartbeat is the only way the
9
+ * server gets to revoke a seat that's already activated.
10
+ */
11
+ import { request } from "undici";
12
+ import { SERVER_URL, HEARTBEAT_INTERVAL_MS, HEARTBEAT_FAIL_OPEN_MS } from "./config.js";
13
+ import { getSecret, setSecret } from "./keychain.js";
14
+ const state = {
15
+ lastOkAt: null,
16
+ status: "ok",
17
+ message: null,
18
+ };
19
+ export function currentState() {
20
+ return state;
21
+ }
22
+ export function isAllowedToRun(now = Date.now()) {
23
+ if (state.status === "revoked")
24
+ return false;
25
+ // past_due continues to work within the grace window; revoked never works.
26
+ if (state.lastOkAt == null)
27
+ return true; // never heartbeated yet — allow first run
28
+ return now - state.lastOkAt < HEARTBEAT_FAIL_OPEN_MS;
29
+ }
30
+ export async function runHeartbeat() {
31
+ const jwt = await getSecret("jwt");
32
+ if (!jwt)
33
+ throw new Error("Not activated. Run `crosscheck activate <token>` first.");
34
+ const res = await request(`${SERVER_URL}/api/crosscheck/heartbeat`, {
35
+ method: "POST",
36
+ headers: {
37
+ authorization: `Bearer ${jwt}`,
38
+ "content-type": "application/json",
39
+ },
40
+ body: JSON.stringify({}),
41
+ });
42
+ const body = (await res.body.json());
43
+ if (res.statusCode === 401) {
44
+ state.status = "revoked";
45
+ state.message = "JWT no longer valid. Re-activate.";
46
+ return body;
47
+ }
48
+ state.status = body.status;
49
+ state.message = body.reason ?? null;
50
+ if (body.status === "ok") {
51
+ state.lastOkAt = Date.now();
52
+ if (body.refreshJwt)
53
+ await setSecret("jwt", body.refreshJwt);
54
+ }
55
+ return body;
56
+ }
57
+ /** Long-running scheduler. Returns a stop fn. */
58
+ export function startScheduler() {
59
+ let stopped = false;
60
+ let timer = null;
61
+ const tick = async () => {
62
+ if (stopped)
63
+ return;
64
+ try {
65
+ await runHeartbeat();
66
+ }
67
+ catch (err) {
68
+ // Network error — don't change state.status, let isAllowedToRun fall
69
+ // back to the fail-open window.
70
+ // eslint-disable-next-line no-console
71
+ console.error("[heartbeat] network error:", err instanceof Error ? err.message : err);
72
+ }
73
+ finally {
74
+ if (!stopped) {
75
+ timer = setTimeout(tick, HEARTBEAT_INTERVAL_MS);
76
+ }
77
+ }
78
+ };
79
+ // Fire once on start so the user sees status quickly, then on the
80
+ // 24h cadence.
81
+ void tick();
82
+ return () => {
83
+ stopped = true;
84
+ if (timer)
85
+ clearTimeout(timer);
86
+ };
87
+ }
88
+ //# sourceMappingURL=heartbeat.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"heartbeat.js","sourceRoot":"","sources":["../src/heartbeat.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,86 @@
1
+ /**
2
+ * One-command install orchestrator — the path documented for Claude Code in
3
+ * VS Code. Run as `npx -y crosscheck-cli@latest install` (Claude Code will
4
+ * run this for you when you ask it to "Install Crosscheck").
5
+ *
6
+ * Steps:
7
+ * 1. Node version sanity check.
8
+ * 2. Activate this machine (browser device-auth) — skipped if already
9
+ * activated with a valid license.
10
+ * 3. Set up provider keys (browser / terminal / env import).
11
+ * 4. Register the MCP server with Claude Code (user scope).
12
+ * 5. Print next steps.
13
+ *
14
+ * Each step is resilient: a failure prints a clear message and the user can
15
+ * re-run `crosscheck install` (it's idempotent) or the individual subcommand.
16
+ */
17
+ import { activate } from "./activate.js";
18
+ import { runKeysSetup } from "./keys-setup.js";
19
+ import { registerMcpServer } from "./mcp-register.js";
20
+ import { getSecret } from "./keychain.js";
21
+ import { verifyJwt } from "./jwt.js";
22
+ function log(s = "") {
23
+ // eslint-disable-next-line no-console
24
+ console.log(s);
25
+ }
26
+ function step(n, total, title) {
27
+ log("");
28
+ log(`[${n}/${total}] ${title}`);
29
+ }
30
+ function checkNode() {
31
+ const major = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
32
+ if (major < 20) {
33
+ log(`✗ Node ${process.versions.node} detected — crosscheck needs Node 20+.\n` +
34
+ ` Install from https://nodejs.org and re-run \`crosscheck install\`.`);
35
+ process.exit(1);
36
+ }
37
+ }
38
+ async function alreadyActivated() {
39
+ const jwt = await getSecret("jwt");
40
+ if (!jwt)
41
+ return false;
42
+ const v = await verifyJwt(jwt);
43
+ return v.ok;
44
+ }
45
+ export async function runInstall() {
46
+ const TOTAL = 4;
47
+ log("crosscheck — let's get you set up.");
48
+ checkNode();
49
+ // 1. Activate.
50
+ step(1, TOTAL, "Activate this machine");
51
+ if (await alreadyActivated()) {
52
+ log("✓ Already activated on this machine.");
53
+ }
54
+ else {
55
+ await activate(); // device-auth: opens the browser, polls for the token
56
+ }
57
+ // 2. Keys.
58
+ step(2, TOTAL, "Set up provider keys");
59
+ await runKeysSetup();
60
+ // 3. Register MCP server with Claude Code.
61
+ step(3, TOTAL, "Register the MCP server with Claude Code");
62
+ const reg = registerMcpServer();
63
+ if (reg.method === "cli") {
64
+ log("✓ Registered with Claude Code (user scope) via the claude CLI.");
65
+ }
66
+ else if (reg.method === "file") {
67
+ log(`✓ Registered in ${reg.configPath} (user scope).`);
68
+ }
69
+ else {
70
+ log("! Couldn't register automatically. Add this to your Claude Code MCP config:");
71
+ log("");
72
+ log(reg.snippet);
73
+ log("");
74
+ log(` (reason: ${reg.reason})`);
75
+ }
76
+ // 4. Done.
77
+ step(4, TOTAL, "Done");
78
+ log("✓ crosscheck is installed.");
79
+ log("");
80
+ log("Next:");
81
+ log(" • Reconnect MCP servers in Claude Code (or restart it) to load crosscheck.");
82
+ log(' • Try it: ask Claude Code to "use crosscheck to confer on <your question>".');
83
+ log(" • Manage seats & machines at https://crosscheckagent.com/account");
84
+ log("");
85
+ }
86
+ //# sourceMappingURL=install.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"install.js","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"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"}
package/dist/jwt.js ADDED
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Local Ed25519 JWT verification. The activation flow stores the JWT in the
3
+ * keychain along with the public key that signed it; every subsequent CLI
4
+ * invocation parses + verifies it locally with the keychain-stored public
5
+ * key before making any privileged decision (e.g., whether to keep running,
6
+ * whether to refresh on heartbeat).
7
+ *
8
+ * No network round-trip on the hot path — heartbeats are scheduled, not
9
+ * per-call. This is the offline-tolerance the design depends on.
10
+ *
11
+ * Public-key source precedence:
12
+ * 1. Keychain entry "jwtPublicKey" (set by activate.ts on first activation)
13
+ * 2. CROSSCHECK_JWT_PUBLIC_KEY env var (dev override)
14
+ * 3. Fall back to bundled config.ts constant (empty by default; deprecated)
15
+ *
16
+ * verifyJwt is async because it reads from the OS keychain. Callers must
17
+ * await it. The first call caches the resolved KeyObject in memory.
18
+ */
19
+ import crypto from "node:crypto";
20
+ import { getSecret } from "./keychain.js";
21
+ import { EMBEDDED_PUBLIC_KEY_PEM } from "./config.js";
22
+ function fromB64url(s) {
23
+ const pad = s.length % 4 === 0 ? "" : "=".repeat(4 - (s.length % 4));
24
+ return Buffer.from(s.replace(/-/g, "+").replace(/_/g, "/") + pad, "base64");
25
+ }
26
+ let _cached = null;
27
+ async function resolvePublicKey() {
28
+ if (_cached)
29
+ return _cached;
30
+ let pem = (await getSecret("jwtPublicKey")) ||
31
+ process.env.CROSSCHECK_JWT_PUBLIC_KEY ||
32
+ EMBEDDED_PUBLIC_KEY_PEM ||
33
+ "";
34
+ if (!pem) {
35
+ throw new Error("No JWT public key available. Re-run `crosscheck activate <token>` to fetch one.");
36
+ }
37
+ // Tolerate "\n" escaped form (common when keys are stored in env files).
38
+ if (pem.includes("\\n"))
39
+ pem = pem.replace(/\\n/g, "\n");
40
+ _cached = crypto.createPublicKey(pem);
41
+ return _cached;
42
+ }
43
+ export async function verifyJwt(jwt) {
44
+ const [body, sig] = jwt.split(".");
45
+ if (!body || !sig)
46
+ return { ok: false, reason: "malformed_jwt" };
47
+ let valid = false;
48
+ try {
49
+ const pub = await resolvePublicKey();
50
+ valid = crypto.verify(null, Buffer.from(body), pub, fromB64url(sig));
51
+ }
52
+ catch {
53
+ valid = false;
54
+ }
55
+ if (!valid)
56
+ return { ok: false, reason: "bad_signature" };
57
+ try {
58
+ const parsed = JSON.parse(fromB64url(body).toString("utf8"));
59
+ if (typeof parsed.exp !== "number" || Date.now() > parsed.exp) {
60
+ return { ok: false, reason: "expired" };
61
+ }
62
+ return { ok: true, payload: parsed };
63
+ }
64
+ catch {
65
+ return { ok: false, reason: "bad_json" };
66
+ }
67
+ }
68
+ //# sourceMappingURL=jwt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jwt.js","sourceRoot":"","sources":["../src/jwt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC1C,OAAO,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAatD,SAAS,UAAU,CAAC,CAAS;IAC3B,MAAM,GAAG,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC;IACrE,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,QAAQ,CAAC,CAAC;AAC9E,CAAC;AAED,IAAI,OAAO,GAA4B,IAAI,CAAC;AAE5C,KAAK,UAAU,gBAAgB;IAC7B,IAAI,OAAO;QAAE,OAAO,OAAO,CAAC;IAC5B,IAAI,GAAG,GACL,CAAC,MAAM,SAAS,CAAC,cAAc,CAAC,CAAC;QACjC,OAAO,CAAC,GAAG,CAAC,yBAAyB;QACrC,uBAAuB;QACvB,EAAE,CAAC;IACL,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,KAAK,CACb,iFAAiF,CAClF,CAAC;IACJ,CAAC;IACD,yEAAyE;IACzE,IAAI,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACzD,OAAO,GAAG,MAAM,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;IACtC,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,GAAW;IAKX,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACnC,IAAI,CAAC,IAAI,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC;IACjE,IAAI,KAAK,GAAG,KAAK,CAAC;IAClB,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,gBAAgB,EAAE,CAAC;QACrC,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;IACvE,CAAC;IAAC,MAAM,CAAC;QACP,KAAK,GAAG,KAAK,CAAC;IAChB,CAAC;IACD,IAAI,CAAC,KAAK;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC;IAC1D,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAwB,CAAC;QACpF,IAAI,OAAO,MAAM,CAAC,GAAG,KAAK,QAAQ,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,GAAG,EAAE,CAAC;YAC9D,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;QAC1C,CAAC;QACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IACvC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;IAC3C,CAAC;AACH,CAAC"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * OS keychain abstraction. Uses `keytar` on every platform:
3
+ * - macOS: Keychain
4
+ * - Windows: Credential Manager
5
+ * - Linux: libsecret (GNOME Keyring / KWallet)
6
+ *
7
+ * On unsupported environments (CI, headless Linux without a keyring), falls
8
+ * back to an Argon2id-encrypted file under ~/.config/crosscheck/.
9
+ *
10
+ * The fallback path is documented in the EULA: keys never leave the local
11
+ * machine, only their storage form differs.
12
+ */
13
+ import keytar from "keytar";
14
+ import { KEYCHAIN_SERVICE } from "./config.js";
15
+ export async function setSecret(name, value) {
16
+ // TODO(phase-3.5): implement Argon2id-encrypted file fallback for
17
+ // environments where keytar can't talk to a system keyring (CI, some
18
+ // headless Linux containers). For v1 we assume a working keychain.
19
+ await keytar.setPassword(KEYCHAIN_SERVICE, name, value);
20
+ }
21
+ export async function getSecret(name) {
22
+ return await keytar.getPassword(KEYCHAIN_SERVICE, name);
23
+ }
24
+ export async function deleteSecret(name) {
25
+ return await keytar.deletePassword(KEYCHAIN_SERVICE, name);
26
+ }
27
+ /**
28
+ * Convenience: list every Crosscheck (XC)-owned credential. Used by `crosscheck
29
+ * keys list` and by deactivation to wipe everything in one pass.
30
+ */
31
+ export async function listSecrets() {
32
+ return await keytar.findCredentials(KEYCHAIN_SERVICE);
33
+ }
34
+ //# sourceMappingURL=keychain.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keychain.js","sourceRoot":"","sources":["../src/keychain.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,MAAM,MAAM,QAAQ,CAAC;AAC5B,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE/C,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,IAAY,EAAE,KAAa;IACzD,kEAAkE;IAClE,qEAAqE;IACrE,mEAAmE;IACnE,MAAM,MAAM,CAAC,WAAW,CAAC,gBAAgB,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;AAC1D,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,IAAY;IAC1C,OAAO,MAAM,MAAM,CAAC,WAAW,CAAC,gBAAgB,EAAE,IAAI,CAAC,CAAC;AAC1D,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,IAAY;IAC7C,OAAO,MAAM,MAAM,CAAC,cAAc,CAAC,gBAAgB,EAAE,IAAI,CAAC,CAAC;AAC7D,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW;IAG/B,OAAO,MAAM,MAAM,CAAC,eAAe,CAAC,gBAAgB,CAAC,CAAC;AACxD,CAAC"}
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Browser-paste flow for upstream LLM keys. Same trust pattern as
3
+ * `gh auth login`: the CLI spins up a localhost HTTP server, opens a
4
+ * Crosscheck (XC)-served form in the user's browser, and the form POSTs the
5
+ * pasted key directly back to localhost. Crosscheck's servers never
6
+ * see the value.
7
+ *
8
+ * Why this exists: pasting a 200-character API key into a terminal is
9
+ * (a) easy to screw up (newlines, shell history), (b) reveals the value
10
+ * on-screen, and (c) culturally foreign to non-CLI-native users. The
11
+ * browser form is masked-input, has Cmd-V "Paste from Clipboard"
12
+ * affordances built in, and is what every developer is used to from
13
+ * console.anthropic.com etc.
14
+ *
15
+ * Security properties:
16
+ * - State token (32 bytes urlsafe) bound at server-start. Mismatched
17
+ * state → CLI rejects the POST.
18
+ * - Loopback only — server binds to 127.0.0.1 (NOT 0.0.0.0). Other
19
+ * machines on the LAN can't hit it.
20
+ * - Browser → CLI path is HTTP, but it's loopback, so it never crosses
21
+ * the network. Modern browsers special-case localhost CORS.
22
+ * - 5-minute timeout. Server self-terminates after the value lands or
23
+ * the deadline elapses.
24
+ * - Server accepts exactly one valid POST. Subsequent calls 410.
25
+ * - Provider list pinned at compile time on both sides (server + CLI)
26
+ * so an attacker-controlled query string can't store a key under a
27
+ * made-up provider name.
28
+ */
29
+ import crypto from "node:crypto";
30
+ import http from "node:http";
31
+ import { spawn } from "node:child_process";
32
+ import { SERVER_URL } from "./config.js";
33
+ const SUPPORTED_PROVIDERS = new Set([
34
+ "anthropic",
35
+ "openai",
36
+ "gemini",
37
+ "grok",
38
+ "groq",
39
+ "deepseek",
40
+ "mistral",
41
+ ]);
42
+ const FLOW_TIMEOUT_MS = 5 * 60 * 1000;
43
+ const MAX_KEY_BYTES = 4096; // generous; real keys are << this
44
+ /**
45
+ * Run the browser flow. Returns the captured key on success; throws on
46
+ * timeout, invalid state, or server error.
47
+ */
48
+ export async function captureKeyViaBrowser(args) {
49
+ if (!SUPPORTED_PROVIDERS.has(args.provider)) {
50
+ throw new Error(`Unsupported provider: ${args.provider}`);
51
+ }
52
+ const state = crypto.randomBytes(24).toString("base64url");
53
+ const pageBase = args.pageBaseUrl ?? `${SERVER_URL}/cli/keys`;
54
+ return new Promise((resolve, reject) => {
55
+ let captured = null;
56
+ let consumed = false;
57
+ const server = http.createServer((req, res) => {
58
+ // CORS preflight from the browser. The page lives on
59
+ // crosscheckagent.com; the fetch to localhost triggers a preflight.
60
+ if (req.method === "OPTIONS") {
61
+ res.writeHead(204, corsHeaders(pageBase));
62
+ res.end();
63
+ return;
64
+ }
65
+ if (req.method !== "POST") {
66
+ res.writeHead(405, { "content-type": "text/plain", ...corsHeaders(pageBase) });
67
+ res.end("method not allowed");
68
+ return;
69
+ }
70
+ if (consumed) {
71
+ res.writeHead(410, { "content-type": "text/plain", ...corsHeaders(pageBase) });
72
+ res.end("already used");
73
+ return;
74
+ }
75
+ const url = new URL(req.url ?? "/", `http://localhost`);
76
+ if (url.pathname !== "/key") {
77
+ res.writeHead(404, { "content-type": "text/plain", ...corsHeaders(pageBase) });
78
+ res.end("not found");
79
+ return;
80
+ }
81
+ // Body cap so a malicious page can't OOM us.
82
+ let received = 0;
83
+ const chunks = [];
84
+ req.on("data", (chunk) => {
85
+ received += chunk.length;
86
+ if (received > MAX_KEY_BYTES) {
87
+ res.writeHead(413, { "content-type": "text/plain", ...corsHeaders(pageBase) });
88
+ res.end("payload too large");
89
+ req.destroy();
90
+ return;
91
+ }
92
+ chunks.push(chunk);
93
+ });
94
+ req.on("end", () => {
95
+ if (req.destroyed)
96
+ return;
97
+ let body;
98
+ try {
99
+ body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
100
+ }
101
+ catch {
102
+ res.writeHead(400, { "content-type": "text/plain", ...corsHeaders(pageBase) });
103
+ res.end("invalid json");
104
+ return;
105
+ }
106
+ if (body.state !== state) {
107
+ res.writeHead(403, { "content-type": "text/plain", ...corsHeaders(pageBase) });
108
+ res.end("state mismatch");
109
+ return;
110
+ }
111
+ if (body.provider !== args.provider) {
112
+ res.writeHead(400, { "content-type": "text/plain", ...corsHeaders(pageBase) });
113
+ res.end("provider mismatch");
114
+ return;
115
+ }
116
+ const value = typeof body.value === "string" ? body.value.trim() : "";
117
+ if (!value) {
118
+ res.writeHead(400, { "content-type": "text/plain", ...corsHeaders(pageBase) });
119
+ res.end("empty key");
120
+ return;
121
+ }
122
+ consumed = true;
123
+ captured = { provider: args.provider, value };
124
+ res.writeHead(200, { "content-type": "application/json", ...corsHeaders(pageBase) });
125
+ res.end(JSON.stringify({ ok: true }));
126
+ // Close the server shortly after responding so the test can finish.
127
+ setTimeout(() => {
128
+ server.close();
129
+ }, 100);
130
+ });
131
+ });
132
+ server.on("error", (err) => reject(err));
133
+ // Bind to a random port on loopback only (127.0.0.1, not 0.0.0.0).
134
+ server.listen(0, "127.0.0.1", () => {
135
+ const addr = server.address();
136
+ if (!addr || typeof addr === "string") {
137
+ server.close();
138
+ reject(new Error("Could not determine local port."));
139
+ return;
140
+ }
141
+ const port = addr.port;
142
+ const callback = `http://127.0.0.1:${port}/key`;
143
+ const pageUrl = `${pageBase}?provider=${encodeURIComponent(args.provider)}&state=${encodeURIComponent(state)}&callback=${encodeURIComponent(callback)}`;
144
+ printAndOpen(args.provider, pageUrl);
145
+ // Timeout.
146
+ const timeoutId = setTimeout(() => {
147
+ if (!captured) {
148
+ server.close();
149
+ reject(new Error(`Timed out waiting for the browser. Re-run \`crosscheck keys add ${args.provider}\` to try again.`));
150
+ }
151
+ }, FLOW_TIMEOUT_MS);
152
+ server.on("close", () => {
153
+ clearTimeout(timeoutId);
154
+ if (captured)
155
+ resolve(captured);
156
+ // If captured is null at this point and no error was thrown,
157
+ // the timeout above already rejected.
158
+ });
159
+ });
160
+ });
161
+ }
162
+ function corsHeaders(origin) {
163
+ // Lock the allowed origin to the served page's origin so a malicious
164
+ // page on another origin can't POST to the loopback server. We use the
165
+ // origin form (scheme + host) not the full URL with path.
166
+ let allow = "*";
167
+ try {
168
+ const u = new URL(origin);
169
+ allow = `${u.protocol}//${u.host}`;
170
+ }
171
+ catch {
172
+ // Fall through with *.
173
+ }
174
+ return {
175
+ "access-control-allow-origin": allow,
176
+ "access-control-allow-methods": "POST, OPTIONS",
177
+ "access-control-allow-headers": "content-type",
178
+ "access-control-max-age": "600",
179
+ "vary": "origin",
180
+ };
181
+ }
182
+ function printAndOpen(provider, pageUrl) {
183
+ // eslint-disable-next-line no-console
184
+ console.log("");
185
+ // eslint-disable-next-line no-console
186
+ console.log(` Open this URL in your browser to paste your ${provider} key:`);
187
+ // eslint-disable-next-line no-console
188
+ console.log(` ${pageUrl}`);
189
+ // eslint-disable-next-line no-console
190
+ console.log("");
191
+ // eslint-disable-next-line no-console
192
+ console.log(" Waiting for the browser… (5 min timeout; Ctrl-C to cancel)");
193
+ // eslint-disable-next-line no-console
194
+ console.log("");
195
+ // Best-effort browser auto-open. Failure is silent — the user can copy
196
+ // the URL above. We don't shell out to fancy `open` packages so the CLI
197
+ // stays dep-light.
198
+ const opener = process.platform === "darwin"
199
+ ? "open"
200
+ : process.platform === "win32"
201
+ ? "cmd"
202
+ : "xdg-open";
203
+ const args = process.platform === "win32" ? ["/c", "start", "", pageUrl] : [pageUrl];
204
+ try {
205
+ const child = spawn(opener, args, {
206
+ stdio: "ignore",
207
+ detached: true,
208
+ });
209
+ child.unref();
210
+ child.on("error", () => {
211
+ // Swallow — the URL is printed above, user can click it.
212
+ });
213
+ }
214
+ catch {
215
+ /* ignore */
216
+ }
217
+ }
218
+ //# sourceMappingURL=keys-browser-flow.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keys-browser-flow.js","sourceRoot":"","sources":["../src/keys-browser-flow.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Interactive upstream-key setup wizard. For each provider in the panel it
3
+ * offers three ways to add a key, none of which send the key to our servers:
4
+ *
5
+ * [b] browser — opens a crosscheck-served form that POSTs the key directly
6
+ * to a localhost loopback server we run; key lands in the OS keychain.
7
+ * [t] terminal — paste it here (read from stdin).
8
+ * [e] env — import a key already present in the environment
9
+ * (ANTHROPIC_API_KEY, OPENAI_API_KEY, …).
10
+ * [s] skip.
11
+ *
12
+ * Keys are stored ONLY in the OS keychain and never transit crosscheckagent.com.
13
+ */
14
+ import readline from "node:readline/promises";
15
+ import { stdin, stdout } from "node:process";
16
+ import { getSecret, setSecret } from "./keychain.js";
17
+ import { captureKeyViaBrowser } from "./keys-browser-flow.js";
18
+ /** The default panel. `grok` maps to XAI_API_KEY inside the engine. */
19
+ const PANEL = [
20
+ { key: "anthropic", label: "Anthropic (Claude)", env: "ANTHROPIC_API_KEY" },
21
+ { key: "openai", label: "OpenAI (GPT)", env: "OPENAI_API_KEY" },
22
+ { key: "gemini", label: "Google (Gemini)", env: "GEMINI_API_KEY" },
23
+ { key: "grok", label: "xAI (Grok)", env: "XAI_API_KEY" },
24
+ ];
25
+ function log(s = "") {
26
+ // eslint-disable-next-line no-console
27
+ console.log(s);
28
+ }
29
+ export async function runKeysSetup() {
30
+ log("");
31
+ log("Set up your provider keys. They're stored in your OS keychain and");
32
+ log("never sent to Crosscheck — you bring your own keys.");
33
+ log("");
34
+ const rl = readline.createInterface({ input: stdin, output: stdout });
35
+ let configured = 0;
36
+ try {
37
+ for (const p of PANEL) {
38
+ const existing = await getSecret(`key:${p.key}`);
39
+ if (existing) {
40
+ const ans = (await rl.question(`• ${p.label}: already set. Replace? [y/N] `))
41
+ .trim()
42
+ .toLowerCase();
43
+ if (ans !== "y" && ans !== "yes") {
44
+ configured += 1;
45
+ continue;
46
+ }
47
+ }
48
+ const fromEnv = process.env[p.env];
49
+ const options = fromEnv ? "[b]rowser / [t]erminal / [e]nv / [s]kip" : "[b]rowser / [t]erminal / [s]kip";
50
+ const envNote = fromEnv ? ` (found ${p.env} in your environment)` : "";
51
+ const choice = (await rl.question(`• ${p.label}${envNote}\n ${options}: `))
52
+ .trim()
53
+ .toLowerCase();
54
+ if (choice === "s" || choice === "skip" || choice === "") {
55
+ continue;
56
+ }
57
+ if (choice === "e" || choice === "env") {
58
+ if (fromEnv) {
59
+ await setSecret(`key:${p.key}`, fromEnv.trim());
60
+ log(` ✓ Imported ${p.label} key from ${p.env}.`);
61
+ configured += 1;
62
+ }
63
+ else {
64
+ log(` (no ${p.env} in environment — skipped)`);
65
+ }
66
+ continue;
67
+ }
68
+ if (choice === "t" || choice === "terminal" || choice === "paste") {
69
+ const value = (await rl.question(` Paste your ${p.label} key: `)).trim();
70
+ if (value) {
71
+ await setSecret(`key:${p.key}`, value);
72
+ log(` ✓ Stored ${p.label} key in your OS keychain.`);
73
+ configured += 1;
74
+ }
75
+ else {
76
+ log(" (empty — skipped)");
77
+ }
78
+ continue;
79
+ }
80
+ // Default: browser flow.
81
+ try {
82
+ const captured = await captureKeyViaBrowser({ provider: p.key });
83
+ await setSecret(`key:${p.key}`, captured.value);
84
+ log(` ✓ Stored ${p.label} key in your OS keychain.`);
85
+ configured += 1;
86
+ }
87
+ catch (err) {
88
+ log(` ✗ ${err instanceof Error ? err.message : String(err)} — skipped.`);
89
+ }
90
+ }
91
+ }
92
+ finally {
93
+ rl.close();
94
+ }
95
+ log("");
96
+ if (configured === 0) {
97
+ log("No keys configured yet. Run `crosscheck keys setup` any time to add them.");
98
+ }
99
+ else {
100
+ log(`Configured ${configured} provider${configured === 1 ? "" : "s"}.`);
101
+ }
102
+ return { configured };
103
+ }
104
+ //# sourceMappingURL=keys-setup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keys-setup.js","sourceRoot":"","sources":["../src/keys-setup.ts"],"names":[],"mappings":"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"}