跳至主要内容

RFC:Mock Access Session 的 Host 承接與正式 Session 共同契約

implemented/2026-10-25(接受決策:2026-10-24)/設計 revision 4/scope=server。框架維護者接受方案 A,與 Web RFC配對共同交付;目標 Server 4.0.0-alpha.39/Web 19.0.0-alpha.136。設計與 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。

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

摘要與交付範圍​

採共同交付:Vite Mock 與 Java Host Mock 提供等價的 opaque Access Session 控制契約;正式 Server 保留簽章 JWT、既有認證與資源授權,承接相同的固定 Session 截止、Access 期限上限及撤銷語意。Web/Server 分別維護公開 API 與版本,但以一份配對發布清單完成此功能,不以「Vite 完成」宣稱功能整體已完成。

本稿解決 Web RFC revision 3 的 G1、G2、G4 之公開能力與政策選擇;G5 的瀏覽器協調 API 由 Web RFC 定義,Host/正式 Server 按此協定接線。G3/G6 是消費端遷移與配對驗證責任,不再留作未選定的架構方案。

動機與原始碼基準​

本機查核基準為 Web 19.0.0-alpha.135、Server 4.0.0-alpha.38 的 source,commit 1816762270b8ba5e6655ed82a6d2596349999a57;參考前端宣告 pin 為 alpha.132。這不是新能力已發布或目標應用已升級的證據。

  • MockAuthSessionController.inMemory() 只有 Refresh-only issue/refresh/replace/revoke,不能承接 Vite 新模式;production build 不會攜帶 Vite middleware。
  • SessionPolicyBinding.validUntil 與 RefreshSessionStore.Session.policyExpiredAt() 已有絕對期限語意,但公開一般 login/issue 與 Email OTP 接線未傳入 binding。
  • LoginService.logout(null, refreshCredential) 目前可撤銷 family,既有 cookieOnlyLogoutWithRotatedCredentialRevokesFamily 測試明確斷言不 blacklist Access family。
  • CacheTokenBlacklistStore 在 cache 停用時跳過 add/contains;不能單靠它承諾所有未到期 Access 即時失效。

因此本稿重用既有 Session store/policy binding/JWT issuer,新增一般發行 overload、Host 新模式與 opt-in family 查核,不在應用另建 Session 或 Token 引擎。

設計裁決一:Host 與 Vite 共同交付​

Host 新增具名 factory,保留既有 inMemory() 與 inMemory(String):

// Accepted design; not available in the current artifact.
public record MockAccessSessionOptions(
String cookieName,
Duration maxAccessTtl,
Duration maxSessionTtl,
Duration issuanceReplayWindow) {
public static MockAccessSessionOptions defaults();
}

public static MockAuthSessionController inMemoryWithAccessSessions(
MockAccessSessionOptions options);

defaults 對應 Web 候選的 900/43200/86400 秒與既有預設 Cookie 名;Duration 必須為正整秒,replay window 不小於 max Session TTL。使用具名 factory 避免與 inMemory(String) 產生 null overload 歧義;舊 factory 仍只有舊模式。

新 factory 仍僅在 app.msw.enabled=true 的 Host mock 組裝下使用,持有同一個 Host process 的狀態。它在 servlet context-path 下提供 Web RFC 的四個新端點與相同 DTO/狀態碼/Problem、requestId 去重、雙向模式矩陣、固定截止、條件清 Cookie 及 family→Access 撤銷契約。Cookie Path 加上實際 context-path,Secure 依有效 HTTPS 設定處理;名稱與前端協調 scope 必須取自同一 runtime 設定。

產品 MSW adapter 只映射產品 DTO 與資格,不依 Vite/Host 分岔產生 Token。兩端的 golden contract cases 使用相同輸入與可控時鐘驗證語意,不要求亂數 credential 或 process metadata 的位元內容相同。

正式 Server 不註冊這些 Mock 端點,不接受瀏覽器自述「已驗證 subject」建立正式 Session。OTP/密碼/OIDC 等仍先完成正式認證才進入下節的引擎發行入口。

設計裁決二:共同期限與撤銷契約​

一般發行沿用 policy binding​

保留 SessionOptions 的 remember/idle 意義,不新增平行期限引擎,也不把 12 小時當框架所有產品的預設。新增一般發行 overload:

// Proposed additions to LoginService; existing overloads remain.
public LoginResult login(
Authentication request, String app, SessionOptions options,
SessionPolicyBinding binding);

public LoginResult issue(
AuthPrincipal verifiedPrincipal, String app, SessionOptions options,
SessionPolicyBinding binding);

