跳至主要内容

RFC 2026-10-24-mock-session-access-issuance:Mock Session的Access發行與principal查核接縫

implemented/2026-10-25(接受決策:2026-10-24)/設計 revision 7/scope=web。框架維護者接受方案 A,與 Server RFC 配對共同交付;目標 Web 19.0.0-alpha.136/Server 4.0.0-alpha.39。設計與 runtime 實作人審均已通過;配對 artifact 已發布,應用採用另行追蹤。

本輪實作狀態​

2026-10-25:框架 runtime 實作已通過第 3 輪人審,審閱識別 20261024-session-runtime-implementation、版本 r3-e496f38f4c412908;審閱者 ada@leandev.io 已確認 5 條意見閉合,結案依據見本稿 Decision Log。Web 19.0.0-alpha.136/Server 4.0.0-alpha.39 已配對發布,遠端套件已核對;本稿更新為 implemented,發布證據見配對發布紀錄。來源應用與參考應用均未自動切換 profile,產品接線與 UAT 另行完成。採用接線見 Web 指南及 Server 指南。

本輪實作審閱補正持有證明:replacement 與 Cookie-only logout 都以結構合法 Cookie 內的高熵秘密 selector 定位 family(legacy 無 selector 時要求完整 credential hash 相符)。因此目前 verifier、已輪替的舊 verifier、同秘密 selector 但未見過的 verifier,在這兩種操作都證明持有同一 family;expectedFamilyId 只做 fence,公開 family UUID 本身不是證明。新 proof 完成且新 family 建立成功後才撤舊,失敗須 rollback。refresh 另要求 current verifier;帶同秘密 selector 的非目前 verifier 仍走 replay 拒絕/撤銷,不能取得新 Access。

以下「現況差異」、查核缺口表及原始碼基準保留設計審議當時的證據;其「待實作」描述屬歷史基準。現行框架交付以本節及配對發布紀錄為準,應用採用不能由框架發布推定完成。

摘要​

在@appfuse/appfuse-web/vite提供可選的Mock Access Session接縫,讓應用在完成Email OTP等業務驗證後,取得由框架產生的短效Access及既有原生HttpOnly Refresh Cookie;MSW以另一個公開接縫查核Access principal。應用不再自行拼JWT、解析自造token、保存Refresh metadata或處理rotation。

既有mockAuthPlugin({cookieName})及__mock-session/issue|refresh|replace|revoke維持原行為。新能力明確opt-in、開發專用;正式JWT、金鑰、OTP安全儲存與郵件投遞仍不在此提案。框架維護者已接受方案A;公開能力已隨上述配對版本實作與發布。

本提案以執行模式界定影響:web-mockup與web只要在Vite下啟用MSW,就會消費同一類Mock接縫;部署至Host且啟用MSW時,Native Session改由Java adapter承接;關閉MSW才走正式Server。revision 7維持兩框架共同交付,補齊正式peer-review第2輪指出的reconcile錯誤分流、舊分頁實際行為及replacement/discard取捨,以兩份關聯RFC分別維護API。三環境框架契約實作與驗證已完成;產品須完成自己的接線與驗收後啟用,不能以框架 fixture 代替產品證據。

動機​

使用場景​

training-management應用的自助網站讓不常登入的學員輸入Email、收到六位驗證碼後登入;匿名報名驗證只是限用資格,不自動登入。一般登入成功要能hard reload恢復,同時保留固定Session截止、短效Access、換人隔離及Email更換後撤銷。

目前消費端的MSW handler在驗證密碼後,自行拼.mock JWT作Access,只有Refresh Cookie/metadata委派Vite plugin。目標應用已採不由業務handler自簽Token或複製Session引擎的界線,因此不能把OTP塞入密碼欄位,也不能直接複製舊token()當成新接線。框架若能接收已驗證的穩定subject、發行Access並提供principal查核,應用只需維護Email/帳號/業務用途及資源資料範圍。

目前的限制​

  1. 現有API只管理Refresh:已安裝alpha.132的mockAuthPlugin() options只有cookieName。POST /api/v1/auth/__mock-session/issue回201 {issued:true}及Cookie,refresh回{payload,remembered},不回Access。
  2. 升級未解決此接縫:在應用自身npm releases registry查得latest alpha.135;其lib/vite/mock-auth-plugin.ts、公開.d.ts及README與alpha.132完全相同。middleware SHA-256 7aa096c57bba150a2fd4d5b50429bc33ebad0bc34f1c4cd630d478ad453967d9。
  3. 目前繞道在應用承擔Token形狀(以下為泛化消費端路徑,不是框架檔案):web-mockup/src/mocks/handlers/authentication.ts的token()組Base64URL JSON+.mock,claims()自行判斷。這只是一個已存在的Prototype方式,不是正式安全能力;目標應用不希望繼續擴張。
  4. 不是缺少OTP輸入元件:框架components/form OtpInput、唯一HTTP client的authMode及被動refresh已可用。缺口是驗證後的Mock Access發行/principal查核,非Email寄送或OTP業務政策。

版本證據與可重播probe已實際驗證alpha.132的issue201、原生HttpOnly Session Cookie、refresh輪替、revoke及後續401;未宣稱消費端E2E或alpha.135獨立runtime測試通過。

版本證據與可重播probe​

查核對象是已發布的alpha.132與alpha.135。兩者的lib/vite/mock-auth-plugin.ts SHA-256皆為7aa096c57bba150a2fd4d5b50429bc33ebad0bc34f1c4cd630d478ad453967d9,dist/vite/mock-auth-plugin.d.ts SHA-256皆為73236df7f159eeefd7e37eef31339c1b13f07162181f552450993dbcb6f84cf8;公開options只有cookieName。alpha.135發布包integrity為sha512-Ti9SC4kVcxFB/2eS6dLc1oAmct5lyU2rJPguQ77y/EQ16Db7D+X037aeXV33j9VfZhZPmJTk6WElPzxcOLznkg==。

以下腳本在具已安裝vite與@appfuse/appfuse-web的模組目錄,以獨立臨時server執行。已在alpha.132重播通過;alpha.135的source/types等同不冒充其獨立runtime測試。腳本不輸出Cookie/Token,只證明目前Refresh middleware,不能當成新提案已實作。

node --input-type=module <<'JS'
import { createServer } from 'vite'
import { mockAuthPlugin } from '@appfuse/appfuse-web/vite'
import assert from 'node:assert/strict'
const server = await createServer({
configFile: false, root: '/tmp', appType: 'custom',
plugins: [mockAuthPlugin()], server: { host: '127.0.0.1', port: 0 },
})
try {
await server.listen()
const port = server.httpServer.address().port
const base = `http://127.0.0.1:${port}/api/v1/auth/__mock-session`
const send = (action, body, cookie) => fetch(base + '/' + action, {
method: 'POST', headers: { 'Content-Type': 'application/json', ...(cookie ? { Cookie: cookie } : {}) },
body: JSON.stringify(body),
})
const now = Math.floor(Date.now() / 1000)
const issue = await send('issue', {
payload: { sub: 'otp-probe-synthetic', tokenType: 'refresh', iat: now, exp: now + 43200 }, remembered: false,
})
assert.equal(issue.status, 201)
assert.deepEqual(await issue.json(), { issued: true })
const cookie = issue.headers.get('set-cookie')
assert.ok(cookie.includes('HttpOnly') && cookie.includes('SameSite=Lax') && !cookie.includes('Max-Age='))
const refreshed = await send('refresh', {}, cookie.split(';')[0])
assert.equal(refreshed.status, 200)
const summary = await refreshed.json()
assert.equal(summary.payload.sub, 'otp-probe-synthetic')
assert.ok(!('accessToken' in summary))
const nextCookie = refreshed.headers.get('set-cookie').split(';')[0]
assert.equal((await send('revoke', {}, nextCookie)).status, 200)
assert.equal((await send('refresh', {}, nextCookie)).status, 401)
console.log('Native Refresh probe passed; Access issuance remains unproven')
} finally { await server.close() }
JS

建議設計​

API​

以下名稱為單一web範疇的非破壞候選,型別從@appfuse/appfuse-web/vite公開;瀏覽器只用type import,不引入Node-side執行碼。Host adapter與正式Server需要的變更須由對應Server範疇決策承接;本稿列明承接要求與缺口,未批准其實作,也未承諾既有版本已有相同能力。

type MockClaim = string | number | boolean | null

interface MockAccessSessionsOptions {
enabled: true
maxAccessTtlSeconds?: number // default 900
maxSessionTtlSeconds?: number // default 43200; absolute lifetime, not idle timeout
issuanceReplayWindowSeconds?: number // default 86400; technical request replay only
}
interface MockAuthPluginOptions {
cookieName?: string // existing field, unchanged
accessSessions?: MockAccessSessionsOptions // omitted => new routes unavailable
}
interface MockAccessSessionIssueRequest {
requestId: string // non-secret, generated before this issuance intent
subject: string // application's verified stable userId
claims: Record<string, MockClaim> // no Email/PII/OTP/grant; reserved keys forbidden
accessTtlSeconds: number
sessionTtlSeconds: number
remembered: false // first scope: non-remembered only
}
interface MockAccessPrincipal {
subject: string
familyId: string // framework-generated, never a caller-selected family
claims: Record<string, MockClaim>
issuedAt: string // Instant with UTC offset
accessExpiresAt: string
sessionExpiresAt: string // fixed original hard deadline
}
interface MockAccessSessionIssued {
state: 'issued'
replayed: false
accessToken: string // framework-generated opaque mock credential
principal: MockAccessPrincipal
}
interface MockAccessSessionReplay {
state: 'issued'
replayed: true // no secret, no new Set-Cookie
}
type MockAccessSessionRevokeRequest =
| { scope: 'current'; expectedFamilyId: string }
| { scope: 'subject'; subject: string }

