Prototype 體驗容器與工具頁
設計 revision 3/accepted/2026-10-26。第 3 輪設計人審已通過並結案,13 條意見皆已處置;接受追溯見 ADR-011及本稿 Decision Log。API/協定/15 項驗收正文維持通過內容;runtime 尚未實作或發布。
現況註記(2026-10-27):上段及下方待實作文字保留設計結案時點。首版 runtime 已完成獨立實作人審並隨 alpha.139 發布;同日後續已由使用者接受「保持登入配合 Native Session 生命週期、不增加應用設定」的修正方向,取代正文對已登入體驗也固定兩小時的部分,決策與限制見 ADR-011。原受審 API/協定/驗收正文不覆寫成新事實。
摘要
從下游 Prototype 的實際用法整理共用能力:一般瀏覽器可從獨立工具頁取得合成驗證碼、切換業務起點、注入異常與重新開始。框架負責體驗容器、Native Session 隔離、世代檢查、生命週期與工具介面;應用提供情境、合成信件與業務 fixture。
首版限定 Vite 開發服務且應用已啟用 Mock 模式。HTTP 相容沿用現行框架的開發模式判斷,不新增相容政策設定;正式 Session、credential 儲存與 HTTPS 下的協調保證不因本提案改變。建置後共享站台與 Java Host 承接留待後續提案。
動機與來源
體驗者先在產品頁申請登入驗證碼,再開啟工具頁讀取合成信件,返回原頁輸入。需要查看空資料、既有資料或待確認結果時,工具準備 Given,使用者仍從產品入口完成操作;恢復正常服務保留現有業務資料,重新開始才清除。
來源應用已實作獨立工具分頁、自然時鐘、每個瀏覽器範圍一份 fixture 與 Native Session、重設世代防護、手機排列及複製失敗退路。本稿承接其行為,不將報名、學員、示範 Email 或專案網址寫進框架。
現行框架公開 mockAuthPlugin(options);其 MockRefreshSessionStore 建立在 plugin factory 內,configureServer 直接登記 middleware。來源應用以 Proxy 收集 hook 的 middleware 來組成隔離實例。回流後以正式 middleware factory 取代此做法,既有 plugin 繼續提供原用法。前置契約見 Mock Access Session RFC。
來源應用的背景狀態輪詢會更新閒置時間;故障切換有多段非同步工作但沒有每體驗的控制操作佇列。本提案將兩者明確化。這是原始碼設計分析,不能代替新框架的測試證據。
範圍與責任
| 面向 | 框架責任 | 應用責任 |
|---|---|---|
| 體驗識別 | 原生 HttpOnly Cookie、來源/連接埠範圍、建立及回收 | 不自訂瀏覽器傳入的 scope ID |
| 業務狀態 | 每份體驗獨立建立、替換與銷毀 fixture | 實作帳號、草稿、業務資料及信件容器,不使用共用可變 singleton |
| Session | 每體驗一份 Native store,沿用既有發行/refresh/inspect/revoke 契約 | 驗證業務 proof 後才呼叫 Native 發行,不自行簽 token |
| 情境 | 描述清冊、選取/套用流程、重建交易 | 情境 ID、名稱、說明、初始 Given |
| 故障 | 控制命令序列化、啟用摘要、恢復按鈕 | 可重複套用的故障集合;正常值清除故障但不清業務資料 |
| 信箱 | 用途、收件者、狀態、期限、複製及錯誤狀態呈現 | 只提供這份體驗的合成信件、驗證碼生命週期 |
| 入口與頁面 | 具名輔助分頁、快捷鍵、頁面骨架、提示與跨分頁更新 | 掛入 router/layout、登入 dialog 的入口、產品跳轉連結與重設後清理接線 |
新增工具需在程式組裝時註冊業務能力,這與增加 HTTP 相容環境旗標不同。沿用應用既有 Mock 啟用來源,不新增第二個 runtime 環境開關。
不包含:真實郵件投遞、繞過產品身分驗證、任意腳本執行器、正式管理後台、跨裝置同步、資料持久化、HTTP 下完整可靠的跨分頁鎖,以及此次未接線的其他下游模組。
隔離與狀態模型
隔離單位是同一瀏覽器設定檔、同一應用來源的一份「體驗」。同來源分頁共用;不同瀏覽器 context、無痕環境與裝置分開。同一設定檔內多人操作不會自動隔離成每人一份。
框架維護兩種識別:
- 體驗 selector:伺服器產生的高熵秘密,僅由專用 HttpOnly Cookie 傳遞;不放 URL、Web Storage、JSON 或 log。
- generation:公開的不透明世代 ID,每次重建更換;只做新舊請求檢查,不作授權憑證,也不要求數值遞增。
Cookie 不設 Domain,Prototype Cookie 使用 SameSite=Lax、Path=/,HTTPS 時加 Secure。工具模式同時命名空間化 Prototype 與 Refresh Cookie:由經驗證的 definition ID 與瀏覽器來源埠產生兩個不同且合法的 Cookie 名。Refresh 的 Path 仍為 /api/v1/auth(有 API base 時加對應前綴)、HttpOnly/期限/輪替規則沿用框架。這個覆寫只限新工具模式;未採用者的 mockAuthPlugin() 仍預設 APP_SERVER_REFRESH。
命名依據是瀏覽器看到的來源埠(無明示埠時 HTTP=80、HTTPS=443),不是 Vite 設定的預定監聽埠。伺服器從已驗證的 request Host authority 取得,client 以 location.origin 核對 initialize 所回傳的 sessionScope;Session coordinator 與 Native middleware 必須使用同一個回傳 Cookie 名/Path。Vite 自動改埠後以實際 request Host 的新埠產生名字;來源不同就不共享體驗。
經反向代理時要求保留瀏覽器 Host 與一致的 scheme;首版不信任任意 X-Forwarded-* 推斷 Cookie namespace。會改寫 Host 或終止 TLS 且無既有可信來源還原的代理不在首版支援範圍,來源不符須明確拒絕。P-10 同時驗證兩類 Cookie;保留 Host 的代理、雙埠及自動改埠都要有證據,不只測 Prototype Cookie。
Native 路徑在邏輯上為 /api/v1/auth/__prototype/{generation}/__mock-session。有 Cookie 的操作須同時符合該體驗與 generation;inspect 沿用現行不帶 Cookie 的公開契約,generation 只定位候選 store,仍由該 store 驗證 Bearer。新模式禁止落回未隔離的全域 Native store。
公開 API 草案
本節是擬新增 API 的設計宣告,未提供可執行 import 範例;名稱與型別均待本輪審閱。Node 與 browser 入口分開,不讓 browser bundle 引入 Node fixture 或 Native store。
Node/Vite
在既有 @appfuse/appfuse-web/vite 新增以下能力,既有 mockAuthPlugin(options) 不變:
interface MockAuthMiddleware {
beforeCors: Connect.NextHandleFunction
handle: Connect.NextHandleFunction
dispose(): Promise<void>
}
interface MockAuthMiddlewareOptions extends MockAuthPluginOptions {
basePath?: string
}
declare function createMockAuthMiddleware(options?: MockAuthMiddlewareOptions): MockAuthMiddleware
interface PrototypeChoice {
id: string
label: string
description: string
}
interface PrototypeLease {
readonly generation: string
readonly signal: AbortSignal
assertCurrent(): void
}
interface PrototypeFixtureRequest {
domain: string
action: string
data: Record<string, unknown>
}
interface PrototypeFixtureResult {
status: number
body: unknown
}
interface PrototypeFixture {
dispatch(request: PrototypeFixtureRequest, lease: PrototypeLease): Promise<PrototypeFixtureResult>
mailbox?(lease: PrototypeLease): Promise<readonly PrototypeMail[]>
applyFault?(faultId: string, lease: PrototypeLease): Promise<void>
dispose(): Promise<void>
}
interface PrototypeDefinition {
id: string
initialScenario: string
scenarios: readonly PrototypeChoice[]
faults: readonly PrototypeChoice[]
links: readonly { id: string; label: string; href: string }[]
createFixture(context: {
scenarioId: string
generation: string
signal: AbortSignal
now: () => number
}): Promise<PrototypeFixture>
}
declare function prototypeToolsPlugin(definition: PrototypeDefinition): Plugin
Connect、Plugin 與 MockAuthPluginOptions 沿用現行 Node 型別;PrototypeMail 見共用 DTO。createMockAuthMiddleware 與既有 plugin 共用同一引擎、路由及 store 實作,不複製邏輯;每次 factory 呼叫建立獨立 store。dispose 冪等,撤銷並清除該實例全部 Session、終止持有工作,之後請求不得發行或恢復 Session。
prototypeToolsPlugin 內部為每個體驗建立 middleware,管理 close/回收/重建的 dispose,不讓應用持有其他體驗的 store。既有 plugin 的安裝順序與非 POST/preflight 行為須保持。
Native 掛載與前置方法檢查
basePath 是完整、同來源的絕對 pathname,預設 /api/v1/auth/__mock-session;拒絕 URL origin、query、fragment、空白、尾斜線及不在 auth scope 的值。只接受 {apiBase}/api/v1/auth/__mock-session 或 {apiBase}/api/v1/auth/__prototype/{generation}/__mock-session 形狀,Cookie Path 由共同引擎取其中 {apiBase}/api/v1/auth,不能用 generation 細分 Cookie Path。由框架傳給工具實例的值為後者。路由判定必須是 basePath 本身或其 / 子路徑,不接受只因字串前綴相同而命中的其他路徑。呼叫端不剝除、不改寫 req.url;共同引擎依 basePath 解析 suffix,避免工具複製路由與 Cookie 邏輯。
factory 不自行修改 Vite server。beforeCors 共用現行 Access 路由的方法判定:已知 Access 端點非 POST 回 405+Allow=POST;具 Origin 與 Access-Control-Request-Method 的 OPTIONS preflight 交給既有 CORS 規則,其餘未知路徑 next。handle 仍再次驗證方法及協定,不依賴前置層作唯一檢查。
mockAuthPlugin() 包裝 factory,在 configureServer 以現行 stack.unshift 位置註冊 beforeCors,再以 use 掛 handle。全域包裝將格式合法的跨來源 preflight 交給 Vite 既有 CORS 規則決定是否允許,不建立體驗、不發 Cookie。
工具包裝則在同一前置位置安裝來源/路由 dispatcher,範圍同時包含 /api/__prototype/* 與帶 generation 的 Native 路徑。跨來源 Origin 在進入 Vite CORS 或 Native beforeCors 之前即回 403 prototype-origin-rejected;工具模式沒有合法的跨來源 preflight,也不宣告跨來源允許標頭。同來源瀏覽器請求不會觸發 preflight,直接走來源檢查及 Native handle;人工構造的同來源 OPTIONS 不代表工具支援跨來源。
通過工具來源檢查後,dispatcher 才委派所選 Native 實例的 beforeCors/handle;未知/失效 generation 的實際 POST 回 409,不落到全域 store。兩種包裝共用 factory 的一般方法規則(例如已知 Native 端點的非 POST、非 preflight 回 405),但來源准入與跨來源 preflight 的預期不同;P-10 必須分別驗證,不以同一項「合法 OPTIONS」涵蓋兩者。
可選能力與 normal
清冊只傳遞可序列化資料;框架拒絕重複/非法 ID、不存在的 initialScenario、javascript: 或跨來源的產品連結。faults 只列應用提供的異常,不列保留的 normal,不接受以 normal 作自訂選項。
faults=[] 時 applyFault 可省略,manifest 的 faults 為空,UI 隱藏異常與恢復按鈕;直接 setFault(含 normal)回 prototype-capability-unavailable,不呼叫 fixture。faults 非空時 applyFault 必須存在;初始 faultId 固定 normal,createFixture 必須建立正常服務。恢復操作確實呼叫 applyFault('normal', lease),成功保留業務資料並更新 revision;已是 normal 時可冪等回現況,不增加 revision。情境重建建立全新正常容器,不另對舊容器呼叫 normal。
mailbox 方法也可省略,UI 隱藏信箱;直接讀取缺席能力同樣回 capability-unavailable。各情境的可選能力集合須一致,候選不符即準備失敗。顯示字串由應用經其翻譯流程提供,框架通用文案沿用 i18n。
共用 DTO 與 browser client
擬新增 @appfuse/appfuse-web/prototype browser-only 入口:
interface PrototypeMail {
id: string
purpose: string
recipient: string
code: string
sentAt: string
expiresAt: string
state: 'active' | 'used' | 'expired' | 'superseded'
}
interface PrototypeSnapshot {
generation: string
revision: number
scenarioId: string
faultId: string
clock: 'natural'
idleMinutes: number
}
interface PrototypeManifest {
id: string
scenarios: readonly PrototypeChoice[]
faults: readonly PrototypeChoice[]
links: readonly { id: string; label: string; href: string }[]
mailbox: boolean
sessionScope: { cookieName: string; cookiePath: string }
}
type PrototypeInitialization =
| { mode: 'enabled'; snapshot: PrototypeSnapshot; manifest: PrototypeManifest }
| { mode: 'disabled'; reason: 'plugin-not-installed' }
interface PrototypeClient {
initialize(): Promise<PrototypeInitialization>
manifest(): PrototypeManifest
state(): Promise<PrototypeSnapshot>
mailbox(): Promise<readonly PrototypeMail[]>
setScenario(id: string, expectedRevision: number): Promise<PrototypeSnapshot>
setFault(id: string, expectedRevision: number): Promise<PrototypeSnapshot>
restart(expectedRevision: number): Promise<PrototypeSnapshot>
touch(): Promise<void>
fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>
nativeBaseURL(): string
dispose(): void
}
declare function createPrototypeClient(): PrototypeClient
時間欄位為 ISO 8601 Instant(帶 Z/offset),以瀏覽器時區顯示;不加入業務 Local 型別或固定時鐘設定。每份新 generation 的 revision 從 0 開始,成功且實際改變狀態的 fault 命令才 +1;scenario/restart 成功時換 generation 並回 revision 0。revision 僅在相同 generation 內可比較,不能跨世代排序或單憑 revision 0 接受命令。
initialize 驗證並快取清冊,enabled 後 manifest() 提供工具頁所需的情境、故障、連結與 Session scope。未初始化呼叫操作拋 prototype-not-initialized;disabled 後除 initialize/dispose 外的工具操作拋 prototype-disabled,不默默轉發舊路徑。各情境的能力集合與來源 scope 須一致,不一致在準備候選時拒絕。
client 綁定初始化世代,fetch 加入世代 header,拒絕非同來源、非框架 fixture 允許路徑;不允許以一次 state 查詢把舊操作靜默改綁新世代。Native baseURL 使用公開世代路徑;應用仍使用既有 Mock Access client。framework API/工具頁都不接收任意體驗 selector。
初始化須確認瀏覽器確實保留 Scope Cookie,失敗回可辨識錯誤。已有體驗時不重建;容量保留與建立在伺服器端原子完成,並行首次開頁不能建立超過上限。瀏覽器有 Web Locks 時可協調初始化;HTTP 無鎖時仍靠伺服器容量保護及初始化後重讀收斂,允許首次同時開頁有一頁需重載,不承諾跨分頁無競爭。
React 工具組件
同一 browser 入口擬提供 PrototypeProvider、PrototypeToolsEntry、PrototypeToolsPage、PrototypeToolsLink。Provider 接收 client、現行 Session coordinator diagnostics,以及應用的 onExperienceChanged 清理回呼。回呼只清除應用已知的 runtime Session 與流程 locator;不做全域 storage.clear(),也不保存 credential。
Provider 負責訂閱與重設通知,Entry 只負責入口及快捷鍵;即使工具頁隱藏 Entry,同步仍然運作。應用在 DEV 且 Mock 啟用時才動態 import 並掛入 router/layout,登入 dialog 內可放 ToolsLink;關閉時不載入這個子樹。固定工具路由 /_prototype/tools,API 路徑由 client 與 plugin 的共同契約解析,不讓應用複製 URL 拼接。
未安裝工具 plugin 與固定時鐘回歸
安裝哪個 Vite plugin 由應用程式組裝決定,框架不新增旗標。client.initialize 先以無副作用的 GET /api/__prototype/capabilities、自訂 X-Appfuse-Prototype-Discover: 1 探測;安裝工具時回帶固定協定識別 {protocol: 'appfuse-prototype', version: 1} 的 JSON,不建立體驗、不續命。
discovery 使用 redirect: 'manual',不跟隨登入頁或其他重導。回應分類採封閉規則,Content-Type 依不含參數的 media type 比對(不區分大小寫):
| discovery 結果 | 分類 |
|---|---|
| 未重導的 HTTP 404 | disabled/plugin-not-installed |
| 未重導的 HTTP 200,Content-Type 為 text/html | disabled/plugin-not-installed,視為 Vite SPA fallback |
| 未重導的 HTTP 200,Content-Type 為 application/json,JSON 與固定協定識別均合法 | enabled 候選;再 POST initialize 並確認 Cookie、manifest 與 Session scope |
| 其餘一切結果 | 可辨識的 discovery 錯誤,不判 disabled、不啟動 legacy adapter |
最後一列包含 network/timeout、3xx 或 manual fetch 的 opaqueredirect、400/401/403/405/409 及其他 4xx、5xx、其他 2xx、Content-Type 缺席/非 JSON 也非 HTML、JSON 解析/協定錯誤;例如 Spring problem JSON 不能當成 plugin 缺席。只有前兩列允許 disabled,沒有其他 fallback。enabled 之後任何 state/fixture/Native 失敗亦不得退回其他資料來源。P-13 必須覆蓋這個預設錯誤分支,不能只測常見的成功與 404。
應用先 await 這個分流,再啟動 worker 及建立相應 Native/coordinator 接線:
| 模式 | Provider/router | MSW handler/Native 接線 |
|---|---|---|
| enabled | 掛工具頁、入口、同步與互動 touch | 選定域委派 client.fetch;Native 使用世代 URL 與 manifest.sessionScope |
| disabled | Provider 原樣呈現 children,不掛入口、工具 route、計時器或同步;直接工具元件不呈現功能 | 應用明確選擇既有 fixture adapter 與既有 Native baseURL/Cookie;不呼叫 prototype.fetch |
| 初始化錯誤 | 呈現可辨識錯誤,可重試 | 不啟動另一份全域資料容器掩蓋錯誤 |
來源應用可維持既有固定時鐘測試分支:不安裝工具 plugin,仍安裝原 mockAuthPlugin+固定時鐘 fixture,產品 handler 在 disabled 分支沿用 /api/__test/.../store adapter。這是應用既有接線,不把 TMS_R2_FIXTURE_CLOCK 搬進框架;自然時鐘工具與固定時鐘回歸分別在獨立服務程序執行。若應用沒有 legacy adapter,disabled 可繼續其未採用工具的頁面,但依賴工具 fixture 的流程須明確報不支援,不能假稱可用。
控制協定與錯誤
以下為 Vite 的邏輯路徑;實作須共同處理 Vite base 與現行 API scope,並以非根 base 測試,不另要求環境旗標。
| 方法與路徑 | 契約 |
|---|---|
GET /api/__prototype/capabilities | 不帶體驗資料的協定探測;不建立、不續命 |
POST /api/__prototype/initialize | 既有體驗則讀取;沒有才配置並以 Set-Cookie 建立,回 snapshot 及清冊 |
GET /api/__prototype/state | Cookie+必填 generation 自訂 header;回權威世代,允許 header 為舊世代;無體驗回 409,不建立、不續命 |
GET /api/__prototype/mailbox | 依 Cookie+固定 generation 讀取,不延長閒置 |
POST /api/__prototype/touch | Cookie+必填 X-Appfuse-Prototype-Generation,須符合目前世代;僅由可信使用者互動觸發,節流後延長閒置 |
POST /api/__prototype/scenario | 固定 generation header+{id, expectedRevision},完整重建;成功回新世代、revision 0 |
POST /api/__prototype/fault | 固定 generation header+{id, expectedRevision},同容器更換故障;實際改變才 revision +1 |
POST /api/__prototype/restart | 固定 generation header+{expectedRevision},重建 initialScenario,回新世代、revision 0 |
POST /api/__prototype/fixture | Cookie+必填 X-Appfuse-Prototype-Generation,須符合目前世代;固定 schema 的 domain/action/data,委派此體驗 dispatch |
除不含體驗資料的 capabilities 外,initialize 以外端點不允許未知 Cookie 隱式建體驗;無體驗回 unavailable。變更方法限定 POST/JSON、限制 64 KiB,拒絕不合 schema。scenario/restart/fault 的 expectedRevision 必須是非負安全整數,並與同一請求的 generation 一起檢查;先驗 generation、再驗 revision。舊世代 revision 0 不能命中新世代 revision 0。
HTTP 缺少來源標頭的判定
不以 Origin 或 Sec-Fetch-* 必然存在為前提。先依已驗證 Host authority 與服務 scheme 建立該請求的同來源基準(代理限制見 Cookie 段),再採下列一致規則:
- 有標頭即嚴格檢查:Origin 必須恰為本來源,
null/不符即 403;Sec-Fetch-Site 存在時必須為 same-origin,same-site/cross-site/none 均拒絕。缺席本身不拒絕,不放寬已存在而不符的標頭。 - 缺席時的必要下限:除 discovery/initialize 外,所有工具請求(包含 state 的 GET)必帶
X-Appfuse-Prototype-Generation,格式須合法;只有 state 允許舊值以查知權威世代,其他路徑須與 Cookie 選定的 generation 相同。缺少 header 回 400 prototype-request-invalid;generation 不是秘密,不能代替 Cookie。 - bootstrap 例外:capabilities 必帶
X-Appfuse-Prototype-Discover: 1;initialize 必帶X-Appfuse-Prototype-Initialize: 1,Content-Type=application/json,固定空 JSON body。兩者不要求尚未取得的 generation,但同樣檢查已有 Origin/Sec-Fetch-Site。 - 瀏覽器跨來源限制:以上自訂 header 讓跨來源腳本必經 preflight;工具 namespace 不回 Access-Control-Allow-Origin/Allow-Headers,跨來源 preflight 在 Vite CORS 前拒絕。HTML form/直接導航沒有必要 header,也無法建立或讀取體驗。這些 header 是瀏覽器來源防護下限,不是對任意 Node client 的身分驗證。
Native 仍使用現行 X-Appfuse-Session-Protocol、JSON 與 Bearer/Cookie 契約,不要求新工具 header;工具 plugin 的來源前置 dispatcher 同樣嚴查已存在的來源標頭,帶 generation 的 Native 路徑之跨來源 OPTIONS 也須在 Vite CORS 前回 403,沒有工具模式的合法跨來源 preflight。世代在 Native pathname 固定,不改現行 Mock Access client 的請求格式。P-07/P-10 要涵蓋兩種來源標頭都缺席的 LAN GET、bootstrap、自訂 header 缺席及跨來源拒絕。
JSON 狀態、信件與錯誤均 Cache-Control: no-store。錯誤回應以 RFC Problem 形式含穩定 errorCode、HTTP status 與英文 title,不含 OTP/credential/stack。框架 UI 依碼翻譯,應用可辨識:
| HTTP | errorCode | UI 行為 |
|---|---|---|
| 409 | prototype-experience-changed | 拒絕舊操作,清理後重載;不自動重送舊業務操作 |
| 409 | prototype-revision-conflict | 讀取最新摘要,請使用者重新選擇,不覆寫他頁新設定 |
| 409 | prototype-experience-unavailable | 提示已到期/服務重啟,可重新開始建立體驗 |
| 503 | prototype-capacity-unavailable | 提示體驗容量已滿,可稍後重試 |
| 400/405/403 | prototype-request-invalid/method-not-allowed/prototype-origin-rejected | 顯示操作不被接受,不偽稱服務故障 |
| 503 | prototype-fixture-unavailable | 準備/操作失敗,未確認成功;允許重試 |
| 409 | prototype-capability-unavailable | 該體驗未提供信箱或故障能力;不呼叫不存在的方法 |
框架既有 BrowserCapabilityError 與 Session diagnostics 沿原契約呈現,與上述工具狀態分開;不可統一顯示為後端服務故障。
生命週期、併發與回收
建立與重建
每份體驗有控制命令佇列。scenario/restart/fault 在進佇列時保留 expected generation/revision,執行前再驗證;不因排隊自動升級舊命令的版本。
重建先在獨立容器準備 fixture 與 Native store,成功後原子替換;準備失敗銷毀候選、保留舊體驗。替換後立即將舊 lease 標為失效並 abort,再 dispose 舊容器;舊資料永不移交給新容器。dispose 失敗需可診斷與重試清理,不能恢復舊路由或讓已成功替換的結果回報「未替換」。
重建不靠瀏覽器必定成功刪 Cookie 來達成隔離。仍送往舊 Native generation URL 的請求回 409 prototype-experience-changed,且不 Set-Cookie;舊 Access 不能在新 store inspect。若舊 Refresh Cookie(auth Path 沒有 generation)被送往新 URL,新 store 不認得該 credential:Access refresh 回現行 401 refresh-session-invalid,僅在該 generation 仍 current 時沿現行引擎清除對應 namespaced Cookie,不建立 Session、不返回 Access;legacy refresh 亦依既有 401 契約處理。新驗證成功的 issue 才能在新 store 發行 Session。跨世代晚回 Set-Cookie 一律抑制,同世代 HTTP 多分頁登入/refresh 的既有限制仍須揭露。
長時間 fixture 業務請求不持有控制佇列,避免被 hold 的請求擋住重設。fixture 必須只寫入自己的實例、在 await 後的重要寫入前驗證 lease,並回應 signal;框架在完成回應前再次檢查世代。晚回的失效 Native 請求不能 Set-Cookie,失效 Access 也不能由新 store inspect 成功。
故障切換必須原子套用完整故障狀態,不能先清一半再失敗而保留舊摘要。adapter 的 applyFault 須先準備新設定再提交,失敗保留原狀;框架在命令成功後才更新 faultId/revision。normal 清除故障但保留帳號、草稿、信件與待確認結果,不把業務結果改成成功。
閒置與資源
首版提議固定預設 120 分鐘閒置、最多 128 份體驗;作為框架內部常數,不暴露新的部署旗標,本輪由人審評估數值。查詢 snapshot/mailbox/capabilities、背景輪詢、focus、initialize 的既有體驗讀取與 BroadcastChannel 通知都不延長期限。使用者可信的鍵盤/指標互動才觸發 touch(最多每分鐘一次);產品 fixture/Native 的有效業務操作也更新活動時間。自動 refresh/inspect 不延長 Prototype 容器期限,Session 自身期限仍由原引擎判斷。
容量壓力採逐出,不把 128 份當兩小時的硬封鎖。配置新體驗時先回收過期者;仍滿則先選未曾 touch/有效業務互動者,再依 lastActivity 最久者逐出,時間相同以建立序號決定。初始化與純查詢不算互動,因此連續新建/關閉 browser context 可持續騰出名額,不依賴 unload/beacon 必達,也不偵測「是否為測試」。已互動者在無較低優先候選時仍可能因容量被回收;120 分鐘是無容量壓力時的閒置期限,不能宣稱期間一定保留。
容量選擇與 reservation 原子執行;已有正在執行的控制命令或 Native 發行/輪替者暫不逐出,也不逐出本次建立尚未完成的 reservation。排隊命令不能永久佔位;長時間 fixture hold 不固定容量,選中後 abort lease 並拒絕晚回。若 128 份全被短期工作/reservation 佔用,回 503 capacity-unavailable+Retry-After: 1;工作結束後新建即可再選候選,不必等待閒置兩小時。建立失敗即釋放 reservation。
伺服器定期回收過期容器及失敗候選,不必等待下一筆 HTTP;close 清除 interval 並等待冪等 dispose。expire/容量逐出/重建透過同一個體驗生命週期互斥,不能誤刪已替換的新容器;非同步業務舊工作由 lease 拒絕。已被逐出的瀏覽器再呼叫會收到 409 experience-unavailable,提示「已到期或因資源回收,請重新開始」,不偷偷建立或退到全域資料,也不假稱服務故障。只銷毀選中的容器,不清空其他體驗。
P-05 必跑同一個 server、連續至少 256 個新 context(含純開頁與有業務互動兩組),每個關閉後繼續下一個;不改資源常數、不推進兩小時,後續 initialize 仍成功且容器+reservation 不超過 128。另測 128 份同時 busy 的暫時 503、釋放後可重試,以及被逐出頁面的可辨識恢復流程。
跨分頁
BroadcastChannel 只發失效通知,不傳 OTP/credential/Scope selector,也不作權威資料;接收方向 state 核對。可見分頁以低頻輪詢作補充,背景分頁停輪詢,focus 重新讀取。generation 改變便清理已知 locator 並重載;revision 改變則更新工具摘要,不必丟失產品表單。
HTTP 無 Web Locks 仍允許使用工具。伺服器控制佇列只保證本體驗的工具操作順序,不擴張成框架 Session 在 HTTP 下的跨分頁可靠性保證;同時登入/登出限制必須依實際 diagnostics 揭露。
介面與可及性
入口使用具名工具分頁,桌面支援 Alt+Shift+P、手機可點選;快捷鍵忽略輸入法組字及重複鍵,不攔截一般文字輸入。開頁被拒絕時提供可點擊入口。登入 dialog 內入口必須位於其 focus scope,返回產品頁保留表單、滾動與焦點。
工具頁優先顯示合成環境、隔離範圍、已啟用情境與故障。信箱桌面主欄、手機先列出;進階異常預設收合、恢復正常保持易找。情境選取與已套用摘要分開;情境/重新開始需確認,正常恢復不刪資料。
信箱區分 loading/empty/failed/stale;更新失敗保留上次信件並標示,不能當成空信箱。顯示用途與收件者、期限及狀態,只讓有效碼複製;Clipboard 不可用時碼仍可選取並說明手動複製。按鈕 busy、防重送、錯誤重試與完成訊息有可及名稱/狀態提示。用 390/768/1440 px 驗證,不複製來源應用品牌或建立第二套基礎元件。
相容、採用與替代方案
既有 mockAuthPlugin() 的預設 Cookie、Refresh-only/Access opt-in、HTTP 路由與 Session 引擎行為保持。新增工具採用者由工具 plugin 管理隔離 Native,依 manifest 設定其專用 Cookie scope,不應再同時註冊全域 plugin;啟動時檢出雙重註冊並報明確錯誤。未採用工具的應用無行為改變。
browser seed、Node fixture 與 promote
產品資料接縫維持 applet → service/apiClient → 產品 MSW handler;只有 handler 後面的資料來源不同,service/元件不能直接呼叫 Prototype API 或 import mock 值。未接 Prototype 的 applet 繼續使用 src/mocks/data 的 browser seed/既有 handler,維持既有頁面載入生命週期,不因安裝工具就自動搬到 Node。
選定接入的域,其資料工廠與 seed 值仍位於 src/mocks/data,由 Node scripts 組裝每體驗實例;產品 handler 透過共用 adapter 委派 /api/__prototype/fixture。同一域在一個執行模式內只有一個可變權威來源;不得一半寫 browser seed、一半寫 Node,或在 Node 失敗時改讀本機舊資料。跨域需要共同可變資料時須一起接入同一體驗 fixture;純唯讀 seed 可共用值,投影型別仍由產品契約定義。
應用在組裝處維護哪些域已接入的清冊與 backend 選擇,不新增環境旗標。enabled 模式選 Node adapter;discovery 明確 disabled 的固定時鐘回歸選既有 adapter。選擇在 worker 啟動前確定;後續 409/503 不切換來源。工具的「跨 reload 保留/重設」承諾限於已接入的域,未接入者按既有 seed 行為;介面說明須揭露涵蓋範圍,不宣稱重設了所有 applet。
此設計保持 m-mockup-boundary 的資料位置與 handler 接縫,但對已接入域新增體驗級記憶體生命週期,取代該域每頁 seed 重置;不增加磁碟/Web Storage 持久化或模擬正式系統交易。在參考 mockup 落地前,須把這個 opt-in 生命週期與作用範圍同步到 canonical 邊界說明/對應 practices,經既有方法論同步流程處理;本輪只提出必要修訂,不靜默改寫現行 rule 或宣稱已同步。
/promote 仍投影 mockup-owned src/,產品 services 不改,web 預設 VITE_MSW=false 時不啟動 worker/client discovery/Provider,產品請求走真後端;不新增依檔案性質排除 src 的規則。Node 組裝維持在會被剝除的 scripts/,src 只能依賴 browser 公開入口/型別,不得反向 import scripts 或 Node 模組。vite.config.ts 的工具組裝與 scripts import 屬 Prototype 專屬,必須在 promote 的 Vite 配置轉換時去除,不能只刪 scripts 留下失效 import;src 中已被投影但休眠的 mocks 也不得在 build 時要求 Node。
若 promoted web 在 DEV 再啟用 MSW、但未安裝工具 plugin,依 discovery disabled 契約使用應用明確提供的 legacy adapter。production Host 的 MSW demo 與 Java adapter 維持既有能力;新工具不進 production,依賴新 Node fixture 卻沒有 legacy adapter 的流程不能宣稱支援部署型 demo。參考接線須驗證 MSW=false 的真 API 接縫與未採用域仍可編譯,不以調整產品 service 解決 mock 依賴。
參考 mockup 提供中性的兩個情境、合成信箱與一個故障,示範完整接線。來源應用則保留其產品情境/故障/驗證流程,先以測試套件驗證新 API,再在已發布版本升級時移除本地基礎設施;本設計回合不改動來源專案,不假稱下游採用或產品 UAT 完成。
| 方案 | 評估 |
|---|---|
| 維持每個下游各自實作 | 短期最少框架工作,但隔離、回收與競態修正會持續分歧 |
| 直接搬入來源頁與全部 fixture | 帶入業務情境、品牌及具體 URL,不符合框架責任 |
| 共用容器、協定、工具組件,應用註冊業務 adapter | 建議方案;以公開契約承接既有成熟行為 |
| 首版同時交付 Java Host 與部署型 Prototype | 擴大跨框架生命週期與部署範圍,另案評估 |
設計人審通過才進行實作;實作完成另開 change 人審,通過與結案後依既有流程發布不可變 Web pre-release。本提案不預占下一個 alpha 號、不要求 Server 發版。設計通過不等於 runtime 已完成。
驗收矩陣
下列均為未執行的交付條件,實作階段需提供實際命令、結果與證據。
| ID | 命題 | 驗證方式 |
|---|---|---|
| P-01 | 兩個 browser context 使用同 Email,業務資料、信件、Native inspect 與重設各自隔離 | 真瀏覽器 E2E |
| P-02 | 同 context 多分頁共享;重設傳播且保留其他 context | E2E:有/無 BroadcastChannel、背景後回前景 |
| P-03 | 舊 generation 的 fixture、Native 路徑、晚回 Cookie 與 Access 不能污染新體驗 | 受控 hold+重設+晚回的整合測試 |
| P-04 | queue+generation/expectedRevision 拒絕過時命令;重建 revision 歸 0、不能跨世代誤配;故障原子套用與 normal/無故障能力一致 | 併發整合測試與 adapter 契約測試 |
| P-05 | 背景查詢不續命、互動續命;定期回收;容量逐出、reservation 不超限;256 個依序 context 不受兩小時封鎖;全 busy 暫時 503 後可重試 | Node 可控時鐘/排程與同 server browser-context churn 測試;UI 仍自然時間 |
| P-06 | prepare 失敗、dispose 失敗、斷線重試與容量不足顯示正確狀態,沒有全域 fallback | 故障整合測試+UI 錯誤呈現 |
| P-07 | HTTP 缺 Origin/Sec-Fetch-* 時可 bootstrap、讀 state/信箱並登入;安全來源保留 Web Locks;訊息依實際 diagnostics | LAN/localhost 瀏覽器 E2E,不改安全旗標;補無來源標頭的協定測試 |
| P-08 | reload、deep link、refresh/replay/revoke 沿用 Session 契約,credential 不進 Web Storage | 現有 Native suite+採用者接線 E2E |
| P-09 | production、MSW 關閉與 preview 無入口、頁、監聽及工具 API;JS/source map 無工具來源模組 | build 掃描+實際 HTTP/瀏覽器;200 SPA HTML 不算 API 成功 |
| P-10 | schema/自訂 header 缺席/Origin 衝突;全域包裝的合法跨來源 preflight 交 Vite CORS,工具及世代 Native 的跨來源 OPTIONS 在 CORS 前回 403;一般非 POST、非 preflight 的 Native 405;兩類 Cookie 雙埠/改埠/代理 Host;舊 Refresh 在新世代回 401 | 分開測兩種包裝的 preflight 結果及無副作用;API/雙 port/auto-port/保留 Host 代理整合測試,另驗無 Cookie inspect 仍驗 Bearer |
| P-11 | 快捷鍵、dialog focus、手機排列、loading/stale/copy fallback 可操作 | 390/768/1440 px 瀏覽器測試與介面審閱 |
| P-12 | 既有 plugin 不變,包入口無 Node 混入、未採用無影響;非根 Vite base 可操作 | 原回歸、打包/型別與消費端 smoke |
| P-13 | 僅直接 404/200 HTML discovery 為 disabled,Provider 無入口與計時器,固定時鐘既有 fixture/Native 可用;有效 200 JSON 才初始化;其他所有結果均錯誤,不 fallback | 獨立固定/自然時鐘遷移 E2E;discovery 表測試包含 400/401/403/405/409/418/429、3xx/opaqueredirect、5xx、204/其他 2xx、未知或缺席 Content-Type、problem JSON、無效 JSON/協定及網路失敗 |
| P-14 | 已接/未接域資料來源單一;未接域沿 browser seed;Node 失敗不讀舊 seed;promote 移除 scripts 及 Vite import,MSW=false 不呼叫工具而走真 API | 混合域 fixture 測試、promote 暫存投影 build 與真 API 請求 smoke |
| P-15 | 新公開入口/型別具 Unreleased、README/指南;邊界生命週期說明與 promote 轉換對齊,沒有假冒新功能已發布 | 變更清冊、文件 lint、契約/投影檢查及人審 |
交付順序與本輪待決點
- 設計/API 人審:確認本稿範圍、獨立 middleware、fixture 契約、queue+revision、閒置與回收定義。
- Node 基礎設施:共用 Native middleware factory、beforeCors/basePath、容器與協定、生命週期測試;完成
lib/viteREADME,依30-public-api.md與 changelog 規範新增[Unreleased]的公開 export/行為條目。本設計回合不先寫成已交付功能。 - browser client/工具頁:discovery 分流、世代同步、錯誤呈現、無障礙及正式建置排除;完成新 prototype 入口 README、公開型別/exports 與對應
[Unreleased]條目。 - 參考 mockup 最小接線、來源應用自然/固定時鐘相容性、混合域與 promote 投影驗證;完成 docs-web 的接線指南、遷移/限制與 AI 索引,同步 README 與指南。補齊 canonical mock 邊界說明/promote Vite 轉換及其 practices 相容投影,不直接複製下游規則;方法論變更依既有同步/審閱流程處理。不混入其他產品改動。
- change 人審、發布、下游正式版本採用,分別核對,不能只完成畫面便結案。
本段保留送審時的待決點:公開入口、basePath/beforeCors 與 adapter 粒度;queue/generation/revision 與舊工作失效的責任;120 分鐘/128 份加容量逐出的取捨;discovery disabled、混合域及 promote 接線。上述設計已隨 revision 3 接受,實作及驗收仍待完成,不能將設計結案視為 runtime 通過。
Decision Log
| 日期 | 結論 | 依據與範圍 |
|---|---|---|
| 2026-10-26 | accepted,設計人審結案 | 20261026-prototype-experience-design 第 3 輪 r3-5eb4247b84ed3e76,Jason Lin 於 2026-10-26T13:25:11Z verdict approve;R1-01~R1-10、R2-01~R2-03 共 13 條核對皆已處置,無 blocking/major 或新意見 |
持久架構結論與刻意不採用的方案保存於 ADR-011。受審原稿 SHA-256 為 42ef6780f52ecadf1aa649d9ab5d71a068f522a444e30b445f7e3423dad02463;結案後僅同步接受 metadata、狀態說明、ADR 關聯與此決策紀錄,未改 API/協定/驗收正文。從「摘要」至「交付順序」之前的通過正文 SHA-256 為 f02860a97ac6e9ce5fcf7619fef1197e11727b7c4ba59f857c67cd151ec440af,可用於核對本次狀態同步沒有改動設計。
暫時審閱交換及附件已依 peer-review 流程移除;未留下永久簡報副本。設計通過解除實作前閘門,後續 runtime 變更需另開 change 人審並取得實際驗收證據後才發布。