feat(oauth): 账号名改从 zone.account 取,去掉 account-settings.read 依赖

实测确认 GET /zones 每个 zone 自带 account {id,name},该接口只需 zone.read,
因此账号 id 与账号名都不必再调 GET /accounts(account-settings.read)。
顺带修掉 account_id 旧字段在当前响应里已为 null 导致的降级路径取不到账号 id。

- RealCFApi.discover 只列 /zones,移除 listAccounts 与 accountSource
- scope 候选集去掉 account-settings.read;设置页移除「含账号名权限」按钮
- 设置页 Scope 说明与 docs/02、README 同步(补 User Details Read 的区别说明)
- 85/85 测试通过(新增 zone 带 account / 不带 account 两种断言)
This commit is contained in:
bigcode
2026-09-30 17:03:59 +08:00
parent 7a1625d575
commit 5b887908e2
8 changed files with 82 additions and 88 deletions
+7 -20
View File
@@ -21,10 +21,8 @@ export interface CfCall {
/** # 成员变量: 桩的返回数据配置 **/
export interface CfMockOptions {
/** /accounts 返回的账户列表 **/
accounts?: { id: string; name: string }[];
/** /zones 返回的域名列表 **/
zones?: { id: string; name: string; account_id: string; status: string }[];
/** /zones 返回的域名列表;每个 zone 自带 account(与真实 Cloudflare 响应一致) **/
zones?: { id: string; name: string; account_id: string; status: string; account?: { id: string; name: string } }[];
/** OAuth token 端点签发的 access token **/
accessToken?: string;
/** token 端点 HTTP 状态(测失败分支用) **/
@@ -35,8 +33,6 @@ export interface CfMockOptions {
scopesAllowed?: string[];
/** 授权端点固定错误(走 Cloudflare 自己的 /oauth/error 页,不弹回回调地址) **/
authorizeError?: string;
/** /accounts 返回 403(模拟只授权了 zone 读、没有账号级读权限) */
failAccounts?: boolean;
}
/** # 成员变量: Cloudflare API 桩句柄 **/
@@ -58,13 +54,14 @@ const API_BASE = "https://api.cloudflare.com/client/v4";
/** # 函数
* 安装 Cloudflare API 桩:接管 globalThis.fetch,返回记录与恢复句柄
* 参数: opts.accounts/zones 决定发现到的资源,tokenStatus 可模拟 token 兑换失败
* 参数: opts.zones 决定发现到的资源,tokenStatus 可模拟 token 兑换失败
*/
export function installCloudflareMock(opts: CfMockOptions = {}): CfMock {
const accounts = opts.accounts ?? [{ id: "cf-acct-real-1", name: "Real Cloudflare Account" }];
const account = { id: "cf-acct-real-1", name: "Real Cloudflare Account" };
// zone 自带 account(真实 Cloudflare 就在 zone 上附带),账号名因此无需账号级读权限
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" },
{ id: "zone-example", name: "example.com", account_id: account.id, status: "active", account },
{ id: "zone-demo", name: "demo.com", account_id: account.id, status: "active", account },
];
const accessToken = opts.accessToken ?? "cf-oauth-access-token-xyz";
const calls: CfCall[] = [];
@@ -134,16 +131,6 @@ export function installCloudflareMock(opts: CfMockOptions = {}): CfMock {
headers: { "Content-Type": "application/json" },
});
}
if (url.startsWith(`${API_BASE}/accounts`)) {
// 只拦账户列表本身;workers/scripts 上传同为 /accounts 前缀,不应受影响
if (opts.failAccounts && url === `${API_BASE}/accounts`) {
return new Response(
JSON.stringify({ success: false, errors: [{ code: 9109, message: "Invalid access token" }], result: null }),
{ status: 403, headers: { "Content-Type": "application/json" } },
);
}
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 });