binding 的 policy/reference 由應用的 Session issuance policy 決定;validUntil 由可信 Server Clock 與產品要求在初次登入時建立,不能來自未驗證的 browser body。一般登入的 binding 不代表 impersonation,不因使用同一 record 就加入 actor/代行語意。未opt-in共用profile時,既有overload沿舊行為,不為其他產品隱式增加固定期限。

本功能採用的產品政策為non-remembered、固定絕對截止;示例為12小時,但由來源應用規格與設定明定。具體共用profile為:

public record SessionFamilyProfile(String policyId, Duration absoluteLifetime) { }

profile由Server受信任組裝建立,供LoginService的新constructor overload與ResourceServerSecurity.Builder.sessionFamilies(profile, store, clock)共用;不以browser的app字串挑選較寬鬆profile。此Server組裝啟用profile時,所有本地互動式發行入口都受它約束,包括既有login/issue overload;未啟用profile的原有應用保留原行為。新profile不支援remembered=true,應用在消耗proof前驗證此選項。

啟動驗證器核對引擎profile、Resource Server的安裝紀錄、同一權威store與必需capabilities;缺一或設定不一致即啟動失敗。每次發行另在引擎共同入口執行guard:初次新認證以可信Clock建立profile截止,與caller提供的binding.validUntil取較早者;binding缺少時由引擎依profile建立,不讓舊overload漏傳就繞過期限。一般profile的policyId不產生actor/代行claim。

首版profile沒有獨立的較短idle期限:初始idle expiry必須不早於本family的固定截止,refresh也不能把idle提前;傳入options不符時明確拒絕,不靜默引入提早到期語意。參考應用的發行policy據此產生SessionOptions。因而產品sessionExpiresAt代表實際固定截止,Mock與正式Server不會在「idle先到」時分歧;需要較短idle的產品須另擴充三環境契約。

行為共同契約
Session 截止初次發行的 validUntil 固定;refresh、rotation、換 key 均不延長。現在時間到達截止即拒絕,採 exclusive 邊界
Access 截止min(發行時間+Access TTL, Session validUntil);本profile的idle不會更早。JWT exp向下取整,family filter以同一Clock獨立檢查Access exp與Session截止,不因JWT leeway延長任何exclusive邊界
TTL 與時鐘bounded profile 的 TTL 為正整秒;發行/驗證使用注入 Clock。截止不足以產生有效 Access 時以明確 Session 拒絕結束,不發已到期 Token
權限與資格refresh 重載目前 principal/policy;資源授權、owner 與必要帳號版本查核仍由正式 Server 政策執行。Mock adapter 在相同產品邊界模擬這些結果
重新登入完成新的業務/正式認證後才建立新截止;replay 摘要與普通 refresh 不得建立新 Session
同 scope 再登入/換人新認證成功時,撤銷被目前 Cookie 替換的前一 family 與其全部 Access,再發新 family/Cookie;同一帳號再登入亦相同。另一 scope/裝置的其他 family 不因此撤銷;失敗不得先撤銷舊 family

Cookie-backed 的密碼、OTP 與其他正式入口都必須將原 Cookie 交給引擎,不可只寫新 Cookie。新增受信任組裝參數與 overload:

public record SessionIssuanceContext(String previousRefreshCredential) { }
public LoginResult login(Authentication request, String app, SessionOptions options,
SessionPolicyBinding binding, SessionIssuanceContext issuanceContext);
public LoginResult issue(AuthPrincipal verifiedPrincipal, String app, SessionOptions options,
SessionPolicyBinding binding, SessionIssuanceContext issuanceContext);

HTTP adapter 從實際 Cookie scope 讀入 previousRefreshCredential;沒有 Cookie 也傳 context(null),不接受 browser body 自選舊 family ID。引擎驗證舊 credential 並只定位其證明的 family,不對舊 Cookie 執行 refresh/rotation。無法定位的過期/損壞 Cookie 不推定 subject 或撤銷其他 family;真正 replay 仍走原安全政策。新 family 建立與可定位舊 family 撤銷在同一權威 store 交易完成;新發行失敗須 rollback,成功 commit 後才寫 Cookie。新 profile 的 Cookie-backed 接線缺少此 context 視為組裝缺陷;既有非 HTTP overload 仍受期限 guard,但不能作為 Cookie replacement 接線。Vite/Host 以同一原子 replacement 語意執行,不要求正式端採用 Mock requestId 去重。

無新proof的identity-change不重設期限:進入、退出impersonation或其他身分切換,新family截止取min(來源family截止, 受信任目標policy截止),只可縮短。原exitImpersonation()傳null binding的路徑,在profile啟用時仍由引擎繼承來源截止,不能退回新的now+lifetime;必要時提供接收target binding的公開overload。退出時由受信任policy建立正常身分所需binding,清除代行身分並保留仍適用限制,但不延長繼承截止。只有新的正式認證才能開始新lifetime。

