/** * Local OAuth 2.1 Authorization Server as an HTTP request handler. * * Provides a self-hosted Authorization Server for single-user MCP servers. * Implements the OAuth 2.1 PKCE flow with credential collection via a * browser-rendered form. * * Routes: * - GET / -- Auto-bootstrap PKCE then redirect to /authorize * - GET /authorize -- Render credential form * - POST /authorize -- Save credentials, return auth code * - POST /otp -- Submit multi-step credential (OTP / 2FA password) * - POST /token -- Exchange auth code + PKCE verifier for JWT * - GET /setup-status -- Poll background setup completion * - GET /callback-done -- Friendly "tab can be closed" page after PKCE callback * - GET /.well-known/oauth-authorization-server -- RFC 8414 metadata * - GET /.well-known/oauth-protected-resource -- RFC 9728 metadata * * The /mcp endpoint is NOT included -- it is mounted by the transport layer. * * This is a TypeScript port of core-py's ``local_oauth_app.py``. Behavior, * protocol, and TTL constants are kept identical for cross-language parity. */ import { JWTIssuer } from '../oauth/jwt-issuer.js'; import { type RelayConfigField, type RelayConfigSchema } from './credential-form.js'; import { type RequestHandler } from './router.js'; /** Next-step hint returned by credential / step callbacks. */ export type NextStep = Record; /** * Context passed to credential / step callbacks so the consumer can scope * stored credentials by subject (JWT ``sub``). Generated fresh per GET * /authorize and reused for the subsequent POST /authorize + /token exchange, * so the JWT issued after credential save carries the SAME ``sub`` the * consumer used to persist the credentials. This is the primitive that * enables multi-user isolation for `remote-relay` mode: without it consumers * had to fall back to a single shared `config.enc`, leaking credentials * across concurrent browser sessions. */ export interface SubjectContext { /** Per-authorize-request UUID, also becomes the JWT ``sub`` after /token. */ sub: string; } /** * Callback invoked when the user submits credentials via POST /authorize. * * Receives the submitted credential map + the authorize-session ``SubjectContext`` * so the consumer can persist credentials keyed by ``sub``. Return ``null`` to * finish the flow or a ``next_step`` dict to trigger a follow-up (OAuth * device code, OTP, 2FA password, etc). May be sync or async. * * Consumers that don't need multi-user isolation (stdio fallback, local-relay * single-user mode) can ignore the ``context`` parameter. */ export type CredentialsCallback = (creds: Record, context: SubjectContext) => NextStep | null | Promise; /** * Callback invoked when the user submits step input via POST /otp. * * Receives the submitted step data + the authorize-session ``SubjectContext`` * so multi-step flows (OTP, 2FA password) can route the input to the correct * per-user state — e.g. the Telethon client that started the sign-in under * this ``sub``. Without this, consumers would be forced to keep a single * global "currently auth'ing user" and concurrent remote-relay users would * corrupt each other's 2FA flow. * * Return ``null`` to complete the flow, a ``{type: "otp_required" | * "password_required", ...}`` dict to chain to another step, or * ``{type: "error", text: "..."}`` to reject the current input and allow * retry. Callbacks comparing secrets MUST use a timing-safe comparison. * May be sync or async. */ export type StepCallback = (data: Record, context: SubjectContext) => NextStep | null | Promise; export interface LocalOAuthAppOptions { /** Identifier for the MCP server (used for JWT iss / aud). */ serverName: string; /** RelayConfigSchema describing the credential form. */ relaySchema: RelayConfigSchema; /** Optional callback invoked with credentials after POST /authorize. */ onCredentialsSaved?: CredentialsCallback; /** Optional callback invoked with step data after POST /otp. */ onStepSubmitted?: StepCallback; /** Optional pre-created JWT issuer. If omitted, one is created automatically. */ jwtIssuer?: JWTIssuer; /** * Optional renderer used in place of the default credential form on GET * /authorize. Receives the relay schema and an options object with * ``submitUrl`` (which embeds the PKCE nonce) plus an optional ``prefill`` * mapping carrying skret-derived field values from * ``?prefill_=`` query params. Consumers (email, telegram) * use this to inject rich UX while reusing core OAuth plumbing; renderers * may safely ignore ``prefill`` if they don't display matching inputs. */ customCredentialFormHtml?: (schema: RelayConfigSchema, options: { submitUrl: string; prefill?: Record; }) => string; /** * Opt in to a stable, username-derived subject. When set, the credential * form shows a workspace-username field and a submitted username replaces * the random per-authorize subject, so a returning user reaches the same * per-sub bucket. Mirrors core-py's ``stable_sub_enabled``. */ stableSubEnabled?: boolean; } export interface LocalOAuthAppResult { /** HTTP request handler to mount on a Node ``http.Server``. */ handler: RequestHandler; /** JWT issuer, needed by the transport layer to verify Bearer tokens. */ jwtIssuer: JWTIssuer; /** Mark a background setup step as complete (polled by GET /setup-status). */ markSetupComplete: (key?: string) => void; /** * Mark a background setup step as failed (polled by GET /setup-status). * Encodes the status as ``"error:"`` so the browser poll handler * can distinguish success, failure, and still-pending states without * spinning forever on upstream errors (e.g. Google returning * ``invalid_grant`` / ``expired_token`` / ``access_denied``). */ markSetupFailed: (key?: string, error?: string) => void; } /** * Create OAuth 2.1 Authorization Server HTTP handler. * * Returns a handler compatible with ``http.createServer`` along with the * ``JWTIssuer`` (for the transport layer to verify Bearer tokens) and a * ``markSetupComplete`` function for background setup callbacks (e.g. GDrive * device code flow). */ export declare function createLocalOAuthApp(options: LocalOAuthAppOptions): Promise; /** * Render an HTML ``