interface MockAccessSessionRefreshRequest {
expectedFamilyId?: string // omitted only for a clean bootstrap without any confirmed expectation
}

interface MockAccessSessionProblem {
type: string // urn:appfuse:error:{errorCode}
title: string
status: number // same as the HTTP response status
errorCode: string
detail?: string // human-readable; never used for branching
}
新增的Mock-only端點輸入/回應候選
POST /api/v1/auth/__mock-session/access-sessionsIssueRequest →201 Issued及原生Refresh Cookie;同鍵同內容200 Replay、不重發Token/Cookie;同鍵異內容409 request-key-conflict
POST .../access-sessions/refresh原生Refresh Cookie及RefreshRequest →200 Issued與輪替Cookie;已知family須帶expectedFamilyId,僅無expectation且無orphan時才用空body bootstrap。沿現有rotation/replay family revoke及穩定refresh-session-invalid401,不延長固定截止;replayed=false是本次refresh發行,不是新Session
POST .../access-sessions/inspectAuthorization: Bearer <access>、空body →200 MockAccessPrincipal;不回Token/Cookie,不以目前Cookie替代Bearer,不觸發refresh
POST .../access-sessions/revokeRevokeRequest;current僅在Cookie family符合expectedFamilyId時撤銷並清Cookie,有效credential但family不符時409且無副作用;refresh 的 verifier replay 另依原政策拒絕/撤銷。subject撤銷指定subject全部新模式families/Access,即使帶Cookie也不能降為只撤一筆。200 {revoked:true};subject只有在目前Cookie指向本次撤銷的新模式family時才清Cookie,其他subject或舊模式Cookie保持

命名保留既有__mock-session namespace,用access-sessions與舊Refresh-only流程區分;新模式不覆寫舊body/response。max值僅為Mock護欄預設,產品TTL仍由呼叫端明確提供;設定必須滿足issuanceReplayWindowSeconds≥maxSessionTtlSeconds,否則啟動期失敗;此消費端以900/43200作明確TTL示例,非所有應用共用的營運政策。issuanceReplayWindowSeconds是發行請求技術去重保留,不是Email、PII、報名或audit保存政策;應用原次結果仍自己維護。

claims不得含sub,subject,familyId,sessionId,sessionKind,actor,actorSubject,grantors,tokenType,iat,exp,issuedAt,accessExpiresAt,sessionExpiresAt,__proto__,prototype,constructor;TTL與設定皆須正安全整數,未知mode/scope拒絕,不接受默認降級。

Opaque Access避免引入JWT signing key或讓應用依賴Token形狀。已讀目前HTTP client:非JWT取不到exp時退回既有被動refresh,而非阻止登入。MSW先inspect再做應用owner/securityEpoch等查核,不能在產品端解析Token授權。本輪維持opaque A;Mock不涵蓋JWT exp驅動的主動refresh,該路徑由HTTP client既有測試與正式整合驗證,不把被動refresh通過冒稱主動路徑也已驗證。框架client的相容性不代表應用自己的JWT依賴已遷移,見跨環境對齊區段。

新模式的錯誤回應與adapter責任​

啟用accessSessions後,已辨識的新模式端點以application/problem+json回上述Problem,type與errorCode必須一致;title/detail不得用於程式分支。未啟用新模式時仍沿既有404處理,不改舊端點的{error}回應。

情境HTTP/errorCode
malformed JSON、缺/額外欄位、錯型別、非法TTL或claims、未知scope400/invalid-access-session-request
requestId相同但凍結內容不同409/request-key-conflict
refresh/current revoke的expectedFamilyId與已驗證目前family不同409/session-changed;不rotate、不撤銷、不Set-Cookie;不自動重送到另一family
採協調profile的Cookie mutation缺少/不符active protocol409/session-client-upgrade-required;不消耗proof、不修改family或Cookie,要求更新頁面
inspect未帶、未知、過期或已撤銷Bearer401/access-token-invalid
refresh缺少、未知、過期、撤銷、replay或模式不符的Cookie401/refresh-session-invalid
current revoke帶入可辨識的舊模式Cookie401/refresh-session-invalid,不撤銷也不清除舊Cookie
新模式已知端點使用非POST405/method-not-allowed,附Allow: POST
暫時依賴不可用503/mock-auth-unavailable
未預期程式錯誤500/internal-error

MSW native bridge須保留HTTP status與Problem欄位,讓應用原有API adapter與唯一HTTP client收到可分類的錯誤;不得先轉成遺失status/code的一般Error,也不得把transport/timeout/5xx轉成401或null。refresh callback沿既有type/errorCode分類;inspect的401只代表Access拒絕,由受保護請求既有流程處理,不直接宣告Refresh Session失效。

行為​

情境候選行為
未啟用accessSessions新路徑不處理/404,既有Refresh-only端點及Cookie行為完全不變
發行前的Email OTP驗證應用自己的flow/purpose/Email與安全版本驗證;plugin只接收已驗證subject,不寄信、不建立帳號
TTL與型別非空requestId/subject、整數TTL>0且不超config max,accessTTL≤sessionTTL;缺/額外欄位、錯型別及保留claims鍵400,不靜默截斷
Clock與hard deadline使用Node process單一Clock;初次issue凍結sessionExpiresAt=now+sessionTTL及該family的accessTTL;accessExpiresAt=min(now+accessTTL,原Session deadline)。截止exclusive,refresh不延長deadline;issuedAt為本次Access發行時間。測試以框架受控Clock注入測,不新增瀏覽器授權時鐘
同鍵重放/回覆遺失首次受理凍結subject/claims/TTL/remembered的內容fingerprint,claims鍵順序不影響等價性;同鍵只回無秘密摘要且不重新Set-Cookie,也不撤銷目前Cookie的Session。回覆遺失若Cookie已落地可明確refresh恢復,無Cookie時不得從摘要生成Session;應用沿原次結果與重新OTP驗證處理
撤銷後的發行重放replay窗口內同鍵同內容仍只回歷史Issued Replay摘要;不恢復family、不發Access、不Set-Cookie。state:'issued'描述歷次發行已發生,不能作目前Session仍有效的證據
requestId與重新登入新一次明確OTP登入用新鍵;24h技術重放窗口示例不續期。窗口後不再承諾原鍵重放或永久tombstone,應用須阻止未知舊意圖的自動重送;需要再登入時重新驗證並明確新鍵,不由查無推定原次未發行。process重啟全部Mock state一起重置
hard reload/不同分頁Native Cookie及metadata留在Vite process;access只留memory,重載後沿既有refresh恢復,不放Web Storage
Inspect只查框架產生、仍有效且未撤銷的Access;未知/過期/撤銷同形401 access-token-invalid;不回其他subject或秘密,不延長期限
refresh與舊Access新Access沿同family截止;已發行Access可用到自己期限,logout/subject revoke則立即拒絕全部相關Access,不靠等待15min
Cookie rotation真正重放沿框架既有replay family revoke,同步撤銷該family的Access;一般timeout/503不偽稱401
授權/故障MSW仍須核對可操作帳號、securityEpoch、Email版本及資源owner;依賴故障503停止,不信任單靠inspect曾成功的cache
process重啟/清理in-memory credential及operation一併失效;過期access/session/replay metadata回收,無跨process/production可靠性承諾

發行去重是框架內部技術request replay,不是通知事件等價政策、帳號合併或報名唯一性。OTP的一次消耗及驗碼結果不重發Session仍需應用process fixture協調;framework回覆摘要不可當新的OTP grant。

原生Cookie與晚回覆的支援邊界​

requestId去重只處理同一次發行意圖,不替不同登入意圖排序。目前HTTP client的Web Lock只協調refresh。本輪新增下方公開SessionCoordinator與Native bridge,統一序列化issue/refresh/revoke/identity-change;它保證同一Cookie scope的作業依鎖取得順序執行,不宣稱跨分頁的使用者意圖有全域時間排序。

應用adapter負責流程意圖識別、取消過時的排隊意圖與晚回資料隔離;丟棄JSON回覆不會撤回瀏覽器已處理的Set-Cookie。協調器須等原生請求drain後才放行下一筆Cookie修改;已知family的refresh/logout另帶expectedFamilyId,避免另一分頁已換人時仍對新family操作。跨分頁須使用同一Browser origin與同一協調契約;缺少Web Locks時僅支援明確選定的single-context模式。