JWT issuer沿既有generateToken(UserDetails, claims, expiresAt)擴充明確issuedAt/expiresAt的overload:

public String generateToken(UserDetails principal, Map<String, Object> claims,
Instant issuedAt, Instant expiresAt);
public String generateDelegationToken(UserDetails principal, List<String> grantors,
Map<String, Object> claims, Instant issuedAt, Instant expiresAt);
public String generateImpersonationToken(UserDetails principal, String actor,
Map<String, Object> claims, Instant issuedAt, Instant expiresAt);

三條路徑接收同一裁切結果並保留原claim ownership/簽章;不要求應用解碼JWT才能取得期限。

引擎新增可信metadata投影,保留LoginResult與RefreshResult原本的record components/constructor,避免只加component就破壞既有record pattern/binary契約:

public record SessionMetadata(UUID sessionId, Instant issuedAt,
Instant accessExpiresAt, Instant absoluteSessionExpiresAt) { }
public SessionMetadata describe(LoginResult issued);
public SessionMetadata describe(RefreshResult refreshed);

describe由框架驗證自己產出的Access JWT,取得受保護sessionId/issuedAt/exp並對照權威family的subject與deadline;不信任browser輸入或未驗簽JWT。這補足RefreshResult原本沒有公開family ID的缺口,也適用既有身分切換回傳的LoginResult,不要求應用自己實作JWT decoder。查無/不符/缺必要欄位不能偽造metadata成功。

無絕對期限的舊profile可有空absoluteSessionExpiresAt;新bounded assembler必須取得非空deadline及可信sessionId才回成功。產品expiresIn依metadata的實際Access期限換算,不固定回原jwtSettings TTL。login/refresh DTO映射sessionExpiresAt與sessionId,讓reload bootstrap也能建立fence;原user/roles投影不變,未採用的產品不被迫新增欄位。metadata查核或組裝失敗時不寫成功Cookie,不假稱登入完成。

known-family fence​

已知Session的產品refresh/logout帶X-Appfuse-Session-Family;MSW映射至Native的expectedFamilyId。一般HTTP controller將它交給新增的引擎overload,先驗證credential,再核對family與fence,最後才進行正常rotation或撤銷:

public RefreshResult refreshSession(String credential, UUID expectedFamilyId);
public void logout(String accessToken, String credential, UUID expectedFamilyId);

refresh的初次乾淨bootstrap可無fence;reload或owner中斷時,SDK依持久expectation執行Web定義的reconcile,已知family仍須帶fence,不盲目採用另一Cookie身分。current logout須有已知expectedFamilyId,但引擎仍須驗證Cookie或Access,不能只靠wire上的UUID撤銷family。Cookie已屬於另一family、或Access/Cookie/expected之間不一致時回409 session-changed,不清Cookie、不發新Token、不自動對另一個登入者重試。

409使用獨立SessionChangedException與Problem映射,不落入會清Cookie的AuthenticationException catch分支。無效credential或真正replay仍先依原安全政策拒絕/撤銷,不能用偽造expectedFamilyId規避;409無副作用只適用已驗證credential的family不符。應用保留草稿/意圖並進受控reconcile,不盲目採用另一Cookie身分。舊overload在未opt-in者行為保留,新profile的HTTP接線改用帶fence的overload。

profile 切換的 refresh 拒絕。 所有新 profile 的 refresh 路徑,在驗證 credential/replay 與適用的 family fence 後、任何 rotation 或 Access 發行前,查核持久 family 的絕對 deadline 與 profile policyId。缺少 deadline、已到截止或 policyId 不符一律 401 refresh-session-invalid;不補寫 now+lifetime、不轉換舊 family、不先發一枚資源 filter 必拒絕的 Access。HTTP adapter 清除此已驗證且 fence 相符的目前 Cookie,要求新認證;有效 credential 指向另一 family 時仍優先 409 且不清 Cookie。store outage 維持 503 且不清 Cookie。無 fence 的乾淨 bootstrap 也必須檢查 profile,不能成為遷移旁路。

發行與恢復入口清冊​

