feat: WmailHub v1.0.0 —— Cloudflare Email Workers 邮件接收与转发平台(本地可用版本)
- Console:React 19 + antd 6.6.5 前端(登录 admin/admin)+ Hono 管理 API (Nodes/Domains/Mailboxes/Emails/Webhooks/Keys/OAuth/Admin) - Node:Bun 转发节点(HMAC 签名、有限重试、心跳、结构化日志、优雅退出) - Email Worker:Cloudflare email handler + 本地联调入口 - shared:HMAC 协议(docs/04)、wmh_ Key(32 位)、AES-GCM 加密、SSRF 防护 - 邮件管线:postal-mime 解析、附件、catch-all、限额、malformed 降级、TTL 1 天 - Webhook:签名投递、指数退避重试、SSRF 校验、投递日志 - API v1 + MCP(JSON-RPC,六工具,凭据 kind 隔离) - Cloudflare OAuth:真实 + 本地 sim 模式,自动安装管线(幂等,无需手工命令) - 测试:bun test 41 例(协议安全/管线/TTL/Webhook/SSRF/MCP/OAuth/鉴权)+ Playwright UI 冒烟 - LOGO/favicon 由 codex CLI 生成
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
/**
|
||||
* Cloudflare API 客户端抽象。
|
||||
*
|
||||
* - RealCFApi:真实调用 api.cloudflare.com / dash.cloudflare.com(OAuth access token)
|
||||
* - SimCFApi :本地 sim 模式。不访问外网,把"创建"的资源记录到 cf_sim_resources 表,
|
||||
* 让 Add Node 自动安装链路在本地完整可演示、可测试。
|
||||
*
|
||||
* Console 通过 env.CF_OAUTH_CLIENT_ID/SECRET 判断模式;两者齐备为 real,否则 sim。
|
||||
*/
|
||||
|
||||
import { renderEmailWorkerScript } from "../../../shared/src/index.ts";
|
||||
import type { Env } from "../env.ts";
|
||||
import { ids } from "../../../shared/src/index.ts";
|
||||
import { nowIso } from "../util.ts";
|
||||
|
||||
/** # 成员变量: OAuth 授权端点(real 模式) **/
|
||||
export const CF_OAUTH_AUTHORIZE_URL = "https://dash.cloudflare.com/oauth2/auth";
|
||||
|
||||
/** # 成员变量: OAuth token 端点(real 模式) **/
|
||||
export const CF_OAUTH_TOKEN_URL = "https://dash.cloudflare.com/oauth2/token";
|
||||
|
||||
/** # 成员变量: Cloudflare API 基址(real 模式) **/
|
||||
export const CF_API_BASE = "https://api.cloudflare.com/client/v4";
|
||||
|
||||
/** # 成员变量: OAuth 申请权限(docs/08:最小权限——账户与 Zone 读、Email Routing 读写、Workers 脚本写) **/
|
||||
export const CF_OAUTH_SCOPES = [
|
||||
"account:read",
|
||||
"zone:read",
|
||||
"email_routing:edit",
|
||||
"workers_scripts:edit",
|
||||
].join(" ");
|
||||
|
||||
/** # 成员变量: 邮件发现的 Cloudflare 资源描述 **/
|
||||
export interface CFAccount {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface CFZone {
|
||||
id: string;
|
||||
name: string;
|
||||
account_id: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
/** # 成员变量: Cloudflare API 客户端接口(自动安装所需操作) **/
|
||||
export interface CFApi {
|
||||
/** 用 code 换 access token(real);sim 直接签发假 token */
|
||||
exchangeToken(code: string, redirectUri: string): Promise<{ accessToken: string; expiresIn: number }>;
|
||||
/** 发现 token 可访问的账户 */
|
||||
listAccounts(accessToken: string): Promise<CFAccount[]>;
|
||||
/** 发现账户下的域名 Zone */
|
||||
listZones(accessToken: string, accountId: string): Promise<CFZone[]>;
|
||||
/** 创建或更新 Email Worker(幂等) */
|
||||
putEmailWorker(opts: {
|
||||
accessToken: string;
|
||||
accountId: string;
|
||||
scriptName: string;
|
||||
ingressUrl: string;
|
||||
ingressSecret: string;
|
||||
}): Promise<{ id: string }>;
|
||||
/** 开启 Zone 的 Email Routing(幂等) */
|
||||
enableEmailRouting(opts: { accessToken: string; zoneId: string }): Promise<{ enabled: boolean }>;
|
||||
/** 配置 Zone catch-all 规则指向 Email Worker(幂等) */
|
||||
setCatchAllRule(opts: {
|
||||
accessToken: string;
|
||||
zoneId: string;
|
||||
workerName: string;
|
||||
}): Promise<{ ok: boolean }>;
|
||||
}
|
||||
|
||||
/** # 函数
|
||||
* 根据环境构造 Cloudflare API 客户端(real 或 sim)
|
||||
*/
|
||||
export function getCFApi(env: Env, db: Env["DB"]): CFApi {
|
||||
const real = env.CF_OAUTH_CLIENT_ID && env.CF_OAUTH_CLIENT_SECRET && !env.WMAILHUB_SIM_CF;
|
||||
if (real) return new RealCFApi(env);
|
||||
return new SimCFApi(db);
|
||||
}
|
||||
|
||||
/** # 成员变量: 真实 Cloudflare API 实现 **/
|
||||
export class RealCFApi implements CFApi {
|
||||
/** # 成员变量 **/
|
||||
constructor(private env: Env) {}
|
||||
|
||||
async exchangeToken(code: string, redirectUri: string) {
|
||||
const resp = await fetch(CF_OAUTH_TOKEN_URL, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
grant_type: "authorization_code",
|
||||
code,
|
||||
client_id: this.env.CF_OAUTH_CLIENT_ID ?? "",
|
||||
client_secret: this.env.CF_OAUTH_CLIENT_SECRET ?? "",
|
||||
redirect_uri: redirectUri,
|
||||
}),
|
||||
});
|
||||
if (!resp.ok) throw new Error(`cloudflare token exchange failed: ${resp.status}`);
|
||||
const data = (await resp.json()) as { access_token: string; expires_in?: number };
|
||||
return { accessToken: data.access_token, expiresIn: data.expires_in ?? 3600 };
|
||||
}
|
||||
|
||||
private async api<T>(token: string, path: string, init?: RequestInit): Promise<T> {
|
||||
const resp = await fetch(`${CF_API_BASE}${path}`, {
|
||||
...init,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
...(init?.headers ?? {}),
|
||||
},
|
||||
});
|
||||
const data = (await resp.json()) as { success: boolean; errors?: { message: string }[]; result: T };
|
||||
if (!data.success) throw new Error(`cloudflare api ${path}: ${data.errors?.[0]?.message ?? resp.status}`);
|
||||
return data.result;
|
||||
}
|
||||
|
||||
listAccounts(token: string) {
|
||||
return this.api<CFAccount[]>(token, "/accounts");
|
||||
}
|
||||
|
||||
listZones(token: string, accountId: string) {
|
||||
return this.api<CFZone[]>(token, `/zones?account.id=${encodeURIComponent(accountId)}&per_page=50`);
|
||||
}
|
||||
|
||||
async putEmailWorker(opts: {
|
||||
accessToken: string;
|
||||
accountId: string;
|
||||
scriptName: string;
|
||||
ingressUrl: string;
|
||||
ingressSecret: string;
|
||||
}) {
|
||||
// Email Worker 即带 email handler 的 Worker 脚本;ingress 地址与密钥以明文常量打进脚本,
|
||||
// 避免给 Worker 配置 Console 凭据(docs/08:Worker 只持有 Node ingress secret)。
|
||||
const script = renderEmailWorkerScript(opts.ingressUrl, opts.ingressSecret);
|
||||
const form = new FormData();
|
||||
form.append(
|
||||
"metadata",
|
||||
new Blob([JSON.stringify({ main_module: "worker.js", bindings: [] })], { type: "application/json" }),
|
||||
);
|
||||
form.append("worker.js", new Blob([script], { type: "application/javascript+module" }));
|
||||
const resp = await fetch(
|
||||
`${CF_API_BASE}/accounts/${opts.accountId}/workers/scripts/${opts.scriptName}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: { Authorization: `Bearer ${opts.accessToken}` },
|
||||
body: form,
|
||||
},
|
||||
);
|
||||
if (!resp.ok) throw new Error(`upload email worker failed: ${resp.status}`);
|
||||
return { id: opts.scriptName };
|
||||
}
|
||||
|
||||
async enableEmailRouting(opts: { accessToken: string; zoneId: string }) {
|
||||
await this.api(opts.accessToken, `/zones/${opts.zoneId}/email/routing`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ enabled: true, skip_wizard: true }),
|
||||
});
|
||||
return { enabled: true };
|
||||
}
|
||||
|
||||
async setCatchAllRule(opts: { accessToken: string; zoneId: string; workerName: string }) {
|
||||
await this.api(opts.accessToken, `/zones/${opts.zoneId}/email/routing/rules/catch_all`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
matchers: [{ type: "all" }],
|
||||
actions: [{ type: "worker", value: [opts.workerName] }],
|
||||
enabled: true,
|
||||
name: "wmailhub catch-all",
|
||||
}),
|
||||
});
|
||||
return { ok: true };
|
||||
}
|
||||
}
|
||||
|
||||
/** # 成员变量: 本地 sim 实现:资源写入 cf_sim_resources,行为与真实链路一致 **/
|
||||
export class SimCFApi implements CFApi {
|
||||
/** # 成员变量 **/
|
||||
constructor(private db: Env["DB"]) {}
|
||||
|
||||
private async record(kind: string, refId: string, payload: unknown): Promise<void> {
|
||||
await this.db
|
||||
.prepare(
|
||||
"INSERT INTO cf_sim_resources (id, kind, ref_id, payload, created_at) VALUES (?, ?, ?, ?, ?) " +
|
||||
"ON CONFLICT(id) DO UPDATE SET payload = excluded.payload",
|
||||
)
|
||||
.bind(ids.simResource(), kind, refId, JSON.stringify(payload), nowIso())
|
||||
.run();
|
||||
}
|
||||
|
||||
async exchangeToken(_code: string, _redirectUri: string) {
|
||||
return { accessToken: "sim_" + ids.oauth(), expiresIn: 3600 };
|
||||
}
|
||||
|
||||
async listAccounts(_token: string): Promise<CFAccount[]> {
|
||||
// sim 固定一个演示账户;幂等记录
|
||||
const account = { id: "sim-account-0001", name: "WmailHub Sim Account" };
|
||||
await this.record("account", account.id, account);
|
||||
return [account];
|
||||
}
|
||||
|
||||
async listZones(_token: string, accountId: string): Promise<CFZone[]> {
|
||||
const zones = [
|
||||
{
|
||||
id: "sim-zone-example.test",
|
||||
name: "example.test",
|
||||
account_id: accountId,
|
||||
status: "active",
|
||||
},
|
||||
];
|
||||
for (const z of zones) await this.record("zone", z.id, z);
|
||||
return zones;
|
||||
}
|
||||
|
||||
async putEmailWorker(opts: {
|
||||
accessToken: string;
|
||||
accountId: string;
|
||||
scriptName: string;
|
||||
ingressUrl: string;
|
||||
ingressSecret: string;
|
||||
}) {
|
||||
// 与真实实现同一模板渲染,但只记录不外发
|
||||
await this.record("email_worker", opts.scriptName, {
|
||||
account_id: opts.accountId,
|
||||
ingress_url: opts.ingressUrl,
|
||||
script: renderEmailWorkerScript(opts.ingressUrl, opts.ingressSecret).length,
|
||||
});
|
||||
return { id: opts.scriptName };
|
||||
}
|
||||
|
||||
async enableEmailRouting(opts: { zoneId: string }) {
|
||||
await this.record("routing_settings", opts.zoneId, { enabled: true });
|
||||
return { enabled: true };
|
||||
}
|
||||
|
||||
async setCatchAllRule(opts: { zoneId: string; workerName: string }) {
|
||||
await this.record("routing_rule", opts.zoneId, {
|
||||
type: "catch_all",
|
||||
action: "worker",
|
||||
value: opts.workerName,
|
||||
});
|
||||
return { ok: true };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user