import { type Credential } from "./store.js"; /** * Enrolling an installation, without a human ever handling the secret. * * `GOAL.md` sets the requirement and it is stricter than "do not paste a key * into a file": the credential must travel from the service to the * installation without passing through the founder's hands *or the agent's * context*, because a secret in an agent's context window is a secret in a * transcript, and transcripts are stored, replayed and pasted into issues. * * That rules out every design where the founder copies a key from a dashboard, * and every design where the agent is handed one to write down. What is left is * the device authorization grant — RFC 8628, the flow a television uses to sign * in to a streaming service — and it fits exactly: * * 1. The installation asks the service to begin, and is given two codes. The * `deviceCode` is the secret half and never leaves this process. The * `userCode` is short, human, and worthless on its own. * 2. The agent shows the founder a URL and the short code. That is the only * thing that crosses into the transcript, and it authorizes nothing by * itself. * 3. The founder approves, once, in their own browser, signed in as * themselves. * 4. The installation polls with the secret half and receives the token, * which it writes to disk and never returns to its caller. * * The founder never sees a credential. The agent never holds one. What each of * them handles is useless to an attacker who intercepts it. */ /** Where the service lives, overridable so a stub — or a staging deployment — can be pointed at. */ export declare const SERVICE_ENV = "CROSSLINE_SERVICE_URL"; export declare const DEFAULT_SERVICE = "https://api.crossline.dev"; export interface EnrollStart { /** Secret. Never returned to an agent, never logged, never written down. */ deviceCode: string; /** Short, human, and authorizes nothing on its own. Safe to show. */ userCode: string; verificationUri: string; /** The same URL with the code already in it, when the service offers one. */ verificationUriComplete?: string; expiresIn: number; interval: number; } export type EnrollOutcome = { ok: true; credential: Credential; path: string; } | { ok: false; reason: string; retryable: boolean; }; export interface EnrollDeps { fetch?: typeof globalThis.fetch; env?: NodeJS.ProcessEnv; /** Injected so tests do not spend real seconds waiting. */ sleep?: (ms: number) => Promise; now?: () => number; } /** * The service base URL, refusing plaintext anywhere it could be intercepted. * * A token that arrives over http:// on a real network is a token an attacker * already has. Loopback is exempt because that is a stub or a developer's own * machine, and there is no network to intercept. */ export declare function serviceUrl(env?: NodeJS.ProcessEnv): string; /** Ask the service to begin an enrollment. */ export declare function startEnrollment(info: { project?: string; version: string; }, deps?: EnrollDeps): Promise; export type PollStep = { state: "approved"; credential: Credential; path: string; } | { state: "pending"; } | { state: "slow_down"; } | { state: "denied"; reason: string; } | { state: "expired"; reason: string; } | { state: "failed"; reason: string; } /** The request itself did not complete. Not an answer, so not a refusal. */ | { state: "unreachable"; }; /** * One exchange with the service, and the only place a token is ever stored. * * Split out from the loop because the agent-facing surface cannot loop: a tool * call that blocks for the fifteen minutes a human might take to click approve * is not something an agent can drive, so it asks once and is told to come * back. Both callers go through here, so the blocking and the non-blocking * paths cannot disagree about what `slow_down` means or about when a token * gets written. */ export declare function pollOnce(deviceCode: string, deps?: EnrollDeps): Promise; /** * Poll until the founder approves, then store the token. * * The token is written and never returned in a form a caller could print. The * `Credential` handed back carries it because the writer needs it, and every * caller in this codebase is expected to report `path` rather than contents — * `describeEnrollment` is the only thing that should render this. */ export declare function completeEnrollment(start: EnrollStart, deps?: EnrollDeps): Promise; /** * What to say to the founder, given only what is safe to say. * * Everything here crosses into an agent transcript, so everything here has to * be useless to whoever reads that transcript later. The short code is; the * device code would not be, and is not present. */ export declare function describeEnrollment(start: EnrollStart): string;