@authhero/ 代理服务器
多租户 HTTP 逆向代理库 浩诺解决收到的请求 Host 头到租户,与JSON配置的路由列表相匹配(路径/方法/主机/头头/查询),并通过命令的处理器链来运行每个匹配路由——中间软件风格的转换,然后是产生响应的终端处理器.
设计运行于Cloudflare Workers,Node.js, Bun,或任何Hono相容的运行时间. 这个软件包仅是数据平面 – 代理路由 CRUD lives in the authhero 核心资源(/api/v2/proxy-routes),而基础计划由每个数据库适配器运出(@authhero/kysely-adapter, (中文(简体) ), @authhero/drizzle, (中文(简体) ), @authhero/aws-adapter) (中文(简体) ).
安装
pnpm add @authhero/proxy hono @hono/zod-openapi
hono 财务报告和审定财务报表 @hono/zod-openapi 是同伴的依赖关系.
概念
A 类 路径 有两个部分:
match—— 说{ hosts?, methods?, path, headers?, query? }。路径使用Hono的模式语法(/api/*, (中文(简体) ),/users/:id) (中文(简体) ).hosts接受准确或*.example.com通通证.headers财务报告和审定财务报表query名称 \ regex.handlers——命令的处理器配置阵列. 每个操作员是{ type, options }编辑 最后一个 处理器是终端(它产生响应); 早期的处理器像Hono中间软件((c, next) => …) (中文(简体) ).
内置处理器 :
| 类型 | 作用 | 它会做什么 |
|---|---|---|
cors | 中间软件 | 对选择方案预先飞行的答复;在答复中增加CORS标题. |
basic_auth | 中间软件 | 需要在 Authorization: Basic 头曰. |
headers | 中间软件 | 添加/删除请求和回复标题. |
cache | 中间软件 | 喷出 Cache-Control: public, max-age=N 如果还没有设置. |
forwarded_headers | 中间软件 | 设定 X-Forwarded-For, (中文(简体) ), X-Forwarded-Host, (中文(简体) ), X-Forwarded-Proto, (中文(简体) ), X-Real-IP, (中文(简体) ), X-Original-URL 从CF-Concting-IP。 自动准备到任何遗漏的链条. |
rewrite_cookies | 中间软件 | 重写上游 Set-Cookie: …; Domain= 到请求主机. |
rewrite_location | 中间软件 | 重写 Location 关于3xxx从上游来源对请求来源的答复的标题. |
http | 终端 | 转发到上游的 HTTP 通过 fetch。 。 。 . |
service_binding | 终端 | 转发到 Cloudflare 服务绑定(env.MY_API.fetch) (中文(简体) ). |
dispatch_namespace | 终端 | 向云旗工人派出平台命名空间(env.DISPATCHER.get(scriptName).fetch) (中文(简体) ). script_name 接受 {tenant_id}, (中文(简体) ), {custom_domain_id}, (中文(简体) ), {domain}, (中文(简体) ), {host} 占位符 . |
redirect | 终端 | 返回 a 301/302/307/308重新定向. |
static | 终端 | 返回一个静态身体(健康检查效果良好). |
客户端 IP 转发
forwarded_headers 保证:代理机将它预先加到每个路由链上(和 defaultHandlers),这还没有宣布,所以存储在KV或控制平面中的路由表不能默默地放下访客IP. 处理器列表中任何位置的链条都完全按照配置.
处理器忽略一个 CF-Connecting-IP 属于Cloudflare自己的出版范围——当代理到达工人与工人之间时,信头会随身携带,盖上它就可以覆盖真正的客户端IP,一个已经录入的更早的跳出 X-Forwarded-For。在这种情况下,入境链保持不变;入境 X-Real-IP 永远不会被信任为一个倒置的背面,因为它是可客户端的。 选择退出 { type: "forwarded_headers", options: { skip_cloudflare_client_ip: false } }。 。 。 .
通过 a 注册自定义处理器 HandlerRegistry。 。 。 .
快速启动( 静态配置)
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 服务绑定
向服务机构(如内部API工人)发送信息,在工人登记簿上声明,并传递给 createProxyApp数字 :
export default {
fetch(req: Request, env: Env, ctx: ExecutionContext) {
const app = createProxyApp({
data: ...,
bindings: { API2: env.API2 },
});
return app.fetch(req);
},
};
然后一个路线可以使用:
{ match: { hosts: ["api.example.com"] }, handlers: [
{ type: "forwarded_headers", options: {} },
{ type: "service_binding", options: { binding: "API2" } },
] }
向工人派遣平台
前往Cloudflare发送点名空间的普通工人的路线 脚本名称支持根据请求从已解决的主机替换的占位符 :
# 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(...)
}},
] }
可用的占位符 : {tenant_id}, (中文(简体) ), {custom_domain_id}, (中文(简体) ), {domain}, (中文(简体) ), {host}。前三个来自已解决的东道主记录; {host} 是请求 Host 头曰.
取代由上游来源的饼干和再定向
遗产 Sesamy 代理重写 Set-Cookie: Domain=upstream.example.com 和 3xx 数据 Location: https://upstream.example.com/… 信头因此指向公共自定义域. 等同于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" } },
],
}
(你可以针) upstream_host 编号 : upstream_origin 很明显,如果你不想自动推论。 )
动态路线(数据库支持)
生产时,将路线储存在数据库中并按要求解决. AuthHero 适配器包装船 a createProxyDataAdapter(db) 返回满分的助手 ProxyDataAdapter (中文(简体) ) resolveHost与他们的计划接通:
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),
});
这个 proxy_routes 表格带有标准的 AuthHero 迁移——通过 migrateToLatest(db) (或等同的细雨/锯子阶)已足.
遥控平面( HTTP 适配器)
如果代理运行与AuthHero控制平面(不同的工人,不同的VPC,不同的区域)分开,则使用 createHttpProxyAdapter 以读取 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 },
});
HTTP 适配器交换 client_id + 键 client_secret 用于在 ${baseUrl}/oauth/token,然后打电话 GET ${baseUrl}/api/v2/proxy/control-plane/hosts/:host. 控制平面必须使用系统范围证书(与租户代币分开)来暴露这一端点.
控制平面端点(服务器侧)
在您的 AuthHero 服务器中,选择通过线接解析器并指向
JWKS, 标志控制飞机无记名标志。 认证在
认证持证人JWT(RS/ES algs, iss === env.ISSUER范围
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),
},
});
KV 出版的读取复制品( 在光亮的 HTTP 适配器上推荐)
HTTP 适配器通过双跳认证调用读取路由(token mint +)
解决),每一次缓存出错。 为了把跳跃从热道上移出,有
控制平面 发布 每个解析出的主机块到 Cloudflare KV 命名空间,
并拥有代理 读取 与一个单一的, 未经认证 KV.get编辑
control-plane DB 保留了真理的来源;KV是已出版的已读复制品.
代理侧 —— 说 createKvProxyAdapter,将HTTP适配器保留为
迁移中错失/误差倒计时 :
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 });
控制平面 ——将适配器打包一次
wrapProxyAdaptersWithKvPublish (从) authhero将包裹的一对传给
管理API 和 createApplySyncEvents,然后种子现有主机
与 backfillProxyHostsToKv。 KV 命名空间必须生活在 不变
云浮账户 作为代理工人。 全文见出版物 * 种子 * 使用指南
文档中: 代理 ~ 形状 3b。 。 。 .
主机缓存
resolveHost 每一个请求都要求。 用内置的 stale- while- revalidate 缓存包起来 :
const app = createProxyApp({
data,
cache: {
freshTtlMs: 5 * 60_000,
staleTtlMs: 60 * 60_000,
negativeTtlMs: 30_000,
waitUntil: (p) => ctx.waitUntil(p),
},
});
云花岗工人,线条 ExecutionContext.waitUntil 与 AsyncLocalStorage 所以背景更新 幸存的反应.
自定义处理器
定义您的处理器类型 :
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 });
路线计划( Zod)
所有计划作为Zod计划再出口(proxyRouteSchema, (中文(简体) ), matchSchema, (中文(简体) ), handlerConfigSchema用于在您自己的管理 UI 中验证.
出口
createProxyApp(options)- 主Hono应用工厂createProxyDataPlaneRouter编号 :createProxyDataPlaneHandler- 数据平面管道createStaticProxyAdapter编号 :httpRoute——静态配置助手createHttpProxyAdapter- HTTP控制平面适配器createInMemoryHostCache编号 :createCacheAdapterHostCache——缓存层(createCacheApiHostCache也可以导出但为适应器包装而贬值)HandlerRegistry编号 :defineHandler编号 :registerBuiltinHandlers+ 每个内置处理器compileHostApp, (中文(简体) ),sortRoutes, (中文(简体) ),matchesHost, (中文(简体) ),matchesAnyHost, (中文(简体) ),buildMatchFilter
数据库支持的适配器生活在 AuthHero 适配器包中: @authhero/kysely-adapter, (中文(简体) ), @authhero/drizzle, (中文(简体) ), @authhero/aws-adapter。 。 。 .
许可证
麻省理工学院