export interface AuthUser { id: string; email?: string; /** * Whether the provider that returned `email` vouched for it. * * OIDC Core §5.7 is explicit that `email` is not a verified identifier unless * `email_verified` is true, and an address nobody verified is an address * anybody can claim. Account identity in this package is keyed on `id` * (`google:${sub}`, `github:${id}`), so a session is safe without reading * this — but an application that *links* an OAuth login to an existing * account by address, or authorizes on the domain part, is deciding who * somebody is from this field and must check it. * * Absent means the provider said nothing, which is not the same as `false` * and is never a reason to treat the address as verified. Present without an * `email` is not produced: there is no claim to make about an address that * was not returned. */ emailVerified?: boolean; name?: string; image?: string; roles?: readonly string[]; claims?: Readonly>; } export interface AuthSession { id: string; user: AuthUser; createdAt: string; expiresAt: string; remember: boolean; } /** Durable stores must implement atomic `take` to prevent token replay. */ export interface AuthStore { readonly name: string; readonly durable: boolean; get(key: string): Promise; set(key: string, value: string, ttlSeconds: number): Promise; delete(key: string): Promise; take(key: string): Promise; } export interface RateLimitDecision { allowed: boolean; remaining: number; retryAfterSeconds: number; } /** Production implementations must increment and expire a key atomically. */ export interface AuthRateLimitStore { readonly name: string; readonly durable: boolean; consume(key: string, limit: number, windowSeconds: number): Promise; } export interface CredentialsProvider { type: 'credentials'; authorize(input: Record, request: Request): Promise; } export interface OAuthTokenSet { accessToken: string; tokenType?: string; scope?: string; expiresIn?: number; refreshToken?: string; raw: Readonly>; } export interface OAuthProvider { type: 'oauth'; id: string; authorizationUrl: string; tokenUrl: string; userInfoUrl: string; clientId: string; clientSecret?: string; scopes: readonly string[]; authorizationParams?: Readonly>; mapProfile(profile: unknown, tokens: OAuthTokenSet): AuthUser | Promise; } export interface MagicLinkProvider { type: 'magic-link'; send(message: { email: string; url: string; expiresAt: Date; }): Promise; resolveUser(email: string): Promise; } export interface WebAuthnProvider { type: 'webauthn'; options(input: unknown, request: Request): Promise; verify(input: unknown, request: Request): Promise; } export type AuthProvider = CredentialsProvider | OAuthProvider | MagicLinkProvider | WebAuthnProvider; export interface AuthOptions { /** At least 32 characters; used as the HMAC key for opaque token indexes. */ secret: string; /** Canonical application origin, for example `https://app.example.com`. */ origin: string; store: AuthStore; rateLimitStore: AuthRateLimitStore; providers: Readonly>; basePath?: string; session?: { ttlSeconds?: number; rememberTtlSeconds?: number; cookieName?: string; secure?: boolean; sameSite?: 'Lax' | 'Strict'; }; /** * The base ceiling every authentication bucket is derived from: `max` * requests per `windowSeconds` from one client against one scope. * * Two wider ceilings are derived from it and are not separately configurable. * One client may make `max × 5` requests across every scope in the same * window, which is what bounds a sweep across many accounts from one source. * One account may be attempted `max × 20` times — or mailed a magic link * `max × 1` times — by all clients combined, in a window of at most a minute, * which is what bounds a sweep of one account from many sources. A magic-link * send additionally costs a fifth of `max` per client, because sending mail is * not the same cost as checking a password. */ rateLimit?: { max?: number; windowSeconds?: number; }; /** * Resolve the client IP for rate-limit bucketing. Off by default because * forwarded headers are attacker-controlled unless a trusted proxy sets * them; without an IP the limiter falls back to the user-agent, which a * client can rotate. Deployments behind a proxy or platform edge should * provide this — `forwardedClientIp` covers the common `x-forwarded-for` * case, or read a platform header directly, for example * `(request) => request.headers.get('cf-connecting-ip')`. * A thrown error or empty result falls back to the user-agent-only key. * * What survives the fallback and what does not: the account-wide ceilings * have no client in their key, so rotating the user-agent cannot lift them — * one address stays bounded either way. The per-client ceiling is what a * rotated user-agent escapes, so without a resolver an attacker can still * spread requests thinly across *many different* addresses. Configure this * for any deployment where a `magic-link` provider can be reached from the * internet. */ clientIp?(request: Request): string | null | undefined; /** Observe full server-side failures without exposing them in HTTP responses. */ onError?(error: unknown, request: Request): void | Promise; } export interface AuthResult { user: AuthUser; session: AuthSession; headers: Headers; } /** * The route handlers that mount the auth endpoints. * * Re-export them from `app//[...path]/route.ts`: * `export const { GET, POST } = auth.handlers`. A path under `basePath` the * runtime does not serve answers 404 from here rather than falling through to * the application's not-found page. */ export interface AuthRouteHandlers { GET(context: { request: Request; }): Promise; POST(context: { request: Request; }): Promise; } export interface AuthRuntime { readonly basePath: string; /** Route handlers for `app//[...path]/route.ts`. */ readonly handlers: AuthRouteHandlers; handle(request: Request): Promise; login(provider: string, input: Record, request?: Request): Promise; getSession(request: Request): Promise; logout(request: Request): Promise; } //# sourceMappingURL=types.d.ts.map