@authhero/proxy

中文文档
0.11.13发布于 2026/11/05AGPL-3.0-onlyTypeScriptESM + CJS构建来源已验证近 30 天下载 1912

A multi-tenant HTTP reverse proxy library for Hono. Resolves an incoming request's Host header to a tenant, matches against a JSON-configured list of routes (path / method / host / headers / query), and runs each matching route throu

原始使用文档
文档目录16 节
  1. Install
  2. Concepts
  3. Client IP forwarding
  4. Quick start (static config)
  5. Cloudflare service bindings
  6. Dispatching to Workers for Platforms
  7. Replacing the upstream-origin in cookies and redirects
  8. Dynamic routes (database-backed)
  9. Remote control plane (HTTP adapter)
  10. Control-plane endpoint (server side)
  11. KV-published read replica (recommended over the bare HTTP a…
  12. Host cache
  13. Custom handlers
  14. Route schema (zod)
  15. Exports
  16. License

@authhero/proxy

A multi-tenant HTTP reverse proxy library for Hono. Resolves an incoming request's Host header to a tenant, matches against a JSON-configured list of routes (path / method / host / headers / query), and runs each matching route through an ordered chain of handlers — middleware-style transformations followed by a terminal handler that produces the response.

Designed to run on Cloudflare Workers, Node.js, Bun, or any Hono-compatible runtime. This package is data-plane only — proxy route CRUD lives in the authhero core (/api/v2/proxy-routes), and the underlying schema is shipped by each database adapter (@authhero/kysely-adapter, @authhero/drizzle, @authhero/aws-adapter).

Install

pnpm add @authhero/proxy hono @hono/zod-openapi

hono and @hono/zod-openapi are peer dependencies.

Concepts

A route has two parts:

  • match — { hosts?, methods?, path, headers?, query? }. Path uses Hono's pattern syntax (/api/*, /users/:id). hosts accepts exact or *.example.com wildcards. headers and query are name → regex.
  • handlers — an ordered array of handler configs. Each handler is { type, options }. The last handler is the terminal (it produces the response); earlier handlers wrap it like Hono middleware ((c, next) => …).

Built-in handlers:

TypeRoleWhat it does
corsmiddlewareReplies to OPTIONS preflight; adds CORS headers to the response.
basic_authmiddlewareRequires a username/password in the Authorization: Basic header.
headersmiddlewareAdds/removes request and response headers.
cachemiddlewareInjects Cache-Control: public, max-age=N if not already set.
forwarded_headersmiddlewareSets X-Forwarded-For, X-Forwarded-Host, X-Forwarded-Proto, X-Real-IP, X-Original-URL from CF-Connecting-IP. Prepended automatically to any chain that omits it.
rewrite_cookiesmiddlewareRewrites upstream Set-Cookie: …; Domain= to the request host.
rewrite_locationmiddlewareRewrites the Location header on 3xx responses from upstream origin to request origin.
httpterminalForwards to an HTTP upstream via fetch.
service_bindingterminalForwards to a Cloudflare service binding (env.MY_API.fetch).
dispatch_namespaceterminalDispatches to a Cloudflare Workers for Platforms namespace (env.DISPATCHER.get(scriptName).fetch). script_name accepts {tenant_id}, {custom_domain_id}, {domain}, {host} placeholders.
redirectterminalReturns a 301/302/307/308 redirect.
staticterminalReturns a static body (great for healthchecks).

Client IP forwarding

forwarded_headers is guaranteed: the proxy prepends it to every route chain (and to defaultHandlers) that doesn't already declare it, so route tables stored in KV or the control plane can't silently drop the visitor IP. A chain that declares it anywhere in its handler list is left exactly as configured.

The handler ignores a CF-Connecting-IP that falls inside Cloudflare's own published ranges — that's what the header carries when the proxy is reached worker-to-worker, and stamping it would overwrite the real client IP an earlier hop already recorded in X-Forwarded-For. In that case the inbound chain is kept as-is; an inbound X-Real-IP is never trusted as a fallback because it is client-spoofable. Opt out with { type: "forwarded_headers", options: { skip_cloudflare_client_ip: false } }.

Register custom handlers via a HandlerRegistry.

Quick start (static config)

import { createProxyApp, createStaticProxyAdapter, httpRoute } from "@authhero/proxy";

const data = createStaticProxyAdapter({
  hosts: {
    "acme.example.com": {
      tenant_id: "acme",
      routes: [
        httpRoute("https://acme.vercel.app"),                     // sugar for { match: {path:"/*"}, handlers: [{type:"http",...}] }
        {
          match: { path: "/healthz" },
          handlers: [
            { type: "static", options: { status: 200, json: { ok: true } } },
          ],
        },
        {
          match: { path: "/api/*" },
          handlers: [
            { type: "cors", options: { origins: ["https://app.example.com"] } },
            { type: "http", options: { upstream_url: "https://api.acme.vercel.app" } },
          ],
        },
      ],
    },
  },
});

const app = createProxyApp({ data });
export default app;

Cloudflare service bindings

To dispatch to a service binding (e.g. an internal API Worker), declare it in your Worker entry and pass it to createProxyApp:

export default {
  fetch(req: Request, env: Env, ctx: ExecutionContext) {
    const app = createProxyApp({
      data: ...,
      bindings: { API2: env.API2 },
    });
    return app.fetch(req);
  },
};

Then a route can use:

{ match: { hosts: ["api.example.com"] }, handlers: [
    { type: "forwarded_headers", options: {} },
    { type: "service_binding", options: { binding: "API2" } },
] }

Dispatching to Workers for Platforms

Route to a per-tenant worker deployed into a Cloudflare dispatch namespace. The script name supports placeholders that are substituted at request time from the resolved host:

# wrangler.toml
[[dispatch_namespaces]]
binding = "DISPATCHER"
namespace = "authhero-tenants"
// One catch-all route per host dispatches to that tenant's worker.
{ match: { path: "/*" }, handlers: [
    { type: "dispatch_namespace", options: {
        binding: "DISPATCHER",
        script_name: "tenant-{tenant_id}-auth",  // e.g. "tenant-acme-auth"
        // Optional: cpu_ms, subrequests — forwarded to dispatcher.get(...)
    }},
] }

Available placeholders: {tenant_id}, {custom_domain_id}, {domain}, {host}. The first three come from the resolved host record; {host} is the request Host header.

Replacing the upstream-origin in cookies and redirects

The legacy Sesamy proxy rewrote Set-Cookie: Domain=upstream.example.com and 3xx Location: https://upstream.example.com/… headers so they pointed at the public custom domain. Equivalent with v2:

{
  match: { path: "/*" },
  handlers: [
    { type: "forwarded_headers", options: {} },
    { type: "rewrite_cookies", options: {} },   // infers upstream host from the http/service_binding handler
    { type: "rewrite_location", options: {} },  // ditto
    { type: "http", options: { upstream_url: "https://upstream.example.com" } },
  ],
}

(You can pin upstream_host / upstream_origin explicitly if you don't want the auto-inference.)

Dynamic routes (database-backed)

For production, store routes in a database and resolve them per request. AuthHero adapter packages ship a createProxyDataAdapter(db) helper that returns a full ProxyDataAdapter (CRUD + resolveHost) wired to their schema:

import { Kysely } from "kysely";
import { createProxyApp } from "@authhero/proxy";
import { createProxyDataAdapter } from "@authhero/kysely-adapter";

const db = new Kysely({ /* dialect */ });

const app = createProxyApp({
  data: createProxyDataAdapter(db),
});

The proxy_routes table comes with the standard AuthHero migrations — running them via migrateToLatest(db) (or the equivalent drizzle/aws step) is enough.

Remote control plane (HTTP adapter)

If the proxy runs separately from the AuthHero control plane (different worker, different VPC, different region), use createHttpProxyAdapter to read route config over HTTP:

import {
  createProxyApp,
  createHttpProxyAdapter,
  createCacheAdapterHostCache,
  createInMemoryHostCache,
} from "@authhero/proxy";
import { createCloudflareCache } from "@authhero/cloudflare-adapter";

const httpAdapter = createHttpProxyAdapter({
  baseUrl: "https://auth.example.com",
  clientId: env.PROXY_CLIENT_ID,
  clientSecret: env.PROXY_CLIENT_SECRET,
});

// Three-tier cache: in-memory (per-isolate) → Cloudflare Cache API (per-colo) → control plane.
const inMemory = createInMemoryHostCache(httpAdapter, { freshTtlMs: 60_000, staleTtlMs: 600_000 });
const resolver = createCacheAdapterHostCache({
  upstream: inMemory,
  cache: createCloudflareCache({ cacheName: "authhero-proxy-hosts" }),
  freshTtlMs: 60 * 60_000,           // 1 hour fresh
  staleTtlMs: 23 * 60 * 60_000,      // SWR for 23 more hours (24h total)
  waitUntil: (p) => ctx.waitUntil(p),
});

const app = createProxyApp({
  data: httpAdapter,
  resolver,                                  // share across requests
  bindings: { API2: env.API2 },
});

The HTTP adapter exchanges client_id + client_secret for an access token at ${baseUrl}/oauth/token, then calls GET ${baseUrl}/api/v2/proxy/control-plane/hosts/:host. The control plane must expose this endpoint with a system-scoped credential (separate from tenant tokens).

Control-plane endpoint (server side)

In your AuthHero server, opt in by wiring the resolver and pointing at the JWKS that signs control-plane bearer tokens. Authentication is built in — authhero verifies the bearer JWT (RS/ES algs, iss === env.ISSUER, scope proxy:resolve_host):

import { init } from "authhero";
import { createProxyDataAdapter } from "@authhero/kysely-adapter";

const proxyData = createProxyDataAdapter(db);

const { app } = init({
  dataAdapter,
  proxyControlPlane: {
    resolveHost: proxyData.resolveHost,
    jwksUrl: `${env.ISSUER}/.well-known/jwks.json`,
    // On Workers, route through a service binding to keep the request
    // off the public network:
    // jwksFetch: (url) => env.JWKS_SERVICE.fetch(url),
  },
});

The HTTP adapter reads routing over a two-hop authenticated call (token mint + resolve) on every cache miss. To remove that hop from the hot path, have the control plane publish each resolved host blob to a Cloudflare KV namespace, and have the proxy read it with a single, unauthenticated KV.get. The control-plane DB stays the source of truth; KV is a published read replica.

Proxy side — createKvProxyAdapter, with the HTTP adapter kept as the miss/error fallback during migration:

import {
  createProxyApp,
  createKvProxyAdapter,
  createHttpProxyAdapter,
  createCacheAdapterHostCache,
  createInMemoryHostCache,
  type ProxyDataAdapter,
} from "@authhero/proxy";
import { createCloudflareCache } from "@authhero/cloudflare-adapter";

const kv = createKvProxyAdapter({ kv: env.PROXY_HOSTS, timeoutMs: 1000 });
const http = createHttpProxyAdapter({ baseUrl, clientId, clientSecret });

const upstream: ProxyDataAdapter = {
  proxyRoutes: kv.proxyRoutes, // read-only
  async resolveHost(host) {
    try {
      const hit = await kv.resolveHost(host);
      if (hit) return hit;
    } catch {
      /* KV slow/unavailable — fall through */
    }
    return http.resolveHost(host); // miss / error fallback
  },
};

const resolver = createCacheAdapterHostCache({
  upstream: createInMemoryHostCache(upstream, { freshTtlMs: 60_000 }),
  cache: createCloudflareCache({ cacheName: "authhero-proxy-hosts" }),
  freshTtlMs: 60 * 60_000,
  negativeTtlMs: 60_000,
  waitUntil: (p) => ctx.waitUntil(p),
});

createProxyApp({ data: upstream, resolver });

Control-plane side — wrap the adapters once with wrapProxyAdaptersWithKvPublish (from authhero) and pass the wrapped pair to both the management API and createApplySyncEvents, then seed existing hosts with backfillProxyHostsToKv. The KV namespace must live in the same Cloudflare account as the proxy Worker. See the full publish → seed → use guide in the docs: Proxy → Shape 3b.

Host cache

resolveHost is called on every request. Wrap it with the built-in stale-while-revalidate cache:

const app = createProxyApp({
  data,
  cache: {
    freshTtlMs: 5 * 60_000,
    staleTtlMs: 60 * 60_000,
    negativeTtlMs: 30_000,
    waitUntil: (p) => ctx.waitUntil(p),
  },
});

On Cloudflare Workers, thread ExecutionContext.waitUntil through with AsyncLocalStorage so background refreshes survive the response.

Custom handlers

Define your own handler type:

import { z } from "@hono/zod-openapi";
import { HandlerRegistry, defineHandler, registerBuiltinHandlers, createProxyApp } from "@authhero/proxy";

const myHandler = defineHandler<{ secret: string }>({
  type: "hmac_verify",
  optionsSchema: z.object({ secret: z.string() }),
  build(options) {
    return async (c, next) => {
      const sig = c.req.header("x-signature");
      if (!sig || !await verify(sig, options.secret, c.req.raw)) {
        return new Response("Bad signature", { status: 401 });
      }
      await next();
    };
  },
});

const registry = new HandlerRegistry();
registerBuiltinHandlers(registry);
registry.add(myHandler);

const app = createProxyApp({ data, registry });

Route schema (zod)

All schemas are re-exported as Zod schemas (proxyRouteSchema, matchSchema, handlerConfigSchema) for validation in your own management UIs.

Exports

  • createProxyApp(options) — main Hono app factory
  • createProxyDataPlaneRouter / createProxyDataPlaneHandler — data plane plumbing
  • createStaticProxyAdapter / httpRoute — static config helpers
  • createHttpProxyAdapter — HTTP control-plane adapter
  • createInMemoryHostCache / createCacheAdapterHostCache — cache layers (createCacheApiHostCache is also exported but deprecated in favor of the adapter-based wrapper)
  • HandlerRegistry / defineHandler / registerBuiltinHandlers + each builtin handler
  • compileHostApp, sortRoutes, matchesHost, matchesAnyHost, buildMatchFilter

Database-backed adapters live in the AuthHero adapter packages: @authhero/kysely-adapter, @authhero/drizzle, @authhero/aws-adapter.

License

MIT

版本与发布记录

@authhero/proxy 共发布 63 个版本,首次发布于 2026/05/27;过去 12 个月发布了 63 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.11.13latest2026/11/052 天前
0.11.122026/11/043 天前
0.11.112026/10/309 天前
0.11.102026/10/2910 天前
0.11.92026/10/2612 天前
0.11.82026/10/2315 天前

关于 @authhero/proxy 的常见问题

@authhero/proxy 是什么?

A multi-tenant HTTP reverse proxy library for Hono. Resolves an incoming request's Host header to a tenant, matches against a JSON-configured list of routes (path / method / host / headers / query), and runs each matching route throu

如何安装 @authhero/proxy?

在项目目录运行 npm install @authhero/proxy,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:hono(^4.6.8)、@hono/zod-openapi(^1.4.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@authhero/proxy 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@authhero/proxy 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@authhero/proxy 可以商用吗?

@authhero/proxy 使用 AGPL-3.0-only 许可。最严格的 Copyleft:除分发外,通过网络向用户提供基于它的服务也需要开源对应源码。闭源 SaaS 使用前务必评估。以上为概括,不构成法律意见,请以许可原文为准。

@authhero/proxy 的下载量有多少?

根据 npm 官方下载统计,@authhero/proxy 在 2026/10/07 – 2026/11/05 这 30 天内被下载 1,912 次,日均约 64 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@authhero/proxy 还在维护吗?

最新版本 0.11.13 发布于 2026/11/05(2 天前);至今共发布 63 个版本,其中过去 12 个月发布了 63 个;首次发布于 2026/05/27。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。