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.
- package/LICENSE.md +18 -0
- package/README.md +77 -0
- package/dist/activate.js +196 -0
- package/dist/activate.js.map +1 -0
- package/dist/cli.js +186 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.js +45 -0
- package/dist/config.js.map +1 -0
- package/dist/fingerprint.js +92 -0
- package/dist/fingerprint.js.map +1 -0
- package/dist/heartbeat.js +88 -0
- package/dist/heartbeat.js.map +1 -0
- package/dist/install.js +86 -0
- package/dist/install.js.map +1 -0
- package/dist/jwt.js +68 -0
- package/dist/jwt.js.map +1 -0
- package/dist/keychain.js +34 -0
- package/dist/keychain.js.map +1 -0
- package/dist/keys-browser-flow.js +218 -0
- package/dist/keys-browser-flow.js.map +1 -0
- package/dist/keys-setup.js +104 -0
- package/dist/keys-setup.js.map +1 -0
- package/dist/mcp-register.js +74 -0
- package/dist/mcp-register.js.map +1 -0
- package/dist/mcp.js +117 -0
- package/dist/mcp.js.map +1 -0
- package/dist/open-browser.js +25 -0
- package/dist/open-browser.js.map +1 -0
- package/dist/signing.js +37 -0
- package/dist/signing.js.map +1 -0
- package/dist/telemetry.js +80 -0
- package/dist/telemetry.js.map +1 -0
- package/package.json +41 -0
|
@@ -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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"}
|
package/dist/install.js
ADDED
|
@@ -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
|
package/dist/jwt.js.map
ADDED
|
@@ -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"}
|
package/dist/keychain.js
ADDED
|
@@ -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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"}
|