協調能力由框架提供,應用不另建Refresh引擎、HTTP client或Token parser。新公開接縫尚未實作,配對artifact與接線驗證仍是啟用條件;這是實作完成條件,不再是尚未選定的設計替代方案。未接入協調器的並行Cookie修改不列入新模式支援範圍。

互動(若適用)​

本提案無UI元件或焦點行為。應用使用既有OtpInput/form/Dialog,切換身分與成功回饋依消費端自己的規格;Access/Refresh不顯示在DEV面板、URL、日誌或證據。

與既有行為的相容性​

面向相容策略
既有options/端點cookieName與原issue/refresh/replace/revoke保持;不替舊caller自動改發opaque Access
未提供新options新能力關閉,無舊caller遷移負擔
啟用新options但舊Session仍在模式不隱式互轉;新refresh不接受舊Refresh-only Cookie(401且無自動轉換),切換時顯示明確需重新登入;明確成功issue才可替換身分,見下方矩陣
options/body異常新options非法啟動期失敗;新端點非法body400;不改舊解析器的已接受輸入
API client沿唯一createHttpClient、authMode none/session-no-refresh/既有被動refresh;不新增本地攔截器或Token parser
Cookie原預設名稱/Path/HttpOnly/SameSite、non-remembered Session Cookie保留;舊相容覆寫不擴張
正式Server/production host不變;新Vite routes僅development,不能從此推論正式能力或自動替換Host契約

破壞性變更:否(預設關閉,新增模式)。啟用新模式的消費端需明確重新登入,不能保留舊Cookie冒稱已遷移;此為opt-in切換行為。消費端既有已接受紀錄保持歷史,不由RFC撤銷。

雙向Cookie與路由矩陣​

兩模式共用plugin的cookieName與Path=/api/v1/auth,同一瀏覽器在此Cookie scope只有一個目前credential。沿既有store擴充family模式標記(legacy-refresh/access-session)與新模式的絕對截止;內部Map數量與索引形式留到實作期決定。模式查核必須在rotation/replace等修改前完成,不能只靠新URL區分。

路由/目前Cookie行為
舊issue/refresh/replace/revoke+舊模式Cookie原body、response、remember policy與滑動期限維持;不產生新Access
新refresh+新模式Cookie僅輪替同family;沿凍結的accessTTL產生Access,絕對截止不延長
新refresh或current revoke+已驗證的新模式Cookie,但expectedFamilyId不符409 session-changed且無Cookie/store副作用;不得自動操作另一family
新refresh或current revoke+舊模式Cookie401 refresh-session-invalid;不輪替、不撤銷、不Set-Cookie
新refresh+缺少、未知或失效Cookie(可辨識舊模式除外)401 refresh-session-invalid並清Cookie;不發Access;若確認為新模式credential replay,先同步撤銷該family與全部Access
舊refresh或replace+新模式Cookie401,沿舊{error:'refresh-session-invalid'}格式;不輪替、不轉換、不Set-Cookie,不得滑動新模式截止或繞過Access失效
新issue+任一模式Cookie新requestId首次成功發行才原子撤銷Cookie所證明的目前family並建立新family;同帳號重登或換人均相同,被替換的新模式family全部Access同步失效。失敗、同鍵重放或409均不得替換目前Session;另一scope/裝置的family不連帶撤銷。正式Server以相同replacement契約承接,不只覆寫Cookie
舊issue+新模式Cookie明確的legacy重新登入;成功時原子替換目前family,同步撤銷其Access並發legacy Cookie;失敗不得先撤銷目前Session。維持舊{issued:true},不發新Access
舊revoke+新模式Cookie保留舊body的目前Cookie優先語意;撤銷該family時同步撤銷全部Access、清Cookie並沿舊{revoked:true}回應,不把subject改成新模式的顯式scope語意
舊revoke+無Cookie、帶subject沿既有subject撤銷語意處理該subject的families;觸及新模式family時同步撤銷Access。舊回應格式保持
新subject revoke+任一Cookie或無Cookie指定subject的全部新模式families/Access失效;只有目前Cookie屬於本次撤銷的新模式family才Set-Cookie清除,另一subject或legacy Cookie不動
新current revoke+缺少、未知或失效Cookie(可辨識舊模式除外)冪等200 {revoked:true}並清除無效Cookie;不推定其他subject,也不產生新Session

新inspect只看Bearer,不因上表目前Cookie的模式或身分改變查核對象。任何路徑實際撤銷/替換新模式family、確認Refresh credential replay,均須同步失效其Access;只有發行Replay摘要不觸發這些副作用。

Vite Mock/Host Mock/正式 Server 對齊​

核對基準與證據範圍​

本節是2026-10-24的本機原始碼與宣告版本核對,不是已發布artifact或跨環境執行通過的證明。讀取的框架與參考實作原始碼無工作樹差異,所在commit為1816762270b8ba5e6655ed82a6d2596349999a57;文件修訂不代表下列能力已交付。

對象來源/宣告版本本輪核對結果
Web框架appfuse-web/package.json:19.0.0-alpha.135;lib/vite/mock-auth-plugin.ts、lib/utils/http/http-client.ts、refresh-session-error.tsNative plugin仍是Refresh-only;HTTP client可對opaque Access使用被動refresh。原提交稿alpha.132/135的middleware指紋證據保留於上方
Server框架appfuse-server/gradle.properties:4.0.0-alpha.38;security/auth/mock/MockAuthSessionController.java、LoginService.java、SessionOptions.java、SessionPolicyBinding.java、RefreshSessionStore.java有Host Refresh-only adapter、正式JWT/Refresh引擎與policy deadline primitive;未看到新的Mock Access端點
參考前端app-office、app-office-mockup、app-console-mockup的package.json均宣告19.0.0-alpha.132已讀web/mockup共通adapter;本機框架source為alpha.135不代表這些消費端已升級;本輪未以各消費端實際安裝包跑整合測試
Host組裝app-office-host/.../mockauth/MockAuthConfiguration.javaapp.msw.enabled=true時明確組裝MockAuthSessionController.inMemory();Vite plugin不隨production build成為Host端點
正式參考Servertenant與tenantless的controller/auth/AuthController.java、controller/emailotp/EmailOtpController.java及service/emailotp/EmailOtpService.java;tenantless的AuthSessionResponseAssembler.java、RefreshSessionPolicy.java兩type皆有Email OTP驗證後交給共用引擎issue的接線;不能據此推定來源應用已選type/版本或其Email登入政策已匹配

security/auth/來源路徑相對appfuse-server/src/main/java/io/leandev/appfuse/;參考Server的controller/service路徑相對各模組的src/main/java/io/leandev/app/。來源應用的Server type、依賴版本、公開URL與實際政策仍待消費端選定,不以參考實作的存在代填。

設計基準為ADR-030的Session/錯誤分類/rotation原則,以及Bearer Token指南的產品接線。ADR索引仍將ADR-030列為提議中,原文則為已接受、分階段實作;本稿以原文並列原始碼證據,不把索引列當作實作證據。正式金鑰設定依ADR-033及現行取代註記,不沿用ADR-030已被取代的臨時key例外。

執行模式與責任邊界​

執行模式Session權威來源本RFC的作用目前判定
web-mockup或web,Vite+MSWVite process的Mock store;MSW承接產品API與業務資格本RFC提出新的opaque Access Session模式accepted;待實作,非只有web-mockup目錄會受影響
web,由Host托管且MSW開啟Java Host process的Mock adapter;同一前端MSW消費Native接縫須有與Vite新模式等價的Mock HTTP契約,或維持完整舊模式;不可只換前端adapterHost新接縫缺席,G1未完成
web,MSW關閉、串正式Server正式認證引擎、持久化Session與Resource Server核對產品可觀察行為與遷移;不讓正式登入接受Mock的已驗證subject body或呼叫__mock-session已有相關primitive;產品接線及整合證據未完成

三種模式可以使用不同Token形狀與儲存技術;要一致的是選定產品的登入結果、帳號/資源資格、期限、撤銷、錯誤分類及恢復行為。Mock控制端點不要求在正式Server公開;產品API由MSW adapter或正式controller分別承接,UI與service不依執行環境分岔出另一套Token引擎。

共同語意與現況差異​

