fix(portal): 傳播空窗期不再銷毀 session——讀不到 ≠ 這個人不存在(arcrun-rag#66)

leo 2026-08-10 在 youlin stage 改完密碼被登出,之後怎麼登都回不去。
病灶是「這一瞬間讀不到」被當成「這個人不存在」,而且做的是**不可逆**的動作。

認證的家是 CF Workers Secret,改它會產生 worker 新版本,既有 isolate 讀到的
還是舊 env(#55 實測 ≥15 秒)。舊的 requirePortalUser 在那個空窗裡直接把
session 從 KV 刪掉——不是擋下讓你重試,是當場銷毀,等 secret 鋪開也回不來。
#55 補的「讀不到就再問一次加速器」只加在登入路徑(findAndVerifyUser),這道門沒有。

三處修法(缺一不可):

1. requirePortalUser 先問一次加速器再判定(與登入路徑同一招、同一支函式)
2. **永不因「讀不到」刪 session**;仍讀不到且正在傳播空窗 → 回 503
   `auth_store_propagating` 而不是 401(讀得到 record 的「已停用」照舊刪,那是確定的事實)
3. 前端 boot() 從「任何非 2xx 都 dropSession」收斂成**只有 401 才算被登出**
   ——後端不刪、前端卻自己丟掉 localStorage 的 token,症狀一模一樣

順帶修掉同一族的一個資料遺失路徑:mutateAuthStore 舊版拿「可能是舊版 env」當底稿做
read-modify-write,而 writeAuthStore 會重切分片並刪掉多出來的舊分片 ⇒ 底稿若是
「某帳號被建立之前」的版本,那個帳號會在這次寫入中被抹掉且無法還原(secret 是唯一真相源)。
改成先問加速器、再把 env 版與 overlay 版取聯集當底稿;刪除仍有效(fn() 在聯集之後才跑)。

驗證:tsc 與 baseline 同為 23 個既有錯誤(零新增);tests/portal-auth.test.ts 27/27 綠。
stage 實測見交付回報。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8vF2zS2XpaZjzkC75Fjss
This commit is contained in:
Claude
2026-08-10 13:22:34 +00:00
parent 035e8b255b
commit 417d69ceb3
3 changed files with 109 additions and 6 deletions
+20 -1
View File
@@ -821,7 +821,20 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
fetch(API_BASE + '/portal/session', { headers: authHeaders() })
.then(function (r) { return safeJson(r).then(function (d) { return { ok: r.ok, status: r.status, d: d }; }); })
.then(function (x) {
if (!x.ok) { dropSession(); return; }
// 🔴 arcrun-rag#66**只有 401 才算「你被登出了」**。
// 舊版是 `if (!x.ok) dropSession()` 任何非 2xx 都清掉 token——
// 包含改完密碼後那幾十秒的 503(認證 secret 正在鋪開)與 502(KBDB 暫時不可達)。
// 那正是 leo 08-10「改完密碼、重新整理就回不去」的最後一哩:
// 後端就算不刪 KV,前端自己把鑰匙丟了,結果一樣。
if (x.status === 401) { dropSession(); return; }
if (!x.ok) {
// 暫時性故障:留著 session,告訴他這是暫時的、下一步做什麼。
showAuth();
$('login-status').textContent = (x.d && x.d.code === 'auth_store_propagating')
? '認證資料正在更新中(通常幾十秒),請稍候重新整理——你並沒有被登出。'
: '服務暫時不可用,請稍後重新整理(你的登入沒有失效)。';
return;
}
S.profile = x.d;
showApp();
})
@@ -1185,6 +1198,12 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
}
// 任何 data 請求收到 401 → session 失效 → 回登入殼
//
// 🔴 arcrun-rag#66 的前端那一半:後端在「認證資料正在鋪開」時改回 503
// `auth_store_propagating`(不再回 401、也不再刪 KV 那筆 session)。
// 這裡**必須跟著只認 401**——若前端把任何錯誤都當登出,後端不刪也沒用:
// 使用者手上的 token 會被自己的瀏覽器丟掉,症狀跟被踢出去一模一樣。
// (503 由各呼叫點自己顯示錯誤訊息,session 原封不動。)
function guard401(status) {
if (status === 401) { dropSession(); return true; }
return false;