入口opt-in profile 下的要求
login/issue(含密碼、OTP 及既有 overload)共同期限 guard;Cookie-backed controller 另傳 issuance context,成功原子替換舊 family
進入/退出 impersonation繼承來源截止、只可縮短;沿相同 profile/identity 查核
refreshSession(所有 overload)不建立新截止;先驗現存 deadline/policyId,再執行 rotation 與 metadata 投影
activateSession(pending entry activation)pending 建立時已綁 profile、canonical subject 與原截止;activation 不算新 proof、不重設截止。先查 profile/期限,再在同交易執行 activatePending 與 refresh;任何拒絕 rollback,不留下已啟動但無有效 Access 的 family。舊 pending 缺 profile/deadline 時 401,HTTP 清該 Cookie,不自動補建
deprecated refresh(String) 的 JWT Refresh 路徑啟用 profile 時明確拒絕為 401 refresh-session-invalid,不產生 Access;HTTP rollout fallback 同樣不能呼叫它繞過 store。未 opt-in 者保留舊行為

family 狀態是即時撤銷權威​

本功能的正式接線啟用 stateful family validation。簽章 JWT 繼續用於認證;每次新的互動式資源授權決定,須確認其 sessionId 對應 family 仍存在、未撤銷、未到 idle/policy 截止,且 subject/tenant 等已驗證脈絡與 family 相符。已開始的業務交易不因稍後撤銷被自動回滾。

新增 store 能力宣告與操作(舊 adapter 預設宣告空能力;新操作預設明確 unsupported,不能回空成功):

public enum SessionStoreCapability { FAMILY_LOOKUP, PRINCIPAL_REVOCATION, POLICY_REVOCATION }

Set<SessionStoreCapability> capabilities();
Optional<RefreshSessionStore.Session> findBySessionId(UUID sessionId);
Set<UUID> revokeByPrincipal(String principalSubject, Instant revokedAt, String reason);

兩個參考Server type的adapter實作全部三項capability;Host新模式實作其Native契約需要的能力,不宣告未支援項目。revokeByPrincipal匹配family.subject == principalSubject OR family.actorSubject == principalSubject,在單一權威store交易中撤銷目前匹配families,回傳全部匹配IDs(含已撤銷者),供冪等重試。Alice代行Bob時,撤銷Alice也須撤銷該actor family;不只按subject欄位查詢。

principalSubject是application-scoped認證store內全域唯一、不可變的account ID,不是email或tenant-local username。這是本 profile 加嚴的必要條件;目前 AuthPrincipal.subject() 預設仍回 getUsername(),不能宣稱框架預設已保證不可變。兩個參考 Server 已覆寫為 Account UUID;tenant server 的 Session 認證表為 application-scoped,tenantId 是驗證/audit 脈絡。撤銷入口先依目前本人/管理政策取得目標ID,不由任意browser字串選取;tenant-local identity須先映射canonical subject,不能讓兩租戶同名帳號誤撤。此選擇也保留跨tenant actor的正確撤銷,不用目標family的tenantId錯誤限制actor。delegation grantor的授權撤回走對應policy revocation,不假稱principal集合已涵蓋所有grant依賴。

profile 組裝必須登記所有可發行的 principal type 與明確的 immutable application account ID 契約;缺少宣告或有效 subject 實作仍是 AuthPrincipal 預設方法,啟動即拒絕。執行期拒絕未登記 principal type。啟動檢查只能驗證組裝與方法來源,不能以反射證明資料永遠不可變;因此兩個 type 的驗收必須實際改 username,再證明舊 Token/family subject 不變、用原 Account UUID 撤銷可涵蓋改名前後 Session 與 actor family。自訂 adapter 也須提供此契約測試,不能只宣告 capability 就宣稱通過。

DefaultAuthPrincipal 的 fallback 亦須封閉。 目前該類雖覆寫 subject(),builder 未設 subject 時仍退回 username,不能只靠方法 declaring class 放行。新 profile 要求所有 DefaultAuthPrincipal 實例(含其子類)在建構時明示非空白 immutable account ID;builder/建構子保留由框架讀取的「subject 明確提供」來源標記,不從 browser claims 或 subject 與 username 是否相等推定。未提供、null 或空白均不能成為有效標記;未 opt-in 的舊 fallback 維持。啟動能檢查已存在的組裝實例,動態 UserDetailsService 產生的每個實例則在 login/issue/refresh/activation 及 actor 重載後、任何 family/Token mutation 前檢查,不得假稱啟動時已驗過所有未來帳號。缺少明示 subject 是組裝缺陷,執行期回 500 且不發行/輪替/寫 Cookie,不偽裝成 credential 401。驗收含最簡 builder、subject(null)、空白、明示 UUID、子類 fallback,以及改名後以原 UUID 撤銷;自訂實作仍須遵守上段契約。