命題Vite新模式候選Host Mock現況正式Server現況與對齊要求
登入/refresh/me產品回應Native Issued/Principal由MSW映射至該應用既有DTO原issue不回Access,refresh回payload/remembered,由MSW自行產Access參考controller/assembler回應用DTO,包括accessToken、expiresIn及登入user;不得直接把MockPrincipal當正式user。若產品要新增Session截止欄位,須另明定產品API,不能假設正式Server已回傳
Token形狀與UI身分opaque Access;inspect查有效性,UI身分由產品user/me投影仍依MSW生成的Mock JWT正式使用簽章JWT;HTTP client保留主動與被動refresh。應用不得依賴Mock Token解碼取得身分/授權,需盤點JWT到期工具的實際使用,G3未完成
登入資格/Email OTP應用先完成業務驗證才呼叫Native issue;refresh前及資源處理仍須重查目前帳號/版本與owner,拒絕時保留穩定錯誤語意資格由MSW處理,Native adapter不查產品帳號已有Email OTP service交給LoginService.issue;參考challenge以username定位帳號,不等同任意Email登入政策。來源應用仍須對齊Email/username映射、不可變subject與匿名驗證不建立Session的規則
hard reload/direct link原生Refresh Cookie+process metadata恢復;Access只在memory舊Refresh模式有頁面外metadata;新Access模式缺席正式Cookie+持久Session經refresh/me恢復;同一US需跨模式驗證相同使用者與資料範圍,Mock通過不能代替正式整合
固定Session與Access截止sessionTTL為絕對上限;Access截止不得超過Session截止現行refresh以idleTimeout滑動expiry一般SessionOptions為idleTimeout,refresh仍推進idle expiry;SessionPolicyBinding.validUntil已能限制family有效期,但一般公開issue/OTP接線未傳入此binding。需驗證公開承接方式及Access exp/資源拒絕邊界,不能宣稱已對齊,也不能斷言框架沒有絕對期限primitive,見G2
logout/subject revoke立即失效Access所有被撤銷新模式family的Access立即不可inspect;subject revoke不誤清其他Cookie目前只管理Refresh,Access仍在MSW;新模式撤銷契約缺席有Access的logout會blacklist其family;已讀Cookie-only logout測試則明確保留未blacklist的結果。subject全域撤銷與即時Access拒絕也須核對正式接縫;G4未完成,不把Mock較強行為當正式保證
錯誤分類新端點有穩定Problem;產品adapter保留status/type/errorCode舊Native回{error},由MSW映射產品錯誤共用client以Refresh 401要求重新認證,transport/5xx保留Session;正式揭露政策可不同。需驗證最終產品錯誤分類,不能只核對Native JSON相似
rotation/晚回Cookie沿strict rotation與既有refresh協調;issue/revoke重疊另受本文整合前提限制有舊strict rotation;不代表已有所有Cookie修改的協調能力正式strict/DEV限定reusable需按實際設定測試;共享Cookie只由受支援的Browser origin消費。新增Mock能力不證明三種模式的換人/登出/refresh晚回已隔離,見G5
process重啟in-memory重置,要求重新登入Host process重啟重置正式依持久store/keys的設計保持或明確撤銷;這是刻意的環境差異,不能靠固定帳號ID或搬移Mock credential消除

查核缺口的設計處置與交付責任​

下表的G1–G6源自revision 3查核;本輪已選定其設計處置,Java公開API見關聯Server RFC。設計稿完成與實作完成分開記;兩稿已配對accepted,目標Web 19.0.0-alpha.136/Server 4.0.0-alpha.39;實作與發布完成後才可標implemented。

ID待承接事項責任與完成證據
G1選定Vite/Host共同交付,Server新增具名opt-in factory與等價Native契約Server框架及Host組裝維護者實作;前端維護者以同一adapter跑兩環境golden cases。設計見D1;artifact/執行證據待完成
G2選定一般發行overload重用policy binding、裁切Access exp及產品timing投影Server框架提供公開能力;應用Server以單一產品policy接所有入口。設計見D2與Server RFC;實作/deadline測試待完成
G3選定共同產品DTO與sessionId/sessionExpiresAt投影,採opaque相容接線前端與應用Server移除自造Token及授權解碼依賴,盤點JWT到期工具;保留唯一apiClient,核對Email/username/subject。設計已明列;遷移及產品US驗證待完成
G4選定Cookie-only及subject撤銷都失效全部相關Access,以持久family狀態作權威Server框架補family lookup/subject revoke能力與opt-in Resource Server組裝。設計見D2;舊未blacklist測試保留基線,新profile另驗實際資源拒絕
G5選定框架公開SessionCoordinator與Mock bridge、原生drain及family fenceWeb框架提供SDK與共用HTTP client接線;應用只委派與保留意圖guard。設計見D3;Browser取消/晚回/context終止限制驗證待完成
G6選定兩框架配對發布清單與共同消費端切換閘門框架維護者填具體artifact版本與證據;應用維護者選type/pin並執行三環境驗證。版本數值是後續發布規劃,不由source版號猜填

遷移順序與驗證責任​

  1. 先定產品契約:由應用規格維護者確認non-remembered/固定截止、登入方式、Email變更、logout與subject撤銷、user投影及owner範圍;既有published規格若實際改變,沿其變更流程處理。Mock新增模式不自行改正式產品政策。
  2. 完成框架承接設計:Web框架維護者完成本Vite接縫;Server框架維護者完成G1/G2/G4的公開能力評估與必要決策;雙方確認G5協調接縫及G6配對版本。可重用既有primitive者優先重用,新增公開能力才進對應RFC。
  3. 再遷移消費端:前端維護者用同一產品adapter接Vite與Host的新模式;正式Server維護者保留正式認證入口與Resource Server,按相同產品契約接線。web-mockup/web依既有同步機制承接,不能只改一側或以專案本地Token引擎補缺口。新模式啟用前清楚要求重新登入,不搬移舊Cookie/metadata到新模式。
  4. 逐環境驗證:Vite middleware、Java Host adapter、正式Server引擎/HTTP、產品Browser整合各自保存來源版本、模式與結果。G1未完成的Host新模式必須標為blocked;正式Server未選type/pin不得標為已通過。原有模式繼續可用,不在缺少Host能力時靜默降級回應用自造Token。
  5. 完成配對驗收才宣稱整體就緒:Prototype、Host展示及正式整合分別依其實際依賴的G項目判定;Vite新模式的單獨驗證只證明Vite能力。任何一次RFC接受或套件發佈,都不自動推進應用的E2E/UAT階段。
驗證面維護者/現有入口應新增或核對的命題本輪證據狀態
Vite NativeWeb框架;lib/vite/mock-auth-plugin.spec.ts本文新舊模式矩陣、期限、Problem、Cookie及replay/revoke策略已補;新模式未實作/未執行
共用HTTP clientWeb框架;http-client.spec.ts、refresh-session-error.spec.tsopaque被動/JWT主動refresh、401與5xx分類、single-flight與跨分頁、G5新增協調責任已讀source;本輪未重跑runtime測試
Host NativeServer框架;MockAuthSessionControllerTest、MockRefreshSessionStoreTest;Host組裝維護者與Vite相同的Mock契約;servlet context-path/Secure/Cookie與新舊模式邊界現有入口是Refresh基線,新Access模式blocked(G1)
正式引擎/HTTPServer框架與選定type的應用Server;LoginServiceRefreshSessionTest、RefreshTokenAuthorityIT、RefreshSessionPersistenceStoreIT、RefreshSessionCookieTest及Email OTP API測試G2/G4、目前principal eligibility、產品DTO、正式授權/資料隔離;tenant與tenantless各依所選type驗證已讀部分source/既有測試斷言;未當作本輪執行通過
產品Browser旅程前端與應用Server維護者在Vite Mock、Host Mock、MSW關閉的正式Server三種模式,各驗登入/OTP、reload/deep link、期限、logout/subject revoke、錯誤恢復與晚回Cookie;後兩種模式另驗baseURL/context-path待接線與執行;Mock結果不替代正式授權或整合證據

本輪將設計選擇具體化如下;原先只針對Vite API的建議與revision 2確認問題,不作新版接受依據。尚未實作/測試通過不構成Deferred理由;維護者已接受完整配對設計稿,進入實作階段。

本輪設計裁決​

以下D1–D3已由框架維護者於2026-10-24配對接受;接受依據與目標版本見Decision Log。設計定案不代表runtime已完成。

D1:一個功能、兩scope文件、配對共同交付​

選擇Vite與Host Mock共同交付;Java公開API由Server RFC定義,兩稿雙向related_rfcs。兩框架版本可各自發布,但產品切換必須等Web+Server配對artifact、Host factory與適用的正式profile接線齊備。僅Vite完成可標為子項完成,不宣稱整個功能或Prototype部署已完成。

Host新增MockAuthSessionController.inMemoryWithAccessSessions(options);舊factory保持Refresh-only。新factory承接本稿的Native DTO、Problem、期限、模式矩陣、family fence與條件清Cookie,MSW adapter不因Vite/Host改寫Token生成邏輯。

D2:固定期限與即時撤銷採共同profile​

正式Server以受信任共用profile約束所有本地互動式發行,不能以漏傳binding/改app字串繞過;無新認證的代行進出亦繼承原截止,idle不早於hard deadline。正式Server保留JWT,Mock維持opaque;兩者共同規則為:初始絕對截止固定,Access截止不超過該截止,Cookie-only logout/subject revoke使全部相關Access失效。Server重用SessionPolicyBinding.validUntil,以新一般login/issue overload承接;產品設定由可信Clock建立deadline,不另起期限引擎。

正式profile新增持久family查核組裝,以family狀態作撤銷權威;blacklist是輔助,不能在cache停用/重啟時復活Access。每次互動式資源請求會增加store lookup;此成本與profile的明確opt-in列入Server RFC,不強制既有所有產品更改架構。family store不可用回503,未知缺陷500,不改成401。

採用profile的login/refresh DTO映射accessToken、實際expiresIn、sessionExpiresAt與非機密sessionId;UI身分仍由user/me投影,不解碼Mock Token。sessionId是操作fence,不能用作授權或當credential。正式發行與Mock兩種native DTO的內部命名可不同,由產品adapter映射共同欄位。

