import { createContextKey, type Middleware, type RequestContext } from '@remix-run/fetch-router' /** * Failure details for an unauthenticated request. */ export interface AuthFailure { /** Auth method that reported the failure. */ method?: string /** Failure category used by built-in auth middleware and schemes. */ code: 'missing_credentials' | 'invalid_credentials' /** Human-readable explanation of the auth failure. */ message: string /** Optional challenge value for `WWW-Authenticate`. */ challenge?: string } /** * Auth state for a successfully authenticated request. */ export interface GoodAuth { /** Indicates that the current request is authenticated. */ ok: true /** Application-defined identity resolved for the current request. */ identity: identity /** Auth method that successfully authenticated the request. */ method: string } /** * Auth state for a request that did not authenticate successfully. */ export interface BadAuth { /** Indicates that the current request is not authenticated. */ ok: false /** Failure details when authentication was attempted and rejected. */ error?: AuthFailure } /** * Request auth state stored in the router context. */ export type AuthState = GoodAuth | BadAuth /** * Context key used to read auth state with `context.get(Auth)`. * The `auth()` middleware also installs auth state as `context.auth`. */ // Keep exported context keys structurally typed so TypeScript 7 declaration emit does not // reference fetch-router internals. Logger and Renderer require the same annotation. export const Auth: { defaultValue?: AuthState } = createContextKey() /** * Successful result returned by an auth scheme. */ export interface AuthSchemeSuccess { /** Marks the scheme result as a successful authentication. */ status: 'success' /** Application-defined identity resolved by the scheme. */ identity: identity } /** * Failure result returned by an auth scheme. */ export interface AuthSchemeFailure { /** Marks the scheme result as an authentication failure. */ status: 'failure' /** Failure category reported by the scheme. */ code?: AuthFailure['code'] /** Human-readable explanation of the failure. */ message?: string /** Optional challenge value for `WWW-Authenticate`. */ challenge?: string } /** * Non-skipped results an auth scheme can return. */ export type AuthSchemeResult = AuthSchemeSuccess | AuthSchemeFailure /** * Full return type for an auth scheme, including skipped requests. */ export type AuthSchemeAuthenticateResult = | AuthSchemeResult | null | undefined | void /** * Authentication scheme contract consumed by `auth()`. */ export interface AuthScheme { /** Stable method name exposed on the resolved auth state. */ name: string /** Authenticates the current request or returns `null`/`undefined` to skip the scheme. */ authenticate( context: RequestContext, ): AuthSchemeAuthenticateResult | Promise> } type AuthSchemeIdentity = scheme extends AuthScheme ? identity : never type AuthForSchemes[]> = AuthState< AuthSchemeIdentity > /** * Options for loading auth state for each request. */ export interface AuthOptions[] = AuthScheme[]> { /** Auth schemes to run in order for each request. */ schemes: readonly [...schemes] } /** * Loads auth state for the current request by running each configured auth scheme in order. * * @param options Auth scheme configuration for the middleware. * @returns Middleware that resolves auth state into `context.auth` and `context.get(Auth)`. */ export function auth[]>( options: AuthOptions, ): Middleware<{ key: typeof Auth; value: AuthForSchemes; property: 'auth' }> { if (options.schemes.length === 0) { throw new Error('auth() requires at least one authentication scheme') } return async (context, next) => { for (let scheme of options.schemes) { let result = await scheme.authenticate(context) if (result == null) { continue } if (result.status === 'success') { setAuthState(context, { ok: true, identity: result.identity, method: scheme.name, }) return next() } if (result.status !== 'failure') { throw new Error( `Invalid result from "${scheme.name}" auth scheme. Return null/undefined to skip, or a { status: 'success' | 'failure' } object.`, ) } setAuthState(context, { ok: false, error: createFailure(scheme, result), }) return next() } setAuthState(context, { ok: false, }) return next() } } function setAuthState(context: RequestContext, auth: AuthState): void { context.set(Auth, auth, { property: 'auth' }) } function createFailure(scheme: AuthScheme, result: AuthSchemeFailure): AuthFailure { return { method: scheme.name, code: result.code ?? 'invalid_credentials', message: result.message ?? 'Invalid credentials', challenge: result.challenge, } }