crosscheck-cli 0.0.63 → 0.0.64

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.
Files changed (53) hide show
  1. package/dist/activate.d.ts +55 -0
  2. package/dist/activate.js +8 -2
  3. package/dist/activate.js.map +1 -1
  4. package/dist/cli-enterprise.d.ts +11 -0
  5. package/dist/cli.d.ts +8 -0
  6. package/dist/config.d.ts +55 -0
  7. package/dist/config.js +1 -1
  8. package/dist/doctor.d.ts +13 -0
  9. package/dist/engine-version.d.ts +27 -0
  10. package/dist/enterprise/compliance.d.ts +20 -0
  11. package/dist/enterprise/doctor.d.ts +17 -0
  12. package/dist/enterprise/install.d.ts +13 -0
  13. package/dist/enterprise/keys-setup.d.ts +8 -0
  14. package/dist/enterprise/login.d.ts +8 -0
  15. package/dist/enterprise/policy.d.ts +26 -0
  16. package/dist/explain.d.ts +38 -0
  17. package/dist/fingerprint.d.ts +31 -0
  18. package/dist/heartbeat.d.ts +32 -0
  19. package/dist/http.d.ts +33 -0
  20. package/dist/install.d.ts +19 -0
  21. package/dist/jwt.d.ts +35 -0
  22. package/dist/key-format.d.ts +40 -0
  23. package/dist/key-status-signing.d.ts +25 -0
  24. package/dist/key-status.d.ts +61 -0
  25. package/dist/key-verify.d.ts +31 -0
  26. package/dist/keychain.d.ts +120 -0
  27. package/dist/keys-browser-flow.d.ts +68 -0
  28. package/dist/keys-manage-cli.d.ts +11 -0
  29. package/dist/keys-session.d.ts +89 -0
  30. package/dist/keys-setup.d.ts +38 -0
  31. package/dist/lib.d.ts +45 -0
  32. package/dist/lib.js +57 -0
  33. package/dist/lib.js.map +1 -0
  34. package/dist/mcp-register.d.ts +49 -0
  35. package/dist/mcp.d.ts +24 -0
  36. package/dist/model-policy.d.ts +44 -0
  37. package/dist/model-prefs.d.ts +104 -0
  38. package/dist/model-resolve.d.ts +61 -0
  39. package/dist/models-cli.d.ts +11 -0
  40. package/dist/onepassword/browser-flow.d.ts +35 -0
  41. package/dist/onepassword/config.d.ts +53 -0
  42. package/dist/onepassword/op.d.ts +54 -0
  43. package/dist/onepassword/reference.d.ts +29 -0
  44. package/dist/onepassword/setup.d.ts +54 -0
  45. package/dist/open-browser.d.ts +6 -0
  46. package/dist/otel-setup.d.ts +69 -0
  47. package/dist/pref-conflicts.d.ts +36 -0
  48. package/dist/reconcile.d.ts +59 -0
  49. package/dist/signing.d.ts +48 -0
  50. package/dist/single-instance.d.ts +97 -0
  51. package/dist/telemetry.d.ts +25 -0
  52. package/dist/update-check.d.ts +17 -0
  53. package/package.json +14 -3
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Report which providers this machine has keys for — and nothing else.
3
+ *
4
+ * WHY THIS EXISTS: keys are per-machine and never leave the machine, which is
5
+ * the right security posture but leaves the browser blind. Open /account on
6
+ * your laptop and it cannot tell you that your desktop lost its OpenAI key
7
+ * two days ago, because it has no idea the desktop ever had one. Every
8
+ * machine but the one you are sitting at is a black hole. This closes that
9
+ * with metadata, without weakening the posture at all.
10
+ *
11
+ * WHAT LEAVES THIS MACHINE, exhaustively:
12
+ * provider name · a key is present yes/no · the last 4 characters ·
13
+ * how it last behaved · when that was
14
+ *
15
+ * The last-4 is the only part derived from the key itself, and it exists for
16
+ * one reason: so a human can tell whether the key on the dashboard is the one
17
+ * they think it is before replacing it. `suffix4()` is the ONLY place in this
18
+ * file that touches key material, it is four characters wide by construction,
19
+ * and the server's schema independently rejects anything longer. Two
20
+ * independent bounds, because a single one is a single edit away from gone.
21
+ *
22
+ * Reporting is opt-OUT via CROSSCHECK_NO_KEY_STATUS=1. An org that considers
23
+ * provider coverage sensitive can turn it off and lose only the dashboard.
24
+ */
25
+ /** How a key last behaved. Mirrors cb_key_result on the server. */
26
+ export type KeyResult = "ok" | "invalid_key" | "quota_billing" | "network" | "provider_down" | "unknown";
27
+ export type KeyStatusItem = {
28
+ provider: string;
29
+ present: boolean;
30
+ suffix?: string;
31
+ lastResult?: KeyResult;
32
+ lastCheckedAt?: string;
33
+ };
34
+ /**
35
+ * The last four characters, or nothing.
36
+ *
37
+ * Short keys return undefined rather than a padded or partial value: a key
38
+ * under 8 characters is not a real key, and reporting a suffix of a 5-char
39
+ * string would expose most of it. The threshold is deliberately well above 4.
40
+ */
41
+ export declare function suffix4(key: string | null | undefined): string | undefined;
42
+ export declare function keyStatusDisabled(env?: NodeJS.ProcessEnv): boolean;
43
+ /**
44
+ * Read the local store and describe it. Absent providers are reported
45
+ * explicitly as `present: false` rather than omitted, because the server
46
+ * treats a report as a full replacement of the seat's provider set and
47
+ * "missing from the list" has to mean "no longer configured".
48
+ */
49
+ export declare function collectKeyStatus(): Promise<KeyStatusItem[]>;
50
+ /**
51
+ * Send the report. Best-effort and silent: this runs on the MCP startup path,
52
+ * where a network problem must cost a stale dashboard row and never a failed
53
+ * connection.
54
+ */
55
+ export declare function reportKeyStatus(args: {
56
+ seatId: string;
57
+ orgId: string;
58
+ jwt: string;
59
+ serverUrl?: string;
60
+ timeoutMs?: number;
61
+ }): Promise<"sent" | "skipped" | "failed">;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * "Does this key actually work?" — answered by asking the provider, cheaply.
3
+ *
4
+ * Every probe here is a model-list GET: no completion, no tokens, no cost.
5
+ * That matters, because a verify button that quietly bills you is a verify
6
+ * button people learn not to press.
7
+ *
8
+ * THE DISTINCTION THIS FILE EXISTS FOR: "we could not reach the provider" and
9
+ * "the provider rejected your key" are completely different problems with
10
+ * completely different fixes, and they arrive looking similar. Collapsing
11
+ * them sends someone to regenerate a key because their wifi dropped. So the
12
+ * default for anything ambiguous is `network` or `unknown` — never
13
+ * `invalid_key`. We accuse a key only when a provider actually refused it.
14
+ */
15
+ /** Mirrors cb_key_result on the server and KeyResult in key-status.ts. */
16
+ export type VerifyResult = "ok" | "invalid_key" | "quota_billing" | "network" | "provider_down" | "unknown";
17
+ export type VerifyOutcome = {
18
+ result: VerifyResult;
19
+ /** One line, safe to show a user. NEVER contains the key. */
20
+ detail: string;
21
+ };
22
+ export declare function canVerify(provider: string): boolean;
23
+ export declare function classifyStatus(status: number, body: string): VerifyOutcome;
24
+ /**
25
+ * Probe one provider. `fetchImpl` is injectable so the classification can be
26
+ * tested without touching the network.
27
+ */
28
+ export declare function verifyKey(provider: string, key: string, opts?: {
29
+ fetchImpl?: typeof fetch;
30
+ timeoutMs?: number;
31
+ }): Promise<VerifyOutcome>;
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Secret store — NO native dependencies.
3
+ *
4
+ * Replaces the former `@napi-rs/keyring` backend, whose per-platform native
5
+ * binary kept corrupting on `npx` installs (truncated `.node` → dlopen
6
+ * failure; npm/cli#4828). The public API is unchanged so call sites don't move.
7
+ *
8
+ * Three backends:
9
+ * - "file" (DEFAULT, recommended): an encrypted file at <dir>/secrets.enc
10
+ * (AES-256-GCM) keyed by a random 32-byte key at <dir>/secret.key
11
+ * (mode 0400); data file 0600, dir 0700. Pure Node `crypto`, ZERO password
12
+ * prompts. <dir> defaults to ~/.crosscheck — in your home directory, never
13
+ * inside the repo you're working in, never committed to git. Threat model:
14
+ * protects against casual file copying / sync, not a process already
15
+ * running as you (same as gh / aws / .env).
16
+ * - "keychain" (opt-in, macOS only): shells out to `/usr/bin/security`. More
17
+ * OS-integrated, but macOS prompts for your LOGIN PASSWORD on every write —
18
+ * so a fresh setup asks several times. Not the default for that reason.
19
+ * Every spawn is hard-timeout-bounded with captured stdio, so a Keychain
20
+ * GUI prompt can never block a non-interactive `serve` (a timeout is
21
+ * treated as "not found" — we degrade, never hang).
22
+ * - "1password" (opt-in, enterprise): routes ONLY "key:<provider>" lookups
23
+ * that have an entry in ~/.crosscheck/1password.json to the 1Password
24
+ * backend (see src/onepassword/); everything else — the activation JWT,
25
+ * the HMAC signing key, installUuid, and any provider NOT listed in that
26
+ * config — still uses the file store, always. 1Password can't hold a
27
+ * machine-local secret and was never asked to; this is explicit routing
28
+ * by secret class, not a fallback. Read-only: writes to a 1Password-
29
+ * mapped provider throw ReadOnlyBackendError. A configured-but-failing
30
+ * 1Password read throws OnePasswordReadError rather than silently
31
+ * reading a stale value from the local file store — see
32
+ * docs/onepassword-security.md for the full design.
33
+ *
34
+ * Selection precedence:
35
+ * 1. CROSSCHECK_SECRET_STORE = file | keychain | 1password (env override)
36
+ * 2. saved preference in <dir>/store.json (set by `keys store` / `keys setup --backend 1password`)
37
+ * 3. "file" (default)
38
+ *
39
+ * Migration: when the file backend is active and no file store exists yet, we
40
+ * transparently import any existing macOS-keychain items once (those reads are
41
+ * prompt-free), so upgrading from a keychain build never strands a user.
42
+ *
43
+ * Keys never leave the machine, except when the 1password backend is active
44
+ * and a provider is explicitly mapped to it — that key never touches
45
+ * crosscheck's own storage at all, encrypted or not.
46
+ */
47
+ import { type OpReadErrorKind } from "./onepassword/op.js";
48
+ /**
49
+ * "1password-only" is crosscheck-enterprise's mode: unlike "1password"
50
+ * (opt-in per provider, unmapped providers silently fall back to the file
51
+ * store), it closes that fallback specifically for provider keys — an
52
+ * unmapped provider throws instead of reading/writing the file store.
53
+ * Machine-local secrets (jwt, hmacSigningKey, etc.) are unaffected either
54
+ * way; see onePasswordProviderFor's null-for-non-"key:"-names behavior.
55
+ */
56
+ export type Backend = "keychain" | "file" | "1password" | "1password-only";
57
+ export declare function dataDir(): string;
58
+ /** Backend label for `crosscheck doctor`. */
59
+ export declare function secretBackend(): Backend;
60
+ /** Where the encrypted file store lives (shown by `doctor`). */
61
+ export declare function fileStorePath(): string;
62
+ /** Thrown by setSecret/deleteSecret for a "key:<provider>" that's mapped to
63
+ * the 1Password backend — it's read-only in this version. */
64
+ export declare class ReadOnlyBackendError extends Error {
65
+ constructor(name: string);
66
+ }
67
+ /** Thrown by getSecret when a provider IS mapped to 1Password but the read
68
+ * failed — never silently falls back to the local file store instead,
69
+ * which would defeat the point of an enterprise-mandated backend and could
70
+ * resurrect a stale or already-rotated key. */
71
+ export declare class OnePasswordReadError extends Error {
72
+ readonly provider: string;
73
+ readonly code: OpReadErrorKind;
74
+ constructor(provider: string, code: OpReadErrorKind, detail: string);
75
+ }
76
+ /** Thrown by getSecret/setSecret/deleteSecret in "1password-only" mode for a
77
+ * "key:<provider>" that has no 1Password mapping — the enterprise-mode
78
+ * equivalent of ReadOnlyBackendError, but for the CLOSED fallback rather
79
+ * than a read-only mapped entry. */
80
+ export declare class NotMappedInEnterpriseModeError extends Error {
81
+ constructor(provider: string);
82
+ }
83
+ export declare function setSecret(name: string, value: string): Promise<void>;
84
+ export declare function getSecret(name: string): Promise<string | null>;
85
+ export declare function deleteSecret(name: string): Promise<boolean>;
86
+ /**
87
+ * List every Crosscheck-owned credential. Used by `crosscheck keys list` and
88
+ * by deactivation to wipe everything in one pass.
89
+ *
90
+ * For a 1Password-mapped provider this NEVER performs an actual 1Password
91
+ * read: doing so here would mean every `listSecrets()` call (doctor, `keys
92
+ * list`, deactivation) silently fans out into one `op` invocation per
93
+ * mapped provider — in desktop mode, that's N sequential GUI unlock
94
+ * prompts for an operation the user didn't ask to unlock anything for. The
95
+ * placeholder marker reports "configured," never the resolved value.
96
+ */
97
+ export declare const ONEPASSWORD_LIST_PLACEHOLDER = "(configured via 1Password \u2014 value not read)";
98
+ export declare function listSecrets(): Promise<Array<{
99
+ account: string;
100
+ password: string;
101
+ }>>;
102
+ /**
103
+ * Switch the secret store backend and migrate existing secrets into it.
104
+ * Persists the choice so all future invocations use it. Returns how many
105
+ * credentials were migrated. NOTE: migrating *to* keychain triggers a macOS
106
+ * login-password prompt per credential — callers should warn first.
107
+ */
108
+ /**
109
+ * Persist "1password" as the active backend WITHOUT setSecretBackend()'s
110
+ * copy-existing-secrets-in behavior, which is meaningless for a read-only,
111
+ * config-driven backend. Callers (`keys setup --backend 1password`) must
112
+ * validate config + connectivity themselves FIRST — this only flips the
113
+ * switch once that validation already succeeded.
114
+ */
115
+ export declare function enableOnePasswordBackend(): void;
116
+ /** crosscheck-enterprise only — see the Backend type's "1password-only" doc. */
117
+ export declare function enableOnePasswordOnlyBackend(): void;
118
+ export declare function setSecretBackend(target: Backend): Promise<{
119
+ migrated: number;
120
+ }>;
@@ -0,0 +1,68 @@
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
+ export declare const SUPPORTED_PROVIDERS: Set<string>;
30
+ export type CapturedKey = {
31
+ provider: string;
32
+ value: string;
33
+ };
34
+ /**
35
+ * Run the browser flow. Returns the captured key on success; throws on
36
+ * timeout, invalid state, or server error.
37
+ */
38
+ export declare function captureKeyViaBrowser(args: {
39
+ provider: string;
40
+ /**
41
+ * Optional override for the page URL — useful for local dev / preview
42
+ * deployments. Defaults to ${SERVER_URL}/cli/keys (crosscheckagent.com).
43
+ */
44
+ pageBaseUrl?: string;
45
+ }): Promise<CapturedKey>;
46
+ /**
47
+ * Batch variant: collect keys for MULTIPLE providers in ONE browser page.
48
+ * The CLI opens /cli/keys?providers=a,b,c&state&callback; the page POSTs a
49
+ * single { state, keys: { provider: value } } to the loopback /keys endpoint.
50
+ * Returns the captured (non-empty) keys map. Used by `keys setup` / `install`
51
+ * so there is zero interactive terminal input. Keys never transit our backend.
52
+ */
53
+ export declare function captureKeysViaBrowser(args: {
54
+ providers: string[];
55
+ pageBaseUrl?: string;
56
+ timeoutMs?: number;
57
+ /** When false, the CLI does NOT open /cli/keys itself — the keys form is
58
+ * hosted elsewhere (e.g. /cli/start) and posts to this same loopback. */
59
+ openPage?: boolean;
60
+ /** Invoked once the loopback is listening, with its callback URL + state, so
61
+ * the caller can route the user to a page that posts keys back here (e.g.
62
+ * run device-auth opening /cli/start with these params). The keys timeout
63
+ * starts after this resolves. */
64
+ onReady?: (info: {
65
+ callback: string;
66
+ state: string;
67
+ }) => Promise<void> | void;
68
+ }): Promise<Record<string, string>>;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * `crosscheck keys manage` — open a live key-management session in the browser.
3
+ *
4
+ * Runs in the foreground on purpose. The session is a capability over your
5
+ * local key store, and a capability should be visible while it exists: the
6
+ * terminal shows what's happening, and closing it closes the session. A
7
+ * background daemon quietly holding this open would be the wrong shape.
8
+ */
9
+ export declare function runKeysManage(opts?: {
10
+ open?: boolean;
11
+ }): Promise<void>;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * A live, authenticated key-management session on this machine.
3
+ *
4
+ * WHAT THIS CHANGES. Key management used to be one-shot: `crosscheck keys`
5
+ * opened a page, the page POSTed one batch, the CLI applied it and exited.
6
+ * You could not sit in your account page and manage keys, and you could not
7
+ * see what a change did. This turns that into a session the browser can drive
8
+ * for as long as it's open — which is what makes /account a place you return
9
+ * to rather than a form you submit.
10
+ *
11
+ * WHAT IT DOES NOT CHANGE: keys still never reach our servers. The browser
12
+ * talks straight to 127.0.0.1. Everything below exists to make that channel
13
+ * safe to leave open for ten minutes instead of ten seconds.
14
+ *
15
+ * THE TOKEN NEVER TOUCHES OUR SERVER. It is delivered to the page in the URL
16
+ * *fragment* (`#xc=port.token`), and fragments are not sent in HTTP requests.
17
+ * So crosscheckagent.com — and its logs, and its proxies, and anyone who ever
18
+ * breaches it — cannot obtain the capability to manage keys on your machine.
19
+ * This is deliberately stricter than the design called for: registering the
20
+ * token server-side would have enabled a nicer "click a button in the browser
21
+ * first" flow, at the cost of making our server able to reach into your key
22
+ * store. That trade wasn't worth making, so the flow starts in the terminal.
23
+ *
24
+ * Defence in depth, each layer assuming the others failed:
25
+ * - binds 127.0.0.1 only, never 0.0.0.0 — nothing on the LAN can see it
26
+ * - every request needs the bearer token, compared in constant time
27
+ * - CORS locked to our origin, so a hostile page can't read responses
28
+ * - Private Network Access preflight answered explicitly (see below)
29
+ * - idle TTL of 10 minutes, hard cap of 60, so a forgotten tab isn't
30
+ * an indefinitely open door
31
+ * - responses carry the last 4 characters of a key and never more, so a
32
+ * bug in this file cannot turn into a key disclosure
33
+ */
34
+ import { type VerifyResult } from "./key-verify.js";
35
+ /** Idle timeout. Every authenticated request pushes it out. */
36
+ export declare const IDLE_TTL_MS: number;
37
+ /** Absolute ceiling, regardless of activity. A session left open all day is
38
+ * not a session, it's a hole. */
39
+ export declare const MAX_SESSION_MS: number;
40
+ export type ProviderState = {
41
+ provider: string;
42
+ label: string;
43
+ present: boolean;
44
+ suffix?: string;
45
+ lastResult?: VerifyResult;
46
+ lastCheckedAt?: string;
47
+ canVerify: boolean;
48
+ };
49
+ /** In-memory record of verifies done this session, so the page can show the
50
+ * result of the button the user just pressed. Not persisted: a verify is a
51
+ * point-in-time fact and pretending otherwise is how "healthy" badges lie. */
52
+ type VerifyMemo = Map<string, {
53
+ result: VerifyResult;
54
+ at: string;
55
+ }>;
56
+ export declare function isManageableProvider(name: string): boolean;
57
+ export declare function readProviderStates(memo: VerifyMemo): Promise<ProviderState[]>;
58
+ /**
59
+ * CORS, plus the header that stops Chromium blocking this outright.
60
+ *
61
+ * Chromium's Private Network Access treats a request from a public HTTPS page
62
+ * to 127.0.0.1 as a privilege escalation and sends a preflight carrying
63
+ * `Access-Control-Request-Private-Network: true`. If the listener doesn't
64
+ * answer `Access-Control-Allow-Private-Network: true`, the request is blocked
65
+ * with no visible error — the fetch just fails, and the page has no way to
66
+ * tell that apart from "the CLI isn't running". Cheap to send, and it is the
67
+ * difference between this feature working and mysteriously not.
68
+ */
69
+ export declare function sessionCorsHeaders(origin?: string): Record<string, string>;
70
+ export declare function tokensMatch(expected: string, provided: string | undefined): boolean;
71
+ export declare function bearerFrom(header: string | undefined): string | undefined;
72
+ export type SessionHandle = {
73
+ port: number;
74
+ token: string;
75
+ url: string;
76
+ /** Resolves when the session ends, for any reason. */
77
+ done: Promise<"ended" | "idle" | "expired">;
78
+ close: (reason?: "ended" | "idle" | "expired") => void;
79
+ };
80
+ /**
81
+ * Start the session server. Resolves once it's listening; the caller decides
82
+ * whether to open a browser and how to report progress.
83
+ */
84
+ export declare function startKeysSession(opts?: {
85
+ onEvent?: (msg: string) => void;
86
+ }): Promise<SessionHandle>;
87
+ /** Providers currently holding a key — used for the terminal summary. */
88
+ export declare function configuredProviders(): Promise<string[]>;
89
+ export {};
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Provider-key setup. Non-interactive by design — it never reads stdin (so it
3
+ * can't hang in a non-TTY / agent context):
4
+ *
5
+ * 1. Silently import any keys already present in the environment
6
+ * (ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, XAI_API_KEY,
7
+ * KIMI_API_KEY).
8
+ * 2. For any provider still missing a key, open ONE browser page that
9
+ * collects them all at once and posts them straight to a localhost
10
+ * loopback server → the local secret store. Zero terminal typing.
11
+ *
12
+ * Keys are stored only in the local secret store (see keychain.ts: OS keychain
13
+ * on macOS, an encrypted file elsewhere) and never transit crosscheckagent.com.
14
+ * Power users who want to type a single key can still use
15
+ * `crosscheck keys add <provider> --paste`.
16
+ */
17
+ export type PanelProvider = {
18
+ key: string;
19
+ label: string;
20
+ env: string[];
21
+ };
22
+ /** The default panel. `grok` maps to XAI_API_KEY inside the engine. `env` lists
23
+ * the environment variables we'll auto-import from, in priority order — the
24
+ * aliases catch people who set GROK_API_KEY / GOOGLE_API_KEY / etc. */
25
+ export declare const KEY_PANEL: PanelProvider[];
26
+ /**
27
+ * Use what's already available: count existing keychain entries, import any
28
+ * env-present keys (writing them to the keychain), and return the providers
29
+ * that still need a key. No prompts, no network.
30
+ */
31
+ export declare function importEnvKeys(): Promise<{
32
+ configured: number;
33
+ need: PanelProvider[];
34
+ }>;
35
+ /** Standalone `crosscheck keys setup`: env-import, then one browser page for the rest. */
36
+ export declare function runKeysSetup(): Promise<{
37
+ configured: number;
38
+ }>;
package/dist/lib.d.ts ADDED
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Library surface — the API Crosscheck Desktop consumes.
3
+ *
4
+ * WHY THIS FILE EXISTS
5
+ * --------------------
6
+ * The desktop app needs the same vault, the same activation, the same model
7
+ * resolution and the same key verification the CLI has. There are three ways
8
+ * to get that and only one of them is safe:
9
+ *
10
+ * 1. Copy the code into the desktop repo. REJECTED. The secret store and
11
+ * the activation signing are exactly the code that must never diverge
12
+ * between two clients writing the same `~/.crosscheck/` directory. Two
13
+ * copies is two encryption implementations and eventually two file
14
+ * formats.
15
+ * 2. Extract into a third npm package. REJECTED for now. It buys nothing
16
+ * over this file and adds a package to version, publish and keep in
17
+ * lockstep — the same npx-cache trap that already forces a CLI bump on
18
+ * every engine release (see config.ts, ENGINE_FALLBACK_VERSION).
19
+ * 3. Export from the CLI itself. THIS. The desktop app takes a single
20
+ * dependency on `crosscheck-cli`, which already pins `crosscheck-mcp`
21
+ * exactly — so one dependency delivers the vault AND a version-locked
22
+ * engine, and the CLI and the desktop app are the same code by
23
+ * construction rather than by discipline.
24
+ *
25
+ * WHAT IS DELIBERATELY NOT HERE
26
+ * -----------------------------
27
+ * Nothing that prints to the console or reads stdin. Every export below is a
28
+ * pure function or an async call that returns data. The CLI's interactive
29
+ * wizards (`keys-setup`, `keys-manage-cli`, `doctor`) stay out: the desktop
30
+ * app renders its own UI over the same primitives, and a library that can
31
+ * `process.exit()` is a library that can take an Electron main process down
32
+ * with it.
33
+ */
34
+ export { dataDir, fileStorePath, secretBackend, setSecretBackend, getSecret, setSecret, deleteSecret, listSecrets, ONEPASSWORD_LIST_PLACEHOLDER, ReadOnlyBackendError, OnePasswordReadError, NotMappedInEnterpriseModeError, type Backend, } from "./keychain.js";
35
+ export { CANONICAL_PROVIDERS, KEY_FORMATS, resolveProvider, looksLikeApiKey, validateKey, formatWarning, } from "./key-format.js";
36
+ export { canVerify, classifyStatus, verifyKey, type VerifyResult, type VerifyOutcome, } from "./key-verify.js";
37
+ export { suffix4, keyStatusDisabled, collectKeyStatus, reportKeyStatus, type KeyResult, type KeyStatusItem, } from "./key-status.js";
38
+ export { activate, activateDeviceFlow } from "./activate.js";
39
+ export { currentState, isAllowedToRun, runHeartbeat, startScheduler, type HeartbeatState, } from "./heartbeat.js";
40
+ export { readLocalModelPrefs, readLocalModelModes, readLocalOrigins, effectiveForMode, setChainForMode, setLocalModelPref, addLocalModelFallback, removeLocalModelFallback, unsetLocalModelPref, syncModelPreferencesFromServer, pushModelPreferenceToServer, deleteModelPreferenceOnServer, readPendingConflicts, writePendingConflicts, clearPendingConflicts, type ModelPreferences, type ModeScopedPreferences, type ModelMode, type PrefOrigin, type OriginMap, type PrefConflict, } from "./model-prefs.js";
41
+ export { MODEL_ENV_VARS, resolveModelForProvider, resolveAllModels, type ModelSource, type ResolvedModel, } from "./model-resolve.js";
42
+ export { resolveEffectiveModel, isBlockedByPolicy, type ModelPolicy, type ModelPolicyEntry, } from "./model-policy.js";
43
+ export { SERVER_URL, CLI_VERSION, ENGINE_PACKAGE, CLI_PACKAGE } from "./config.js";
44
+ export { resolveEngineVersion } from "./engine-version.js";
45
+ export { registerMcpServer, SERVER_NAME as MCP_SERVER_NAME, SERVER_COMMAND as MCP_SERVER_COMMAND, SERVER_ARGS as MCP_SERVER_ARGS, type RegisterResult, type ServerRegistration, } from "./mcp-register.js";
package/dist/lib.js ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Library surface — the API Crosscheck Desktop consumes.
3
+ *
4
+ * WHY THIS FILE EXISTS
5
+ * --------------------
6
+ * The desktop app needs the same vault, the same activation, the same model
7
+ * resolution and the same key verification the CLI has. There are three ways
8
+ * to get that and only one of them is safe:
9
+ *
10
+ * 1. Copy the code into the desktop repo. REJECTED. The secret store and
11
+ * the activation signing are exactly the code that must never diverge
12
+ * between two clients writing the same `~/.crosscheck/` directory. Two
13
+ * copies is two encryption implementations and eventually two file
14
+ * formats.
15
+ * 2. Extract into a third npm package. REJECTED for now. It buys nothing
16
+ * over this file and adds a package to version, publish and keep in
17
+ * lockstep — the same npx-cache trap that already forces a CLI bump on
18
+ * every engine release (see config.ts, ENGINE_FALLBACK_VERSION).
19
+ * 3. Export from the CLI itself. THIS. The desktop app takes a single
20
+ * dependency on `crosscheck-cli`, which already pins `crosscheck-mcp`
21
+ * exactly — so one dependency delivers the vault AND a version-locked
22
+ * engine, and the CLI and the desktop app are the same code by
23
+ * construction rather than by discipline.
24
+ *
25
+ * WHAT IS DELIBERATELY NOT HERE
26
+ * -----------------------------
27
+ * Nothing that prints to the console or reads stdin. Every export below is a
28
+ * pure function or an async call that returns data. The CLI's interactive
29
+ * wizards (`keys-setup`, `keys-manage-cli`, `doctor`) stay out: the desktop
30
+ * app renders its own UI over the same primitives, and a library that can
31
+ * `process.exit()` is a library that can take an Electron main process down
32
+ * with it.
33
+ */
34
+ // --- The vault. Same file, same lock, same AES-256-GCM. ---------------------
35
+ export { dataDir, fileStorePath, secretBackend, setSecretBackend, getSecret, setSecret, deleteSecret, listSecrets, ONEPASSWORD_LIST_PLACEHOLDER, ReadOnlyBackendError, OnePasswordReadError, NotMappedInEnterpriseModeError, } from "./keychain.js";
36
+ // --- Which providers exist, and does this string look like one's key? -------
37
+ export { CANONICAL_PROVIDERS, KEY_FORMATS, resolveProvider, looksLikeApiKey, validateKey, formatWarning, } from "./key-format.js";
38
+ // --- Does the key actually work? Asks the provider, never our server. -------
39
+ export { canVerify, classifyStatus, verifyKey, } from "./key-verify.js";
40
+ // --- Metadata-only reporting. Presence, last 4, last outcome. ---------------
41
+ export { suffix4, keyStatusDisabled, collectKeyStatus, reportKeyStatus, } from "./key-status.js";
42
+ // --- Seat activation and the signed heartbeat. ------------------------------
43
+ export { activate, activateDeviceFlow } from "./activate.js";
44
+ export { currentState, isAllowedToRun, runHeartbeat, startScheduler, } from "./heartbeat.js";
45
+ // --- Model pinning: local preferences, org policy, effective resolution. ----
46
+ export { readLocalModelPrefs, readLocalModelModes, readLocalOrigins, effectiveForMode, setChainForMode, setLocalModelPref, addLocalModelFallback, removeLocalModelFallback, unsetLocalModelPref, syncModelPreferencesFromServer, pushModelPreferenceToServer, deleteModelPreferenceOnServer, readPendingConflicts, writePendingConflicts, clearPendingConflicts, } from "./model-prefs.js";
47
+ export { MODEL_ENV_VARS, resolveModelForProvider, resolveAllModels, } from "./model-resolve.js";
48
+ export { resolveEffectiveModel, isBlockedByPolicy, } from "./model-policy.js";
49
+ // --- Identity and versions, so the app can report what it is running. -------
50
+ export { SERVER_URL, CLI_VERSION, ENGINE_PACKAGE, CLI_PACKAGE } from "./config.js";
51
+ export { resolveEngineVersion } from "./engine-version.js";
52
+ // --- Registering the MCP server with Claude Code. -------------------------
53
+ // The wizard's last step. Shared so the app and `crosscheck install` write
54
+ // byte-identical entries to ~/.claude.json — two spellings of the same server
55
+ // is how a user ends up with a duplicate that fails opaquely.
56
+ export { registerMcpServer, SERVER_NAME as MCP_SERVER_NAME, SERVER_COMMAND as MCP_SERVER_COMMAND, SERVER_ARGS as MCP_SERVER_ARGS, } from "./mcp-register.js";
57
+ //# sourceMappingURL=lib.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lib.js","sourceRoot":"","sources":["../src/lib.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,+EAA+E;AAC/E,OAAO,EACL,OAAO,EACP,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,SAAS,EACT,SAAS,EACT,YAAY,EACZ,WAAW,EACX,4BAA4B,EAC5B,oBAAoB,EACpB,oBAAoB,EACpB,8BAA8B,GAE/B,MAAM,eAAe,CAAC;AAEvB,+EAA+E;AAC/E,OAAO,EACL,mBAAmB,EACnB,WAAW,EACX,eAAe,EACf,eAAe,EACf,WAAW,EACX,aAAa,GACd,MAAM,iBAAiB,CAAC;AAEzB,+EAA+E;AAC/E,OAAO,EACL,SAAS,EACT,cAAc,EACd,SAAS,GAGV,MAAM,iBAAiB,CAAC;AAEzB,+EAA+E;AAC/E,OAAO,EACL,OAAO,EACP,iBAAiB,EACjB,gBAAgB,EAChB,eAAe,GAGhB,MAAM,iBAAiB,CAAC;AAEzB,+EAA+E;AAC/E,OAAO,EAAE,QAAQ,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAC7D,OAAO,EACL,YAAY,EACZ,cAAc,EACd,YAAY,EACZ,cAAc,GAEf,MAAM,gBAAgB,CAAC;AAExB,+EAA+E;AAC/E,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,iBAAiB,EACjB,qBAAqB,EACrB,wBAAwB,EACxB,mBAAmB,EACnB,8BAA8B,EAC9B,2BAA2B,EAC3B,6BAA6B,EAC7B,oBAAoB,EACpB,qBAAqB,EACrB,qBAAqB,GAOtB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,cAAc,EACd,uBAAuB,EACvB,gBAAgB,GAGjB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,qBAAqB,EACrB,iBAAiB,GAGlB,MAAM,mBAAmB,CAAC;AAE3B,+EAA+E;AAC/E,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACnF,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAE3D,6EAA6E;AAC7E,2EAA2E;AAC3E,8EAA8E;AAC9E,8DAA8D;AAC9D,OAAO,EACL,iBAAiB,EACjB,WAAW,IAAI,eAAe,EAC9B,cAAc,IAAI,kBAAkB,EACpC,WAAW,IAAI,eAAe,GAG/B,MAAM,mBAAmB,CAAC"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Self-register the crosscheck MCP server with Claude Code so the user never
3
+ * hand-edits JSON.
4
+ *
5
+ * Primary path: the `claude` CLI — `claude mcp add --scope user crosscheck --
6
+ * npx -y -p crosscheck-cli@latest crosscheck serve`. User scope means it's
7
+ * available in every workspace, which matches a per-seat license.
8
+ *
9
+ * Fallback: if the `claude` CLI isn't on PATH, merge-write the user's
10
+ * ~/.claude.json `mcpServers` map in place — never clobbering existing
11
+ * servers or other settings.
12
+ *
13
+ * The bin name is pinned explicitly via `-p <pkg> <bin>` rather than the
14
+ * shorter `npx -y crosscheck-cli@latest serve` — bare `npx <pkg>` only
15
+ * auto-selects a bin when the package ships exactly one, or one matching
16
+ * the package name. crosscheck-cli briefly shipped a second bin
17
+ * (crosscheck-enterprise) in 0.0.50, which made every existing
18
+ * registration written with the short form fail with "could not
19
+ * determine executable to run" (silently, since this string gets written
20
+ * to ~/.claude.json and only fails later as an opaque MCP connection
21
+ * error). Keep this form even now that crosscheck-cli is back to a single
22
+ * bin — it's what makes a future second bin safe to add again.
23
+ */
24
+ export declare const SERVER_NAME = "crosscheck";
25
+ export declare const SERVER_COMMAND = "npx";
26
+ export declare const SERVER_ARGS: string[];
27
+ export type RegisterResult = {
28
+ method: "cli";
29
+ } | {
30
+ method: "file";
31
+ configPath: string;
32
+ } | {
33
+ method: "manual";
34
+ snippet: string;
35
+ reason: string;
36
+ };
37
+ export type ServerRegistration = {
38
+ name: string;
39
+ command: string;
40
+ args: string[];
41
+ };
42
+ /**
43
+ * `reg` defaults to the standard crosscheck-cli registration. crosscheck-
44
+ * enterprise passes its own {name: "crosscheck-enterprise", command: "npx",
45
+ * args: ["-y", "crosscheck-enterprise@latest", "serve"]} so Claude Code
46
+ * spawns the right binary — both can coexist in the same ~/.claude.json
47
+ * under different server names.
48
+ */
49
+ export declare function registerMcpServer(reg?: ServerRegistration): RegisterResult;
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * MCP server façade. Spawns the TypeScript crosscheck-mcp engine as a
3
+ * subprocess and lets the IDE's MCP stdio JSON-RPC traffic flow through it.
4
+ *
5
+ * (Previously this bundled and spawned a Python server. crosscheck-mcp is now
6
+ * pure TypeScript and ships as a normal npm dependency — no Python required.)
7
+ *
8
+ * Pre-flight (before spawning the child):
9
+ * 1. Verify the activation JWT from the OS keychain — refuse to serve if
10
+ * missing, expired, or revoked.
11
+ * 2. Resolve the crosscheck-mcp engine entry from node_modules (falling back
12
+ * to `npx -y crosscheck-mcp` if it isn't installed alongside).
13
+ * 3. Pull every upstream LLM key from the keychain into the child's env
14
+ * (ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, XAI_API_KEY,
15
+ * GROQ_API_KEY, DEEPSEEK_API_KEY, MISTRAL_API_KEY). The customer never
16
+ * hand-manages env vars.
17
+ *
18
+ * The child inherits stdio so the IDE's MCP client talks to it directly; we
19
+ * don't proxy or interpret the JSON-RPC traffic. The CLI is the
20
+ * license/onboarding wrapper; crosscheck-mcp is the engine.
21
+ */
22
+ export declare function startMcpServer(opts?: {
23
+ enforceProviderPolicy?: boolean;
24
+ }): Promise<void>;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Pure resolution logic for "which model actually gets called for this
3
+ * provider" — no I/O. Two independent things feed it:
4
+ * - a candidate model (explicit env var override > a saved personal
5
+ * preference > nothing), resolved by the caller before this runs
6
+ * - an optional enterprise model policy entry for that provider
7
+ * (crosscheck-enterprise only — a standard install never has one)
8
+ *
9
+ * A model policy is a hard clamp, not a suggestion: if the candidate isn't
10
+ * allowed, this never returns it. Deliberately does NOT know crosscheck-mcp's
11
+ * own DEFAULT_MODELS (duplicating that map here would just reintroduce the
12
+ * exact staleness bug detectStalePins was built to catch) — when nothing
13
+ * safe can be resolved, it returns undefined and the caller leaves the
14
+ * env var unset, deferring to the engine's own built-in default. An admin
15
+ * who wants a *specific* fallback should set `pinned`, or list it first
16
+ * in `allow`.
17
+ */
18
+ export type ModelPolicyEntry = {
19
+ /** "*" = no model-level restriction for this provider (only the existing
20
+ * provider-level allowlist applies). Otherwise the exact list of model
21
+ * ids this org permits for this provider. */
22
+ allow: "*" | readonly string[];
23
+ /** Always wins over `allow`, including over "*". */
24
+ deny: readonly string[];
25
+ /** Admin-chosen fallback when neither the caller's candidate nor a
26
+ * user-set preference is usable. Optional. */
27
+ pinned?: string;
28
+ };
29
+ export type ModelPolicy = Readonly<Record<string, ModelPolicyEntry>>;
30
+ /**
31
+ * Resolve the effective model for one provider. `policy` is this
32
+ * provider's entry only (or undefined — most providers on a standard
33
+ * install have none). Returns undefined when nothing safe to inject was
34
+ * found, meaning: don't set the env var, let the engine use its own
35
+ * default.
36
+ */
37
+ export declare function resolveEffectiveModel(args: {
38
+ candidate?: string;
39
+ policy?: ModelPolicyEntry;
40
+ }): string | undefined;
41
+ /** True when `model` would be rejected by `policy` — used to tell the user
42
+ * WHY their chosen preference didn't take effect, distinct from "no
43
+ * preference was set at all." */
44
+ export declare function isBlockedByPolicy(model: string, policy: ModelPolicyEntry): boolean;