正式profile要求FAMILY_LOOKUP、PRINCIPAL_REVOCATION與POLICY_REVOCATION。既有revokeByPolicy預設空集合僅留給舊profile;新profile啟動時缺capability即失敗,不能把空集合當成功的即時撤銷。若adapter宣告支援卻回unsupported,視為組裝缺陷500,不降級放行。

ResourceServerSecurity.Builder.sessionFamilies(profile, store, clock)在JWT authentication之後、authorization之前加入family filter。資料庫查核不放進會把查詢錯誤包成invalid JWT的decoder分支;profile/store與引擎安裝不一致時啟動失敗。

  • 新本地Access JWT由引擎保護的sessionKind正向分類:interactive必須帶sessionId;client-credentials不得帶sessionId或Refresh。caller claims不得覆寫此marker,不能以缺少sessionId推定M2M。
  • 新profile對缺少/未知marker的本地JWT回401;舊互動Token重新登入,M2M重新執行client_credentials取得新marker。M2M仍無Refresh,不進互動式family lookup;未opt-in的其他Resource Server保留原組裝。
  • interactive須具匹配的family、subject、tenant/actor等已驗證脈絡;查無、撤銷、Access exp或idle/policy截止回401 access-token-invalid。新profile亦拒絕沒有必要絕對deadline的互動family,不能以舊store row繞過profile。
  • 暫時store outage回503 authentication-service-unavailable,未知缺陷500;不把它們轉成JWT 401。查詢讀可立即觀察撤銷commit的權威資料,不以正向active快取或延遲replica回覆有效;blacklist僅作提前拒絕。
  • 本profile適用STANDALONE本地JWT。FEDERATED IdP、Basic密碼與API key不屬Session撤銷集合;要求完整停權的帳號事件還須走其既有credential/eligibility政策,不能以session revoke宣稱密碼與API key也失效。

成本是每次互動式 JWT 資源請求增加 family store lookup;這是採用即時撤銷產品的明確取捨。其他未 opt-in 的既有產品保留原組裝;不能對它們宣稱本 profile 的保證。

logout 與 principal revoke​

// Proposed public orchestration; not a new anonymous HTTP endpoint.
public int revokeSessionsByPrincipal(String principalSubject, String reason);

新 profile 的共同規則:

  1. 帶有效Access或僅有可定位family的Refresh Cookie,且符合expectedFamilyId時,logout都撤銷該family;Cookie已輪替但仍可定位的舊generation同樣可登出。只有不透明Cookie時也不能保留該family的未到期Access;expected UUID本身不是撤銷證明。
  2. family 撤銷以權威 store commit 為成功點;此後的 family filter/Mock inspect立即拒絕。可繼續更新 blacklist,但不能把 best-effort cache 當唯一撤銷證明。store 故障不回成功、不清成功 Cookie、不讓UI先假登出;部分完成可冪等重試。
  3. 帳號安全事件的principal revoke撤銷指定帳號作為subject或actor的全部既存families,所有相關Access隨family失效。Native首版沒有代行發行模型,其subject revoke因此對應直接subject集合;不得把任意claims.actor當已支援代行。正式應用只能在已核准的本人/管理/帳號變更政策內呼叫此 orchestration,不能公開等同Mock body的匿名撤銷API。
  4. 只清除屬於實際撤銷目標的目前 Cookie;另一 subject/另一模式的Cookie保持。unknown/已撤銷目標可以冪等成功,但不推定其他 subject,也不重新發行憑證。
  5. 舊 Cookie-only logout 的「未 blacklist」測試保留為舊組裝基線;新 profile 必須另證明持久 family 撤銷後原Access被拒絕。改測試不能替代修正正式接線。

撤銷與並行發行的邊界。 revokeSessionsByPrincipal 的 commit 是「既存 family 集合」的線性化點,不是永久禁止帳號再登入。若另一筆發行在它之後 commit,單純 Session revoke 不承諾該新 family 也被撤銷。帳號停權/密碼重設等安全事件須以應用可信交易結合 credential/eligibility 變更與 principal revoke,並與最後發行檢查共用同一 account 鎖或等價版本比較:proof 驗證時記錄的帳號/credential 版本,在建立 family 前於權威交易重查,不符則拒絕,不能沿用撤銷前已通過的 proof。actor 與 subject 同時受此檢查,多 account 鎖採固定順序。先 commit 的發行由後續 revoke 涵蓋;先 commit 的安全事件使舊 proof 發行失敗;事件後的新有效認證才可依新政策發行。這項帳號安全保證由兩個參考 type 的交易 adapter 與帳號政策共同承擔,不以 family 表快照撤銷冒充完整停權。

