fix(auth): 全新安裝補上 CF_SECRETS_API_TOKEN 的缺口——第一個帳號建得起來了(arcrun-rag#99)

根因:D61(認證與資料分離,c4cee35)把 /console/setup、/portal/admin/bootstrap
的寫入全部改走 CF Workers Secrets(env.CF_SECRETS_API_TOKEN),但這把 token
從安裝那天起就沒被種過——07-29 已知缺口(pending-changes.md「credential 走
n8n 模式」),當時只降級某個功能;D61 之後升級成「連第一個帳號都建不起來」
的硬斷點,每台全新安裝必中(leo 本人+封測者實撞:裝得起來但卡在註冊)。

修法:putWorkerSecret/deleteWorkerSecret/authStoreWritable/writeAuthStore/
mutateAuthStore 新增可選的 tokenOverride 參數(呼叫端提供 > worker 自身 env)。
/console/setup、/portal/admin/bootstrap 讀取 x-arcrun-install-token 表頭,
只有安裝精靈(裝機當下手上有一把自己還有效的 OAuth token,workers-scripts.write
scope,同一把已用於 putWorkerSecretDirect/seedCredential)會帶這個表頭;
一般使用者自己在瀏覽器操作不受影響。沿用既有「D36 安裝器代寫」precedent,
不是新開一條路;bootstrap 本身已被「已有 admin → 409」擋成只能成功一次,
不會被拿來反覆濫用。

測試:cypher-executor vitest 443/457(14 個既存失敗與本改動無關,已用
git stash 對照確認);新增 2 則直接證明「缺 token→502 auth_store_not_writable/
帶 token→200」。tsc --noEmit 無新增錯誤。已跑 build-worker-artifacts.mjs
重打 tier2 bundle 供驗證(工作區未 commit 前提下的本地驗證版)。

未完成:安裝器(products/arcrun-rag/installer/oauth-prototype/worker.js)
端的 x-arcrun-install-token 表頭傳遞已另外修好,但兩邊都還沒部署——需要
①重打正式 worker artifact ②install.arcrun.dev 的安裝器 wrangler deploy
③已卡住的封測者要再走一次安裝精靈讓他的 cypher worker 拿到新 bundle。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
uncle6me-web
2026-08-14 11:46:56 +08:00
parent c3856470ad
commit 88f308642e
7 changed files with 160 additions and 46 deletions
+12 -3
View File
@@ -367,7 +367,12 @@ interface CreateUserInput {
* 寫入路徑未就緒就誠實拋錯(AuthStoreWriteError → 502),不偷偷退回舊家——
* 退回去等於這個帳號下次搬資料時又會不見,那正是本案要根治的病。
*/
async function createPortalUser(env: Bindings, input: CreateUserInput): Promise<string> {
/**
* `installToken`2026-08-14arcrun-rag#99):只有 `/portal/admin/bootstrap`(安裝精靈那條路)
* 會傳這個值——見 `lib/portal-auth-store.ts authStoreWritable` 的完整說明。`/portal/admin/users`
* 這條「管理員事後手動加人」路徑不傳,行為不變(仍要 `env.CF_SECRETS_API_TOKEN` 就緒)。
*/
async function createPortalUser(env: Bindings, input: CreateUserInput, installToken?: string): Promise<string> {
const now = new Date().toISOString();
const id = newAuthUserId();
await mutateAuthStore(env, (data) => {
@@ -382,7 +387,7 @@ async function createPortalUser(env: Bindings, input: CreateUserInput): Promise<
created_at: now,
updated_at: now,
});
});
}, installToken);
return id;
}
@@ -1073,13 +1078,17 @@ portalRouter.post('/portal/admin/bootstrap', (c) =>
if (password.length < 8) return c.json({ error: '密碼至少 8 碼' }, 400);
if (await findUserRecordId(c.env, email)) return c.json({ error: '此 email 已存在' }, 409);
// arcrun-rag#99:安裝精靈裝機當下把自己還有效的 OAuth token 隨這個表頭遞來(一般用戶
// 自己在瀏覽器完成 bootstrap 不會帶這個表頭,行為與今天完全一樣)。只有這條「建立第一個
// admin」的路徑吃它——bootstrap 已經被上面的「已有 admin → 409」擋成只能成功一次,
// 不會被拿去反覆濫用;見 lib/portal-auth-store.ts authStoreWritable 的完整說明。
const recordId = await createPortalUser(c.env, {
email,
display_name: displayName,
role: 'admin',
libraries: ['*'], // bootstrap admin 預設全庫(design §3.3["*"]=不注 library filter
password_hash: await hashPassword(password),
});
}, c.req.header('x-arcrun-install-token'));
return c.json({ success: true, record_id: recordId, email, role: 'admin' });
}),
);