已知Session的產品refresh與logout傳X-Appfuse-Session-Family,先驗證credential再核對family;有效credential的family不符時409 session-changed且不Set-Cookie。真正replay仍依原政策處置,不能以偽造fence規避。reload優先使用持久的非機密expectation;current logout沒有已知family時先reconcile,不新增無fence reset旁門,也不將舊UI操作改套另一個登入者。

三環境同scope的新認證成功皆撤銷被Cookie替換的舊family及其Access;失敗不先撤銷。正式profile的refresh先檢查原family deadline/policyId,缺少或不符即401 refresh-session-invalid並清相符Cookie,不將舊Cookie升級或先回成功再讓資源401;啟動pending與deprecated JWT refresh入口也依Server稿封閉遷移旁路。

D3:框架公開協調器與Native bridge​

以下Browser公開API已隨配對版本發布,由@appfuse/appfuse-web/utils輸出;不從/vite引入Node runtime,也不在應用另寫mutex或Session引擎:

type SessionMutationKind = 'issue' | 'refresh' | 'revoke' | 'identity-change' | 'bootstrap'
interface SessionCookieScope {
apiOrigin: string
cookieName: string
cookiePath: string
}
interface SessionMutationLease {
readonly operationId: string // non-secret; SDK-owned lease
readonly kind: SessionMutationKind
readonly expectedFamilyId?: string
confirmSession(sessionId: string): void
confirmSignedOut(): void
}
type SessionReconciliation<T> =
| { state: 'authenticated'; sessionId: string; value: T }
| { state: 'signed-out' | 'reauthentication-required' | 'upgrade-required'; value?: T }
interface SessionCoordinator {
mutate<T>(
kind: Exclude<SessionMutationKind, 'bootstrap'>,
task: (lease: SessionMutationLease) => Promise<T>,
options?: { signal?: AbortSignal; expectedFamilyId?: string },
): Promise<T>
reconcile<T>(
bootstrap: (lease: SessionMutationLease) => Promise<SessionReconciliation<T>>,
): Promise<SessionReconciliation<T>>
}
declare function createSessionCoordinator(options: {
scope: () => SessionCookieScope
mode: () => 'mock' | 'server'
protocol: '1' // must match the scope's active protocol
crossTab?: 'required' | 'single-context' // default required
lockTimeoutMs?: number // default 60000; queue only
responseTimeoutMs?: number // default 15000; caller/UI deadline only
}): SessionCoordinator

interface MockAccessSessionClient {
withOperation<T>(operationId: string, handler: () => Promise<T>): Promise<T>
issue(input: MockAccessSessionIssueRequest, operationId: string):
Promise<MockAccessSessionIssued | MockAccessSessionReplay>
refresh(input: MockAccessSessionRefreshRequest, operationId: string):
Promise<MockAccessSessionIssued>
inspect(accessToken: string): Promise<MockAccessPrincipal>
revoke(input: MockAccessSessionRevokeRequest, operationId: string): Promise<void>
}
declare function createMockAccessSessionClient(options: {
baseURL: () => string
sessionCoordinator: SessionCoordinator
}): MockAccessSessionClient

// Proposed additions to the existing client/config types:
// HttpClientConfig.sessionCoordinator?: SessionCoordinator
// HttpRequestConfig.sessionLease?: SessionMutationLease
// AuthConfig.getSession?: () => { sessionId: string; accessToken: string } | null
// AuthConfig.refresh(signal, lease?: SessionMutationLease): Promise<AuthTokens | null>
// AuthTokens.sessionId?: string // required at runtime for the coordinated profile

一個scope、一份版本一致的協調契約。 page內共用一個coordinator;scope與mode於bootstrap完成後在排隊時快照,取得鎖時重新比對runtime設定,變更則送出前拒絕,不把舊意圖送到新API環境。跨頁面沿appfuse:refresh-session Web Lock namespace;同一Cookie scope全部context都須更新並opt-in,舊bundle/未協調context不能混跑drain profile。queue與response timeout各自計時。single-context用框架同頁序列化,明確不支援多分頁。

部署與協定版本隔離。 coordinator 的 protocol 固定為部署所選的 active protocol,本版為 '1';所有產品 Cookie mutation(包括初次登入與乾淨 bootstrap)送 X-Appfuse-Session-Protocol: 1,Mock Native client 也轉送。Vite/Host 與正式產品 controller 在任何 proof 消耗或副作用前核對;缺少/不符回409 session-client-upgrade-required,不修改 Cookie/family,UI 要求更新頁面,不當成 refresh 401。沿原 Web Lock namespace,不另開版本鎖讓兩版同時修改 Cookie。非機密 scope 狀態保存 protocol,取得鎖後版本不符即拒絕舊排隊意圖;不能靠 BroadcastChannel 握手宣稱偵測到所有舊 bundle。

首次切換先部署所有 mutation gate,再以配對版本啟用profile;舊無marker Access由資源filter回401,舊family Cookie即使由新頁bootstrap也須401清除並重新認證。舊頁缺少protocol的mutation於入口409,不讓舊client的逾時釋鎖影響新版Cookie。這只強制Server拒絕舊協定,沒有把已載入的舊JavaScript強制換版:目前舊bundle的refresh分類器只認401,收到409會向上拋錯且不因這次失敗清除Session或觸發重新認證;使用者可能仍看到原畫面/舊身分,操作持續失敗或由應用顯示一般錯誤,不保證看到更新提示。部署說明須明確要求使用者手動重新載入或關閉舊分頁;維運提供站外通報/支援指引,不能宣稱409已替舊頁實作更新UI。若產品要求自動更新提示,須先另行部署具此能力的過渡版並確認舊分頁退場,再切profile;本稿沒有這種機制的完成證據。

新版bundle才依session-client-upgrade-required進upgrade-required並提示重新載入;保持草稿與expectation、不偽裝登出或要求重新OTP、不自動循環reload。下一次協調器協定升級沿同一Cookie scope只允許一個active protocol,舊版header及持久狀態不符皆拒絕;具有更新提示能力的版本可顯示提示,沒有的版本仍須人工更新。新版本以reconcile建立自己的狀態,不接手舊排隊意圖。切換前已送出的作業仍須依drain/orphan與family fence處理,版本header不能撤回已送出的Set-Cookie。未opt-in的原有應用維持舊行為;採用profile後,涵蓋同scope的所有mutation入口是啟用前提。

可信身分快照。 新profile的AuthConfig.getSession提供同一次store讀取的sessionId/Access快照。refresh/revoke/identity-change在排隊時綁定expectedFamilyId,不能取得鎖後偷偷改成當時另一位使用者的family。HTTP client自動傳對應family header;回應與401重試前再次核對原請求family與目前Session,身分已改變時不把舊請求換成新使用者的Token重放。正常session請求不接受另一Session的手工Bearer覆寫;獨立認證流程使用明確authMode none。

避免重入。 login/logout/identity-change由應用service以mutate包一次產品API;請求帶SDK lease且authMode為none或session-no-refresh。帶lease的default session模式在送出前拒絕,不等待自己持有的鎖。自動refresh由共用HTTP client取lease後呼叫低階delegate,delegate不再呼叫mutate;其回傳AuthTokens.sessionId須與lease expectation相符。

登記整個MSW handler。 mode=mock時HTTP client才附非機密X-Appfuse-Session-Operation(只送已配置API origin);正式模式不送此header。MSW mutation handler進入後、任何await或業務副作用之前,先由Native client.withOperation登記整個handler生命週期。Native子請求在同lease登記至response/body處理完;即使產品XHR先逾時,尚在驗證proof的handler也不能未被追蹤就晚發Cookie。遲到handler若lease已結束,必須在業務/Native修改前拒絕。Native client只join不重入鎖,不建立第二個產品apiClient;它也核對refresh/current revoke的expectedFamilyId等於lease快照,不讓handler自選另一family;只有reconcile允許的乾淨bootstrap可缺省。server模式禁止呼叫它,inspect不改Cookie可在lease外執行。

取消與逾時採drain。 排隊取消不送請求。已開始作業由coordinator擁有transport;UI取消/response timeout先回報availability/結果不明,並禁止該lease再開始新的Cookie子請求;已送出者及已登記handler繼續drain。Web Lock的promise等待產品請求、handler與Native子請求全部settle才釋放,不能在Promise.race的UI期限到達時釋鎖,也不自動重發issue或舊Refresh。這是新opt-in profile對原ADR-030 callback逾時釋鎖行為的局部演進;接受時須記錄適用範圍,舊未opt-in client不直接改語意。

持久的是expectation,不是credential。 SDK在scope下保存非機密的generation、pending owner/operationId及已確認family expectation(known family/signed-out/unconfirmed)。不保存Token、Cookie、OTP、subject或user。storage不可用時回報能力不足,不能啟用此恢復契約。只有經產品成功回應、flow仍有效的lease.confirmSession才更新已確認family;current logout成功用confirmSignedOut。MSW Native的單獨成功不等於產品成功,不能自動認證。confirmSession/confirmSignedOut還須核對lease未被取消且generation相符;過期或UI已取消的結果不更新已確認expectation。