unconfirmed/orphan 無已確認 family 時的 reauthentication-required 僅表示 UI 暫停採用 Cookie,不是 Server logout。Cookie family 仍可能有效,刪除 site storage 後也可能經乾淨 bootstrap 恢復;前端不得顯示「已安全登出」。共享裝置須完成已確認 family 的正式 logout,或由受保護帳號管理入口以 canonical account ID 執行 principal revoke;無法確認時明示尚未登出及需協助撤銷。本稿保留已確認 D3 的 fence,沒有新增無 fence 的 Cookie-only discard;審閱者已於 2026-10-24 第 3 輪接受此可用性/殘留 Session 取捨,接受依據見 Decision Log。

replacement 與獨立 discard 的取捨。 就「Cookie 可證明撤銷哪個 family」而言,兩者沒有額外授權差異;本稿不主張 Cookie-only discard 本質不安全。replacement 額外要求新的有效 proof 與新的登入意圖,撤銷舊 family 是同 scope 成功建立新 Session 的原子收尾,登入失敗不撤銷。舊 logout/orphan 意圖沒有這項新認證,仍不能任意改套送出時另一位使用者的 Cookie;若要另提供 discard,可以設計成全新的使用者明確操作並定義其目標、競態及恢復契約,不能重播舊意圖。首版只提供 replacement,是選擇維持現有已知 family logout 與新 proof login 兩個操作契約、減少獨立破壞性恢復操作的範圍;代價就是上段的殘留 Session,並非由 fence 原則證明 discard 不可行。審閱者已於 2026-10-24 第 3 輪接受此操作範圍與殘留 Session 取捨。

Vite/Host/正式產品的 issue/refresh/revoke/identity-change 都使用 Web RFC 的同一 SessionCoordinator。Host Mock bridge在目前 mutation lease內加入原生請求,不重入鎖;正式 controller只承接正常認證與Cookie,不知道瀏覽器內的opaque lease物件,也不信任它作授權。

UI取消/response timeout不代表Native Cookie請求已停止。Web協調器維持原生請求drain期間的互斥;owner中斷後以受控bootstrap和持久的非機密family expectation復原,結果不符時只允許新的認證意圖,不沿用舊操作。Server保留正常bootstrap refresh入口並遵守fence/replay政策;family header與適用CORS設定一併遷移,不新增匿名reset或忽略credential的旁門。兩個Browser origin共用同一strict Cookie不在本profile支援範圍;DEV reusable是既有另一種協定,不能用來通過strict驗收。

採用新 profile 的所有 Cookie mutation controller 與 Host bridge 必須在 proof 消耗及任何副作用前查核 X-Appfuse-Session-Protocol: 1,缺少或不符回 409 session-client-upgrade-required,不修改 family/Cookie。Web Native client 對 Vite/Host 新模式也傳此 header;舊 Mock endpoint 收到新模式 Cookie 時同樣先檢查,不能成為繞過協調的入口。此 header 只做協定版本隔離,不是認證或授權。active protocol 由同 scope 的受信任部署設定固定,與 Web coordinator 設定配對,正式 CORS 一併允許它;版本切換不提供可同時修改同一 Cookie 的雙 active protocol。詳見 Web 遷移段。

相容性、遷移與配對發布​

既有使用者相容策略
Host inMemory()/inMemory(String)保留Refresh-only;新功能需明確改用具名factory
一般LoginService既有overload/SessionOptions未opt-in維持舊預設;opt-in後所有互動發行受profile guard,新overload可額外指定受信任binding
舊store adapter舊組裝仍可用;新capability預設unsupported,採用新profile時啟動檢查,不靜默降級
舊Resource Server/外部IdP/M2M未opt-in者保留;新profile不取代原authority/資料範圍政策;M2M在新profile須重取帶正向marker的Token
採用本功能的產品Web/Server配對版本、所有登入入口、Host factory、family filter與DTO一併遷移;明確重新登入,舊Cookie不能直接轉換
舊 bundle/協調器協定升級缺少/不符 active protocol 的 Cookie mutation 409 且無副作用;舊無 marker Access 401,舊 family refresh 401 並清 Cookie。所有 mutation controller 必須先完成版本 gate,才啟用 profile

新公開操作與profile是新增能力;採用時的store lookup成本、部分失敗、舊Token退場與產品DTO變化必須列入升級說明,不宣稱所有環境零設定即可切換。

配對發布清單至少記錄 Web不可變版本、Server不可變版本、相關參考實作commit/type、Mock與正式profile設定及測試證據。框架可分別建置/發布artifact,但本功能的消費端切換閘門必須等配對齊備;不以兩套框架共用同一版號解決。本次接受的目標為Server 4.0.0-alpha.39/Web 19.0.0-alpha.136;兩個 artifact 已於 2026-10-25 發布,指紋、實作 commit、參考 type、設定與驗證範圍見配對發布紀錄;消費端接線與驗收仍需自行完成。

