/**
 * OAuth 2.0 Authorization Server store
 *
 * Studiograph's `/mcp` server is an OAuth resource server AND authorization
 * server: Claude Desktop's Chat/Cowork connector pipeline is OAuth-only, so a
 * client registers (DCR), runs an authorization-code + PKCE flow, and exchanges
 * the code for a short-lived access token (a JWT accepted on `/mcp`) plus a
 * revocable refresh token.
 *
 * This class owns the three authorization-server tables in `auth.db` and all
 * code/token minting. It is deliberately separate from AuthService so the
 * internet-facing AS surface can be audited and unit-tested on its own. It does
 * NOT resolve users or enforce access control itself — it reuses the shared
 * identity primitives via the injected `resolveUser` callback and never
 * declares a local access helper (a past leak came from a route duplicating the
 * shared access check).
 *
 *   register ──▶ oauth_clients ──┐
 *   authorize ─▶ oauth_auth_codes │ (single-use, ≤60s)
 *   token ─────▶ consume code, mint access JWT (aud=/mcp) + refresh token
 *   refresh ───▶ rotate (atomic consume-old + insert-new)
 *   gc ────────▶ drop expired codes/refresh + orphan clients
 *
 * Token format:
 *   - access:  JWT signed with the workspace jwtSecret, ~1h, claims
 *              { userId, email, role, oauth:true, cid:<client_id>, aud:<resource> }.
 *              Accepted by the existing verifyToken path; the auth hook scopes
 *              it to `/mcp` + `/oauth/*` via the `oauth`/`aud` claims.
 *   - refresh: opaque `sgr_…`, only its sha256 hash is stored, revocable,
 *              with an absolute `expires_at`.
 */
import type Database from 'better-sqlite3';
import type { AuthUser } from './auth-service.js';
export interface OAuthClient {
    client_id: string;
    client_name: string;
    redirect_uris: string[];
    created_at: string;
}
export interface AuthCodeInput {
    client_id: string;
    user_id: number;
    redirect_uri: string;
    code_challenge: string;
    scope: string;
    resource: string;
}
export interface ConsumedCode {
    client_id: string;
    user_id: number;
    redirect_uri: string;
    code_challenge: string;
    scope: string;
    resource: string;
}
export interface IssuedTokens {
    access_token: string;
    refresh_token: string;
    token_type: 'Bearer';
    expires_in: number;
    scope: string;
}
export interface OAuthGrant {
    client_id: string;
    client_name: string;
    scope: string;
    created_at: string;
    last_used_at: string | null;
}
/**
 * Validate a redirect URI presented at registration. Public OAuth clients are
 * the riskiest open-redirect surface, so we reject anything that isn't a clean,
 * absolute, fragment-free URL: HTTPS only, except http loopback for local dev.
 */
export declare function isValidRedirectUri(uri: string, opts?: {
    allowLoopback?: boolean;
}): boolean;
export declare class OAuthServerStore {
    private db;
    private jwtSecret;
    private resolveUser;
    private consentSigner;
    constructor(opts: {
        db: Database.Database;
        jwtSecret: string;
        resolveUser: (userId: number) => AuthUser | null;
    });
    private initSchema;
    signConsent(payload: Record<string, unknown>): string;
    verifyConsent(token: string): Record<string, unknown> | null;
    registerClient(input: {
        client_name: string;
        redirect_uris: string[];
    }, opts?: {
        allowLoopback?: boolean;
    }): OAuthClient;
    getClient(clientId: string): OAuthClient | null;
    /** Exact-match redirect_uri against the client's registered set (RFC 8252). */
    clientAllowsRedirect(clientId: string, redirectUri: string): boolean;
    saveCode(input: AuthCodeInput): string;
    /**
     * Single-use exchange: atomically read the row and delete it. Returns null if
     * the code is unknown, already consumed, or expired. The delete-inside-the-
     * transaction is what makes a replayed/parallel exchange fail.
     */
    consumeCode(rawCode: string): ConsumedCode | null;
    static verifyPkce(codeChallenge: string, codeVerifier: string): boolean;
    private insertRefresh;
    createRefreshToken(userId: number, clientId: string, scope: string): string;
    /** Resolve a raw refresh token to its grant, bumping last_used_at. */
    verifyRefreshToken(rawToken: string): {
        user: AuthUser;
        clientId: string;
        scope: string;
    } | null;
    /**
     * Rotate: atomically consume the presented refresh token and issue a new one.
     * Returns the new raw refresh token + grant, or null if the presented token
     * is unknown/expired (replay of an already-rotated token therefore fails).
     *
     * `expectedClientId` is validated INSIDE the transaction, BEFORE the row is
     * deleted: a token presented with the wrong client_id is rejected without
     * consuming the victim's still-valid grant (no griefing-by-invalidation).
     */
    rotateRefreshToken(rawToken: string, expectedClientId?: string): {
        refreshToken: string;
        user: AuthUser;
        clientId: string;
        scope: string;
    } | null;
    revokeRefreshToken(rawToken: string): boolean;
    /** Grants for a user — backs the deferred Settings "connected apps" view. */
    listGrantsForUser(userId: number): OAuthGrant[];
    /**
     * Mint a ~1h access JWT. `cid` is the stable client_id (not the mutable
     * display name); `aud` binds the token to the canonical `/mcp` resource so
     * the auth hook can refuse it on non-MCP routes.
     */
    createAccessToken(user: AuthUser, clientId: string, resource: string): string;
    /** Issue the full token set after a successful code exchange. */
    issueTokens(user: AuthUser, clientId: string, scope: string, resource: string): IssuedTokens;
    gcStale(): void;
}
export declare class OAuthRegistrationError extends Error {
    code: string;
    constructor(code: string, message: string);
}
