refactor: v2.2.0 移除本地 sim 模式,Cloudflare OAuth 只走真实授权
- 删除 SimCFApi / WMAILHUB_SIM_CF / isRealOAuth / POST /api/oauth/cloudflare/sim/approve - 删除前端 OAuthSim 模拟授权页与 /oauth/sim/authorize 路由;账号页「添加账号」整页跳 Cloudflare - 迁移 0005 删除 cf_sim_resources 表 - 未配置 CF_OAUTH_CLIENT_ID/SECRET 时 start/callback 一律 503(不再降级出假账号假资源) - 修 OAuth 回调端点:跨站重定向不携带 SameSite=Strict 会话 cookie,改为由一次性 state 承担 CSRF 防护 - 测试改用 tests/cf-mock.ts 在 fetch 层拦截 Cloudflare API,断言真实 HTTP 调用(72/72 通过) - 本地 dev 无账号时只拉起 Console 并提示人工授权,不再自动造账号
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* Cloudflare API 抓包桩(仅测试用):
|
||||
* 拦截 globalThis.fetch,对 dash.cloudflare.com / api.cloudflare.com 返回固定响应,
|
||||
* 并记录真实发出的 HTTP 调用。生产代码里没有任何模拟实现——接入链路跑的始终是
|
||||
* RealCFApi,测试只是在传输层把它要访问的 Cloudflare 换成本地桩。
|
||||
*/
|
||||
|
||||
/** # 成员变量: 记录到的 Cloudflare API 调用 **/
|
||||
export interface CfCall {
|
||||
/** 请求方法 **/
|
||||
method: string;
|
||||
/** 完整 URL **/
|
||||
url: string;
|
||||
/** 路径(去掉 base) **/
|
||||
path: string;
|
||||
/** Authorization 头(OAuth Bearer token) **/
|
||||
authorization: string | null;
|
||||
/** 请求体文本(URL 编码或 JSON);multipart 上传记 null **/
|
||||
body: string | null;
|
||||
}
|
||||
|
||||
/** # 成员变量: 桩的返回数据配置 **/
|
||||
export interface CfMockOptions {
|
||||
/** /accounts 返回的账户列表 **/
|
||||
accounts?: { id: string; name: string }[];
|
||||
/** /zones 返回的域名列表 **/
|
||||
zones?: { id: string; name: string; account_id: string; status: string }[];
|
||||
/** OAuth token 端点签发的 access token **/
|
||||
accessToken?: string;
|
||||
/** token 端点 HTTP 状态(测失败分支用) **/
|
||||
tokenStatus?: number;
|
||||
}
|
||||
|
||||
/** # 成员变量: Cloudflare API 桩句柄 **/
|
||||
export interface CfMock {
|
||||
/** 已记录的调用 **/
|
||||
calls: CfCall[];
|
||||
/** 恢复原始 fetch **/
|
||||
restore(): void;
|
||||
/** 是否发生过指定 method + 路径子串的调用 **/
|
||||
hit(method: string, pathPart: string): boolean;
|
||||
/** 取出第一条匹配调用的 body **/
|
||||
bodyOf(method: string, pathPart: string): string | null;
|
||||
}
|
||||
|
||||
const TOKEN_URL = "https://dash.cloudflare.com/oauth2/token";
|
||||
const API_BASE = "https://api.cloudflare.com/client/v4";
|
||||
|
||||
/** # 函数
|
||||
* 安装 Cloudflare API 桩:接管 globalThis.fetch,返回记录与恢复句柄
|
||||
* 参数: opts.accounts/zones 决定发现到的资源,tokenStatus 可模拟 token 兑换失败
|
||||
*/
|
||||
export function installCloudflareMock(opts: CfMockOptions = {}): CfMock {
|
||||
const accounts = opts.accounts ?? [{ id: "cf-acct-real-1", name: "Real Cloudflare Account" }];
|
||||
const zones = opts.zones ?? [
|
||||
{ id: "zone-example", name: "example.com", account_id: accounts[0]?.id ?? "", status: "active" },
|
||||
{ id: "zone-demo", name: "demo.com", account_id: accounts[0]?.id ?? "", status: "active" },
|
||||
];
|
||||
const accessToken = opts.accessToken ?? "cf-oauth-access-token-xyz";
|
||||
const calls: CfCall[] = [];
|
||||
const original = globalThis.fetch;
|
||||
|
||||
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
||||
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
||||
const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
|
||||
const headers = new Headers(
|
||||
init?.headers ?? (input instanceof Request ? input.headers : undefined),
|
||||
);
|
||||
const rawBody = init?.body;
|
||||
const body =
|
||||
typeof rawBody === "string"
|
||||
? rawBody
|
||||
: rawBody instanceof URLSearchParams
|
||||
? rawBody.toString()
|
||||
: null;
|
||||
|
||||
calls.push({
|
||||
method,
|
||||
url,
|
||||
path: url.startsWith(API_BASE) ? url.slice(API_BASE.length) : url,
|
||||
authorization: headers.get("authorization"),
|
||||
// multipart 上传(Email Worker 脚本)读不出文本,只记方法与路径
|
||||
body: rawBody instanceof FormData ? null : body,
|
||||
});
|
||||
|
||||
const json = (result: unknown, status = 200) =>
|
||||
new Response(JSON.stringify({ success: true, errors: [], messages: [], result }), {
|
||||
status,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
|
||||
if (url === TOKEN_URL) {
|
||||
if ((opts.tokenStatus ?? 200) !== 200) {
|
||||
return new Response(JSON.stringify({ error: "invalid_grant" }), { status: opts.tokenStatus });
|
||||
}
|
||||
// OAuth token 端点是裸 JSON(不是 Cloudflare API 的 {success,result} 信封)
|
||||
return new Response(JSON.stringify({ access_token: accessToken, expires_in: 3600, token_type: "bearer" }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
if (url.startsWith(`${API_BASE}/accounts`)) return json(accounts);
|
||||
if (url.startsWith(`${API_BASE}/zones`)) {
|
||||
if (url.includes("/email/routing/rules/catch_all")) return json({ id: "catch-all-rule" });
|
||||
if (url.includes("/email/routing")) return json({ enabled: true });
|
||||
return json(zones);
|
||||
}
|
||||
if (url.includes("/workers/scripts/")) return json({ id: url.split("/").pop() });
|
||||
return json(null, 404);
|
||||
}) as typeof globalThis.fetch;
|
||||
|
||||
return {
|
||||
calls,
|
||||
restore: () => {
|
||||
globalThis.fetch = original;
|
||||
},
|
||||
hit: (method, pathPart) =>
|
||||
calls.some((c) => c.method === method.toUpperCase() && c.path.includes(pathPart)),
|
||||
bodyOf: (method, pathPart) =>
|
||||
calls.find((c) => c.method === method.toUpperCase() && c.path.includes(pathPart))?.body ?? null,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user