測試與驗收設計​

驗證層必須證明
Host/Vite contract同一案例集:issue/inspect/refresh/current及subject revoke、deadline exclusive、兩種模式矩陣、Problem、去重、不重發secret、context-path與條件清Cookie
一般發行與JWT密碼/OTP/其他入口同profile,漏binding也不能繞過;一般binding不變代行;進出代行只縮短截止、idle不早於hard deadline;exp與describe投影一致
family驗證正向sessionKind分類、舊無marker Token拒絕、M2M重取而不進Refresh;interactive family查無/撤銷/Access或policy到期/脈絡不符拒絕;cache停用、process重啟、不同instance都不復活;store outage503、未知500
撤銷有Access、Cookie-only、舊generation、subject多family、其他subject Cookie、重複撤銷與部分失敗;以撤銷commit後的新資源請求驗即時性。family fence不符409無副作用,不由舊UI登出另一個登入者
store SPIprofile與三種capability preflight、權威lookup、principal的subject/actor聯集與policy撤銷、兩租戶同名username映射不同ID、並行發行線性化邊界;兩個type分別驗證,不以Mock代替SQL
Browser配對Vite Mock/Host Mock/MSW關閉的正式Server各跑登入、reload、期限、換人、logout、晚回與outage;沿Web協調器的drain/無WebLocks/context終止限制驗證
換人與舊 family三環境同帳號/不同帳號再次登入後,舊 Access 與舊 Cookie 都拒絕,新 family 可用;新發行失敗不撤銷舊 family,其他裝置 family 保留
profile 與舊入口遷移缺 deadline/錯 policyId 的 refresh 在發行前 401 並清相符 Cookie,不出現 refresh 200→資源 401;activateSession 不延長且拒絕舊 pending;deprecated refresh(String) 在 profile 下不發行
identity 與線性化預設 username subject/未登記 type 啟動或執行期拒絕;DefaultAuthPrincipal 未明示、null、空白、子類 fallback 均在 mutation 前拒絕,明示 UUID 才可用;改 username 後以相同 UUID 撤銷;兩種 commit 順序驗 account 變更+revoke 與舊 proof 並行發行,不只測 family 快照
協定與殘留 Cookie舊/新 bundle、首次切換與下次 protocol 升級的 mutation 版本 gate;unconfirmed UI 不假稱登出,刪 storage 可恢復與 principal revoke 後不能恢復各有案例

測試是接受後實作的完成條件,本稿未要求先實作才可審議。接受決策當時只有原始碼與既有測試斷言查核;本輪新增執行證據與採用邊界見本輪實作狀態,不以框架 fixture 代替來源產品整合/UAT。

替代方案與取捨​

  • 只做Vite,Host另等需求再說:會讓同一MSW adapter部署後缺端點;不採用。選擇同功能、兩scope文件與配對交付。
  • 在SessionOptions/應用另建hard deadline引擎:重複既有policy binding;不採用。一般發行補公開overload,重用現有store欄位與expiry語意。
  • 只補Cookie-only logout blacklist:cache停用/重啟與跨instance仍可能使保證不成立;不採用為唯一權威。採opt-in持久family查核,接受lookup成本。
  • 全部改成Cookie-only/正式Server也改opaque Access:擴大現有產品與Resource Server契約;不採用。正式JWT與Mock opaque可以維持不同形狀。
  • 以未實作為理由Deferred:不採用。維護者已接受完整配對設計,實作與執行驗證列為後續交付條件,不把設計接受冒稱實作完成。

不屬於本提案的範圍​

正式OTP密碼學/郵件投遞、M2M與外部IdP lifecycle重設、產品owner/authority重新設計、全面強制所有應用改用stateful JWT驗證、實際部署或應用E2E/UAT進度推進均不在本次文件裁決內。

