import type { IncomingMessage, ServerResponse } from 'node:http'; export interface OAuthProviderOptions { hostname?: string; /** Optional callback to validate an API token before issuing an authorization code. */ validateToken?: (token: string) => Promise; /** * Hostnames whose https callbacks may receive an authorization code. * Exact, case-insensitive match — no subdomain wildcards. Loopback and * private-use schemes are always allowed and need no entry here. * A single '*' disables curation entirely (dangerous; see README). * Defaults to DEFAULT_TRUSTED_REDIRECT_HOSTS. */ trustedRedirectHosts?: string[]; /** * Lifetime of an issued access token, in ms. Defaults to one hour. * Shorter narrows the window after a SeaTable token is revoked; longer * spares users a re-prompt if their client renews badly. */ accessTokenTtlMs?: number; /** Resolves the client IP for audit logging. Falls back to 'unknown'. */ getClientIp?: (req: IncomingMessage) => string; /** * Distinguishes the two SeaTable token types. Called only after validation * failed, to turn "Invalid API token" into a message that names the mistake. */ looksLikeAccountToken?: (token: string) => Promise; /** * Secret used to seal access tokens, refresh tokens and client registrations. * Must be stable across restarts, otherwise every client is forced to re-authorize. */ secret?: string; } /** * Hosted MCP clients whose callbacks live on their own servers. These are the * only destinations that receive an authorization code over the network, so * they are curated rather than open. Operators extend or replace this via * SEATABLE_OAUTH_TRUSTED_REDIRECT_HOSTS. */ export declare const DEFAULT_TRUSTED_REDIRECT_HOSTS: string[]; /** The protected resource clients authorize against — the Streamable HTTP endpoint. */ export declare const MCP_RESOURCE_PATH = "/mcp"; /** RFC 9728 well-known prefix. The resource path is appended to it. */ export declare const PROTECTED_RESOURCE_PATH = "/.well-known/oauth-protected-resource"; export declare class OAuthProvider { private readonly codes; private readonly cleanupInterval; private readonly configuredHostname?; private readonly validateToken?; private readonly cipher; private readonly trustedRedirectHosts; private readonly getClientIp?; private readonly accessTokenTtlMs; private readonly refreshTokenTtlMs; private readonly looksLikeAccountToken?; constructor(options?: OAuthProviderOptions); private clientIp; /** * May this callback be used at all? * * Rejects only destinations that can never be safe: schemes that execute or * read local content, and plaintext http to a remote host. An unknown https * host is permitted — it meets the acknowledgement instead (see below). */ isPermittedRedirectUri(raw: string): boolean; /** * May this callback receive an authorization code without further friction? * * The question is not whether we recognise the client — with open dynamic * registration an attacker registers honestly — but whether the code would * leave the user's machine: * * loopback stays on the user's own machine * private-use scheme goes to a locally installed app * https + curated host a known hosted client (ChatGPT, Claude, ...) * * Everything else is still allowed, but only after the user has explicitly * acknowledged where their token is about to be sent. Curation therefore * removes friction; it is not a gate, and an empty list breaks nothing. */ isTrustedRedirectUri(raw: string): boolean; /** * Resolves one of our own access tokens back to the SeaTable API token it seals. * Returns undefined for anything we did not issue, or that has expired. */ resolveAccessToken(accessToken: string): string | undefined; /** * Derive the base URL from SEATABLE_MCP_HOSTNAME or the incoming Host header. */ private resolveBaseUrl; /** * GET /.well-known/oauth-authorization-server — RFC 8414 metadata */ handleMetadata(req: IncomingMessage, res: ServerResponse): void; /** * The resource identifier clients authorize against: the MCP endpoint itself. */ private resourceIdentifier; /** * Where the protected resource metadata for that identifier lives. * * RFC 9728 section 3.1: the resource's path is appended to the well-known * suffix, so `https://host/mcp` is described at * `https://host/.well-known/oauth-protected-resource/mcp`. */ resourceMetadataUrl(req: IncomingMessage): string; /** * GET /.well-known/oauth-protected-resource[/mcp] — RFC 9728 metadata * * `authorization_servers` MUST agree with the issuer that handleMetadata() * reports. A conformant client follows this document INSTEAD of probing * /.well-known/oauth-authorization-server, so a disagreement here breaks * clients that work today. Both derive from resolveBaseUrl() for that reason. */ handleProtectedResourceMetadata(req: IncomingMessage, res: ServerResponse): void; /** * The WWW-Authenticate challenge for a 401, pointing at the metadata above. * * RFC 6750 section 3.1: a request that carried no credential at all gets no * error code — only one that presented a credential we rejected does. */ challenge(req: IncomingMessage, error?: 'invalid_token'): string; /** * POST /register — Dynamic Client Registration (RFC 7591) * * The returned client_id is a sealed envelope containing the client's name and * its redirect_uris. That makes the registration tamper-proof and verifiable at * /authorize without any server-side storage: a client_id we did not issue * cannot be opened, and the callback list inside it cannot be edited. */ handleRegister(req: IncomingMessage, res: ServerResponse): Promise; /** * GET /authorize — renders the authorization form * POST /authorize — processes the form submission * * Both paths validate the request the same way, and neither shows the token * prompt until the client, the callback and the PKCE challenge all check out. */ handleAuthorize(req: IncomingMessage, res: ServerResponse, url: URL): Promise; /** * POST /token — exchanges an authorization code (or refresh token) for an access token. * * The issued access token is a sealed envelope around the SeaTable API token, * never the API token itself. */ handleToken(req: IncomingMessage, res: ServerResponse): Promise; destroy(): void; private handleRefreshGrant; private issueTokens; private tokenError; private registrationError; private parseRedirectUris; private cleanup; private parseFormBody; private parseBody; private readBody; /** An error page that deliberately does not render the API token prompt. */ private authorizeError; private renderAcknowledgementPage; private renderAuthorizePage; private hiddenFlowFields; /** Shared chrome for every page this provider renders. */ private page; private escapeHtml; } //# sourceMappingURL=oauthProvider.d.ts.map