v2.10.0: 邮件链路 401 修复 + 一账号一 token + 重装 Worker 按钮——workerAuth 同名脚本撞车时按候选逐一验签确定归属(修 itekmedia 收不到件);凭据 token(credential id)烤进 Worker 脚本、Console 按 token 精确鉴权(脚本名仅作旧版兼容);账号详情新增「重新安装 Worker 脚本」

This commit is contained in:
bigcode
2026-10-05 02:04:13 +08:00
parent 1c7dba025b
commit 2dede46509
19 changed files with 242 additions and 68 deletions
+55 -1
View File
@@ -132,6 +132,60 @@ describe("POST /internal/email/authorize 决策矩阵", () => {
expect(((await resp.json()) as Decision).decision).toBe("accept");
});
test("同名 worker 跨账号:凭据按签名归属,不得只取最新一条(v2.9.3 回归)", async () => {
const t = await makeTestEnv();
const now = new Date().toISOString();
// 第二个账号部署了与默认账号同名的 worker 脚本,凭据创建更晚
await t.env.DB.prepare(
"INSERT INTO cloudflare_accounts (id, name, cloudflare_account_id, worker_id, status, provision_steps, created_at, updated_at) VALUES ('acct_later', 'later-account', 'cfacct-2', ?, 'active', '{}', ?, ?)",
)
.bind(t.workerId, now, now)
.run();
const laterSecret = "whs_later_account_secret";
await t.env.DB.prepare(
"INSERT INTO worker_credentials (id, cloudflare_account_id, worker_id, secret_hash, secret_enc, secret_last4, status, created_at) VALUES ('wcred_later', 'acct_later', ?, ?, ?, ?, 'active', ?)",
)
.bind(t.workerId, await sha256Hex(laterSecret), await encryptSecret(t.env, laterSecret), laterSecret.slice(-4), now)
.run();
await t.env.DB.prepare(
"INSERT INTO domains (id, cloudflare_account_id, account_id, name, status, created_at, updated_at) VALUES ('dom_later', 'cfacct-2', 'acct_later', 'later.test', 'active', ?, ?)",
)
.bind(now, now)
.run();
await t.env.DB.prepare(
"INSERT INTO mailboxes (id, domain_id, address, status, is_catch_all, created_at, updated_at) VALUES ('mbx_later', 'dom_later', '*@later.test', 'active', 1, ?, ?)",
)
.bind(now, now)
.run();
// 先接入账号的 secret 签名(身份=脚本名,兼容路径) -> 必须验到先接入的凭据上(v2.9.3 前被最新凭据压住而 401)
const first = await authorize(t, "hello@example.test");
expect(first.status).toBe(200);
expect(first.body.decision).toBe("accept");
// 后接入账号的 secret 签名(身份=脚本名,兼容路径) -> 归属到后接入账号,它自己的域名 accept
const later = await t.app.fetch(
await signedAuthorizeRequest(t, "hello@later.test", "a@b.c", { workerId: t.workerId, secret: laterSecret }),
t.env,
);
expect(later.status).toBe(200);
expect(((await later.json()) as Decision).decision).toBe("accept");
// v2.10.0 一账号一 token:身份=凭据 id,精确匹配;token 钉死凭据,错 secret 直接 401
const tokenFirst = await t.app.fetch(
await signedAuthorizeRequest(t, "hello@example.test", "a@b.c", { workerId: "wcred1", secret: "wrong-secret" }),
t.env,
);
expect(tokenFirst.status).toBe(401);
const tokenLater = await t.app.fetch(
await signedAuthorizeRequest(t, "hello@later.test", "a@b.c", { workerId: "wcred_later", secret: laterSecret }),
t.env,
);
expect(tokenLater.status).toBe(200);
expect(((await tokenLater.json()) as Decision).decision).toBe("accept");
});
test("畸形收件人 -> reject MAILBOX_NOT_FOUND(不抛 500)", async () => {
const t = await makeTestEnv();
for (const bad of ["no-at-sign", "@example.test", "a@"]) {
@@ -170,7 +224,7 @@ function makeWorkerEnv(opts: FakeConsoleOptions): WorkerEnv {
}) as typeof fetch;
return {
CONSOLE_URL: "http://fake-console",
WORKER_ID: "wmailhub-email-test",
CREDENTIAL_TOKEN: "wcred_test_token",
WORKER_SECRET: "whs_fake_secret",
fetchImpl,
};
+1 -1
View File
@@ -140,7 +140,7 @@ export async function loginCookie(test: TestEnv): Promise<string> {
export interface SignOverrides {
/** 换用错误 secret(签名必然不匹配) **/
secret?: string;
/** 声明其它 worker_id **/
/** 请求方声明的身份(token=凭据 id,或旧版 Worker 的脚本名) **/
workerId?: string;
/** 篡改 Authorization */
tamperSignature?: boolean;
+21
View File
@@ -0,0 +1,21 @@
/** 重新安装 Worker 脚本端点测试(v2.10.0):权限/前置校验,不轮换凭据 */
import { describe, expect, test } from "bun:test";
import { authedFetch, loginCookie, makeTestEnv } from "./helpers.ts";
describe("POST /api/accounts/:id/reinstall-worker", () => {
test("不存在的账号 -> 404", async () => {
const t = await makeTestEnv();
const cookie = await loginCookie(t);
const resp = await authedFetch(t, cookie, "http://localhost/api/accounts/acct_no_such/reinstall-worker", { method: "POST" });
expect(resp.status).toBe(404);
});
test("无 Cloudflare 连接凭据 -> 400(不触碰 Cloudflare,不留半截状态)", async () => {
const t = await makeTestEnv();
const cookie = await loginCookie(t);
// 种子账号有 worker_id 但没有 oauth_connections/token -> 应明确报错而非 500
const resp = await authedFetch(t, cookie, `http://localhost/api/accounts/${t.accountId}/reinstall-worker`, { method: "POST" });
expect(resp.status).toBe(400);
expect(((await resp.json()) as { error: string }).error).toContain("no active Cloudflare connection");
});
});
+13
View File
@@ -82,6 +82,19 @@ await page.waitForSelector("text=域名级 Webhook", { timeout: 8000 });
check("域名详情抽屉(Email Workers + Webhook 配置)", true);
await page.screenshot({ path: `${SHOT_DIR}/07-domain-drawer.png` });
await page.keyboard.press("Escape");
await page.waitForTimeout(400);
// 4b. 账号详情抽屉:「重新安装 Worker 脚本」按钮(v2.10.0)
await page.goto(`${BASE}/accounts`, { waitUntil: "domcontentloaded" });
await page.waitForSelector(".ant-table-row", { timeout: 8000 });
await page.locator(".ant-table-row").first().click();
await page.waitForSelector(".ant-drawer-open", { timeout: 8000 });
const hasReinstall =
(await page.locator(".ant-drawer button:has-text('重新安装 Worker 脚本')").count()) > 0;
check("账号详情抽屉含「重新安装 Worker 脚本」按钮", hasReinstall);
await page.screenshot({ path: `${SHOT_DIR}/07b-account-drawer.png` });
await page.keyboard.press("Escape");
await page.waitForTimeout(400);
// 5. 邮件详情(点击第一行)
await page.goto(`${BASE}/inboxs`, { waitUntil: "domcontentloaded" });