import { JwtPayload } from "./jwt.mjs"; //#region src/middleware.d.ts /** * Headers that must never be forwarded to an upstream service. * These are connection-level headers that are meaningful only between * two directly connected peers and become invalid when proxied. */ export declare const HOP_BY_HOP: ReadonlySet; /** * SDK-internal headers that must never be forwarded to the upstream backend. * These headers carry session data between the middleware and server components; * forwarding them upstream would expose internal state and allow header injection. */ export declare const INTERNAL_HEADERS: ReadonlySet; /** * Returns `true` when `pathname` matches at least one entry in `routes`. * * An entry ending with `*` matches any path that starts with the prefix * before the `*`. All other entries match exactly, ignoring a trailing slash * on either side: `/profile` and `/profile/` name the same route, and several * frameworks (Qwik Router by default) serve the slashed form as canonical. An * exact compare would let `/profile/` past a `protectedRoutes: ["/profile"]`. * * @param pathname - The URL pathname to test. * @param routes - The list of route patterns to match against. * @returns `true` if at least one pattern matches. */ export declare function matchesRoutes(pathname: string, routes: readonly string[]): boolean; /** * Where a signed-out request for a protected route is sent: `loginPath`, with * the requested path and query string in `next`, so `` returns * to the page as it was asked for, filters and tabs included. The fragment * never reaches the server, so it cannot be carried. * * A `next` the request carries itself is left out, so a redirect never nests * an earlier one: should the login page be protected by mistake, each pass of * the redirect loop would otherwise wrap the last and grow the URL. The rest * of the query keeps the encoding it was sent with. * * Whatever query `loginPath` has of its own is kept, except a `next`, which * this replaces. * * @param loginPath - The login page, a path starting with a single `/`. * @param requestUrl - The full URL of the signed-out request. * @returns The URL to redirect to, on the request's origin. */ export declare function loginRedirectUrl(loginPath: string, requestUrl: string | URL): URL; /** * Whether a proxied call may carry the confidential project secret. * * ADR 036: the browser flow needs the secret only for the handoff exchange * (`POST /sessions/exchange`). Stamping it onto every proxied operation would * turn the app's public `/__nextgen/*` boundary into an open relay carrying * project-level authority for any anonymous caller. * * @param method - The proxied request's HTTP method. * @param pathname - The path after the proxy prefix, e.g. `/sessions/exchange`. * @returns `true` only for the handoff exchange. */ export declare function requiresProjectSecret(method: string, pathname: string): boolean; /** * Filters upstream response headers for proxying: strips hop-by-hop headers * and `set-cookie` (handled separately via `getSetCookie()`), and reduces * `location` to a redirect that can only land on this app. * * `location` was stripped outright until the identity-provider callback * needed it: the provider returns the browser to `/__nextgen/idp/{slug}/callback`, * and the engine answers `303` back to the page the sign-in started on. That * is a top-level navigation with no JavaScript in the loop, so a dropped * `Location` leaves the browser on an empty page. Outbound redirects to a * provider do not travel this way -- they arrive as `step.redirect_url` in a * JSON body and the widget navigates -- so nothing legitimate points off-origin. * * @param upstream - The upstream response headers. * @param selfUrl - The app's own request URL, which is both the origin a * redirect must land on and the base a relative one resolves against. * Omitting it keeps the old behaviour of dropping every redirect. * @returns A new `Headers` object with filtered headers. */ export declare function filterResponseHeaders(upstream: Headers, selfUrl?: string): Headers; /** The cookie the server sets a session in and reads it from. */ export declare const SERVER_SESSION_COOKIE = "__nextgen_session"; /** * The cookie an app keeps its project's session in: the server's name with the * project id appended (ADR 073). A browser shares one cookie jar across every * port of a host, so under the server's name alone the Console of a local * server and every app on `localhost` would hold their sessions in one slot and * replace each other's. * * Every byte of the id outside `[A-Za-z0-9_-]` is percent-encoded, so any * configured id makes a valid cookie name. Ids the server mints need none. */ export declare function sessionCookieName(projectId: string): string; /** * A session cookie a response sets. An empty `value` with `maxAge: 0` clears * it. */ export interface SessionCookie { readonly name: string; readonly value: string; /** Seconds the browser keeps it, or `undefined` for the browser session. */ readonly maxAge: number | undefined; readonly secure: boolean; } /** * The `Set-Cookie` value for `cookie`, with the attributes the server gives its * own session cookie: `Path=/`, `HttpOnly`, `SameSite=Lax`. */ export declare function serializeSessionCookie(cookie: SessionCookie): string; /** * The `Cache-Control` of a response that sets a session cookie, as the server * sends on its session answers: a shared cache must not hand the cookie, the * session token it carries or its clearing, to the next visitor. */ export declare const SESSION_COOKIE_CACHE_CONTROL = "private, no-store"; /** The browser's request as the app sees it, and as a proxy in front of it forwarded it. */ export interface BrowserRequest { /** The app's own request URL. */ readonly requestUrl: string | URL; /** The request's `X-Forwarded-Proto`, or `null`. */ readonly forwardedProto?: string | null; /** The request's `X-Forwarded-Host`, or `null`. */ readonly forwardedHost?: string | null; } /** * What a proxied request carries that decides its session cookies. The * request URL and the forwarded headers decide whether a cookie needs `Secure`. */ export interface ProxiedRequest extends BrowserRequest { readonly method: string; /** The path after the proxy prefix, e.g. `/sessions/me`. */ readonly path: string; /** The query string, with its leading `?`, or `""`. */ readonly search: string; /** The request's `Cookie` header, or `null`. */ readonly cookie: string | null; } /** The server a proxy forwards to, for the calls it makes of its own. */ export interface ProxyUpstream { /** Base URL of the auth backend. */ readonly url: string; /** How long to wait for an answer. */ readonly timeoutMs: number; } /** What a proxied answer sends the browser. */ export interface ProxiedAnswer { readonly setCookies: string[]; readonly body: ReadableStream | string | null; } /** A proxied call's session cookies, on the way up and on the way back. */ export interface ProxiedSessionCookies { /** The `Cookie` header to send upstream, or `null` to send none. */ readonly upstreamCookie: string | null; /** * The upstream answer's way back to the browser: the `Set-Cookie` values and * the body to send. Send this body, never the upstream response's own, which * this may have read. */ toBrowser(upstream: Response): Promise; } /** * The session cookies of a call the app proxies to the server (ADR 073). The * server knows one session cookie, `__nextgen_session`; the app keeps its * project's session in {@link sessionCookieName}, so it neither replaces nor * reads the session of another app or of the Console on the same host. * * - On the way up, the app's own session cookie goes under the server's name, * and every other session cookie stays behind. Other cookies pass unchanged. * - On the way back, a `Set-Cookie` of the server's session cookie, which sets * or clears it, is renamed to the app's. * - A session kept under the server's name goes up to one call only, the * session probe `GET /sessions/me?project_id=`, which the server * refuses for another project's session. When the answer signs a user of * this project in, the session is copied to the app's cookie; the server's * cookie keeps it, since the Console may share the project. * - When the app signs a user in (`POST /sessions/exchange`) or out * (`DELETE /sessions/me`), a session still kept under the server's name is * retired, or it would sign its user back in once the app's own cookie is * gone: the server is asked about it in this project, and a session of this * project is cleared (and on sign-out also ended), a dead one cleared, and * one of another project, the Console's on `localhost`, left alone. * - Without a project the cookies pass as they are: nothing says which session * is the app's. * * @param request - The proxied request. * @param projectId - The project this app signs its users in to. * @param upstream - The server the proxy forwards to, which it asks about a * session kept under the server's name. */ export declare function proxySessionCookies(request: ProxiedRequest, projectId: string | undefined, upstream: ProxyUpstream): ProxiedSessionCookies; /** * The authenticated session for a signed-in user. Identity follows the * user-ref vocabulary (ADR 058): render `display`, falling back to * `identifier`, then `userId`. */ export type NextgenSession = { /** The user's unique identifier (`sub` claim). */ userId: string; /** * The user schema's designated identifier value (the login identifier, * e.g. an email address or username), or `null` when unresolved. On the * JWT path this carries the `email` claim. */ identifier: string | null; /** * The schema property `identifier` came from (e.g. `"email"`), or `null` * when unknown — JWT-claim identities carry no property attribution. */ identifierProperty: string | null; /** The user's display name rendering, or `null` when the schema designates none. */ display: string | null; /** The raw verified JWT. */ token: string; }; /** Auth state when the user is signed in. */ export type AuthState = { isAuthenticated: true; session: NextgenSession; }; /** Auth state when the user is not signed in. */ export type UnauthState = { isAuthenticated: false; session: null; }; /** Union of all possible auth states. */ export type AuthResult = AuthState | UnauthState; /** * The client-safe session exposed to app UI (headers, account menus). * Identical to {@link NextgenSession} but omits `token` — the raw session * token must never reach client-side JavaScript, whether through an SSR * payload or a client-side fetch result. */ export type ClientSession = { /** The user's unique identifier (`sub` claim). */ userId: string; /** The designated identifier value (login identifier), or `null`. */ identifier: string | null; /** The schema property the identifier came from, or `null`. */ identifierProperty: string | null; /** The display name rendering, or `null`. */ display: string | null; }; /** Client-safe auth state when the user is signed in. */ export type ClientAuthState = { isAuthenticated: true; session: ClientSession; }; /** * Union of all possible client-safe auth states. Returned by the client * session reads (`useAuth()` in sdk-nuxt, `getSession()` in sdk-next). * Token is intentionally absent — use the server-side helpers when the raw * token is needed. */ export type ClientAuthResult = ClientAuthState | UnauthState; /** * Options passed to the SDK middleware factory (`nextgenMiddleware` in * sdk-next, `createNextgenMiddleware` in sdk-nuxt). */ export type NextgenMiddlewareOptions = { /** * Full URL of the Zitadel auth backend. * @default process.env.ZITADEL_URL ?? "http://localhost:8080" */ url?: string; /** * The project this app signs its users in to. A session counts as signed in * only when it belongs to this project. A session of any other project on the * same server (a second app, or the Console, whose cookie the browser sends * to every port of a shared host) reads as signed out, and its cookie is left * alone. * * The app keeps its session in a cookie of its own, * `__nextgen_session_` ({@link sessionCookieName}), so signing in * here neither replaces nor reads the session of another app or of the * Console on the same host (ADR 073). * * An opaque session token is checked by the backend * (`GET /sessions/me?project_id=…`) and against the project its answer names; * a JWT must name the project in its `aud` claim. * * Required: with no project, no session counts as signed in, and the proxy * passes the server's session cookie through unchanged. Each SDK falls back * to the `ZITADEL_PROJECT_ID` environment variable, then to its framework's * public project id variable, which `zitadel setup` writes to `.env.local`. */ projectId?: string; /** * URL path prefix that is reverse-proxied to the auth backend. * * Sign-in with an external provider currently requires the default: the * provider returns to `/__nextgen/idp/{slug}/callback`, which is proxied only under * the default prefix. * @default "/__nextgen" */ proxyPath?: string; /** * Pathnames that require a valid session. Requests to these routes without * a valid token are redirected to {@link loginPath}. * Entries ending with `*` match any sub-path (e.g. `"/admin*"`). * * When the backend cannot say whether a session cookie is live (it answers * `5xx`, does not answer in time, or cannot be reached), the route answers * `503 Service Unavailable` instead: the user may well be signed in, and the * login page would tell them they are not. * @default [] */ protectedRoutes?: string[]; /** * Pathnames that are completely skipped by the middleware — no JWT * verification, no protection check, no header tunnelling. * Useful for webhooks, health-check endpoints, or any route where * running auth logic is undesirable. * Entries ending with `*` match any sub-path (e.g. `"/public*"`). * @default [] */ ignoredRoutes?: string[]; /** * Pathname to redirect unauthenticated users to. * A `next` query parameter carries the requested path and query string, * where `` returns after sign-in. * @default "/login" */ loginPath?: string; /** * Reserved for future use. Local PEM public key for offline JWT verification. */ jwtKey?: string; /** * Restrict accepted JWT `alg` header values. Tokens whose algorithm is not * in this list are rejected before JWKS is fetched. * @default ["RS256", "ES256"] */ allowedAlgorithms?: string[]; /** * Clock skew tolerance in milliseconds applied to `exp`, `nbf`, and `iat` * claim validation. * @default 5000 */ clockSkewMs?: number; /** * Expected value(s) of the `aud` claim. When omitted, audience is not * validated. Pass a single string or an array when a token may carry * multiple audiences. */ audience?: string | string[]; /** * Accepted values for the JWT header `typ` claim (case-insensitive). * Tokens whose `typ` is not in this list are rejected. * Set to `[]` to disable token-type checking entirely. * @default ["JWT", "at+JWT"] */ allowedTokenTypes?: string[]; /** * Timeout in milliseconds for JWKS endpoint requests. * Requests that do not complete within this window are aborted and the * token is rejected, treating the request as unauthenticated. * @default 5000 */ jwksTimeoutMs?: number; /** * Timeout in milliseconds for upstream proxy requests. * Requests that do not complete within this window are aborted with a * network error. Defaults to 5 000 ms. * @default 5000 */ proxyTimeoutMs?: number; /** * Timeout in milliseconds for opaque (non-JWT) session token validation * via the backend's `GET /sessions/me` endpoint. A token the backend does * not answer for within this window is not refused: the request counts as * signed out, its session cookie is kept, and a protected route answers * `503` (see {@link protectedRoutes}). * @default 5000 */ opaqueTokenTimeoutMs?: number; }; /** * What the backend reports for an opaque (non-JWT) session token. * * `userId` is `null` when the session names no user of this app's project. * That is an anonymous session — `CreateSession` sets the session cookie * before any user factor is verified, so an abandoned or in-progress sign-in * holds a live session that names no user — or a live session of another * project. Such a session is **not** signed in, but it is not dead either, so * callers keep its cookie instead of clearing it: an anonymous sign-in may * still complete, and the other project's session belongs to the app that * signed it in. */ export interface OpaqueSession { readonly userId: string | null; readonly identifier: string | null; readonly identifierProperty: string | null; readonly display: string | null; /** When the session expires, as the backend reports it, or `null`. */ readonly expiresAt: string | null; } /** * The error code the backend answers a session read with when the session * belongs to another project than the one the read names. */ export declare const SESSION_PROJECT_MISMATCH = "sess.project_mismatch"; /** * The backend's verdict on an opaque session token, from * {@link checkOpaqueSessionToken}. * * - `live`: the backend holds a live session for the token. Its `userId` is * `null` when the session names no user of this app's project: an anonymous * one, or one of another project (see {@link OpaqueSession}). * - `refused`: the backend judged the token and refused it (`401`), or found * the session it names gone (`404` with the `sess.not_found` code). Only * this verdict makes the cookie dead. * - `undecided`: the backend gave no verdict: a `5xx` or any other answer, no * answer in time, a network error, or a body that cannot be read. The * backend answers `500`, never `401`, when it cannot decide (its token or * key store failed), so that an outage does not sign its users out. The * request counts as signed out, but the cookie is kept. */ export type OpaqueSessionCheck = { readonly verdict: "live"; readonly session: OpaqueSession; } | { readonly verdict: "refused"; } | { readonly verdict: "undecided"; }; /** * Asks the backend (`GET {issuerUrl}/sessions/me?project_id={projectId}`) * whether an opaque session token names a live session in this app's project, * telling a refusal apart from no verdict. * * A live session of another project comes back with no user, like an * anonymous one: it signs no one in here, and its cookie is not dead. The * backend refuses such a session itself (`403 sess.project_mismatch`); the * project its answer names is checked as well, because a backend that predates * the parameter answers for any project. * * A `404` is a refusal only with the `sess.not_found` code. Without it, the * answer came from something in front of the backend (a proxy, a `url` with * the wrong path), which judged nothing. * * When there is no verdict, it logs why: the request it was asked for then * renders signed out, or answers `503` on a protected route, and nothing else * would say what went wrong. * * @param token - The opaque session token from the app's session cookie. * @param issuerUrl - Base URL of the auth backend. * @param timeoutMs - How long to wait for the backend before giving up. * @param projectId - The project this app signs its users in to. Without one, * no session can be shown to belong here: the backend is not asked, and the * session reads as live with no user, so its cookie is kept. * @returns The backend's {@link OpaqueSessionCheck | verdict}. */ export declare function checkOpaqueSessionToken(token: string, issuerUrl: string, timeoutMs: number, projectId: string): Promise; /** * Forgets which session tokens the backend placed in another project * ({@link checkOpaqueSessionToken} remembers them for a while). For tests that * answer for the same token differently. */ export declare function clearSessionCheckCache(): void; /** * Validates an opaque session token by asking the backend * (`GET {issuerUrl}/sessions/me?project_id={projectId}`) whether the session is * live in this app's project. * * `null` folds a refusal and no verdict together, so it says whether the * request is signed in, not whether the cookie is dead: clearing the cookie on * `null` signs users out whenever the backend fails. To decide that, use * {@link checkOpaqueSessionToken}. * * @param token - The opaque session token from the app's session cookie. * @param issuerUrl - Base URL of the auth backend. * @param timeoutMs - How long to wait for the backend before giving up. * @param projectId - The project this app signs its users in to. * @returns The live session — with a `null` `userId` when it names no user of * this project — or `null` when the backend did not confirm one: it refused * the token, or gave no verdict. */ export declare function validateOpaqueSessionToken(token: string, issuerUrl: string, timeoutMs: number, projectId: string): Promise; /** * Whether a verified JWT names `projectId` in its `aud` claim, the one place a * self-contained token can say which project it was issued for. */ export declare function jwtNamesProject(payload: JwtPayload, projectId: string): boolean; /** * What a request carries that decides its session. The request URL and the * forwarded headers decide whether a cookie needs `Secure`. */ export interface SessionRequest extends BrowserRequest { /** * The `Authorization: Bearer` token, or `null`. It takes precedence: when it * signs a user of this project in, the cookie is not consulted. */ readonly bearer: string | null; /** Reads one of the request's cookies by name. */ readonly cookie: (name: string) => string | null | undefined; } /** The settings {@link resolveSession} judges a request's session by. */ export interface ResolveSessionOptions { readonly url: string; /** The project this app signs its users in to; see {@link NextgenMiddlewareOptions.projectId}. */ readonly projectId: string | undefined; readonly allowedAlgorithms: readonly string[] | undefined; readonly clockSkewMs: number; readonly audience: string | readonly string[] | undefined; readonly allowedTokenTypes: readonly string[]; readonly jwksTimeoutMs: number | undefined; readonly opaqueTokenTimeoutMs: number; /** * Whether an opaque session cookie is checked with the backend, a call to * the server on every request. `false` leaves it to a render layer that * checks the session itself, as sdk-next's `auth()` does: the result hands * the token on as {@link ResolvedSession.unchecked}. A JWT is judged either * way, without a call. * @default true */ readonly checkOpaqueSession?: boolean; } /** A request's session, as {@link resolveSession} judged it. */ export interface ResolvedSession { readonly auth: AuthResult; /** * Whether the backend gave no verdict on the session cookie * ({@link OpaqueSessionCheck}). The request counts as signed out and the * cookie is kept; a protected route answers `503` rather than send a user * who may well be signed in to the login page. */ readonly undecided: boolean; /** * The session cookies the response sets, with {@link SESSION_COOKIE_CACHE_CONTROL} * when there are any. A cookie the backend refused is cleared so the browser * stops sending it; a session of this project found in the server's cookie is * copied to the project's own, and the server's cookie keeps it, since the * Console may share the project. No other cookie is * touched: a session that is live but signs no one in here (an anonymous * one, or one of another project) keeps its cookie, and so does one the * backend gave no verdict on, and one behind a Bearer token that signed the * request in, which was never judged. */ readonly cookies: readonly SessionCookie[]; /** * The opaque session token the backend was not asked about, when * {@link ResolveSessionOptions.checkOpaqueSession} is `false`. The request * counts as signed out here and its cookie is left as it is: whatever reads * the session next checks the token itself. */ readonly unchecked?: string; } /** * Judges which user, if any, a request is signed in as in this app's project. * The one implementation behind every server SDK's middleware, so they cannot * drift on who counts as signed in. * * - A Bearer token is judged first, as a JWT. When it signs a user of this * project in, that is the answer and the cookie is not judged; otherwise the * cookie speaks for itself. * - The session cookie is the project's own, else the server's. A session of * this project found in the server's cookie is copied to the project's own. * - A JWT-shaped session cookie signs its `sub` in when it verifies and its * `aud` names the project. It never reaches the backend, which does not * re-check JWT claims. * - Any other session cookie is checked by the backend * ({@link checkOpaqueSessionToken}) and signs its user in when the session * is live in this project. With `checkOpaqueSession: false` it is not: the * result hands it on unchecked, and touches no cookie. * - Only a cookie that held a dead session is cleared: one the backend refused, * or a JWT that fails verification or names no user of this project. When * the backend gives no verdict, the result says so * ({@link ResolvedSession.undecided}) and the cookie stays. * - Without a project nothing signs in, and no cookie is touched. */ export declare function resolveSession(request: SessionRequest, options: ResolveSessionOptions): Promise; //#endregion //# sourceMappingURL=middleware.d.mts.map