owner中斷有明確出口。 正常bootstrap與殘留marker都走reconcile,取得同一鎖並使先前排隊意圖失效。流程如下:

reconcile入口狀態允許的操作與完成點
已確認family A,包括reload後一次受控refresh帶expected A,再以產品me確認投影;200只接受相同A,更新expectation後清marker。refresh 401 refresh-session-invalid或409 session-changed轉reauthentication-required,保留草稿/提示且不自動採用另一family;409 session-client-upgrade-required則轉upgrade-required並提示更新頁面,不轉重新認證
從未有expectation、也無殘留作業可一次無fence的Cookie bootstrap,成功後確認sessionId及me再建立expectation;無Cookie/refresh 401 refresh-session-invalid為signed-out;409 session-changed為reauthentication-required,409 session-client-upgrade-required為upgrade-required,均不採用Cookie身分
signed-out、unconfirmed,或orphan但沒有已確認family不做無fence bootstrap;清掉已失去owner的排隊意圖,轉需新認證狀態,允許新的issue意圖通過同一coordinator;不再無限封鎖scope
已知暫時故障或目前owner尚在drain回availability,保持expectation;不因timeout清成功狀態或啟動另一筆Cookie修改。live owner未釋鎖時仍受queue timeout限制

上述按穩定type/errorCode分流,不以所有401/409概括,也不解析detail。upgrade-required保留expectation/草稿、不呼叫confirmSignedOut、不自動refresh重試或沿用舊意圖;本lease仍完成drain後釋鎖。其他409或me的Access 401不能冒充refresh-session-invalid;依其分類回報錯誤或availability,不憑狀態碼更換登入者。

需重新認證不等於已登出。 unconfirmed/orphan沒有已確認family時,只隔離UI與舊意圖;HttpOnly Cookie及其Server family可能持續有效至截止。清除site storage會失去expectation,之後乾淨bootstrap可能恢復同一Session,這是本方案保留fence的明確代價。UI必須顯示「需重新認證,尚未確認登出」,不能顯示「已安全登出」或以清storage作logout。共享裝置須透過已確認family的正常logout,或受保護的帳號管理流程執行principal revoke;不能完成時須明示殘留風險並提供管理協助路徑。新的proof登入成功會原子撤銷目前Cookie所證明的舊family。本輪沒有新增無fence Cookie-only discard;審閱者已於2026-10-24第3輪接受此殘留Session取捨;接受依據見Decision Log。

replacement與獨立discard對「Cookie證明撤銷哪個family」沒有額外授權差異;不主張Cookie-only discard本質不安全。replacement另有新的有效proof與新登入意圖,撤銷是成功建立新Session的原子收尾;舊logout/orphan意圖不得藉此重播到另一family。可另外設計由使用者明確發起的全新discard操作,但須定義目標、競態與恢復語意。首版只提供replacement,是選擇保留known-family logout與新proof login兩個操作契約、縮小獨立破壞性恢復操作範圍,並承擔已明示的殘留Session代價;不是從fence推導discard不可行。審閱者已於2026-10-24第3輪接受此操作範圍與殘留Session取捨。

reconcile的bootstrap delegate只可refresh及讀取me/inspect,不可登入/撤銷或重做舊意圖;Native client以bootstrap lease限制允許操作。marker由持鎖的coordinator在上述確定狀態寫入後清除,expectation則持續保留。即使已終止owner仍有晚到Cookie,後續known-family fence會拒絕不符操作;無法保持原Session時顯示需新認證,不以清marker宣稱已阻止瀏覽器處理所有舊header。新的issue必須有新的業務/正式proof,成功後才能建立新expectation。

跨頁面與UI隔離。 已確認issue/current logout/identity-change發送不含credential的同scope失效通知;peer依shared expectation標記需重查,不用通知認證。結果不明不假稱logout成功;身分已改變的舊API回覆/401重放由client及應用flow guard拒絕。same-origin、相同profile及非機密storage是支援前提;跨origin共享strict Cookie、無法保存expectation的環境不列入此profile支援。

採用此SDK的參考接線示意(非現有export的使用教學):

const result = await coordinator.mutate('issue', async lease => {
const response = await apiClient.post('/api/v1/auth/email-otp/challenges/example/verify', proof, {
authMode: 'none', sessionLease: lease,
})
// Check the application's flow intent before confirming or applying the result.
lease.confirmSession(response.data.sessionId)
return response.data
})
// MSW: nativeClient.withOperation(operationId, async () => nativeClient.issue(verifiedInput, operationId))
// Native client joins the SDK lease and registers its fetch; it does not acquire another lock.
// Apply the product response only if the application flow intent is still current.

協調器驗收命題​

  • 排隊取消不送出;已開始作業UI逾時後仍持鎖至handler/Native drain;handler在產品XHR逾時前後才進入、或稍後才呼叫Native時,都不能漏追蹤或晚發未授權的Cookie修改。
  • known family的refresh/logout mismatch回409無正常mutation副作用;真正credential replay仍依安全政策處置,不因fence不符跳過。
  • owner終止後分別驗known/signed-out/unconfirmed expectation;reconcile 200、401、409與503各有出口,沒有marker永久封鎖或無fence偷偷採用另一身分。
  • 舊Cookie header在reconcile/新登入之後才到達時,後續fence與API回覆guard必須阻止跨身分操作;不只檢查最後Redux值。
  • Mock/正式模式header、scope在排隊期間改變、lease重入、storage不足、無Web Locks與混跑舊bundle限制均有明確結果。正式模式的family/protocol header另須納入適用的CORS allowlist。
  • 首次切換與下次protocol升級各驗舊分頁mutation 409且零副作用、舊family refresh明確401與清Cookie;unconfirmed狀態不顯示logout成功,刪storage後可恢復的邊界及完成principal revoke後不能恢復均須驗證。
  • known-family與乾淨bootstrap各驗refresh 401 refresh-session-invalid、409 session-changed、409 session-client-upgrade-required分流;upgrade-required不進重新認證、不清expectation、不循環reload。另以未升級舊bundle驗409保留Session且操作失敗,不能只拿新版更新提示當舊頁證據。

設計完成與審閱重點​

三個架構問題已有選定方案與公開API契約,整案設計人審已通過,維護者已正式接受。後續實作驗證重點包括:D2每請求store成本與故障語意、一般policy/actor界線、D3的handler/Native drain歸屬、reconcile與expectation狀態機、family fence與產品DTO遷移,以及配對交付能否實際由應用消費。

RFC維護者的人類裁決與正式peer-review是不同紀錄:已建立peer-review 20261024-session-cross-runtime-design承載多輪意見,結論回這兩份RFC;AI預檢或作者自查都不寫成人審通過。第3輪已於2026-10-24人審通過,R1/R2共12條意見閉合,審閱者已接受殘留Session取捨;兩稿已由維護者配對accepted。ADR-030決策八已記錄opt-in drain、期限/撤銷、replacement與相容語意,FU-151已完成;原未opt-in基線保留,新profile runtime草稿的本輪進度見前述實作狀態。

實作建議​

檔案變更​

  • 框架lib/vite/mock-auth-plugin.ts:新增opt-in模式、Node-side短效Access發行/inspect/subject revoke及技術重放協調;沿既有Refresh store與Cookie helpers,不另起平行Refresh引擎。
  • 框架lib/vite/index.ts:公開Native options/DTO型別;Browser協調器與Native client由utils公開,private store與lease內部狀態不公開。
  • 框架lib/utils/http/及lib/utils/msw/:新增SessionCoordinator/Native client,讓既有apiClient的自動refresh加入共用lease;保留未opt-in者原行為。
  • 框架lib/vite/mock-auth-plugin.spec.ts:新/舊mode邊界、期限、Cookie/rotation/request replay與錯誤分類。
  • 框架lib/vite/README.md及CHANGELOG:公開操作契約、opaque Access與被動refresh限制、開發用途及遷移。
  • 應用後續才更新Session adapter、MSW principal接線與消費端行為/UI測試;Host Mock、正式Server的承接及配對驗證依G1–G6處理。本提交稿沒有改框架或應用runtime。

關鍵實作邏輯​

框架自行產生不透明Access、family識別及metadata;issue由同一process協調Refresh/Access/request指紋,確定兩者可回才成功。opaque值不寫一般日誌;inspect以可靠隨機憑證查Node metadata,再回最小principal,不能把browser自述subject當有效Access。revoke走既有family索引,同步失效尚未到期Access;清理metadata不破壞未到期重放判定。

沿既有store擴充兩種期限策略:legacy保持目前滑動行為;access-session持有初始絕對截止及凍結accessTTL,每次refresh先驗mode與deadline再輪替。這是共用store的模式擴充,不是把現行exp=now+idleTimeoutSeconds原樣套到新模式,也不要求另起平行Refresh引擎。新模式的sessionTTL是non-remembered Session的有限絕對存活上限,Cookie仍為Session Cookie;不宣稱是ADR-030 remembered的滑動idle policy。