Decision Log​

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

  • 狀態:與 Web RFC 一起由 accepted 更新為 implemented;接受決策與已接受殘留 Session 取捨不變。
  • 交付:Server 4.0.0-alpha.39 與 Web 19.0.0-alpha.136 均已發布至 releases registry;Server 遠端 JAR 與本機產物逐位元相同,SHA-256 acb49163079dd901f8baca5bf1b3182077c05fafedc2ae906587e4639eee797b。完整配對清單、Web integrity、實作/參考 type commit、設定與既有驗證範圍見配對發布紀錄。
  • 採用邊界:tenant/tenantless 參考 adapter 同 commit 交付;應用 dependency pin、Session profile、產品 HTTP 接線及 UAT 未自動更新。框架發布完成後,消費端另依兩份採用指南遷移與驗收。

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​

  • 決策者:框架維護者(使用者);明示同意兩案配對接受,並確認目標版本。
  • 理由:Host 必須承接與 Vite 等價的 Mock 契約,正式 Server 則沿既有 policy binding/store/JWT issuer 擴充 bounded profile,避免產品另建期限與撤銷引擎。採方案 A,選擇 opt-in 持久 family 查核的即時撤銷保證,接受查詢成本及 503 故障語意;principal/actor、replacement、舊入口與遷移邊界已完成設計人審。
  • 設計依據:D1/D2/D3 已逐項確認;peer-review 20261024-session-cross-runtime-design 第 3 輪通過,R1/R2 共 12 條意見閉合,包含審閱者已接受的殘留 Session 取捨。
  • 實作:預計納入 appfuse-server@4.0.0-alpha.39,與 appfuse-web@19.0.0-alpha.136 配對;兩個參考 Server type 及三環境契約測試須各自驗證。尚未實作/發布,本稿維持 accepted,完成實作與發布後才改 implemented。
  • 相關 ADR:ADR-030 決策八已完成本次接受的架構落點;FU-151 已完成歸檔。原非 opt-in 組裝與已完成的基線測試仍有效,但不作為新 profile 已完成的證據。

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

  • 人審通過:審閱識別 20261024-session-cross-runtime-design,第 3 輪版本 r3-9edbd900ac25a2e6;審閱者 ada@leandev.io 於 2026-10-24T09:05:06Z 給出 approve,R1/R2 共 12 條意見已閉合。配對審閱範圍為本稿 revision 4 與 Web 稿 revision 7。
  • 已接受的取捨(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。
  • 結論落點:本稿與配對 Web 稿的 Decision Log 及取捨段已回寫;既有 FU-151 繼續追蹤 RFC 接受後修訂 ADR-030,未提前改寫已接受 ADR。
  • 效力與結案:本次通過的是設計 peer-review,RFC 仍為 proposed;不是整案 Accepted、artifact 發布或 runtime 驗收。使用者已指示作者執行 close;結論回寫後移除暫時審閱交換文件,持久依據為兩稿與 FU-151。

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

  • peer-review 第2輪:審閱者對 r2-138589c9e3e57056 給出 changes;R2-01~R2-04 待處置,並非決策層 blocked。
  • 作者處置:補 DefaultAuthPrincipal 明示 subject 來源標記及所有動態實例的 mutation 前檢查;說明 replacement 與 Cookie-only discard 在撤銷授權上無差異,首版未提供 discard 是操作範圍取捨。Web revision 7 修正 reconcile 分流與舊分頁遷移敘述。
  • 效力:兩稿維持 proposed;處置待第3輪複核,未宣稱人審通過或 runtime 已完成。

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

  • 人類方向:使用者已逐項同意 D1/D2/D3;整案仍 proposed,未代寫人審通過。
  • peer-review sync:20261024-session-cross-runtime-design 第 1 輪 R1-01~R1-08:補共同 replacement、refresh/pending/deprecated 路徑的 profile 拒絕、immutable subject 組裝與測試、並行安全事件邊界及 protocol gate;明示 reauthentication-required 不等同 logout。
  • ADR 落點:兩稿關聯 ADR-030;接受後修訂該 ADR 的 opt-in drain/期限/撤銷及相容邊界,由 FU-151 追蹤。本輪不提前改動已接受 ADR。
  • 效力:作者處置待下一輪獨立複核;新契約仍未實作,runtime 測試待接受後執行。

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

  • AI預檢:Claude/Codex對本稿revision 1與Web revision 4均建議先修訂,非人審結論。
  • 作者處置:以describe提供可信refresh metadata並保留舊record形狀;共用profile及執行期guard防止漏binding;明定idle/代行截止、正向JWT種類、principal的subject或actor集合、全域ID與policy revoke能力;Web補reconcile出口與handler drain。
  • 狀態:仍為proposed;作者處置待獨立複核,未標記整案Accepted或peer-review通過。

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

  • 依據:維護者指示「維持 proposed,完成上述設計裁決」。
  • 本輪設計:與Vite共同交付Host Mock;一般發行重用policy binding;統一期限與撤銷;新增opt-in持久family驗證與Web公開Cookie協調器接線。
  • 狀態:本稿與關聯Web稿均維持proposed;設計選擇待整案審議,沒有Accepted/Rejected/Deferred或實作事件。先前雙AI對Web revision 1的意見不視為對本Server稿的接受。

相關連結​

最後更新:2026-10-24。