import type { HexisMcpConfig } from './config.js'; import { type AgentIdentity } from './handshake.js'; /** * Browser sign-in (OAuth) for a person running this server without a * connection key — the default when no `--key`/`HEXIS_CONNECTION_KEY` is set. * * The shape is the standard one for a native app (RFC 8252) against an * MCP-style deployment: * * 1. DISCOVER — poke the deployment's MCP endpoint unauthenticated; its 401 * names the protected-resource metadata (RFC 9728), which names the * authorization server, whose own metadata names the three endpoints. * 2. REFRESH — a prior sign-in left `{clientId, refreshToken}` on disk; a * refresh grant skips the browser entirely. * 3. BROWSER — first run (or a dead refresh token): bind a loopback * redirect, register a client for exactly that redirect (RFC 7591), * send the person's browser to the authorization endpoint with PKCE, * and trade the returned code for tokens. * 4. EXCHANGE — the OAuth access token is traded at the deployment's * `/api/mcp/local-token` for an INTERNAL token, which is what the rest * of this process uses as its bearer. * * The internal token lands in `HexisMcpConfig.connectionKey` — the same field * a connection key would fill, because it means the same thing: the one * credential THIS process attaches to the deployment. Vault separation is * unchanged by any of this: signing in only replaces the key as this * process's identity; remote tools still execute on the server, resolving * their secrets there, and nothing new becomes readable locally. */ /** Same contract as DeploymentError: a message the person can act on, printed without a stack. */ export declare class OAuthError extends Error { } export interface AuthServerEndpoints { authorizationEndpoint: string; tokenEndpoint: string; registrationEndpoint: string; /** RFC 7009, when the server offers it: how a sign-in this process no longer needs is ended. */ revocationEndpoint?: string; } /** * From the MCP endpoint to the three OAuth endpoints, by the MCP-standard * chain: an unauthenticated POST earns a 401 whose `WWW-Authenticate` names * the protected-resource metadata (RFC 9728); that names the authorization * server; the server's `/.well-known/oauth-authorization-server` (RFC 8414) * names the endpoints. Every link is validated loudly — a deployment that * cannot complete the chain gets told what is missing and that a connection * key still works. */ export declare function discoverAuthServer(mcpUrl: string): Promise; export interface StoredOAuthCredentials { clientId: string; refreshToken: string; } /** * `~/.hexis/oauth/--.json` — per deployment (the same keying * the plugin trees use) AND per agent: a sign-in IS the agent's, registered * under its name and revocable on its own, so Claude Code and Cursor on one * machine each keep their own. With no agent known, the plain `` * file the server used before it learned who was running it. */ export declare function oauthStorePath(baseUrl: string, agent?: AgentIdentity | null): string; /** * A corrupt, unreadable or wrong-shaped store is an ABSENT one: the browser * flow rebuilds it, and nothing a person can do to this file should wedge the * server behind an unreadable parse error. */ export declare function readStoredCredentials(baseUrl: string, agent?: AgentIdentity | null): Promise; /** * Owner-only, the same posture materialize.ts documents for the plugin trees: * the refresh token is a durable credential for the person's workspace access * and belongs to no other local user. `writeFile`'s mode only applies on * creation, so the chmod re-asserts it for a pre-existing file — with the * same Windows pass the rest of the package gives chmod. */ export declare function writeStoredCredentials(baseUrl: string, credentials: StoredOAuthCredentials, agent?: AgentIdentity | null): Promise; interface TokenResponse { accessToken: string; refreshToken?: string; } /** * One refresh grant. `null` — not a throw — on a 400/401: an expired or * revoked refresh token is the EXPECTED way a stored sign-in dies, and the * caller's answer is the browser, not an error. Anything else unexpected * still throws, because a 500 from the token endpoint is not a reason to * bother a human with a browser tab. */ export declare function refreshAccessToken(tokenEndpoint: string, clientId: string, refreshToken: string): Promise; /** * Hand a URL to the system browser without ever interpolating it into a shell * string: argument arrays only. On Windows `start` is a cmd builtin, so cmd is * unavoidable — the URL rides as ONE quoted verbatim argument (quotes cannot * occur in it: everything we build is percent-encoded, and any stray `"` is * re-encoded below), which keeps its `&`s from becoming cmd operators. */ export declare function openInBrowser(url: string): void; export interface BrowserFlowOptions { /** Injectable for tests and for `--no-open`; the default opens the system browser. */ openBrowser?: (url: string) => void; /** Where the sign-in URL is ALWAYS written, browser or not. Defaults to stderr. */ print?: (line: string) => void; timeoutMs?: number; /** What the deployment registers this client as — the name its Audit log shows. */ clientName?: string; /** What the callback page tells the person to return to ("Claude Code"); defaults to their terminal. */ returnTo?: string; } export interface BrowserFlowResult { clientId: string; accessToken: string; refreshToken?: string; } /** * The full first-run flow: loopback redirect, dynamic client registration, * PKCE authorization, code exchange. * * Order matters at the start: the loopback is bound FIRST, because dynamic * registration (RFC 7591) must name exactly the redirect_uri that will answer * — a port picked after registering would register a promise nobody keeps. * The `state` check is what stops a stray or hostile request to the loopback * from completing someone else's sign-in; a mismatch refuses the whole flow. */ export declare function authorizeInBrowser(endpoints: AuthServerEndpoints, options?: BrowserFlowOptions): Promise; export interface LocalTokenGrant { token: string; expiresInMs?: number; } /** * Trade the OAuth access token for the deployment's INTERNAL token at * `/api/mcp/local-token`. The 404 is handled by name because it is precisely * how a deployment that predates this endpoint answers — and "too old" is * actionable where "not found" is baffling. */ export declare function exchangeForLocalToken(baseUrl: string, accessToken: string): Promise; export interface OAuthModeOptions { /** Print the sign-in URL only; open nothing (`--no-open` / `HEXIS_NO_BROWSER=1`). */ noOpen?: boolean; /** Injectable for tests; ignored when `noOpen` is set. */ openBrowser?: (url: string) => void; print?: (line: string) => void; browserTimeoutMs?: number; /** * The agent running this server, from the MCP handshake (see handshake.ts). * It decides which stored sign-in is this one, what the sign-in registers * as, and what the callback page says to return to. Null (or absent) is a * client that named itself to nobody: the plain per-machine sign-in. */ agent?: AgentIdentity | null; } /** * The whole OAuth mode, as one call: discover, sign in (refresh or browser), * exchange — and hand back a `HexisMcpConfig` the rest of the process uses * exactly as it would a key-mode one. * * The internal token fills `connectionKey`: same field, same meaning — the * one bearer this process attaches. Vault separation is unchanged: the OAuth * identity only replaces the key as THIS process's credential; remote tools * still execute on the deployment against its Secrets Vault, and nothing new * becomes readable locally. * * `renewConnectionKey` is the answer to an expiring internal token: refresh → * re-exchange, no browser. It is only ever run through `renewal.ts`'s * single-flight (deployment.ts consults that on a mid-run 401; the proactive * timer armed below consults it before expiry); if the refresh itself is dead * mid-run, the error tells the person to restart and sign in again rather * than popping a browser out from under a running MCP session. */ export declare function establishOAuthConfig(baseUrl: string, mcpUrl: string, options?: OAuthModeOptions): Promise; export {}; //# sourceMappingURL=oauth.d.ts.map