import type { AuthagonalBffOptions, ResolvedBffOptions } from './options.js'; import { type IBffSessionStore } from './session.js'; import { type ICookieProtector } from './cookies.js'; import { OidcClient } from './oidc.js'; import { RefreshCoordinator } from './refresh.js'; import { type IBffTenantResolver } from './tenant.js'; /** Attributes for a Set-Cookie. */ export interface CookieOptions { httpOnly?: boolean; secure?: boolean; sameSite?: 'lax' | 'strict' | 'none'; path?: string; maxAgeSeconds?: number; } /** Minimal request/response abstraction the handlers run against; the Express and Next adapters implement it. */ export interface HttpCtx { readonly method: string; /** Request pathname (no query). */ readonly path: string; readonly query: URLSearchParams; /** `scheme://host` of this request, used to build the redirect_uri. */ readonly origin: string; /** The client address this adapter can observe, asserted to upstreams as `X-Forwarded-For`. Absent * where the adapter has no socket to read (a Web `Request`); see {@link assertedForwarding}. */ readonly clientIp?: string; getCookie(name: string): string | undefined; setCookie(name: string, value: string, opts: CookieOptions): void; deleteCookie(name: string, opts: CookieOptions): void; getHeader(name: string): string | undefined; setHeader(name: string, value: string): void; readForm(): Promise; redirect(url: string): void; json(body: unknown, status?: number): void; text(body: string, status?: number, contentType?: string): void; } export interface BffDeps { o: ResolvedBffOptions; store: IBffSessionStore; protector: ICookieProtector; tenants: IBffTenantResolver; /** Memoized OIDC client per authority (one auth host per tenant in multi-tenant mode). */ oidcFor: (authority: string) => OidcClient; refresher: RefreshCoordinator; log: (msg: string, err?: unknown) => void; } export declare function buildDeps(options: AuthagonalBffOptions): BffDeps; /** Dispatch a BFF request. Returns false if the path isn't a BFF route (so an Express host can call next()). */ export declare function routeBff(ctx: HttpCtx, d: BffDeps): Promise; /** True if the path is the BFF token-injecting proxy route (`{basePath}/api/**`). */ export declare function isProxyPath(path: string, o: ResolvedBffOptions): boolean; /** A resolved proxy target + bearer token, or an HTTP error status. */ export type ProxyDecision = { targetUrl: string; accessToken: string; forwarded: Record; } | { error: number; }; /** * True when `path` is under `prefix` on a segment boundary. Mirrors the .NET `BffProxy.PrefixMatches`. * * A bare `startsWith` let an upstream registered for `/user` capture `/userdata/...`, so the request — * carrying the session's bearer token — was forwarded to a backend that was never configured to receive * it. Which upstream a path belongs to is a trust decision, and it has to be made per path segment. */ export declare function prefixMatches(path: string, prefix: string): boolean; /** * Builds the upstream URL and confirms it still addresses the configured upstream, or returns null — * which means the composition escaped and the request must not be sent. Mirrors the .NET * `BffProxy.TryComposeTarget`. * * String concatenation is what the caller used to do, and it trusts that the forwarded path can only * ever be an absolute path. Anything that makes it authority-shaped — a `//host` from a doubled slash, * a backslash the WHATWG parser normalizes to `/` — turns the "target base URL" into a prefix of a URL * pointing somewhere else entirely, with the session's access token attached. Forcing a leading `/` * prevents that; re-parsing against the base and comparing the authority proves it. */ export declare function composeTarget(targetBaseUrl: string, forwardedPath: string, query: string): string | null; /** Authorize + resolve a proxy request: anti-forgery header, session, single-flight refresh, upstream * match. The adapter performs the actual streaming forward with the returned target + token. */ export declare function authorizeProxy(ctx: HttpCtx, d: BffDeps): Promise; /** * The forwarding metadata the proxy asserts from its own state, replacing whatever the caller sent. * * Stripping without re-asserting is not fail-closed, which is what made this half easy to leave out. * The upstream's behaviour on a MISSING `X-Forwarded-For` is not neutral: whether it reads the header * directly or through ASP.NET's `ForwardedHeadersMiddleware`, it falls back to the TCP peer — this BFF. * Every user of the SPA is then one address, so one user's failed attempts against a per-IP-limited * endpoint buy a 429 for everybody, and every audit row names the BFF pod instead of the actor. The * server's own `SourceQuotaKey` records the same failure reached from the other direction: "behind any * reverse proxy, declared or not, every client in the deployment shared one bucket." * * The .NET twin treats strip-then-assert as one control for exactly that reason * (`BffProxy.ProxyAsync`), and `PROXY_STRIP`'s comment already cited it as the rationale for the half * that was ported. Built here rather than in each adapter so there is one place to be wrong. */ export declare function assertedForwarding(ctx: HttpCtx, o: ResolvedBffOptions): Record; /** Headers the proxy never forwards (hop-by-hop + ones we set/strip: cookie, authorization, host). */ export declare const PROXY_STRIP: Set; /** Serialize a Set-Cookie header value. Session id + protected payloads are base64url, so no escaping needed. */ export declare function serializeCookie(name: string, value: string, opts: CookieOptions): string; /** Parse a Cookie header into a name→value map. */ export declare function parseCookies(header: string | undefined): Record;