/** * RFC 7636 PKCE (Proof Key for Code Exchange) OAuth flow implementation. * * Provides the full PKCE lifecycle: * 1. Generate a cryptographically-random code verifier + SHA-256 challenge. * 2. Build the authorization URL the user (or CLI) opens in a browser. * 3. Exchange the authorization code + verifier for tokens. * 4. Refresh an existing access token via the `refresh_token` grant. * * All HTTP calls use `globalThis.fetch` so they can be intercepted in tests * via `vi.spyOn(globalThis, 'fetch')`. * * Shared types (`OAuthTokens`, `PkceFlowConfig`) live in * `@cleocode/contracts/llm/oauth.ts` — the SSoT between core and cleo. * * @module llm/oauth/pkce * @task T9302 * @epic T9261 T-LLM-CRED-CENTRALIZATION * @see RFC 7636 https://datatracker.ietf.org/doc/html/rfc7636 */ import type { OAuthTokens } from '@cleocode/contracts/llm/oauth.js'; export type { OAuthTokens } from '@cleocode/contracts/llm/oauth.js'; /** * A PKCE code-verifier / code-challenge pair. * * The verifier is kept secret and sent only to the token endpoint. * The challenge is derived from the verifier and sent to the authorization * endpoint in plain text. An attacker who intercepts the challenge cannot * reverse it to recover the verifier. * * @task T9302 */ export interface PkcePair { /** * Cryptographically-random base64url-encoded string (43–128 chars, RFC 7636 §4.1). * * MUST be sent to the token endpoint as `code_verifier` during code exchange. * MUST NOT be logged, printed to stdout, or stored in plaintext. */ codeVerifier: string; /** * BASE64URL(SHA256(ASCII(code_verifier))) per RFC 7636 §4.2. * * Sent to the authorization endpoint as `code_challenge` with * `code_challenge_method=S256`. */ codeChallenge: string; } /** * Generate a RFC 7636 PKCE code-verifier / code-challenge pair. * * Uses the Web Crypto API (`crypto.getRandomValues` + `crypto.subtle.digest`) * which is available in Node.js ≥ 19, browsers, and Deno. For Node.js < 19 * the global `crypto` is polyfilled by `globalThis.crypto`. * * Algorithm (RFC 7636 §4.1 + §4.2): * 1. Generate 32 random bytes. * 2. base64url-encode them → `codeVerifier` (43 chars from 32 bytes). * 3. SHA-256 hash the ASCII bytes of `codeVerifier`. * 4. base64url-encode the hash → `codeChallenge`. * * @returns A fresh PKCE pair. Each call returns a unique pair. * @task T9302 */ export declare function generatePkcePair(): Promise; /** * Parameters for building the OAuth authorization URL. * * @task T9302 */ export interface BuildAuthorizationUrlParams { /** OAuth authorization endpoint. */ authorizationEndpoint: string; /** OAuth 2.0 client ID. */ clientId: string; /** Redirect URI where the provider sends the authorization response. */ redirectUri: string; /** Space-separated OAuth scopes. */ scope: string; /** PKCE code challenge (from {@link generatePkcePair}). */ codeChallenge: string; /** Opaque CSRF-prevention state value. Must be round-tripped through the redirect. */ state: string; /** * Provider-specific extra query parameters appended after the RFC-required * ones (e.g. OpenAI/Codex `id_token_add_organizations`, * `codex_cli_simplified_flow`, `originator`). Omitted by Anthropic. */ extraParams?: Readonly>; } /** * Build the authorization URL the user opens to grant consent. * * Encodes `response_type=code`, `code_challenge_method=S256`, and all * required parameters per RFC 7636 §4.3. * * @param params - URL components. * @returns Fully-formed authorization URL string. * @task T9302 */ export declare function buildAuthorizationUrl(params: BuildAuthorizationUrlParams): string; /** * Parse a user-pasted authorization response into `{ code, state }`. * * Accepts every form a provider's callback page can hand the user * (mirrors pi-ai's `parseAuthorizationInput`): * - a full redirect URL — `https://…/callback?code=…&state=…` * - the `code#state` pair Anthropic's hosted callback page displays * - a bare query string — `code=…&state=…` * - a bare authorization code * * Callers MUST validate a returned `state` against the authorize-time state * (CSRF check, RFC 6749 §10.12) before exchanging the code. * * @param input - Raw pasted text from the user. * @returns Extracted `code` and `state` (either may be `undefined`). * @task T11958 */ export declare function parseAuthorizationInput(input: string): { code?: string; state?: string; }; /** * Parameters for the PKCE authorization code exchange. * * @task T9302 */ export interface ExchangePkceCodeParams { /** Provider name (used in error messages only). */ provider: string; /** OAuth 2.0 client ID. */ clientId: string; /** Authorization code returned in the redirect callback. */ code: string; /** PKCE code verifier — the secret counterpart to `codeChallenge`. */ codeVerifier: string; /** Redirect URI used in the original authorization request. */ redirectUri: string; /** Token endpoint URL. */ tokenEndpoint: string; /** Extra headers (e.g. provider-specific beta flags). */ extraHeaders?: Readonly>; /** * The `state` value from the original authorization request. * * Only sent when `bodyFormat` is `'json'` — Anthropic's token endpoint * validates the (code, state) pair bound at authorize time and rejects the * exchange without it. RFC-compliant form-encoded endpoints do not define * `state` on the token request, so it is omitted there. */ state?: string; /** * Token request body encoding. `'form'` (RFC 6749 default) or `'json'` * (Anthropic). See `ProviderOAuthConfig.tokenBodyFormat`. * * @default 'form' */ bodyFormat?: 'form' | 'json'; } /** * Exchange an authorization code + PKCE verifier for access and refresh tokens. * * POSTs `grant_type=authorization_code` to `params.tokenEndpoint` per * RFC 6749 §4.1.3 + RFC 7636 §4.5. * * @param params - Exchange parameters. * @returns Normalized token response. * @throws {Error} On HTTP errors or missing `access_token` in the response. * @task T9302 */ export declare function exchangePkceCode(params: ExchangePkceCodeParams): Promise; /** * Parameters for the PKCE token refresh grant. * * @task T9302 */ export interface RefreshPkceTokenParams { /** Provider name (used in error messages only). */ provider: string; /** OAuth 2.0 client ID. */ clientId: string; /** Refresh token obtained from a previous code exchange or refresh. */ refreshToken: string; /** Token endpoint URL. */ tokenEndpoint: string; /** Extra headers (e.g. provider-specific beta flags). */ extraHeaders?: Readonly>; /** * Token request body encoding. `'form'` (RFC 6749 default) or `'json'` * (Anthropic). See `ProviderOAuthConfig.tokenBodyFormat`. * * @default 'form' */ bodyFormat?: 'form' | 'json'; } /** * Refresh an access token using the `refresh_token` grant (RFC 6749 §6). * * Uses the same token endpoint as code exchange. On success, a new access * token (and optionally a new refresh token) is returned. The caller is * responsible for persisting the updated tokens. * * @param params - Refresh parameters. * @returns Normalized token response with fresh `accessToken`. * @throws {Error} On HTTP errors or missing `access_token` in the response. * @task T9302 */ export declare function refreshPkceToken(params: RefreshPkceTokenParams): Promise; /** * Extract a human-readable error detail string from a non-OK OAuth HTTP * response. * * The single shared extractor for every OAuth surface in the LLM layer * (`pkce.ts`, `google-pkce.ts`, `device-code.ts`) — local copies drifted and * re-introduced the `[object Object]` masking of DHQ-075 (T11958). * * Strategy: * 1. Clone the response so the body stream is not consumed by the primary * error path (callers may need the body for further inspection). * 2. Parse as JSON; use `error_description` or `error` fields (RFC 6749 §5.2). * When the field is an object (Anthropic nests * `{"error": {"type", "message"}}`), extract `message`/`type` — never * `String(object)`. * 3. Fall back to the raw text body when JSON parsing fails (e.g. HTML pages * returned by a WAF or a misconfigured reverse proxy). Truncate at 512 chars * so a multi-kilobyte HTML page does not flood the error message. * 4. Return `''` only when all fallbacks are exhausted. * * This ensures the caller never sees `[object Object]` in the thrown message * and always surfaces the actual HTTP response body for debugging. * * @task T11958 */ export declare function extractOAuthErrorDetail(resp: Response): Promise; //# sourceMappingURL=pkce.d.ts.map