先查框架是否已有可重用的Mock credential/Clock/store primitive。此RFC只指定consumer契約,不批准在應用複製密碼/OTP/Session primitive。預計版號、內部資料結構與Clock注入具體API由維護者決定,發布前須落實上述可觀察行為。

測試建議​

  • Unit:options及body白名單/保留claims、0/最大/超限TTL、exclusive期限、原deadline、同鍵同/異內容及不重發秘密、過期requestId、revoke與metadata清理;Clock可控且不真等15min/12h。
  • Middleware integration(框架):實際Node server驗HttpOnly Session Cookie、issue/inspect/refresh/revoke;Token不由page產生。native credential replay與subject revoke後立即拒絕未到期Access;subject revoke帶另一subject或legacy Cookie時不清Cookie。逐格覆蓋雙向路由矩陣、固定截止不被舊refresh/replace滑動,以及400/401/409/503/500的status與Problem欄位。
  • Browser integration(應用整合):Email OTP成功、hard reload/direct deep link、兩分頁輪替、換人晚回隔離、Cookie已落地/未落地的遺失回覆、非安全區網及唯一HTTP client被動refresh。換人案例須檢查原生Cookie後續恢復的subject,而非只查Redux;同時驗證Cookie修改的序列化前提與缺少公開協調能力時不啟用未就緒流程。與現有alpha.132基線同測,非安全區網不假稱具Web Locks的多分頁支援。
  • 錯誤接線(框架分類器+應用adapter):新refresh 401可由type/errorCode辨識,不進legacy warning;inspect 401不直接要求重新認證;503/timeout不轉401或null。opaque只驗被動refresh;JWT主動refresh維持獨立測試。
  • Compatibility(框架):未opt-in時原mock-auth-plugin全部測試不變;原issue/replace callers不新增Access或改response;新舊模式不得靠refresh/replace隱式轉換。首次成功issue的明確切換及各種revoke依矩陣驗證。
  • Security/Accessibility範圍:只驗Mock接縫與現有OTP UI合約,無新A11y元件;不以此取代正式JWT/金鑰/郵件/OTP安全或跨節點驗證。

替代方案​

方案 A:框架opt-in Access Session接縫(本RFC選擇)​

框架持有Mock Access/Refresh生命週期,應用只做業務驗證及資源授權。新增開發模式、舊caller不變;代價是框架新增Node metadata與公開HTTP接縫,需驗證清理/重放/撤銷。

方案 B:應用繼續拼Mock JWT​

  • 優點:沿既有應用程式可較快接OTP。
  • 缺點:應用維護Token生成/解析及撤銷搭配,與目標應用界線不符,也容易被誤當正式安全能力。
  • 為何不選:會要求消費端另行批准局部Prototype例外及修訂契約;本RFC選擇框架接縫,不要求此例外。

方案 C:先啟用正式Server/Host或改Cookie-only​

  • 優點:若已有公開完整Session能力,可減少Vite Mock責任。
  • 缺點:來源應用尚未選Server type/pin或取得完整接縫證據;本機正式參考實作雖已有Email OTP與Session引擎,仍需核對產品期限/撤銷/帳號政策。Host Mock則仍缺新Access接縫。改Cookie-only會改既有Access API契約及Cookie Path,不能靠頁面composition完成。
  • 為何不選:本輪維持opaque A的方向;這不免除Host/正式Server的承接與對齊。正式Server、Host Mock、Cookie-only是不同變更面,後續決策須分開比較,不能以本地尚無消費端證據宣稱Server完全沒有相關能力。

不屬於本提案的範圍​

  • OTP產生/寄送/配額、安全hash/一次消耗:應用Mock用途與正式Server責任,不放Vite Session plugin。
  • 帳號建立、Email唯一性/變更、會員或報名資料同步/名額/快照:應用domain,不由Token引擎處理。
  • 真實JWT簽章與key rotation、正式DB/分散式store/audit可靠性、production SLA:正式Server設計,不將Mock提升成安全引擎承諾。
  • 既有rememberMe滑動/持久Cookie政策重做:新模式首版只接受remembered=false;延伸另審。Native新模式首版不發行代行/delegation Session;actor類claims為保留鍵,不以自造claims冒稱支援,既有需要代行的Mock維持舊模式直到另有公開接縫。
  • appfuse-server/Host runtime實作與部署:本提案仍為web範疇,跨框架變更需對應決策;Host/正式Server的契約對照、承接缺口及驗證責任則已納入本輪審議,不能因實作另案而略過。

消費端遷移示意​

// Node-only Vite config: accepted design, not available in alpha.132/135
mockAuthPlugin({ accessSessions: { enabled: true } })

// MSW adapter, only after application OTP verification has succeeded.
// nativeMockBridge is the existing development HTTP bridge, not a new product client.
const result = await nativeMockBridge('/access-sessions', {
requestId: verifiedLogin.requestId,
subject: verifiedLogin.userId,
claims: { securityEpoch: verifiedLogin.securityEpoch },
accessTtlSeconds: 900, sessionTtlSeconds: 43200, remembered: false,
})
// First response only: map framework Access + deadlines to application response.
// Replay summary: do not fabricate Access, Cookie, user or login success.
// Protected handler: framework inspect => current account/policy lookup => resource scope.

需刪除應用token()與claims()的自行生成/解碼責任,refresh改走新mode、映射原生截止;保留帳號版本、本人scope、唯一apiClient及晚回隔離。此示意只表示Vite候選API;部署至Host前須完成G1,正式Server切換前須完成適用的G2–G6。舊登入入口及測試的遷移依消費端切換計畫,不隨框架RFC接受自動完成;首次OTP驗碼仍不給匿名報名者Session。

Decision Log​

2026-10-25 — 配對 artifact 發布​

  • 狀態:兩稿由 accepted 更新為 implemented;原接受決策、人審紀錄及殘留 Session 取捨不變。應用採用與產品 UAT 另行追蹤。
  • 配對套件:@appfuse/appfuse-web@19.0.0-alpha.136(npm releases,沿既有 latest 通道);appfuse-server@4.0.0-alpha.39(Maven releases)。已從遠端下載核對公開 runtime/型別與來源;Server JAR 與本機產物逐位元相同。
  • 不可變產物指紋:Web SHA-256 fc3c817e9790f423404b850c89bf743e332efb2aac9cd0a13362b16e776c327f、integrity sha512-fGzxPM92vVNM8HbmzHYQDkS/kvv7D8C2jApxEotISQtwrhDZkewghXQuFF22UjUFiAdNnLrHl1s+xMJCAQfwvg==;Server JAR SHA-256 acb49163079dd901f8baca5bf1b3182077c05fafedc2ae906587e4639eee797b。
  • 程式與參考 type:實作 commit 6c7afe783cd9830215ae46a46e0fafae4e3a0ef5;版本 commit Web b9b3f3e01/Server 84ffbf8f4。同一實作 commit 包含 server-tenant 與 server-tenantless 的 SQL store/AccountSessionTransactions;兩者的應用 profile 均未啟用,dependency pin 也未自動升級。
  • 設定與驗證範圍:Mock 需明示 accessSessions/Host inMemoryWithAccessSessions;Browser protocol 為 1、所有 Cookie mutation 使用同一 coordinator;正式端明示 SessionFamilyProfile、immutable principal registry、權威 store 與 family filter。預設能力保持 opt-in。既有人審證據為 Web 131、security 197、三環境 Browser 24 通過;參考 Server 完整標準測試 tenant 945 通過/8 跳過、tenantless 629 通過/5 跳過。共同案例及重播入口位於 test-contracts/;Browser fixture 使用記憶體 SPI,SQL/重啟另驗,不冒稱產品資料庫或 OTP/郵件/授權/UAT 通過。
  • 發布驗證:Web prepublishOnly 完成 TypeScript/Vite 建置;Gradle publish 完成打包及上傳,其 task graph 不會執行測試,測試證據沿前述已核准結果。npm 新版要求明示 pre-release tag,重試補 --tag latest 後成功;未重發已存在的 Server 版本。
  • 採用入口:Web 協調指南、Server profile 指南。應用先升級配對版本、完成全部 HTTP 入口與同 Cookie scope context 遷移,再以自己的業務規格驗收啟用;框架 implemented 不代表任何來源產品已啟用。

2026-10-25 — runtime 實作人審通過與結案​

  • 人審通過:20261024-session-runtime-implementation 第 3 輪 r3-e496f38f4c412908;審閱者 ada@leandev.io 於 2026-10-25T01:08:33Z 給出 approve。R1-01~R1-04、R2-01 共 5 條意見均已處置並核對,無 blocking、無 open major;審閱者另重跑 framework security 197 項,0 失敗。
  • 實作結論:同 family reconcile 不換 generation;replacement/Cookie-only logout 共用秘密 selector 持有證明,refresh 仍驗 current verifier;legacy 共用行為變更與兩種 Server 回歸已明列。family filter 只查核 Bearer 來源,API key/Basic 保留各自認證與授權,真實 HttpSecurity 整鏈已驗證三者共存。
  • 驗證與限制:定向 Web 131、framework security 197、三環境 Browser 24 通過;兩參考 Server 的完整標準 test task 分別 945 pass/8 skip、629 pass/5 skip。建置、Javadoc、文件站通過。各輪未受影響的結果沿用;13 個 skipped case、外部 IdP/JWKS、獨立 MySQL/HTTPS task 與來源產品 OTP/UAT 不列為通過。可由 test-contracts/verify-session-contracts.py --full-reference 重播框架契約;第 2 輪安全預檢只作用於其明列快照,不冒稱覆蓋後續全部增量。
  • 狀態與後續:本次完成實作 peer-review;兩案仍為 accepted。目標 Web 19.0.0-alpha.136/Server 4.0.0-alpha.39 尚未發布,參考與來源應用未啟用新 profile;配對發布與應用採用另行完成。既有設計人審接受的殘留 Session 取捨維持,不將重新認證當成已安全登出。

