/** * Browser-based OAuth2 PKCE login for Ory agent plugins. * * Spins up a transient localhost callback server on a fixed-port range * (option (a) — operators register the same loopback URIs once with the * Ory OAuth2 client), generates a PKCE verifier/challenge, launches the * user's browser at the project's authorize URL, and exchanges the * resulting code for a token set via /oauth2/token. * * This module never writes to stdout. Browser launching uses the `open` * package via dynamic import so the CJS-emitted core stays compatible * with its ESM-only export shape. */ import { OryOAuth2Tokens } from "./config.js"; /** Loopback ports tried in order. All must be registered as redirect URIs on the Ory OAuth2 client. */ export declare const LOOPBACK_PORTS: readonly [47823, 47824, 47825, 47826]; /** * Hard timeout if the user never returns to the browser. * * Sized to fit **inside** the tightest session-start hook window any harness * will accept, so the login always gives up on its own terms and records a * `timeout` decline the user can act on. Being SIGKILLed by the harness * instead surfaces nothing at all: the session proceeds with no user identity, * and under `enforce` every subsequent tool is denied for a reason nothing * explains (#220). * * The binding constraint is Antigravity, which validates its hook timeout to * `5..120` seconds — so 120s is the ceiling, and this leaves headroom for the * agent DCR registration and delegation record that run after the login inside * the same hook invocation. See `hook-timeout.ts`, which derives every * harness's declared window and asserts it outlasts this value. */ export declare const DEFAULT_LOGIN_TIMEOUT_MS = 90000; export interface PkceLoginOptions { /** Ory project URL (e.g. https://your-project.projects.oryapis.com). */ projectUrl: string; /** Public OAuth2 client id registered with the loopback redirect URIs. */ clientId: string; /** Scopes to request. Defaults to `openid offline_access profile email` * — `profile` / `email` populate the id_token's name/username/email * claims so the signed-in user can be shown by name, not just subject id. */ scope?: string; /** Audience to request, if any. */ audience?: string; /** Login timeout in ms. Defaults to {@link DEFAULT_LOGIN_TIMEOUT_MS}. */ timeoutMs?: number; /** Override the loopback port list (mostly for testing). */ ports?: ReadonlyArray; /** Hook for opening the browser. Defaults to the `open` npm package. */ openBrowser?: (url: string) => Promise; /** Override headless detection (mostly for testing). */ isHeadless?: () => boolean; /** Optional AbortSignal to cancel the login (e.g. on shutdown). */ signal?: AbortSignal; /** @internal Injectable seam for testing default-fetch TLS setup. */ configureSystemCaTrust?: () => void; } export type PkceLoginOutcome = { kind: "ok"; tokens: OryOAuth2Tokens; } | { kind: "declined"; reason: PkceDeclineReason; /** * Concise, secret-free diagnostic for the failure, surfaced in the * `user.auth` audit event and the operator message. Populated for * `token_exchange_failed` (the underlying HTTP status / OAuth2 error, * or a fetch cause code such as `UNABLE_TO_GET_ISSUER_CERT_LOCALLY` / * `ECONNREFUSED`). Never contains the authorization code or a token. */ detail?: string; }; export type PkceDeclineReason = "headless" | "timeout" | "user_denied" | "state_mismatch" | "no_port" | "browser_launch_failed" | "aborted" | "token_exchange_failed" /** The authorization server rejected the requested scope (`invalid_scope`). */ | "invalid_scope" /** The authorization server returned some other `error` on the redirect. */ | "oauth_error"; /** * Default headless heuristic. Returns true only for unattended runs * (CI=true / CI=1) so {@link pkceLogin} can short-circuit instead of * waiting on a callback that will never arrive. * * Heuristics like `SSH_TTY` and "Linux without DISPLAY" used to live here * but turned out to be wrong in practice: an SSH user can paste the * printed authorize URL into their workstation browser, and a Linux box * without a display still works as long as the operator can reach the * loopback callback port (e.g. via `ssh -L`). The PKCE flow always * prints the URL to stderr, so anything short of "no human at all" is * recoverable. */ export declare function detectHeadless(env?: NodeJS.ProcessEnv): boolean; /** * Run a PKCE login and return either a token set or the reason the flow * could not complete. The function never throws on user-driven failures * (timeout, decline, headless); it throws only on programmer error or * unrecoverable I/O failures. */ export declare function pkceLogin(options: PkceLoginOptions): Promise; /** Generate a 64-byte (≈86-char base64url) code verifier per RFC 7636. */ export declare function generateCodeVerifier(): string; /** SHA-256 + base64url, the only supported PKCE challenge method (`S256`). */ export declare function sha256Base64Url(input: string): string; export declare function buildAuthorizeUrl(params: { projectUrl: string; clientId: string; redirectUri: string; state: string; codeChallenge: string; scope: string; audience?: string; }): string; /** * Refresh an access token using a refresh token. Throws on any non-2xx * response so callers can decide whether to fall back to a fresh login. * * Kept as raw fetch for parity with the other two /oauth2/token flows; * see `exchangeCodeForTokens` and `fetchClientCredentialsToken` for why * @ory/client's OAuth2Api.oauth2TokenExchange isn't used. */ export declare function refreshAccessToken(args: { projectUrl: string; clientId: string; refreshToken: string; /** @internal Injectable seam for testing default-fetch TLS setup. */ configureSystemCaTrust?: () => void; }): Promise; /** * Best-effort human-readable name for the signed-in user, pulled from the * id_token's identity claims — `email`, then `name`, then `preferred_username`. * Returns undefined when the token carries none of them (e.g. an `openid`-only * token), so callers fall back to the opaque subject id. */ export declare function displayNameFromIdToken(idToken: string | undefined): string | undefined;