/** * Route-level HTTP authentication primitives. * * Strategy helpers return an {@link AuthFn} for route factories. Verifier * helpers are lower-level pure functions for custom `fetch` handlers. */ import type { SessionAuthContext } from "#channel/types.js"; import { type RuntimeIpAllowList } from "#runtime/governance/network/ip-allow-list.js"; /** * Result returned by the verifier helpers below. On success, `sessionAuth` * is a fully-constructed {@link SessionAuthContext}; on failure no detail is * returned so routes do not leak which credential check failed. */ export type VerifyResult = { readonly ok: true; readonly sessionAuth: SessionAuthContext; } | { readonly ok: false; }; /** * Credentials accepted by {@link verifyHttpBasic}. */ export interface HttpBasicCredentials { readonly username: string; readonly password: string; } /** * Verifies an HTTP Basic credential against the supplied username and * password. Returns `{ ok: true, sessionAuth }` on success or `{ ok: false }` * on a missing or mismatched credential. The password is compared with * constant-time hash equality so a timing side channel cannot leak it; the * username is compared directly. */ export declare function verifyHttpBasic(authorizationHeader: string | null, credentials: HttpBasicCredentials): VerifyResult; /** * Configuration for {@link verifyJwtHmac}. Optional fields have defaults so * adapters can omit unused matchers. `secret` is read by the adapter from its * secret store (e.g. `process.env`) and passed in directly. */ export interface VerifyJwtHmacConfig { readonly algorithm: "HS256" | "HS384" | "HS512"; readonly audiences: readonly string[]; readonly issuer: string; readonly secret: string; /** * Tolerance in seconds for the `exp` and `nbf` claims. Defaults to 30. */ readonly clockSkewSeconds?: number; /** * AWS IAM-style `*`-wildcard patterns matched against the token `sub`. * When supplied, the token is rejected unless one matches. */ readonly subjects?: readonly string[]; /** * Per-claim membership matcher: each named claim must contain at least one * of the listed values, or the token is rejected. */ readonly claims?: Readonly>; } /** * Verifies a bearer JWT signed with an HMAC secret. Pass the token without * the `Bearer ` prefix (see {@link extractBearerToken}). Returns * `{ ok: true, sessionAuth }` on success or `{ ok: false }` when verification * fails or the token's claims don't match the supplied matchers. */ export declare function verifyJwtHmac(token: string | null, config: VerifyJwtHmacConfig): Promise; /** * Configuration for {@link verifyJwtEcdsa}. */ export interface VerifyJwtEcdsaConfig { readonly algorithm: "ES256" | "ES384" | "ES512"; readonly audiences: readonly string[]; readonly issuer: string; /** * PEM-encoded ECDSA public key (`-----BEGIN PUBLIC KEY-----` ...). */ readonly publicKey: string; /** * Tolerance in seconds for the `exp` and `nbf` claims. Defaults to 30. */ readonly clockSkewSeconds?: number; /** * AWS IAM-style `*`-wildcard patterns matched against the token `sub`. * When supplied, the token is rejected unless one matches. */ readonly subjects?: readonly string[]; /** * Per-claim membership matcher: each named claim must contain at least one * of the listed values, or the token is rejected. */ readonly claims?: Readonly>; } /** * Verifies a bearer JWT signed with an ECDSA private key against the supplied * PEM public key. Pass the token without the `Bearer ` prefix (see * {@link extractBearerToken}). Returns `{ ok: true, sessionAuth }` on success * or `{ ok: false }` when verification fails or the token's claims do not * match the configured `subjects`/`claims`. */ export declare function verifyJwtEcdsa(token: string | null, config: VerifyJwtEcdsaConfig): Promise; /** * Configuration for {@link verifyOidc}. */ export interface VerifyOidcConfig { readonly audiences: readonly string[]; readonly issuer: string; /** * OIDC discovery URL. Defaults to * `${issuer}/.well-known/openid-configuration` (any trailing slash on * `issuer` is stripped first). */ readonly discoveryUrl?: string; /** * Tolerance in seconds for the `exp` and `nbf` claims. Defaults to 30. */ readonly clockSkewSeconds?: number; /** * AWS IAM-style `*`-wildcard patterns matched against the token `sub`. * When supplied, the token is rejected unless one matches. */ readonly subjects?: readonly string[]; /** * Per-claim membership matcher: each named claim must contain at least one * of the listed values, or the token is rejected. */ readonly claims?: Readonly>; } /** * Verifies a bearer OIDC token against an issuer's discovery URL. Pass the * token without the `Bearer ` prefix (see {@link extractBearerToken}). The * token must satisfy the configured `subjects`/`claims` matchers; on success * the principal is tagged `principalType: "service"`. Unlike * {@link verifyVercelOidc}, this never grants the Vercel current-project * bypass. Returns `{ ok: false }` on failed verification or matchers. */ export declare function verifyOidc(token: string | null, config: VerifyOidcConfig): Promise; /** * Extracts the bearer token from an `Authorization: Bearer ` header. * Returns `null` if the header is missing, the scheme isn't `Bearer`, or the * value after `Bearer ` is empty. */ export declare function extractBearerToken(authorizationHeader: string | null): string | null; /** * Parsed IP allowlist for repeated checks. Construct once at module load, * then call {@link isIpAllowed} per request. */ export type IpAllowList = RuntimeIpAllowList; /** * Parses exact IP addresses or CIDR blocks into a reusable * {@link IpAllowList}. Entries are trimmed; throws on an empty entry, a `*` * wildcard (use exact IP/CIDR syntax), or an invalid address or CIDR prefix. */ export declare function createIpAllowList(entries: readonly string[]): IpAllowList; /** * Returns whether `ip` is permitted by `allowList`. `null` always returns * `false`. Adapters that need to allow unknown IPs should not call this. */ export declare function isIpAllowed(ip: string | null, allowList: IpAllowList): boolean; /** * One challenge entry attached to the `www-authenticate` header on a failure * response. For a Basic+Bearer prompt, pass both: * `[{ scheme: "Basic", parameters: { realm: "agent" } }, { scheme: "Bearer" }]` */ export interface UnauthorizedChallenge { readonly scheme: "Basic" | "Bearer"; readonly parameters?: Readonly>; } /** * Options accepted by {@link createUnauthorizedResponse}. */ export interface UnauthorizedResponseOptions { /** * HTTP status code. Defaults to `401`. Use `403` for "authenticated but * not allowed" outcomes. */ readonly status?: 401 | 403; /** * Machine-readable error code returned in the JSON body. Defaults to * `"unauthorized"` (401) or `"forbidden"` (403). */ readonly code?: string; /** * Human-readable error message returned in the JSON body. Defaults to a * generic prompt. */ readonly message?: string; /** * Optional `www-authenticate` challenge entries. */ readonly challenges?: readonly UnauthorizedChallenge[]; } /** * Builds a JSON failure response shaped like the framework's other auth * failures: `cache-control: no-store`, one `www-authenticate` header per * challenge, and a `{ ok: false, code, error }` body. */ export declare function createUnauthorizedResponse(opts?: UnauthorizedResponseOptions): Response; /** * Options accepted by auth error classes. The class chooses the HTTP status. */ export type AuthErrorOptions = Omit; /** * Error thrown by auth callbacks to reject a route with a structured 401 * response. `routeAuth` catches it and returns its response; other errors * propagate through the normal channel failure path. */ export declare class UnauthenticatedError extends Error { readonly response: Response; constructor(opts?: AuthErrorOptions); } /** * Error thrown by auth callbacks to reject a route with a structured 403 * response. `routeAuth` catches it and returns its response; other errors * propagate through the normal channel failure path. */ export declare class ForbiddenError extends Error { readonly response: Response; constructor(opts?: AuthErrorOptions); } /** * Route auth callback. Returned value semantics inside {@link routeAuth}: * * - A {@link SessionAuthContext} accepts the request and halts the walk. * - `null` or `undefined` skips to the next entry. * * If every entry skips (including the empty `[]` case), the walker returns a * 401. To reject with a specific response, throw an * {@link UnauthenticatedError} or {@link ForbiddenError}. To accept anonymous * traffic, include {@link none} as the final entry. */ export type AuthFn = (event: TEvent) => SessionAuthContext | null | undefined | Promise; /** * Walks an `AuthFn` (or array) in order against `request`. The first entry * returning a {@link SessionAuthContext} wins; entries returning `null` or * `undefined` are skipped. If the walk exhausts without a winner (including * the empty-array case), returns a 401 {@link createUnauthorizedResponse}. * * Channel factories that share this resolution policy (e.g. `eveChannel`, or * a custom `defineChannel` route handler) should call `routeAuth` rather than * re-implement the walk. */ export declare function routeAuth(request: Request, auth: AuthFn | readonly AuthFn[]): Promise; /** * Returns an {@link AuthFn} for scaffolded apps that makes unfinished * production auth fail as an intentional 401 rather than an internal route * error. Replace it before serving real users: * * ```ts * eveChannel({ auth: [localDev(), vercelOidc(), placeholderAuth()] }); * ``` * * Outside production it returns `null`, so the auth walk keeps the same local * development behavior as any other skipped entry. */ export declare function placeholderAuth(): AuthFn; /** * Returns an {@link AuthFn} that accepts any request anonymously, producing * a synthetic principal with `principalType: "anonymous"`. Use it as the * final entry in an `auth` array to opt routes into unauthenticated access: * * ```ts * eveChannel({ auth: [none()] }); // every request accepted anonymously * ``` * * The returned `SessionAuthContext` halts the {@link routeAuth} walk, so * `none()` terminates whatever array it appears in. It ignores its event * argument, so `TEvent` is inferred from the surrounding array (defaulting * to `unknown`) and composes with `AuthFn` entries without a type * argument. */ export declare function none(): AuthFn; /** * Returns an {@link AuthFn} that authenticates requests during local * development, keyed on the request URL's hostname (not the host process). * A hostname is treated as loopback when it is `localhost` or any * `*.localhost` subdomain (RFC 6761 routes the `.localhost` TLD to * loopback), any IPv4 in `127.0.0.0/8`, or the IPv6 loopback `::1`. * * Matching requests get a synthetic principal with `principalType: * "local-dev"`. Every other request returns `null`, skipping to the next * entry under {@link routeAuth}, which makes `[localDev(), vercelOidc()]` * the canonical "open on localhost, Vercel OIDC in prod" pattern. * * The check is not based on bare `process.env.VERCEL`: a deployment * outside Vercel (Fly, Railway, raw container) leaves `VERCEL` unset and * would then accept every public request. The one process-level exception * is `vercel dev`, detected by `VERCEL=1` and `VERCEL_ENV=development` * together. Only the local `vercel dev` server sets that pair (preview and * production report `VERCEL_ENV=preview`/`production`), so it opens the * dev server (which may serve over a non-loopback host) without opening a * real deployment. * * Caveat: this assumes a sane edge in front of public origins. An origin * that trusts an attacker-controlled `Host` header (no CDN, no normalizing * reverse proxy) lets an attacker spoof `Host: localhost` and reach * `localDev()`. Layer a real authenticator on such deployments. */ export declare function localDev(): AuthFn; /** * Options for {@link verifyVercelOidc} and {@link vercelOidc}. */ export interface VerifyVercelOidcOptions { /** * Optional `sub` patterns granting callers access on top of the always-on * current-project bypass. Patterns use AWS IAM-style `*` wildcards and may * target tokens minted by other Vercel projects (e.g. * `"owner:acme:project:partner-agent:environment:*"`). The current project * is always accepted regardless of this list. */ readonly subjects?: readonly string[]; } /** * Verifies a bearer JWT minted by Vercel OIDC. * * Acceptance rule: * * - Tokens whose `project_id` matches `VERCEL_PROJECT_ID` are **always** * accepted regardless of `subjects`, so the deployment's own runtime * callers (subagent, internal fetches) authenticate without being * enumerated. * - Tokens with an `external_sub` claim authenticate as * `principalType: "user"` when they match the current `VERCEL_PROJECT_ID` * (if set) and `VERCEL_TARGET_ENV` / `VERCEL_ENV` (if set). `external_sub` * becomes the eve subject, `external_iss` or `connector_id` the eve issuer * when present, and string-valued OIDC profile claims (`name`, `picture`, * `email`) are exposed as auth attributes. * - Tokens from other Vercel projects are accepted **only** when their `sub` * matches one of {@link VerifyVercelOidcOptions.subjects}. * * The `environment` claim is not constrained: production, preview, and * development tokens for the current project all authenticate. Non-user * principals are tagged `"runtime"` when the token's `environment` matches * the current deployment, otherwise `"service"`. */ export declare function verifyVercelOidc(token: string | null, opts?: VerifyVercelOidcOptions): Promise; /** * Allowed values for {@link VercelSubjectInput.environment}. Use `"*"` * to match any of `production`, `preview`, and `development` for the * named project. */ export type VercelSubjectEnvironment = "production" | "preview" | "development" | "*"; /** * Strict input shape accepted by {@link vercelSubject}. * * The `sub` claim carries human-readable slugs/names, not IDs. Stable * project and team IDs (`prj_...`, `team_...`) live on the separate * `project_id` / `owner_id` claims, which `verifyVercelOidc` matches * against `VERCEL_PROJECT_ID` for the current-project bind. This helper * composes a `sub` matcher, so its inputs are slugs. */ export interface VercelSubjectInput { /** * Vercel team slug (a.k.a. "owner"; e.g. `"acme"`) as embedded in the * `sub` claim, **not** the stable team ID `team_...` (`VERCEL_TEAM_ID`). * * Must not contain `*` or `:` so authors cannot widen the matcher to * "any team with a project of this name". Hand-write the subject string * when cross-team federation is intentional. */ readonly teamSlug: string; /** * Vercel project name (e.g. `"acme_website"`) as embedded in the `sub` * claim, **not** the stable project ID `prj_...` (`VERCEL_PROJECT_ID`). * Same constraints as {@link teamSlug}. */ readonly projectName: string; /** * Vercel deployment environment, or `"*"` to match any environment for * the named project. Defaults to `"production"` so an unspecified * environment cannot silently accept preview/development tokens. */ readonly environment?: VercelSubjectEnvironment; } /** * Builds a Vercel OIDC `sub` matcher pattern from a typed input. * * Vercel-issued tokens carry a `sub` of the form * `owner:[TEAM_SLUG]:project:[PROJECT_NAME]:environment:[ENVIRONMENT]`. * Hand-writing it invites two foot-guns: a misspelled slug (silently * rejecting all callers) and over-broad wildcards (silently accepting * unrelated callers). This helper rejects malformed inputs at construction * time and forces an explicit `environment`. * * Use it inside {@link VerifyVercelOidcOptions.subjects} to layer * additional Vercel-project callers on top of the current-project bypass: * * ```ts * vercelOidc({ * subjects: [vercelSubject({ teamSlug: "partner", projectName: "data" })], * }); * ``` * * The `sub` claim flips when a team or project name changes, so any policy * built on this helper must update with it; the stable team/project IDs are * not exposed in `sub`. See Vercel's OIDC reference for the token anatomy. */ export declare function vercelSubject(input: VercelSubjectInput): string; /** * Returns an HTTP route auth callback backed by Vercel OIDC. See * {@link verifyVercelOidc} for the always-on current-project bypass and how * `subjects` extends acceptance to other Vercel projects. */ export declare function vercelOidc(opts?: VerifyVercelOidcOptions): AuthFn; /** Returns an {@link AuthFn} that verifies HTTP Basic credentials via {@link verifyHttpBasic}. */ export declare function httpBasic(credentials: HttpBasicCredentials): AuthFn; /** Returns an {@link AuthFn} that verifies an HMAC-signed bearer JWT via {@link verifyJwtHmac}. */ export declare function jwtHmac(config: VerifyJwtHmacConfig): AuthFn; /** Returns an {@link AuthFn} that verifies an ECDSA-signed bearer JWT via {@link verifyJwtEcdsa}. */ export declare function jwtEcdsa(config: VerifyJwtEcdsaConfig): AuthFn; /** * Returns an {@link AuthFn} that verifies an OIDC bearer token on the inbound * request via {@link verifyOidc}. Use {@link vercelOidc} instead for * Vercel-issued tokens: it preconfigures the issuer, audience, and runtime * principal flag. */ export declare function oidc(config: VerifyOidcConfig): AuthFn;