2026-10-24 — Accepted​

  • 決策者:框架維護者(使用者);明示同意兩案配對接受,並確認目標版本。
  • 理由:Prototype、Host Mock 與正式 Server 都需要一致的 Session 期限、換人/撤銷與 Cookie 協調;框架接縫可讓多個應用免於自建 Token/Session 引擎。採方案 A,正式 JWT 與 Mock opaque 各維持適合的形狀;保留既有未 opt-in API,選擇新 profile 的產品接受每請求權威查核、協定升級及已審閱的殘留 Session 取捨。
  • 設計依據:D1/D2/D3 已逐項確認;peer-review 20261024-session-cross-runtime-design 第 3 輪通過,R1/R2 共 12 條意見閉合。B/C 的替代取捨維持本文理由,不重啟已完成的設計審閱。
  • 實作:預計納入 appfuse-web@19.0.0-alpha.136,與 appfuse-server@4.0.0-alpha.39 配對;Vite、Host、正式 profile、產品接線及契約測試齊備後才可啟用。尚未實作/發布,本稿維持 accepted,完成實作與發布後才改 implemented。
  • 相關 ADR:ADR-030 決策八已完成本次接受的架構落點;FU-151 已完成歸檔。未 opt-in 的既有 Session/timeout 語意保持有效。

2026-10-24 — Design Peer Review Passed(revision 7 結案)​

  • 人審通過:審閱識別 20261024-session-cross-runtime-design,第3輪版本 r3-9edbd900ac25a2e6;審閱者 ada@leandev.io 於 2026-10-24T09:05:06Z 給出 approve,R1/R2共12條意見已閉合。配對審閱範圍為本稿revision 7與Server稿revision 4。
  • 已接受的取捨(R1-03/R2-03):審閱者已接受殘留Session:reauthentication-required不等於logout,Cookie/family可能仍有效,清除site storage後可經乾淨bootstrap恢復;首版保留known-family logout與新proof replacement,不新增獨立Cookie-only discard。UI不得假稱已安全登出,共享裝置須完成正式logout或受保護principal revoke。
  • 結論落點:本稿與配對Server稿的Decision Log及取捨段已回寫;既有FU-151繼續追蹤RFC接受後修訂ADR-030,未提前改寫已接受ADR。
  • 效力與結案:本次通過的是設計peer-review,RFC仍為proposed;不是整案Accepted、artifact發布或runtime驗收。使用者已指示作者執行close;結論回寫後移除暫時審閱交換文件,持久依據為兩稿與FU-151。

2026-10-24 — Under Review(revision 7)​

  • peer-review第2輪:審閱者對r2-138589c9e3e57056給出changes;保留其確認與結論,作者進第3輪處置,不改寫為通過。
  • 作者處置:R2-01由Server revision 4封閉DefaultAuthPrincipal的username fallback;R2-02在reconcile兩種入口依stable code分流並新增upgrade-required;R2-03明示replacement/discard無額外撤銷授權差異,僅首版操作範圍取捨;R2-04更正舊bundle收到409不會自動提示更新,需人工重載或另有過渡版。
  • 效力:兩稿維持proposed,作者處置待第3輪複核;runtime與新驗收命題尚未實作/執行。

2026-10-24 — Under Review(revision 6)​

  • 人類方向:使用者逐項同意D1/D2/D3,兩稿維持proposed;未作整案Accepted或peer-review通過。
  • peer-review sync:20261024-session-cross-runtime-design第1輪R1-01~R1-08;與Server revision 3補共同換人replacement、舊family refresh與pending/deprecated入口、immutable principal與並行發行邊界。Web新增protocol版本gate,明示unconfirmed並非logout且清storage可能恢復Cookie Session。
  • 落點:兩稿related_adr加入ADR-030,接受後的ADR修訂由FU-151承接;作者處置待第2輪獨立複核,新契約未實作/未驗runtime。

2026-10-24 — Under Review(revision 5)​

  • 維護者方向:維持proposed並完成設計;人審建議不等同已啟動或通過peer-review。
  • 獨立AI預檢:Claude與Codex檢視Web revision 4初稿及Server revision 1初稿,均建議先修訂;意見不是Accepted/Deferred或人審結論。
  • 作者處置:補受控reconcile與持久expectation,讓owner中斷後可轉重新認證而不無限封鎖;登記完整MSW handler、固定排隊時的family/scope、阻擋跨身分回覆與401重放。Mock才帶operation header,leased request禁止重入refresh。
  • Server處置:可信metadata來源、共用profile與發行guard、代行截止繼承、principal的subject/actor撤銷集合、正向JWT分類與policy revoke capability、固定idle不早於hard deadline,詳見Server revision 2。
  • 作者立場:同意兩方指出的實際缺口;租戶撤銷採既有AuthPrincipal.subject全域不可變ID契約,不以email/tenant-local username操作;Basic/API key不冒稱屬Session撤銷。無要求新功能先實作才審議。
  • 審議效力:上述是作者修訂與自查,尚未由獨立審閱者複核新版,也沒有正式peer-review通過;兩稿status維持proposed。

2026-10-24 — Under Review(revision 4)​

  • 維護者指示:維持proposed並完成三項設計裁決;作者收回「因跨環境尚未交付就先Deferred」的建議,不以未實作作為延後理由。
  • 本輪設計:D1選Vite/Host共同交付並建立關聯Server RFC;D2選重用policy binding、一般發行overload與持久family撤銷權威;D3選框架公開協調器/Native bridge、drain與known-family fence。
  • 審閱:維護者詢問設計peer review;本輪建議對跨框架、store成本與Cookie生命週期作獨立人審。兩份設計稿仍是proposed,尚未建立或取得正式peer-review通過,不將作者選定的設計稿寫為維護者Accepted。
  • 交付:runtime與配對測試待接受後實作;具體版本數值待發布規劃。先前revision 1雙AI意見不視為對revision 4及Server稿的審閱通過。

2026-10-24 — Under Review(revision 3)​

  • 維護者已確認的方向:先補Vite Mock/Host Mock/正式Server對齊矩陣、遷移與驗證責任,再決定接受範圍;維持opaque A,不將本次「同意」當作Accepted事件。
  • 查核補充:本機Host僅有Refresh-only adapter;參考前端宣告pin仍為alpha.132。正式Server已有SessionPolicyBinding.validUntil,但一般issue/OTP未接入該binding;不能僅由idleTimeout推定固定截止已對齊。已讀Cookie-only logout測試明確不blacklist Access family,與Mock候選的即時撤銷要求仍有差異。
  • 修訂:新增執行模式與共同語意矩陣、來源版次與證據界線、G1–G6承接清單及逐環境遷移/測試責任;補正方案C與Server/Host範圍說明,不改任何runtime。
  • 審議效力:Claude/Codex的2票方向建議仍只針對revision 1;revision 2的「可接受Vite API範圍」建議不證明跨環境已就緒。revision 3尚未經雙AI重新審議,status維持proposed;最終決策與交付版本待維護者裁決。

2026-10-24 — Under Review(revision 2)​

  • 維護者已確認的方向:維持A/B/C候選,以opaque A補齊契約;不將審議者新增的Mock JWT A′納入本輪候選。
  • 雙AI審議:Claude與Codex各自獨立檢視revision 1,方向票數A為2票;兩者都建議先修訂再決策,並非2票接受。Claude於OAuth恢復後完成補審。
  • 修訂:補上新模式Problem回應、adapter責任、共用store的模式/期限策略、雙向Cookie與路由矩陣、subject revoke的條件清Cookie、撤銷後的發行重放及測試責任;明列原生Cookie晚回的整合前提與未協調並行流程的支援限制。
  • 狀態:revision 2仍為proposed;雙AI意見針對revision 1,修訂稿尚待框架維護者決策,沒有accepted/rejected/deferred事件。

2026-10-24 — Under Review​

  • 入站至 docs-web/rfcs/。
  • 等待框架維護者以 /rfc-intake decide 2026-10-24-mock-session-access-issuance 執行審議。

2026-10-24 — Proposed​

  • 提案者:training-management應用團隊。
  • 來源:training-management。
  • 紀錄語意:應用端已產出脫敏提交稿,供框架維護者審議;不是框架收件、接受或實作事件。
  • 提案依據:已發布套件的公開API查核與上述native middleware probe;框架接縫候選的設計決策仍由維護者判定。

相關連結​

最後更新:2026-10-24。