import { ProxyRoutesAdapter, ProxyRoute, CacheAdapter, RouteMatch, HandlerConfig } from '@authhero/adapter-interfaces'; export { HandlerConfig, ListProxyRoutesParams, ListProxyRoutesResult, ProxyRoute, ProxyRouteInsert, ProxyRouteUpdate, ProxyRoutesAdapter, RouteMatch, handlerConfigSchema, matchSchema, proxyRouteInsertSchema, proxyRouteSchema, proxyRouteUpdateSchema } from '@authhero/adapter-interfaces'; import { MiddlewareHandler, Hono, Context } from 'hono'; import { z } from '@hono/zod-openapi'; interface ResolvedHost { tenant_id: string; custom_domain_id: string; domain: string; routes: ProxyRoute[]; } /** * Data adapter consumed by the proxy data plane. * * `proxyRoutes` mirrors the tenant-scoped CRUD adapter that AuthHero uses for * its management API. `resolveHost` is the cross-tenant lookup the proxy data * plane needs on every incoming request — it maps a request `Host` header to * the owning tenant, custom domain, and route set. * * `options.signal` aborts when the caller's deadline fires. Implementations * backed by cancellable I/O (fetch) should pass it through so the work stops; * others may ignore it, and the caller's timeout still races them. */ interface ProxyDataAdapter { proxyRoutes: ProxyRoutesAdapter; resolveHost(host: string, options?: ResolveHostOptions): Promise; } interface ResolveHostOptions { signal?: AbortSignal; } interface HostResolverCache { resolveHost(host: string, options?: ResolveHostOptions): Promise; } interface HostCacheOptions { freshTtlMs: number; staleTtlMs?: number; staleIfErrorTtlMs?: number; negativeTtlMs?: number; maxEntries?: number; waitUntil?: (promise: Promise) => void; upstreamTimeoutMs?: number; } declare function createInMemoryHostCache(data: ProxyDataAdapter, ttlMs: number, maxEntries?: number): HostResolverCache; declare function createInMemoryHostCache(data: ProxyDataAdapter, options: HostCacheOptions): HostResolverCache; interface HandlerBuildContext { bindings: Record; } interface HandlerDefinition { type: string; optionsSchema: z.ZodType; build(options: TOptions, ctx: HandlerBuildContext): MiddlewareHandler; } declare class HandlerRegistry { private definitions; private bindings; constructor(bindings?: Record); add(def: HandlerDefinition): this; has(type: string): boolean; build(type: string, rawOptions: unknown): MiddlewareHandler; } declare function defineHandler(def: HandlerDefinition): HandlerDefinition; interface ProxyRouteHandlerSpec { type: string; options?: unknown; } interface ProxyDataPlaneOptions { data: ProxyDataAdapter; cacheTtlMs?: number; cache?: HostCacheOptions; resolver?: HostResolverCache; registry?: HandlerRegistry; bindings?: Record; resolveHostTimeoutMs?: number; defaultHandlers?: ProxyRouteHandlerSpec[]; } declare function createProxyDataPlaneHandler(options: ProxyDataPlaneOptions): (c: Context) => Promise; declare function createProxyDataPlaneRouter(options: ProxyDataPlaneOptions): Hono; interface ProxyAppOptions { data: ProxyDataAdapter; cacheTtlMs?: number; cache?: HostCacheOptions; resolver?: HostResolverCache; registry?: HandlerRegistry; bindings?: Record; resolveHostTimeoutMs?: number; defaultHandlers?: ProxyRouteHandlerSpec[]; } declare function createProxyApp(options: ProxyAppOptions): Hono; interface CacheApiHostCacheOptions { upstream: HostResolverCache; cacheTtlSeconds?: number; negativeCacheTtlSeconds?: number; namespace?: string; cache?: Cache; waitUntil?: (promise: Promise) => void; cacheReadTimeoutMs?: number; cacheWriteTimeoutMs?: number; } /** * Wraps an upstream `HostResolverCache` with a Cloudflare Cache-API layer. * On cache miss, the upstream is queried and the result written back into * the Cache API for subsequent reads in the same colo. * * Invalidation is the caller's responsibility — typically the control plane * issues `cache.delete(syntheticKey(...))` on every proxy_route mutation, * and each colo's cache expires on its own TTL otherwise. * * @deprecated Prefer `createCacheAdapterHostCache` with `createCloudflareCache` * from `@authhero/cloudflare-adapter`. The adapter-based wrapper supports * stale-while-revalidate, stale-if-error, and works with any `CacheAdapter` * (Cloudflare, Redis, in-memory, …). */ declare function createCacheApiHostCache(options: CacheApiHostCacheOptions): HostResolverCache; declare function buildCacheApiKey(namespace: string, host: string): Request; interface CacheAdapterHostCacheOptions { upstream: HostResolverCache; cache: CacheAdapter; freshTtlMs: number; staleTtlMs?: number; staleIfErrorTtlMs?: number; negativeTtlMs?: number; keyPrefix?: string; waitUntil?: (promise: Promise) => void; cacheReadTimeoutMs?: number; cacheWriteTimeoutMs?: number; upstreamTimeoutMs?: number; } /** * Wraps an upstream `HostResolverCache` with a generic `CacheAdapter` layer. * Adds stale-while-revalidate and stale-if-error semantics on top of any * cache implementation — Cloudflare Cache API (via `CloudflareCache` from * `@authhero/cloudflare-adapter`), Redis, in-memory, or anything else * satisfying the `CacheAdapter` interface. * * Typical use on Cloudflare Workers: * * ```ts * import { createCloudflareCache } from "@authhero/cloudflare-adapter"; * import { createCacheAdapterHostCache, createInMemoryHostCache } from "@authhero/proxy"; * * const cache = createCloudflareCache({ cacheName: "authhero-proxy-hosts" }); * const resolver = createCacheAdapterHostCache({ * upstream: createInMemoryHostCache(data, { freshTtlMs: 60_000 }), * cache, * freshTtlMs: 60 * 60_000, // 1 hour fresh * staleTtlMs: 23 * 60 * 60_000, // SWR for 23 more hours (24h total) * staleIfErrorTtlMs: 24 * 60 * 60_000, // serve last-known-good for one more day on upstream errors * waitUntil: (p) => ctx.waitUntil(p), * }); * ``` * * Invalidation is the caller's responsibility — when a host's routes change, * call `cache.delete(buildCacheAdapterKey(keyPrefix, host))` from the control * plane. Otherwise entries expire on their own TTL. */ declare function createCacheAdapterHostCache(options: CacheAdapterHostCacheOptions): HostResolverCache; declare function buildCacheAdapterKey(keyPrefix: string, host: string): string; declare const corsHandler: HandlerDefinition<{ origins?: string[] | undefined; allow_credentials?: boolean | undefined; allow_headers?: string[] | undefined; allow_methods?: string[] | undefined; expose_headers?: string[] | undefined; max_age?: number | undefined; }>; declare const basicAuthHandler: HandlerDefinition<{ username: string; password: string; realm?: string | undefined; }>; declare const headersHandler: HandlerDefinition<{ request?: Record | undefined; response?: Record | undefined; remove_request?: string[] | undefined; remove_response?: string[] | undefined; }>; declare const cacheHandler: HandlerDefinition<{ ttl_seconds: number; }>; declare const forwardedHeadersHandler: HandlerDefinition<{ client_ip_header: string; set_x_real_ip: boolean; set_x_original_url: boolean; skip_cloudflare_client_ip: boolean; }>; declare const rewriteCookiesHandler: HandlerDefinition<{ upstream_host?: string | undefined; }>; declare const rewriteLocationHandler: HandlerDefinition<{ upstream_origin?: string | undefined; }>; declare const httpHandler: HandlerDefinition<{ upstream_url: string; preserve_host: boolean; timeout_ms?: number | undefined; }>; declare const serviceBindingHandler: HandlerDefinition<{ binding: string; preserve_host: boolean; upstream_url?: string | undefined; timeout_ms?: number | undefined; }>; declare const dispatchNamespaceHandler: HandlerDefinition<{ binding: string; script_name: string; cpu_ms?: number | undefined; subrequests?: number | undefined; timeout_ms?: number | undefined; }>; declare const redirectHandler: HandlerDefinition<{ upstream_url: string; status: 301 | 302 | 307 | 308; preserve_path: boolean; preserve_query: boolean; }>; declare const staticHandler: HandlerDefinition<{ status: number; headers?: Record | undefined; body?: string | undefined; json?: unknown; }>; declare function registerBuiltinHandlers(registry: HandlerRegistry): void; interface ProxyHostContext { tenant_id: string; custom_domain_id: string; domain: string; } declare const FORWARDED_HEADERS_HANDLER_TYPE = "forwarded_headers"; /** * Minimal shape shared by stored route handlers (`HandlerConfig`) and the * catch-all specs passed to the data plane (`ProxyRouteHandlerSpec`). */ interface HandlerSpecLike { type: string; options?: unknown; } /** * Guarantee a `forwarded_headers` handler at the head of a chain. Stored route * tables (KV / control plane) predate the handler and routinely omit it, which * silently drops the visitor IP: nothing else in the chain stamps * X-Forwarded-For / X-Real-IP, so upstreams see only the proxy hop's own * source address. Chains that already declare it — anywhere in the list — are * returned untouched so their configured options win and nothing is stamped * twice. Prefer `withForwardedHeadersFor`, which also checks that the registry * knows the handler. */ declare function withForwardedHeaders(handlers: readonly HandlerSpecLike[]): HandlerSpecLike[]; /** * `withForwardedHeaders`, but a no-op unless `registry` knows the handler — a * consumer with a bespoke registry that never registered it would otherwise * get a build-time throw on every route. Every chain the data plane compiles * (per-host routes and the `defaultHandlers` catch-all) goes through here. */ declare function withForwardedHeadersFor(registry: HandlerRegistry, handlers: readonly HandlerSpecLike[]): HandlerSpecLike[]; /** * Build a Hono app whose route table mirrors the configured proxy routes for * a single host. Compiled once per host (and cached alongside the resolved * host) — see `cache.ts`. When `hostContext` is supplied, every request * served by the compiled app gets `tenant_id`, `custom_domain_id`, and * `domain` set in its Hono context (read with `getProxy*` helpers from * `handlers/util.ts`). Handlers that template against those values * (`dispatch_namespace`) depend on this wiring. * * Every route's handler chain is normalized through * `withForwardedHeadersFor`, so a stored route table that omits * `forwarded_headers` still forwards the visitor IP. * * `defaultHandlers` is a prebuilt middleware chain installed as the catch-all * when no per-host route matches. Pass `undefined` to keep the legacy 404 * "No matching route" behavior. It is built by the caller (`router.ts`), which * applies `withForwardedHeadersFor` to it as well. */ declare function compileHostApp(routes: ProxyRoute[], registry: HandlerRegistry, hostContext?: ProxyHostContext, defaultHandlers?: MiddlewareHandler[]): Hono; /** * True when `ip` falls inside a Cloudflare-published range. Unparseable input * returns false — callers treat "not recognizably Cloudflare" as a client IP. */ declare function isCloudflareIp(ip: string): boolean; declare function sortRoutes(routes: ProxyRoute[]): ProxyRoute[]; /** * Match a configured host pattern against a request's `Host`. Supports * exact match and `*.example.com` wildcard prefix. */ declare function matchesHost(pattern: string, host: string): boolean; declare function matchesAnyHost(patterns: string[] | undefined, host: string): boolean; /** * Filter Hono middleware that 404s when the route's `match.headers` / * `match.query` / `match.hosts` predicates don't hold. Path and method * matching is done by Hono itself via `app.on(methods, path, ...)`. */ declare function buildMatchFilter(match: RouteMatch): (c: Context, next: () => Promise) => Promise; interface StaticRouteInput { id?: string; priority?: number; match: RouteMatch; handlers: HandlerConfig[]; created_at?: string; updated_at?: string; } interface StaticHostConfig { tenant_id?: string; custom_domain_id?: string; routes: StaticRouteInput[]; } interface StaticProxyAdapterOptions { hosts: Record; defaultTenantId?: string; } declare function createStaticProxyAdapter(options: StaticProxyAdapterOptions): ProxyDataAdapter; /** * Sugar for building a single HTTP-forwarding route in static configs. * Equivalent to `{ match: { path: "/*" }, handlers: [{ type: "http", options: { upstream_url, preserve_host } }] }`. */ declare function httpRoute(upstream_url: string, options?: { path?: string; preserve_host?: boolean; priority?: number; }): StaticRouteInput; interface HttpProxyAdapterOptions { baseUrl: string; clientId: string; clientSecret: string; audience?: string; scope?: string; resolveHostPath?: string; tokenRefreshSkewSeconds?: number; timeoutMs?: number; fetch?: typeof fetch; } /** * Build a `ProxyDataAdapter` that reads from the AuthHero control plane over * HTTP. Intended for a proxy deployment that does not share a database with * the control plane. * * Authentication is a single `client_credentials` grant against the control * plane. The token is cached in-memory and refreshed before expiry. The * privileged `resolveHost` endpoint must be served by the control plane and * requires the `proxy:resolve_host` scope. */ declare function createHttpProxyAdapter(options: HttpProxyAdapterOptions): ProxyDataAdapter; /** * Minimal shape of a Cloudflare service binding (a `Fetcher`). Declared * locally so `@authhero/proxy` does not depend on `@cloudflare/workers-types`. */ interface ServiceBindingFetcher { fetch(request: Request): Promise; } /** * Wrap a Cloudflare service binding as a `fetch`-compatible function, suitable * for the `fetch` override on {@link HttpProxyAdapterOptions}. * * When the proxy's control plane lives on a hostname that the proxy itself * serves (e.g. the proxy owns `*.token.example.com/*` and the control plane * resolves at `https://controlplane.token.example.com`), resolving it over the * public edge loops the proxy's `resolveHost`/`/oauth/token` calls straight * back into itself — a self-DoS. Routing those calls through a service binding * to the control-plane worker keeps them off the public edge entirely, so the * loop cannot form regardless of the configured `baseUrl`. * * ```ts * const data = createHttpProxyAdapter({ * baseUrl: env.CONTROL_PLANE_URL, * clientId: env.CONTROL_PLANE_CLIENT_ID, * clientSecret: env.CONTROL_PLANE_CLIENT_SECRET, * fetch: createServiceBindingFetch(env.AUTH2), * }); * ``` */ declare function createServiceBindingFetch(fetcher: ServiceBindingFetcher): typeof fetch; /** * Minimal structural view of a Cloudflare KV namespace's read surface. Declared * locally so `@authhero/proxy` stays free of a hard `@cloudflare/workers-types` * dependency — a real `KVNamespace` binding is structurally assignable to this. */ interface KvNamespaceReader { get(key: string, type: "json"): Promise; } /** * Minimal structural view of a Cloudflare KV namespace's write surface, consumed * by the control-plane publisher. A real `KVNamespace` is assignable to this. */ interface KvNamespaceWriter { put(key: string, value: string): Promise; delete(key: string): Promise; } declare const DEFAULT_KV_HOST_KEY_PREFIX = "authhero-proxy-host:"; /** * Build the KV key for a host. Shared by the proxy reader * (`createKvProxyAdapter`) and the control-plane publisher so both agree on the * exact key format. Hosts are lowercased for case-insensitive lookups. */ declare function buildKvHostKey(keyPrefix: string, host: string): string; interface KvProxyAdapterOptions { kv: KvNamespaceReader; keyPrefix?: string; timeoutMs?: number; } /** * Build a `ProxyDataAdapter` that resolves hosts from a Cloudflare KV namespace. * The control plane publishes the full `ResolvedHost` blob per host into KV; the * proxy reads it with a single, unauthenticated, edge-local `KV.get` — faster * and more reliable on cold reads than the two-hop HTTP control-plane adapter. * * Intended to sit in the `upstream` seam of `createCacheAdapterHostCache` as the * durable-edge layer. A null from KV means "not found"; callers can keep the * HTTP adapter as a miss / `stale-if-error` fallback during cutover. */ declare function createKvProxyAdapter(options: KvProxyAdapterOptions): ProxyDataAdapter; /** * Scope a control-plane bearer token must carry to call * `GET /api/v2/proxy/control-plane/hosts/:host` or `POST /sync`. The data * plane requests it when minting tokens; the control plane requires it when * verifying them. */ declare const PROXY_RESOLVE_HOST_SCOPE = "proxy:resolve_host"; export { DEFAULT_KV_HOST_KEY_PREFIX, FORWARDED_HEADERS_HANDLER_TYPE, HandlerRegistry, PROXY_RESOLVE_HOST_SCOPE, basicAuthHandler, buildCacheAdapterKey, buildCacheApiKey, buildKvHostKey, buildMatchFilter, cacheHandler, compileHostApp, corsHandler, createCacheAdapterHostCache, createCacheApiHostCache, createHttpProxyAdapter, createInMemoryHostCache, createKvProxyAdapter, createProxyApp, createProxyDataPlaneHandler, createProxyDataPlaneRouter, createServiceBindingFetch, createStaticProxyAdapter, defineHandler, dispatchNamespaceHandler, forwardedHeadersHandler, headersHandler, httpHandler, httpRoute, isCloudflareIp, matchesAnyHost, matchesHost, redirectHandler, registerBuiltinHandlers, rewriteCookiesHandler, rewriteLocationHandler, serviceBindingHandler, sortRoutes, staticHandler, withForwardedHeaders, withForwardedHeadersFor }; export type { CacheAdapterHostCacheOptions, CacheApiHostCacheOptions, HandlerBuildContext, HandlerDefinition, HandlerSpecLike, HostCacheOptions, HostResolverCache, HttpProxyAdapterOptions, KvNamespaceReader, KvNamespaceWriter, KvProxyAdapterOptions, ProxyAppOptions, ProxyDataAdapter, ProxyDataPlaneOptions, ProxyRouteHandlerSpec, ResolveHostOptions, ResolvedHost, ServiceBindingFetcher, StaticHostConfig, StaticProxyAdapterOptions, StaticRouteInput };