ADR-036:租戶自訂網域與入口租戶綁定
ADR 編號:036 狀態:已接受(Accepted),P0 設計審閱通過 提案日期:2026-10-13 決策日期:2026-10-13 決策者:Development Team 取代:局部擴充 ADR-034 的位址選擇與登入前 scope 設計,僅新增 tenant-domains;single 與既有已發布行為保留 被取代:無 實作狀態:P1–P3 指定本機矩陣完成,實作 change 第 2 輪人審通過;framework 能力已發布為
4.0.0-alpha.35。參考實作依賴採用、正式部署/MySQL/多節點/舊 artifact 回復及 UI 採用證據待補;FU-90 保持開放
摘要與決策來源
同一 Server 以受控網域目錄提供多個租戶入口。網域辨識入口租戶,認證與既有授權決定可存取的租戶;兩者須核對一致。瀏覽器、背景通知及登入交換流程共用 ADR-034 的對外位址能力。
2026-10-13 使用者同意整體方向並授權產出設計草稿;共享工作樹第 3 輪審閱結論為通過,使用者明示執行 close。Q1–Q5 的 P0 設計裁決及 R3-01 minor 收尾落於本 ADR。P0 設計接受不代表新增公開 API 已發布。其後使用者明示開始,已完成下方 P1–P3 指定本機矩陣與回歸;2026-10-14 實作 change 第 2 輪人審通過並 close,其後依使用者指定發布 4.0.0-alpha.35,結論與發布範圍見下方持久紀錄。本機結果不代表完整部署環境驗收,framework 發布也不代表參考實作或 UI 已採用;FU-90 尚未結案。
本案承接 workspace app-docs/docs/specs/progress/follow-ups.md 的 FU-90 與 ADR-034。ADR-034 於 2026-09-12 預留的「ADR-035」是歷史規劃值,現已由報表決策使用;租戶網域正式使用本編號 036。
背景與 P0 本地查核(2026-10-13,歷史快照)
ADR-034 已交付單一 canonical URL 與 resolver SPI,但同 Server 租戶專屬網域仍未交付。單純用 Host 選擇 URL 無法證明帳號、JWT、Refresh、SignedLink 或 OIDC flow 屬於該租戶。
| 現有接點 | 2026-10-13 本地查核 | 對本設計的約束 |
|---|---|---|
PublicServerUrl/PublicServerUrlResolver | appfuse-server/src/main/java/io/leandev/appfuse/web/url/ 已有不可變 URL 與顯式 context SPI | 沿用 URL 組合;不由 request 字串拼接 redirect |
PublicServerUrlContext | scopeId 僅代表應用已確認可信的 scope;登入前使用 unspecified() | 不能直接把原始 Host 當成可信業務 scope;須另定入口信任與 protocol binding |
PublicServerUrlConfig | app-tenant-server/.../config/ 固定回傳單一設定值 | tenant 模式需新增目錄組裝;既有單一入口保留 |
Tenant | assigned、不可變的 String id;slug 唯一且可讀 | 網域映射用 Tenant.id;不由 slug 猜測租戶或作第二權威 |
CompositeTenantIdResolver | 預設由 JWT claim/UserDetails 取得 tenant ID | 保留已驗證認證來源;Host 不加入鏈首覆蓋 tenant ID |
| SignedLink application adapter | consume URL 使用 unspecified();前端跳轉仍有全域 fallback | tenant mode 必須傳 tenant ID 並改為 (tenantId, app) 明確查詢 |
NotificationDeepLinkProvider | deepLinkFor(type, recipient, model) 無顯式 tenant 參數 | 需查核 dispatch/outbox 執行脈絡;若不足,以 additive SPI 擴充,不能依最後一個 request 猜測 |
| 版本 | 框架 source 與 tenant 依賴均為 4.0.0-alpha.34;tenant 使用 Boot 4.0.1 | 本表證明本地接點存在,不證明新增網域能力已發布;實作前重核 artifact 與公開 API |
既有 multi-tenancy guide 含已移除的 Builder/舊 JWT resolver 範例。本 ADR 採本地公開 API 與現行 ADR-016/017/023/024,不複製舊範例;指南修訂在實作階段依實際適用版本處理。ADR-030 仍為 Proposed,不當成已接受的前提。
單一 URL 呼叫點與轉換責任
2026-10-13 以 rg -n 'PublicServerUrlContext.unspecified' app-tenant-server/src/main/java 核對,共 6 檔、7 個呼叫點;以下路徑都相對該模組的 src/main/java/io/leandev/app/。此表是現況與待實作遷移,不是已完成。
| 檔案/位置 | 目前呼叫時點 | tenant-domains 的接入設計 |
|---|---|---|
feature/oidclogin/SessionInfrastructureConfig.java:37 | 啟動期 Cookie serializer customizer,以單一 URL 固定 Path | 啟動只建構不可變目錄與 request-aware serializer,不解析未指定 tenant URL;write/clear 在請求期依入口取得 Path,保留 JDBC Session 交易隔離 |
security/oidclogin/OidcSecurityConfigurer.java:77 | 啟動期逐 registration 比對一個 callback | 改為入口 × registration callback 矩陣驗證,逐個具名目錄 entry 解析;不以無 scope 呼叫作啟動驗證 |
security/oidclogin/OidcSecurityConfigurer.java:108 | 授權請求期設定 redirect URI | 依起始入口與允許的 registration 選擇 callback;固定 protocol binding,callback/exchange 不重新猜測 |
controller/auth/RefreshSessionCookie.java:100 | 請求期組合 write/clear Cookie Path | 接收本次可信入口,credential tenant 核對後取得 Path;不修改 singleton 的全域 Path |
config/PublicActuatorLinksConfig.java:46 | 啟動期建 mapping bean;resolveLinks 本體在 discovery 請求期 | 啟動不 resolve 單一 URL;discovery 依具名 platform entry 產生 links,tenant origin 不曝露平台維運 discovery;授權沿用 ADR-032 |
controller/oidclogin/OidcAuthController.java:66 | 請求期產生 authorization endpoint redirect | 依已解析入口與 registration binding 組合,不用未知 Host 或平台 fallback |
service/signedlink/SignedLinkAppService.java:227 | 簽發 consume URL;可由請求或背景觸發 | 從可信業務/帳號 tenant 或明示 platform purpose 選具名 entry;無 tenant 的背景簽發拒絕 |
single 保留以上單值行為;tenant-domains 需一併改好啟動與 runtime consumers 才切換模式。無 scope runtime 失敗不等於 bean 建構時強制 resolve:啟動驗證走具名 entry 列舉,不得讓新 resolver 的拒絕行為使 OIDC/Session 無條件啟動失敗。
考量的方案
| 方案 | 優點 | 代價/問題 | 結論 |
|---|---|---|---|
| 每次從 Host/Forwarded 推算 tenant 與所有 URL | 初始配置少 | 背景無 request;入口與身分混淆;代理信任及 callback 難一致 | 不採用 |
| 網域自助註冊、DB 目錄、即時更新與憑證自動化 | 支援產品化網域生命週期 | 增加 ownership 證明、DNS/TLS、狀態機、失效與多節點一致性 | 後續另案 |
| 維運受控目錄,入口與身分獨立核對 | 可重現、背景可用、可沿用 resolver | 部署者維護 DNS/TLS/IdP,目錄變更需受控重啟 | 已接受 |
已接受的設計
1. 模式、設定與唯一來源
以下為 Q1 已接受的配置設計;P1/P2 已完成工作樹 binding 與 source ownership;P3 指定本機矩陣已驗證,skeleton 正式下行/升級及部署配置採用仍待各自流程。租戶目錄採清單與明確的 tenant-id 值,避免 Spring relaxed map-key binding 改寫任意租戶 ID。
app:
public-endpoints:
mode: tenant-domains
# P1 原型要求明示可信連線 peer;部署仍須隔離 backend 並驗證 proxy authority。
trusted-proxy-addresses: ["127.0.0.1"]
platform:
server-base-url: https://console.example.com/platform-api
frontend-base-urls:
console: https://console.example.com
tenants:
- tenant-id: "default-tenant"
server-base-url: https://shop-a.example.com/gateway
frontend-base-urls:
office: https://shop-a.example.com
- tenant-id: "tenant-b"
server-base-url: https://shop-b.example.com/service
frontend-base-urls:
office: https://shop-b.example.com
retired-origins:
- "https://old.example.com"
single:預設/未設定新模式時保留 ADR-034 的app.public-base-url、既有前端設定與 DEV 規則。它不以 Host 指派 tenant;認證與資料隔離維持現行契約。tenant-domains:只由server-tenant組裝;app.public-base-url與既有全域前端 fallback 不能同時成為 tenant 流程的來源,衝突設定拒絕啟動。server-tenantless明示此模式時拒絕,保留其單一入口,不引入 tenant runtime。tenant-id欄位原樣使用穩定Tenant.id,不做小寫、去符號或 trim 後替換;拒絕空白值、長度超限、重複 ID,並與 DB ID 精確比對。slug可供維運生成建議子網域,但登入/URL resolver 只查明示目錄;改 slug 不自動改部署網域。- 每租戶一個 canonical Server origin,一個 origin 不分配給多租戶或 platform。不以 public path 作租戶判別,不提供任意 wildcard/alias。
- Server URL 使用
PublicServerUrl的驗證。目錄 lookup 的 host 採固定 IDNA ASCII/小寫/有效 port 正規化,預設 HTTPS;拒絕尾點、空標籤、非法 port、userinfo、模糊 authority 與重複正規化鍵。DEV 明示的 HTTP fixture 可用;HTTP 不自動導向未知 host。 - 第一版同租戶的 Office 與 API 使用同一 HTTPS origin、不同 path,以避免新增 credentialed CORS 責任。跨 origin frontend 若需擴充,須另行修訂 Q1 並納入 CORS/Cookie/CSRF 驗證,不能只填 URL 就視為支援。
- 啟動驗證全部目錄的結構、origin 唯一性與必要 app URL;Tenant 存在/啟用狀態在應用接線驗證,登入與簽發時再次核對,避免租戶停用後仍可使用。資料庫尚未可查時不得宣稱 ownership 已核對。
- 採雙向完整度核對:目錄 entry 的 Tenant 必須存在,提供服務時還須啟用;DB 每個已啟用租戶都須有一個目錄 entry。Q1 採「寫入阻擋+個別租戶隔離」:應用的建立/啟用 Tenant 入口、seed/migration 工具與受控營運寫入,在提交啟用前驗證該 ID 已登錄且必要 entry 結構有效,不符合即拒絕該筆啟用;停用不因目錄缺漏被阻擋。直接 DB 寫入可能繞過此守衛,因此啟動及 runtime 仍須獨立核對。
- 應用在 migrations/seed 後、宣布 ready 前執行資料完整度核對。真正不吻合是「DB 已啟用但未登錄」或「entry 指向不存在的 Tenant ID」:僅排除該租戶的業務入口、登入、refresh、consume/exchange 與背景簽發,對維運發出具名配置告警。已存在、未啟用而已登錄的租戶是合法的「待上線/已停用」狀態,只標示不可服務,不產生配置錯誤告警;既有啟用守衛、停用與 credential 處置仍適用。已核對正常的租戶繼續服務,Server 不因上述單一租戶資料問題拒絕 ready。runtime 與啟動採相同分類、隔離與告警,沒有平台或其他租戶 fallback;未知 origin 仍依第 2 節拒絕。外部 DB 改動在下次啟動或 runtime 租戶狀態核對時偵測,不宣稱能阻擋所有 SQL 寫入。
- 此隔離僅適用「目錄與 Tenant 資料完整度」錯誤。全域目錄結構非法、重複 ID/origin、模式衝突、必要安全接線缺失仍整台拒絕啟動;DB 本身不可查時不能假稱任何租戶已核對,沿既有 datasource/readiness 規則處理。第 3 輪未承諾資料庫整體故障仍可服務。
- 新租戶先保持未啟用,準備網域、目錄、IdP;以完整目錄受控重啟可先將未啟用 entry 標為不可服務,再透過已具寫入守衛的啟用動作使其可用。每次入口/簽發都依既有可信 Tenant 存在/啟用查核取得可服務狀態,不能永久鎖死於啟動快照。發生繞過守衛的誤啟用時,維運可先停用該租戶,或補齊設定/DNS/IdP 並受控重啟,再核對解除隔離;不停止健康租戶、不以放寬核對或改為 single 當自動復原。第一版仍需要維運參與租戶上線,不支援 DB 一改即可登錄新網域。
- 清單可由 YAML、indexed properties(如
app.public-endpoints.tenants[0].tenant-id)或 list-index 環境變數(如APP_PUBLICENDPOINTS_TENANTS_0_TENANTID)輸入;ID 是 value,其大小寫/符號不可被 key 的 relaxed binding 改寫。跨 property source 的清單覆寫視為整份替換,不逐項 merge,必須提供完整一致目錄。兩 Server 的 env-config dev/test/prod 都需驗證設定形式:tenant 涵蓋 single/tenant-domains、任意合法 String ID 與覆寫;tenantless 涵蓋 single 及明示 tenant-domains 被拒絕。不限制 Tenant.id 為 slug 或 UUID。 retired-origins是另列的退役入口清單,不分配 tenant、不含 app URL/registration,不與 active tenant/platform origin 重疊。它只服務第 6 節的固定退役頁,不是 canonical alias、登入入口或 resolver fallback。- DNS ownership、TLS 與代理 route 由部署者人工核對並保存設定證據。第一版沒有使用者提交任意 host 的 endpoint。
2. 可信入口與認證租戶分離
入口解析只產生 request-scoped 的「租戶入口/平台入口」資訊,不賦予 authority、不設定登入主體,也不在認證前直接覆寫 TenantContext。框架的 URL scope 與安全 tenant context 是不同責任。
第一版優先採 ADR-034 已驗證的代理模式:邊界代理清除外來 Forwarded/X-Forwarded-*,傳遞經驗證的 Host/authority;後端只可經受控代理到達,明示不二次套 forwarded prefix。若必須使用代理重新產生的 authority header,須選定唯一可信來源與 hop 規則並實測;應用不得自行切換 Host、X-Forwarded-Host 或任意清單位置作 fallback。
| 情境 | 處置設計 |
|---|---|
| 未登記/非法 authority | 400;英文錯誤,不回顯可用於 redirect 的輸入 |
| 明示 retired origin | 固定 410 退役頁;不進認證/tenant resolver、不讀取 token/code、不 redirect,依第 6 節處理 |
| tenant 入口帳密登入 | 先驗證 credential,再核對 Account 的 tenant 與入口;不符走固定不透明登入失敗,不洩漏帳號所屬租戶 |
| Email OTP challenge/resend/verify | challenge 綁定入口與已解析帳號租戶;verify 在發放 credential 前再次核對;不以投遞 Email 網域推斷 tenant |
| JWT/Basic/API-key 已認證請求 | 由原 resolver 得到可信 tenant;與入口不同為 403;無有效 tenant 時不得退到 root |
| Refresh/SignedLink/OIDC exchange | 比對持久 credential/code/protocol context 綁定與入口後才發放 session/token;細部 wire 由 Q3 定案 |
| 背景/內部排程 | 無 HTTP 入口時使用明示可信 tenant ID;內部執行不是開放公共 HTTP bypass |
一致性核對須在 tenant 業務 EntityManager/交易建立之前完成,保留 ADR-016 的 Session 建構期隔離;登入 bootstrap 僅存取既有認證/平台資料,不因匿名無 tenant 自動開放 root 業務查詢。請求、async dispatch、背景工作均須有明確 cleanup,OSIV on/off 與執行緒重用列為驗收。
可信代理 client IP(實作人審 R1-02,2026-10-14 使用者裁決)
採用獨立的 X-Appfuse-Client-IP 單值 header。trusted-proxy-addresses 只判斷實際連線 peer;完成 peer/Host/Forwarded 拒絕檢查後,active entry 才讀取此 header。必須恰有一個 IPv4/IPv6 literal,拒絕缺值、重複、comma chain、hostname、zone ID、括號及非規範 IPv4 表示;不作 DNS lookup。驗證後 canonical IP 綁入 immutable request attribute,重派發不得改綁。retired 固定 410 的早期出口不讀此 header,也不讀 Session/credential。
AuthController 的 password IP+username limiter、EmailOtpController 的 create/resend 配額與 UserLoginAudit 只讀此已驗證 attribute。不改寫 getRemoteAddr(),入口在 REQUEST/ASYNC/ERROR 仍核對真實 proxy peer;client IP 不能作 tenant/authority/membership。single 未綁此 attribute 時保留實際連線(或 single 模式 operator 明示的代理接線)IP,忽略外來此 header。
operator 必須在對外第一個信任邊界無條件刪除/覆寫客戶端傳入的此 header,並由最後一個受信任代理送出一個經可信接線取得的 client literal。多 hop 必須明示哪些 upstream 可提供原 client IP,逐 hop 清除外來偽造值;不得任選 forwarding chain 的位置。server.forward-headers-strategy=none 及 Host authority 唯一來源仍不變,不能以啟用 Forwarded middleware 取代此接線。缺少/非法 client IP 的 active ingress 400、無 Cookie,會在登入/OTP/audit 前拒絕;這是與新 artifact 同窗口的代理採用要求。
此 IP 仍是網路觀測,不是人的身分;NAT/共享出口可能共用配額,可信代理若錯誤填固定 peer 仍會退化節流及稽核,必須以兩個實際來源檢查 password pair counter/OTP 指紋/audit IP。保留帳號層 lockout 等既有政策,不放寬配額、不接受未登錄 peer 的 header。正式 log 仍不記此 header 或完整 query/credentials。
3. 平台入口、root 與 acting context
tenant-domains 提供獨立 platform origin,供 SUPER_ADMIN/Console 及明示平台管理端點使用。它不是全租戶可用的共同登入或 API fallback。
- platform endpoint 不帶業務 tenant,不將 root 哨兵登記為一般租戶目錄 key。
- 一般租戶帳號不能經平台入口存取租戶業務 API;SUPER_ADMIN 的 root credential 不能直接進租戶入口操作租戶業務。
- 若既有代理/委派流程合法產生 effective tenant,租戶入口須與該 effective tenant 相符;沿用既有 consent、actor/subject、capability 與稽核,不靠 Host 新增切租戶能力。
- 平台登入、Actuator、運維健康與匿名健康檢查的可達入口須明列 endpoint policy;公開探針如需內部 listener,由部署與既有授權設定明示,不提供未登記公共 Host 豁免。
平台入口與 acting context 依 Q2 的上述分類約束接線,P1/P2 逐端點核對實際 consumer;不得建立「root 一律放行」實作。若需改變平台/業務權界,先回本 ADR 修訂人審。
平台入口明示清冊(實作人審 R1-01)
單一程式來源為 config/PlatformEndpointRoutes.inventory(),gate 只據此決定可達性;既有 application matcher、method capability、subject/object scope 與 audit 均繼續執行。prefix 只匹配完整 segment(精確相等或後接 /),未列路由預設拒絕,不能改成 root blanket bypass。
| 路由/類型 | 保留/排除與理由 |
|---|---|
/api/v1/auth/** | 保留平台登入、refresh、acting/consent、OIDC/SignedLink handshake;沿用各 protocol predicate |
/api/v1/accounts/**、/api/v1/roles/** | 保留既有平台帳號/角色/external identity 管理;逐端點 capability/object scope |
/api/v1/service-accounts/** | 保留平台服務身分管理;既有風險及 scope policy 不變 |
/api/v1/system/** | 保留 instance metadata/運維面;仍套用既有公開或授權分類 |
/api/v1/notification-outbox/** | 保留 US-014 已設計的 root 跨租戶讀取/重送;既有 read/resend capability 與稽核 |
/api/v1/mail-settings/** | 保留具權限的 root 設定管理;不把 platform 當 tenant Mailer 選擇,機密遮蔽與寫入權限保留 |
/api/v1/notifications/** | 保留平台帳號自己的站內通知;仍由 stable subject 限縮,不開放任意使用者讀取 |
/api/v1/notification-templates/**、/api/v1/notification-types/** | 保留範本/類型管理;global template writer 仍限 SUPER_ADMIN,既有租戶範圍 policy 保留 |
/oauth2/authorization/**、/login/oauth2/code/** | 保留已登錄平台 OIDC registration/callback;矩陣與 binding 仍須相符 |
/docs/**、/public/** | 保留明示文件/公開資源;public predicate 不因此擴大 |
實際 Actuator mapping(EndpointRequest) | 保留平台運維/公開探針;集中 read/write capability,不固定 /actuator 字面路徑 |
ERROR dispatch 的 /error | 僅容器錯誤派發保留原始回應(例如 500),不改寫成 403;外部 REQUEST /error 不列為平台端點 |
/api/v1/products/**、orders、customers、dashboard | 排除租戶業務;root 先走合法 acting/delegate 建立有效 tenant 再從該租戶入口使用 |
/api/v1/calendars/**、reports、staging/files、references、addresses | 排除本輪沒有明示平台 consumer/全域隔離契約的租戶或共用資源面;新增平台用途須先修訂本清冊與其授權/scope policy,不能用 prefix blanket 放行 |
| 其他未列路由 | 預設拒絕;Feature 缺席不因路由清冊產生該 API |
4. URL 選擇、背景通知與持久綁定
已驗證帳號、入口目錄及可信業務上下文都需要能選擇正確 URL,但不能混用信任層級。沿用 PublicServerUrlResolver,以 additive binding/adapter 把經驗證的入口與 protocol scope 傳給各 consumer;tenant mode 的背景 unspecified() 必須失敗,平台入口另有具名選擇。P1/P2 依 Q3 細化實際公開型別、簽章、wire 與配置 API,並經實作人審,不把本設計當成已存在的公開 API;改變信任/綁定/錯誤處置契約時先回設計人審。
- 背景簽發從可信業務 tenant ID 取得 Server/frontend URL;不同租戶批次在各自 context/交易執行並清理。Outbox/retry 必須保存並恢復事件的 tenant,不依收件人 Email、可任意修改的 model 或最後一個 HTTP request 推斷。
- SignedLink 使用已簽署 attributes/claim 綁定 tenant,consume 時比對;token 所指帳號仍需有效且具權限。exchange code 的持久記錄同樣保存 tenant/目標 app 與入口 binding,跨租戶使用拒絕。
- 登入前 OIDC flow 固定入口、tenant/platform、registration 與 public callback;callback、success/error redirect、exchange 都核對同一 binding,保留 state/nonce/PKCE/既有認證映射。IdP 登記完整 callback URI,不依 wildcard。
- 前端 redirect 由
(tenantId, app)或 platform app 的受控 URL 查詢,缺值即失敗;業務頁面目標仍是相對路徑。既有 API 相對資源、S3/CDN endpoint 與文件 Host 的獨立 URL 契約保留。 - Notification deepLink 已是完整 URL/已寄出/已落 Outbox 的快照,不在投遞時套目前網域改寫。新簽發才使用新目錄;保留與到期規則另依遷移方案。
OIDC registration 與 callback 矩陣(Q3)
第一版採「共用 ClientRegistration+依起始入口決定 callback」,不為相同 IdP 的每租戶複製一組 registration/secret。每個 tenant/platform entry 明示 oidc-registration-ids 清單(未啟用該入口 OIDC 則空清單);可以指向不同的既有 registration,但不依 Host 選任意未登記 registration。此為已接受的配置接點;P1 原型及 P2 consumer 接線已完成,P3 指定本機矩陣通過;skeleton 下行/升級仍由 FU-93 承接,完整部署 IdP 證據另待補。
啟動列舉 iterable registration repository 與全部 active entries,產生 (entry, registrationId, callbackUri) 矩陣。每個允許的配對須存在 registration,callback 僅由該 entry 的 canonical Server base 組合 /login/oauth2/code/{registrationId}。以該 entry 的值展開現有 Spring redirect-uri 模板,逐配對精確核對,保留非法/未知模板變數的 fail-fast;一個 registration 服務多入口時,單一固定 callback literal 無法通過所有配對,必須採可逐入口展開的模板。single 保留 ADR-034 的單 URL 檢查。
此矩陣驗證取代 tenant-domains 下 OidcSecurityConfigurer 的啟動期單值 resolve。runtime 授權請求只取起始 entry 的已驗證矩陣列,保存 entry/registration/callback 於 protocol context;callback、exchange 與 success/error 都核對相同列,不能在 callback 階段重新選租戶。受控 IdP 與實際部署 IdP 必須登記矩陣列的完整 callback allowlist;本機啟動檢查只能證明應用矩陣一致,不能冒稱遠端 IdP 已完成註冊。P1 須證明同一 registration 的 A/B callback 均可用;供應商不允許多 callback 時先回 Q3 審閱,不靜默退成逐租戶註冊。
5. Cookie、協定 Session 與前端
Refresh/OIDC Session Cookie 使用 host-only,不設定父網域 Domain;write/clear 使用相同 name、public Path、Secure、HttpOnly 與已選定 SameSite 規則。Path 由 request 的已解析入口取得,不把 mutable tenant Path 寫入共享 singleton。
OIDC protocol Session 除 Cookie host 隔離外也保存入口 binding,callback 時檢查;host-only 不能代替伺服器端核對。跨租戶切換須重新登入,第一版不做跨域 SSO。
沿用 ADR-034 的 Spring Session JDBC DataSource/交易資源鍵隔離,不因新增 serializer 而重新混入 JPA 交易。動態 serializer 的 Spring 接點、callback 時點及 pool 容量須以瀏覽器實證;不得只比對 Set-Cookie 字串。公開 SPI 若不足,先擴充框架能力,再提供 application wiring,不在 app 自建平行安全 primitive。
Office API base 與 proxy 必須對應該租戶的設定;相對 API 路徑只套一次 public prefix。現有前端 Host mock auth 若啟用,核對 Cookie name/Path 並只作開發驗證,不能代替正式 auth 證據。
6. 遷移、停用與回復
| 舊資料/操作 | 第一版設計 |
|---|---|
| single → tenant-domains | 配置目錄、DNS/TLS、代理、前端與 IdP 一組切換;接受重新登入,隔離並處置舊 refresh/protocol Session |
| 舊共用 origin/舊 SignedLink 沒有可驗證 tenant | 切換時將舊 origin 明示列入 retired-origins;不從 token/Host 推斷 tenant,固定 410 退役頁提示由已知正式入口重新登入或聯絡管理者重發;頁面不自動選租戶、不 redirect、不發 code |
| 舊 OTP/exchange/OIDC flow 缺 binding | 按類型清理/撤銷或等待到期,拒絕沿用;明列 DB/cache migration 與保留紀錄 |
| 租戶停用/目錄移除 | 登入、refresh、consume、exchange 與新簽發皆拒絕;已建立 token/session 的撤銷與短暫窗口按 Q4 決策,不能只刪 DNS 就宣稱撤銷 |
| canonical 網域變更 | 新 origin 成為唯一 active entry,舊 origin 轉 retired。保存退役設定、DNS/TLS 與固定頁直到既有通知/credential 最長有效期及營運通知窗口結束;具體截止日期由部署紀錄明示。期間舊連結回 410,不承諾自動登入相容,不重新分配其他租戶 |
| 回復 single 或舊 artifact | artifact、設定、代理、DNS/TLS、IdP 為一組;已簽 tenant link 與已撤銷 credential 不因回復重新有效 |
本輪保留目錄重啟更新,不實作即時 reload。遷移不得修改舊通知快照;遷移工具不能在日誌記錄 token、OTP、exchange code 或完整 credential。
退役頁對所有 path 使用固定內容與 410,禁止讀取/回顯 query、Cookie、token 或 code,不設 Cookie、無租戶登入表單,不代理至 active API。優先由受控邊界代理直接提供;若落到應用,退役入口 filter 必須位於 SessionRepositoryFilter、Spring Security、任何 session/credential consumer 與可能提交回應的框架處理之前,不只在 Security chain 之前。註冊順序須小於實際 Session filter 的 order(預設可參考 SessionRepositoryFilter.DEFAULT_ORDER,不能只假定預設沒被覆寫),並核對實際 filter 列表與 REQUEST/ASYNC/ERROR dispatch 順序。它只依唯一可信 authority 與不可變 retired 清單判斷,不呼叫 getSession()/getSession(false)、getCookies()、query/parameter/body 解析或認證元件;命中即直接輸出固定 410,不呼叫 downstream chain,也不建立 error dispatch 讓 Session/認證元件補跑。可信 authority 若需 forwarded 處理,該前置步驟同樣不得接觸 session/credential,不能以較晚 filter 補做信任判定。
代理與應用 servlet 容器的 access log 都須移除 query、Cookie、Authorization 等 credential;包含 request line 的格式也要核對,不能只避免單獨 query 欄位。容器 log 可能早於應用 filter,故此義務由部署配置承擔,filter 不能保證替它遮蔽。退役請求帶 Cookie/query token 時,須以實際 filter/SessionRepository 觀測證明沒有 session 讀寫與 Set-Cookie,並核對代理及容器 log 的脫敏結果。兩層 retired 清單須一致。未登記 origin 仍為 400;清單到期移除後舊入口不可達或回 400。若舊 origin 要改作 active platform,須先結束退役、處置舊 credential 並接受舊租戶連結不再可用,不能同時扮演退役與平台入口。
框架與參考實作責任
| 層 | 責任 |
|---|---|
appfuse-server | 中性的 URL primitive/resolver、必要的入口 binding 與一致性核對能力、protocol/credential 綁定接點;additive API 與測試,不擁有 Tenant Entity、產品網域資料或 DNS/TLS 控制器 |
app-tenant-server | Tenant 存在/啟用/account 與 acting 政策、受控目錄及 Feature wiring、平台分類、外部設定與 migration |
app-tenantless-server | single 模式回歸;tenant-domains 拒絕配置,不帶入 tenant context/root 常數 |
| 前端/代理/維運 | Office/Console 位址、API base、DNS/TLS、可信入口隔離與 IdP registration |
可選 Feature 缺席仍須可啟動;OIDC Session 由 OIDC Feature 所有,沿用 ADR-034 的 2026-09-15 修訂。新 capability、Feature 安裝/升級及 skeleton ownership 在 P2 明列 catalog 範圍;不能假定 config/ 已有自動下行管道(FU-93)。
Q1–Q5 設計裁決
| ID | 裁決 | 狀態 | 實作/驗證承接 |
|---|---|---|---|
| Q1 | single 相容;tenant-id 清單;阻擋未登錄啟用、個別租戶真正不吻合才隔離並告警;待上線/停用只標示不可服務 | 設計已接受,2026-10-13 | 寫入守衛、啟動/runtime 一致分類、健康租戶 ready/擴容、營運復原、全域 fail-fast、binding 與三 tier |
| Q2 | 入口辨識不授權;獨立 platform origin,核對 effective tenant | 設計已接受,2026-10-13 | root/acting/M2M/Actuator/probe 的逐端點接線與可信代理來源 |
| Q3 | protocol/credential 綁定,背景顯式 tenant;共用 registration 與逐入口 callback 矩陣 | 設計已接受,2026-10-13 | 6 檔 7 處遷移、啟動/請求/平台分類、模板矩陣與 IdP;P1/P2 細化公開 API/wire 並人審 |
| Q4 | 受控重啟、重新登入;退役 origin 僅固定 410 頁 | 設計已接受,2026-10-13 | 早於 Session/認證的 filter、dispatch 順序、兩層 log 脫敏;部署明示期限與撤銷/回復證據 |
| Q5 | 兩租戶真實垂直驗證先行,M1–M6 加回歸才結案 | 設計已接受,2026-10-13;P1 本機切片已通過 | 實際測試拓樸/IdP、driver/artifact 版本與發布證據邊界 |
本表依共享工作樹第 3 輪「P0 設計審閱通過」結論落回。後續可按 P1–P3 接續;實作驗證、程式碼人審、commit/發布/部署仍依各自流程,不由 P0 通過冒稱完成。
P0 審閱結案與結論落點
2026-10-13 第 3 輪審閱方 verdict 為通過:R1 的 10 條及 R2 的 3 條處置經核對,無 blocking/open major。使用者明示 close,作者同時完成不阻擋通過的 R3-01 minor:正常已登錄且未啟用/停用租戶不告警,真正缺漏才隔離告警,M6 同步驗證。共 14 條意見處置,沒有 declined;審閱只證明 P0 設計接受,沒有 Server 執行結果。
| 結論 | 持久落點 |
|---|---|
| Q1–Q5 政策與權衡 | 本 ADR 的已接受設計、裁決表、呼叫點與遷移矩陣 |
| R3-01 告警分類澄清 | 第 1 節與 M6;直接完成,未另留待辦 |
| 舊 ADR 編號及既有行為 | ADR-034 的承接註記與本案來源說明;保留歷史,single 行為不變 |
| 未實作/未驗證範圍 | 本 ADR P1–P3 及 FU-90;FU-90 保留 ⭐/加入日期/開放狀態,下一步為 P1 垂直驗證 |
| 發布與遷移意圖 | 本輪只有文檔;CHANGELOG/Feature Changelog 於 P2/P3 的實際程式碼變更時撰寫,不虛構已交付能力 |
供追溯的第 3 輪原件 SHA-256(close 寫回及 R3-01 澄清前):簡報 16da40da55f7b31f4b842ddc0f265853fc50af0111bd5026f9a30294138d2956;ADR df225119f912e31cea068f2e258b1eccaf77f8dc09fb2347b9da899b8c35c815。這些摘要識別審閱快照,不是本次更新後檔案摘要,也不冒充可永久取回已刪除的暫時簡報。原簡報與本案展示副本在結論落點核對後清除。
交付計畫
| 階段 | 交付物 | 通過門檻 |
|---|---|---|
| P0 設計 | 本 ADR、完整人審簡報、Q1–Q5 裁決 | peer review 通過;API/信任/遷移責任可實作 |
| P1 垂直驗證 | 同 Server、兩 HTTPS 網域、不同 public path,受控代理/IdP/inbox | M1、M2、M3、M5 真實瀏覽器與背景流程通過;不只 stub URL |
| P2 能力與接線 | framework additive API、tenant adapters/migration、tenantless 回歸、catalog/配置 ownership | 明確 feature 組合與所有 consumer 完成;single 相容、無 optional bean 依賴失敗 |
| P3 完整驗收與人審 | M1–M6、T1–T6 回歸、遷移指南、CHANGELOG/Feature Changelog、change brief | 安全負向矩陣與所宣稱範圍通過;人審後才 commit;FU-90 最後依實際覆蓋結案 |
框架先公開版本,再由下游升級採用。參考實作以本機可重現驗證交付;真實 DNS ownership、公開 TLS、正式 IdP、WAR/多節點與 artifact 回復由實際部署專案保存環境證據。測試 fixture 不冒稱正式部署已驗收。
驗收矩陣
| ID | 情境 | 必須證明 |
|---|---|---|
| M1 | A /gateway、B /service,同 Server 不同 HTTPS 網域 | Cookie write/send/clear、OIDC 與通知 URL 各自正確;資料查詢不串租戶 |
| M2 | 無 request 的 A/B 通知交錯、並行及 retry | 可信 tenant 傳遞、URL/token binding/Outbox 快照一致;執行緒 context 清理 |
| M3 | A JWT/Basic/API-key/Refresh/OTP/SignedLink/exchange code 送 B | 不發 B credential、不執行業務或讀取 A 資料;錯誤與被拒時點可驗證 |
| M4 | 未知網域、外來 forwarded/tenant header、直連及多 hop | 未知入口拒絕;偽造不能改變租戶/callback/redirect;後端直連隔離有部署證據 |
| M5 | A/B 共用 registration 的 OIDC 起始、callback、success/error、exchange;平台 root | 逐入口模板矩陣與 IdP allowlist;binding/state/nonce/PKCE 保留;跨入口拒絕;acting 遵守原授權 |
| M6 | 缺值、任意合法 String ID 綁定、三 tier 設定覆寫、未登錄啟用、繞過守衛後重啟/擴容、正常待上線/停用 entry、retired 舊連結、換網域及回復 | 全域配置錯誤不啟動;未登錄啟用被拒;entry 指向不存在 ID/已啟用未登錄在 runtime/重啟只隔離該租戶並告警,健康租戶 ready/服務保持;已存在、登錄且未啟用/停用只不可服務,不產生配置錯誤告警,正常啟用後可服務。營運修復無 fallback。retired 固定 410,不進 Session/認證、不簽發或設 Cookie;兩層 log 不含 query/credential;退役移除後 400/不可達;credential 回復無復活 |
| R1 | single tenant 與 tenantless,ADR-034 T1–T6 | 舊 URL、Cookie、OIDC、SignedLink、Actuator 及 API 相對資源契約保留 |
| R2 | OSIV off/on、JPA rollback、Session 操作與執行緒重用 | 核對先於業務 Session;保留 JDBC/JPA 隔離;無 context 污染 |
| R3 | auth/OIDC/SignedLink/notification optional 組合 | 已安裝所需接線;Feature 缺席不拖入另一 Feature 的 runtime/tenantless 相依 |
P1 本機垂直驗證(2026-10-13,歷史快照)
使用者在 P0 close 後明示「開始」。本次完成可重現的 P1 原型與 M1/M2/M3/M5 的以下切片;原型保持在工作樹,尚未完成 P2 的所有 consumer/ownership/migration,未 commit、未發布。P0 設計接受與本次執行證據分開記錄。
拓樸與版本
| 項目 | 本次實際配置 |
|---|---|
| Server | 一個 Boot 4.0.1 application context,內部 /app-server,H2 MySQL mode,真實 Spring Security/Hibernate/JDBC Session |
| 租戶 A | https://a.localhost:{隨機 port}/gateway,assigned ID default-tenant |
| 租戶 B | https://b.localhost:{同 port}/service,assigned ID Tenant:B_2;大小寫與符號原樣保留 |
| 平台 | https://platform.localhost:{同 port}/platform-api,root 的 tenant projection 為 null |
| 代理 | 受控 HTTPS proxy,替換各 public prefix 為 /app-server,剝除外來 Forwarded/X-Forwarded;不重寫 Location/Set-Cookie |
| IdP | 一個 fixture registration/client,受控 IdP 精確 allowlist 三個 callback,實際 RS256 ID token、state/nonce/S256 PKCE 與 code exchange |
| 通知 | 真實 provider/NotificationService/SQL Outbox/dispatcher;僅末端 transport 接至受控 inbox,注入一次 transport 失敗後由 dispatcher 重試 |
| 框架 | source/依賴仍標示 4.0.0-alpha.34;-PuseLocalFramework=true 使用 additive 工作樹 API,並非宣稱新 API 已存在於公開 alpha.34 artifact |
| Browser/TLS | installed Playwright Chromium,真實 HTTPS 加密流量;本機短效 fixture certificate、明示忽略其信任錯誤,未驗公開 CA/DNS ownership |
原型公開接點與接線
web.url 新增中性的 PublicOrigin、PublicEndpoint、PublicEndpointBinding、PublicEndpointRegistry、PublicEndpointRequest;它們只保存受控入口/opaque scope 與 request attribute,不依賴 Tenant Entity,不解析認證,也不把 Host 寫入 TenantContext。PublicServerUrl 支援 IDNA host;既有 resolver context 繼續代表可信業務 scope。
SignedLinkService.consume(rawToken, expectedPurposes, Consumer<SignedLinkToken>) 是 additive 接點:在驗章/用途通過後、single-use 消耗前核對 binding,錯入口不消耗合法 token。API 為未發布草稿,公開契約與程式碼人審由 P2/P3 承接。
Tenant reference 的入口在 Session filter 之前解析,Security gate 在 SessionManagementFilter/MVC 之前比較已認證 effective tenant;保留原 JWT/UserDetails resolver。app.public-endpoints.trusted-proxy-addresses 是本次新增的明示連線 peer allowlist;它不取代實際部署的 backend 隔離與 proxy authority 驗證。single 保持缺省。
OIDC 啟動列舉具名 entry × registration;protocol context 與 SQL exchange grant 保存 entry/scope/registration/callback。Session Cookie 由不可變的每入口 delegate 寫入,保留 JDBC/JPA resource-key 隔離。Refresh 在持久 Session 發現後、使用/輪替前核對 tenant;OTP 保存 public entry/scope/server 快照,SignedLink 簽章 attributes 保存 tenant/entry/server。
沒有新增 notification SPI:本切片的既有 provider 在可信 TenantContext 包住的背景交易內執行,dispatcher 已從 Outbox row 恢復事件 tenant。LowStock provider 明示 office,不能以收件地址或任意 model 代替業務 tenant;沒有 HTTP request 的 unspecified resolver 會拒絕。
已通過的垂直切片
| Matrix | 本次實證 |
|---|---|
| M1 | A/B/platform Cookie 同名、host-only 並存,實際 browser write/send/refresh rotation/clear;A logout 不清除 B。A/B 查詢真實商品資料各只見自己的 tenant |
| M2 | A/B 在兩個重用 worker threads 交錯與並行生成通知、入列;每次結束無 TenantContext/request 污染。SQL Outbox URL 快照與 inbox 一致;一次失敗後實際重試,delivery thread 的事件 tenant 正確 |
| M3 | A JWT/有效 Basic/有效 API-key/Refresh/真實 inbox OTP/SignedLink/兩類 exchange code 送 B:拒絕且沒有 Set-Cookie/新憑證。proxy 的 Hibernate SQL observer 比較被拒商品請求前後計數,證明未查詢 business product table;錯入口不燒毀合法 single-use exchange |
| M5 | 同一 registration 的 A/B/platform OIDC 完成,三次實際 token exchange;stolen A protocol Cookie 送 B callback 在 IdP token endpoint 前拒絕。合法 A error flow 仍導回 A。root 只在 platform 看 Actuator;tenant/root 不藉入口取得 business scope |
| M5 acting | A 的 protected target 無 consent 時拒絕;目標批准後合法 impersonation 保留 actor 與 effective tenant,A business 可用、B 拒絕;撤回 consent 後 refresh 失效。root 不繞過 protected-target consent |
原型測試:app-tenant-server/src/test/java/io/leandev/app/security/oidclogin/TenantDomainsProxyIT.java 與 src/test/resources/public-url/tenant-domains-browser.cjs。它使用真實 application filters/services/repositories;只有 test identity mapping、SQL observer、受控網路與末端 inbox 是 fixture。
重現入口(在 app-tenant-server,將 module path 指向已安裝 Playwright):
./gradlew -PuseLocalFramework=true tenantDomainsEvidenceTest \
-Dpublic-url.playwright-module=/absolute/path/to/node_modules/playwright
Framework URL/entry/SignedLink 測試 89 個通過;P1 browser suite 1 個通過、0 skipped。受影響 tenant 測試 112 個:110 通過、2 個 browser fixtures 在一般 test 中刻意 skipped;P1 與 single browser 證據由明示 tasks 執行,不能以 skipped 當 pass。Tenant single publicUrlEvidenceTest 的 T1–T6 六個拓樸均通過。另跑 architecture suite 21 個通過、0 skipped,包含 Feature slice 的 I1/I2 bytecode 檢核。check -x test 的 Feature catalog I3、portable scaffold 與既有離線工具 contract 也通過;不是執行資料庫 migration。
受影響測試的工作樹報告保留在 app-tenant-server/build/reports/tenant-domains-p1-targeted/;P1/single 證據分別在 build/reports/tests/tenantDomainsEvidenceTest//publicUrlEvidenceTest/。這些 build 產物不進 git;上列 commands 與 source cases 是可重現依據。尚未宣稱完整所有模組測試或 tenantless T1–T6。
P1 當時保留的 P2/P3 門檻
本切片不是可直接下行的 release migration:受控 create/enable writer guards、全部 optional 組合、catalog/skeleton 安裝與升級 ownership、既有 DB schema/credential 撤銷、跨 origin 的平台 acting 轉場、tenantless 模式拒絕與回歸、完整 M4/M6/R1–R3 仍由 P2/P3 完成。個別租戶 availability/正常 disabled 不告警與 retired filter 不讀 credential 已有 focused test,但尚未當成 M6 的真實雙層 log/Session/重啟/回復證據。尚未執行正式公開 API/change peer review。
本機 proxy/IdP/H2/Chromium 不證明 production DNS、公開 TLS、正式 IdP registration、WAR/多節點、既有資料庫 adoption 或 artifact rollback。FU-90 保持 ⭐/開放。
P2 能力、接線與採用流程(2026-10-14,歷史快照)
本輪接續工作樹,補上受控租戶啟用、持久憑證入口綁定、M2M 發行政策與合法跨 origin acting 交接。公開 API 仍是未發布草稿;P3 的完整安全矩陣、部署證據與程式碼人審另外承接。
框架與 source ownership
| 層 | 新增/採用接點 | 安裝與升級責任 |
|---|---|---|
| Framework jar | PublicEndpointContext、PublicEndpointAvailability、binding 指紋;SessionContextBinding/SessionContextPolicy、ContextBoundPrincipal;LoginPolicy identity-switch context;Refresh Session pending activation | additive constructor overload 保留;新增 record components 仍影響反射、解構、equals/序列化,adapter 必須保留 context。正式發布後下游才升級,不把 alpha.34 視為已含這些 API |
| required auth core | AuthInfrastructureConfig 可選 SessionContextPolicy 組裝 | 同時更新 tenant/tenantless type source;缺少 policy bean 維持 single 語意 |
| skeleton | config/PublicEndpointAccess/properties/gate/mode guard、ApplicationSecurityConfig 公開兌換路徑 | 現行 features.json.skeleton 的 config 已涵蓋;application-owned 由專案審視採用。FU-93 的骨架自動升級管道尚未交付,既有專案不能只跑 Feature 升級就視為完成 |
| tenant reference | service/tenant/TenantPublicEndpointsConfig、availability、TenantEnableGuard 與 Tenant entity listener | 隨現行 catalog 的 tenant.referenceDomains 進退;未安裝 adapter 時 tenant-domains 明確拒絕啟動,single 不依賴 TenantRepository |
| OIDC optional core/seam | protocol adapters 只依賴框架 PublicEndpointContext;SessionInfrastructureConfig 驗實際 registration order、建立每入口 Cookie delegate | Session datasource/transaction/cookie/order guard 隨 OIDC Feature 安裝;skeleton 不 import Spring Session。tenantless 保留自己的 single protocol 契約,不交叉發布 type source |
| service-account core/reference | client_credentials 可選 context policy;API key 保存指紋並由已認證 principal 攜帶 | 發行以帳號實際 scope 查具名目錄;入口 gate 在認證後比對,legacy unbound key 在 tenant mode 拒絕 |
| auth/impersonation reference | entry-transition HTTP 殼、acting app、consent 通知收件人快照 | HTTP mapping、202 DTO、交易、帳號政策均由應用擁有;required auth 不 import impersonation core |
| schema | tenant V6__public_endpoint_credential_bindings.sql | 只新增 nullable 欄位;舊版本 checksum、離線 Entity Diff snapshot 均不改。operator 明示採用/套用,不自動執行 |
TenantEnableGuard 在 JPA persist/update flush 時拒絕已啟用而未登錄的 exact String ID,包含 seed 與受控 repository writer;正常 disabled staging 與停用允許。目錄以 scalar projection 讀取 DB 實際保存的 ID,再做 String 精確比較;即使 DB collation 不區分大小寫,也不能將不同 ID 視為 alias。受控 writer 同樣比對已存 ID,採 COMMIT flush-mode 查詢避免 callback 內遞迴 auto-flush;case-insensitive H2 已實證拒絕 ORM key alias enable 且資料不變。直接 SQL/bulk DML 不經 entity callback,仍須以 operator 規則把關,且由既有 startup/runtime availability 偵測、只隔離受影響租戶。
JWT 與 Refresh family 保存 entry/exact scope/canonical server URL 的 SHA-256 指紋;Refresh 在資格/policy/使用/輪替前核對。API key 的持久 binding 同樣在認證後由 gate 驗證。缺少 binding 的舊 JWT/Refresh/API key 在 tenant mode fail-closed;入口或路徑改變不能沿用。client_credentials 在認證與非互動資格通過後、簽章之前比對入口,回 RFC 6749 invalid_client。Basic 仍先取得實際 UserDetails tenant,再由 gate 比對。安裝的 LowStock/consent consumer 在啟動時列舉租戶 entry 核對明示 app;安全 Cookie/rotation 設定亦在 relaxed binding 後核對 typed policy,不能用拼字別名繞過。
合法 acting 跨入口的 wire
既有 authority、反提權、protected-target consent、break-glass MFA/案件與 refresh-time 政策不變。引擎先原子驗來源 Refresh family,才提供 identity-switch context;入口政策不能用 Host 替代該證明。同入口沿用 200 LoginResponse;跨入口必須明示目的 app,該 app 須在目的 entry 註冊。
POST /api/v1/auth/impersonationbody 加app;break-glass body 亦加app;exit 使用POST /api/v1/auth/impersonation/exit?app=console。- 合法跨入口回 202:
{serverBaseUrl, frontendBaseUrl, code, app, expiresIn:90}。body 沒有 access token;來源只清除自己的 Refresh Cookie,不寫目的 Cookie。來源 family 已撤銷。 - 待交接 family 只存 code 的 verifier/selector hash、目的 context/app/期限,不能普通 refresh 或 identity switch;內部預鑄 access token 有 pending claim,gate 明確拒絕,且不回 wire。
- client 以受控 frontend URL 轉場,將短效 code 交給目的頁,目的頁以同源
POST {serverBaseUrl}/api/v1/auth/entry-transitionbody{code,app}兌換。不要把 access token/有效 Refresh Cookie放進 URL;code 也不得放 query、日誌或長期儲存。若 client 使用 fragment 搬運 code,須先清除 fragment 再發同源兌換。 - 目的服務在同一 transaction 鎖定 pending family,核對 hash/入口/app/90 秒期限與撤銷/idle/policy expiry,重新驗帳號、actor、consent/案件並 strict rotate,首輪連秘密 selector 一起重建,舊交接 code 不能再定位 active family。成功回 200 LoginResponse,才寫目的 host-only Secure HttpOnly Cookie。
- 錯入口/錯 app/到期/重複兌換不啟用、不輪替、不走 refresh replay 撤銷路徑;舊 code 送一般 Refresh 也不能撤銷新 family。並行只成功一次。政策重驗失敗回滾 activation。失敗後來源 family 不復活,使用者從已知合法入口重新登入。
平台 consent 通知使用已授權查得的收件人快照。來源 transaction commit 後先建立可信目標 TenantContext,再開 REQUIRES_NEW transaction 生成 signed-link/Outbox;不在既有 Hibernate Session 中切換 tenant。遞送仍使用既有 NotificationService/dispatcher,未新增 notification SPI。這也讓 root 提出的合法 consent 不會產生平台入口的租戶登入連結。
這是 server wire 契約與本機驗證,尚未宣稱 Office/Console UI 已實作 202 轉場操作。
Operator 採用、退役與回復
- 先在隔離 clone 比對既有 schema 與 Flyway adoption version,備份並驗 checksum。使用者明示的 Entity Diff/migration evidence 流程獨立執行;本輪只作者化 V6,不連線或套用 production DB,也不改 snapshot。
- 新 artifact 可先維持 single,明示套用 V6 後以 ddl-auto:validate 核對;nullable 新欄位不自動撤銷 single credentials,也不 enable tenant。裁切 optional domain 的既有 schema 若沒有上述表,operator 必須作者化相符 reconciliation,不直接套用來源完整表 migration。
- 切換模式或 canonical URL 必須把 artifact、整份 entry 配置、proxy/DNS/TLS、IdP callback allowlist 視為同一操作窗口。先停登入/交接 writer、等待 in-flight 或撤銷它們;所有啟用租戶必須有 exact ID 登錄。直接 SQL enable 必須先核對已部署的目錄,禁止以 slug 猜 ID。
- single → tenant-domains 時,撤銷舊 Refresh families/API keys,清除 OTP/OIDC exchange grants/protocol Sessions,並輪替 JWT/SignedLink 共用 signing keys。舊未綁定 JWT 在 tenant mode 拒絕,但回復成舊 single artifact 後也不能恢復舊 signing keys,否則舊 bearer/signed-link 可能復活。沒有多 key 過渡驗證時接受全部重新登入/重新發行。
- 換 tenant canonical origin/path 時,撤銷該 scope 的所有 Refresh families/API keys/OTP/exchange,清除受影響或無法精準分區的 protocol Sessions;新目錄指紋使舊 bearer 無法在新入口使用。需要可回復到未檢查 binding 的 artifact 時亦輪替 signing keys。改回舊 URL 不復原 revokeAt、不清 blacklist、不還原舊 key 或已消耗 grants。
- retired origin 僅固定 410,保留到 owner 記錄的移除日期且超過最長 legacy link/credential lifetime。proxy/container access log 都省略 query/credential/request line。移除後由未知入口回 400 或邊緣不可達;沒有依 token 推測新租戶或自動 redirect。
- 已寄出的通知/Outbox deepLink 是不可變快照,retry 不改寫 URL。切換 signing keys/退役會使舊登入連結失效;需要通知使用者時明示重新發一筆通知,保留舊列與 audit。
- 回復優先保留 additive schema、已撤銷 ledger、已輪替 keys;切回 config/artifact 必須仍使用與其契約相容的可信入口策略。舊 artifact 不能與 tenant-domains 配置混跑;不能因回復而重新接通未知/retired credential 消費路徑。
範例操作 SQL 只是採用指南,不是自動 migration,也未在 DB 執行;scope/canonical 變更時應限縮 operator 確認的受影響集合:
SET time_zone = '+00:00';
START TRANSACTION;
UPDATE refresh_session SET revoked_at = UTC_TIMESTAMP(6), revoke_reason = 'public-entry-transition'
WHERE revoked_at IS NULL;
UPDATE service_account_api_key SET revoked_at = UTC_TIMESTAMP(6) WHERE revoked_at IS NULL;
DELETE FROM email_otp_challenge;
DELETE FROM oidc_exchange_grant;
DELETE FROM SPRING_SESSION_ATTRIBUTES;
DELETE FROM SPRING_SESSION;
COMMIT;
signing-key rotation、multi-node writer drain、proxy 日誌與 retired 移除期限需另存環境證據,不能由 SQL 假稱完成。
本輪驗證狀態
以下保留 P2 當輪的 source/本機證據,未 commit/發布;後續 P3 指定矩陣已完成,實作 change 人審正在修訂中,見後續章節。
| 實際執行 | 結果與界線 |
|---|---|
| Framework URL/SignedLink/login/Refresh/M2M/context/mock adapter | 136 通過,0 skipped;含 pending 拒絕、activation adapter fail-closed、首輪 selector 更新、保留 binding 與既有 constructor 回歸。第一次亦跑過既有 report memory/render 與 SFTP dependencies;最後 auth 修正只重跑受影響 suite,未重跑無關的 50,000-row render |
| Tenant focused/JPA/acting/service-account/OIDC/architecture | 129 通過,0 skipped,35 suites;含受控 create/enable/disable、case-insensitive DB 的 ID/ORM key alias 實證、availability、直接 hash row lock 的並行 activation exactly-once、錯 app/到期不消耗、actual Session order、typed policy、Feature I1/I2 及架構 |
| Optional assembly focused | single skeleton 不依賴 Tenant adapter/Spring Session;tenant mode 缺 adapter 明確拒絕;directory/credential-policy context 在排除 OIDC/Spring Session 的 classloader 上可啟動。屬指定組合證據,未宣稱所有 feature powerset 的裁切 artifact 均已實測 |
| tenant-domains proxy/Chromium | 1 通過,0 skipped;保留 M1/M2/M3/M5,新增 M2M A 成功/B/platform 拒絕、legacy unbound JWT/Refresh/API-key 與 pending JWT 拒絕、跨入口 CORS 拒絕;root 的 protected-target consent → 202 交接 → 目標 Cookie/業務 → exit 202 → platform。actor 保留;錯入口/app、code replay、舊 code 送普通 refresh 均不能撤銷 active family,錯誤不洩漏 code/stack |
| Tenant single ADR-034 T1–T6 | 6 通過,0 skipped,最終工作樹重新執行 |
| Tenantless mode/Session/Feature architecture | 11 通過,0 skipped;無直接 tenant-package import,保留既有 single protocol source;tenant-domains/未知 mode 拒絕 |
| Tenantless single ADR-034 T1–T6 | 6 通過,0 skipped,最終工作樹重新執行 |
兩型別 check -x test | Feature catalog I3、portable scaffold 與離線 Entity Diff tooling contract 通過;沒有執行 Entity Diff snapshot 操作或 MySQL migration evidence |
| 文檔 | production build、CHANGELOG 結構與 public import lint 通過;公開 API 與兩個 type 的 Feature Changelog 保留未發布標示,released 歷史內容未改 |
tenant 報告保留於 build/reports/tenant-domains-p2-targeted/;framework 於 build/reports/tenant-domains-p2-framework/。Browser/single 可由同名 Gradle tasks 重現。所有參考 source 測試明示 -PuseLocalFramework=true;不是已發布 alpha.34 能力或正式環境驗收。
以上為 P2 結束時的證據;後續 P3 本機驗證見下一節。Office/Console 的 202 UI 操作尚未宣稱交付。production DNS/CA/IdP/WAR/多節點/MySQL adoption 證據由部署 owner 承接。FU-90 保持 ⭐/開放。
P3 首輪本機安全矩陣與人審準備(2026-10-14,歷史快照)
本輪沒有改動 P2 的 runtime source 或公開 API。新增可重現的入口/配置/交易/重啟與 optional 組合證據,保留 P2 的 single 雙型別及 framework 回歸結果;程式碼成果仍未發布。P0 設計通過不等於本輪 change review 通過。
| 命題 | 本輪實際證據 | 證據界線 |
|---|---|---|
| M4 authority/偽造/直連/多 hop | 真實 Tomcat 拒絕未知與重複 Host、Forwarded/X-Forwarded-*;由 127.0.0.2 的非信任 peer 帶合法 Host 仍 400。外層 HTTP hop 剝除偽造 header,內層 HTTPS proxy 保留 authority,正常請求到達認證 401。browser 以 A 的有效 JWT 加 B tenant headers 仍只能讀 A 商品 | 證明 allowlist 與受控兩 hop;正式 firewall/backend listener 隔離尚待部署證據。同一信任 IP 的其他程序不能靠 IP allowlist 區分 |
| M6 停用與 drift | 真實 SQL disable/enable 即時 503/恢復;SQL 繞過 create/enable guard 的未登錄啟用租戶及已刪除的登錄租戶,於同 DB 的真實 Boot 重啟繼續隔離;健康 A entry 可服務,既有 revoked ledger 保留 | H2 MySQL mode;不是 MySQL migration、WAR、多節點或正式擴容驗收 |
| M6 retired 與日誌 | 實際 HTTPS proxy/Tomcat 410;帶 query/Cookie/Bearer 的 retired 請求未呼叫真實 JDBC Session repository 的 spy,無 Location/Set-Cookie。兩層實際 access log 只記 method/path/status,斷言沒有 query 或 credential。focused test 分別驗 REQUEST/ASYNC/ERROR 的 filter 行為 | 真實容器證明初始 REQUEST 提前終止;ASYNC/ERROR 的既有 dispatch 處理為 focused mock-request 證據。不是正式環境的 log pipeline 證據 |
| M6 換網域與配置回復 | 每個 OSIV 設定各跑三個真正關閉/重啟的 Boot context:原配置 → 新 canonical A+退役舊 A+新 signing keys → 恢復舊 A 配置但保留新 keys/revoked ledger;舊 A 410、retired 移除 400、舊 key JWT 401、新網域 bound JWT 回舊入口 403,未復活 | 同一份未發布 source 的配置回復;沒有把它當成 alpha.34/舊 WAR 的 artifact rollback。完整舊 artifact/single 回復與所有 credential 採用流程仍需環境演練 |
| R2 Session/JPA/thread | OSIV off/on 各重跑完整 Chromium/OIDC/Cookie/M2M/consent/202 acting 流程。真實 JPA transaction 插入商品並 flush 後 rollback,商品消失、同交易內寫入的獨立 JDBC Session attribute 保留。四個背景工作重用兩執行緒、Outbox retry 後 TenantContext/request attributes 為空 | SQL Session spy 僅為取證,沒有替換其實際 JDBC 行為;不是人工 Office/Console 操作驗收 |
| R3 optional | catalog 的 required/requires closure 推導四組來源,實際獨立編譯並 Boot/HTTP;absent OIDC 組合的 compile/runtime classpath 排除 Spring Session/OAuth client jars,且 Class.forName 檢查 Feature 類別不存在 | 宣稱下表四組與既有完整 source browser;未宣稱全部 Feature powerset、所有第三方 adapter 或任意下游裁切 artifact |
Optional 組合與重現
partition 一律消費現有 feature-inventory.py.collect(),不另建另一份分類邏輯。資源包含所選 Feature/structural seed,移除其他 Feature fragments 與產品 demo;隔離 compile/runtime classpath 不加入原本完整 app 的 class output。OIDC 組合啟用真實 client registration/JDBC Session;本 smoke 不向 IdP 發起登入,登入端到端另由 Chromium suite 證明。
| 組合 | 實際 Feature closure | Java source 數(不含 smoke) |
|---|---|---|
| auth | auth、cache、tenant | 87 |
| oidc | auth、cache、external-identity、oidc-login、tenant | 126 |
| links | auth、cache、signed-link、tenant | 92 |
| notifications | auth、cache、mail、notification、persistence-encryption、scheduling、signed-link、tenant | 152 |
在 app-tenant-server 執行:
python3 src/test/resources/public-url/tenant-domains-compositions.py . build/tenant-domains-compositions
./gradlew tenantDomainsCompositionEvidence -I src/test/resources/public-url/tenant-domains-compositions.gradle -PuseLocalFramework=true
./gradlew tenantDomainsEvidenceTest -PuseLocalFramework=true -Dpublic-url.playwright-module=/absolute/path/to/playwright
以上測試使用拋棄式 H2/fixture keys,不執行 Flyway/Entity Diff 或正式環境操作。Browser 的額外邊界與重啟證據由 TenantDomainsBoundaryEvidence 提供。不要從完整 test XML 的環境輸出複製臨時機密到審閱展示;展示只保留 assertion/suite 統計與來源指紋。
配置與日誌的採用提醒
Spring Config Data 對清單採替換語意。operator .properties 覆寫 tenants[0].server-base-url 時,須提供整份 tenant 清單及每個 entry 的全部必要欄位,含 exact tenant-id、server/frontend URL、啟用 OIDC 所需 registrations。只覆寫 URL 會丟失必要 ID,並於啟動 fail-closed;不可期待與低優先序 YAML 逐欄合併。TenantPublicEndpointConfigDataTest 對 dev/test/prod metadata 分別以真實 Boot Config Data 驗完整覆寫、部分覆寫拒絕與 exact String ID;tier 不是 Spring profile。
本機取證已驗證下列 Tomcat access log 設定。代理的 access log 也只記 path,不記含 query 的 request line、Cookie、Authorization、body;正式部署需按實際 proxy/logger 設定驗證,不從本機結果推論。
server.tomcat.accesslog.enabled=true
server.tomcat.accesslog.pattern=%h %m %U %s
人審與剩餘交付門檻
change review 涵蓋 P1–P3 的未提交 source、公開 API、Feature 接線、V6 與採用指南。無對應 US 的 FU/ADR 變更不冒稱執行 US-based code-review;本 session 沒有可用 security-review skill,手動來源核對與負向測試可供人審,不能冒充該審查器通過。
P3 tenant focused/JPA/acting/service-account/OIDC/architecture/Config Data 共 134 通過,0 skipped(36 suites;P2 的 129 加 retired 三種 dispatch 與三 tier Config Data)。OSIV off/on browser 2 通過,0 skipped,每組含三次實際 Boot 啟動。Optional 四組各實際 compile/Boot/HTTP 通過,來源腳本已納入 test resources。Tenant focused 與 browser 報告分別保留在 build/reports/tenant-domains-p3-targeted/、build/reports/tenant-domains-p3-browser/。P3 未變動 runtime,沿用 P2 framework 136、tenantless 11 及兩型別 single T1–T6 各 6 的通過證據;不是重新執行這些未受影響的 suites。
本輪可審閱本機實作與指定驗證範圍;正式 DNS/CA/IdP callback、proxy/backend 網路隔離、兩層 log pipeline、MySQL adoption、multi-node drain/擴容與舊 artifact rollback,仍由部署 owner 提供實際證據。Office/Console 202 UI 採用與 skeleton 下行/升級(FU-93)未宣稱交付。人審通過後才依原流程 commit;FU-90 保持開放,不以本機驗證代替所有交付門檻。
實作人審第 2 輪修訂(2026-10-14)
本輪處置 R1-01~R1-06;已變動 runtime 接線,因此不沿用首輪 tenant/single 結果冒稱本輪通過。平台清冊與可信 client IP 的現行契約見 §2/§3,R1-02 由使用者明示選擇可信代理接線;P0 接受不替代本輪 change 人審。本輪送審時尚未 commit/發布/部署。
- 平台路由的單一程式清冊保留已授權 outbox/mail/template 管理與本人站內通知;缺 capability 仍 403,不新增 production SUPER_ADMIN seed 權限。真實 browser fixture 只為本測試明示提供 read capabilities;容器 ERROR
/error保留 500,外部 REQUEST/error403。 - 真實密碼登入測試:相同 client+username 五次 401、第六次 429;另一 client 同 username 仍 401。OTP rate row 使用已驗證 client IP 的 SHA-256;audit event 的
ipAddress使用該 client,實際 peer 保持 proxy。缺值/重複/格式/DNS 名/變更綁定及非信任 peer 均拒絕;retired 的早期 410 不解析此 header。 - 16 個 consumer 的 17 個新增可缺席欄位改為建構子接線:reference adapter 必須有
PublicEndpointAccess,三個 core consumer 使用中性Optional<PublicEndpointContext>;OIDC Registry 使用 ObjectProvider。tenant mode 的啟動 guard 要求 registry、enabled context 及 availability,全限定型別改用 imports。基線 MailEndpoint 的既有 constructor optional mailer 不屬本次修改。 - OIDC callback 的 tenant-mode 診斷指出 registered entry
server-base-url及 expected callback;single 保留app.public-base-url診斷,各有測試。 - R1-06 所指 01:45 變更只改
tenantDomainsEvidenceTest的說明/註解,沒有改 task classpath、include、fixture properties 或 assertions;本輪又已依最終定義重跑,避免只以時間戳推論證據。
本輪受影響 suites 162 通過、0 skipped(42 suites),含首次加入的真實 notification service 範圍/權限回歸與上述缺 collaborator/IP/callback 測試;tenant-domains OSIV browser 2 通過、single ADR-034 T1–T6 6 通過。報告分別保留於 app-tenant-server/build/reports/tenant-domains-review-r2-targeted/、tenant-domains-review-r2-browser/、tenant-domains-review-r2-single/。Framework 136、tenantless 11 與 tenantless single 六個保留首輪未受影響證據,未冒稱本輪重跑。
四個 optional closure 本輪 source 數為 auth 89、OIDC 128、SignedLink 94、notification 154(新增兩個 skeleton 類別);四組皆已依本輪最終來源完成隔離編譯/Boot/HTTP,缺席類別/Spring Session 的反證維持。所有未驗部署、MySQL、舊 artifact rollback、UI 與 FU-93 邊界維持;FU-90 開放。
實作人審通過結論(2026-10-14)
實作 change 第 2 輪 verdict 為通過,本輪沒有新增意見。R1-01~R1-06 均已逐條核對工作樹成果並處置,無 blocking、無 open major、無 declined;審閱者核對 5 模組/113 檔範圍、42 suites/162 tests 與 OSIV off/on 兩組 browser 報告晚於最終來源且全數通過。此紀錄保存實作人審的結論,與 P0 設計接受分開。
| 命題 | 已確認結論與落點 |
|---|---|
| Q1:未發布實作成果 | 接受本輪公開 API、source ownership 與指定本機證據;解鎖依 01-commit.md 提交。通過不代表 framework 已發布、V6 已套用或已部署 |
| Q2:部署環境交付門檻 | 保留 deployment owner 的正式網路、DNS/CA/IdP、可信 client IP 接線、雙層 log pipeline、MySQL adoption、多節點及舊 WAR/single artifact rollback 演練;由 FU-90 接續,不以受控本機結果替代 |
| Q3:前端與下行採用範圍 | Office/Console 的 202 操作 UI 尚未採用,仍由既有後續工作承接;skeleton 下行/升級由 FU-93 承接。本輪 browser protocol/API 驗證不代表產品 UI 或下游已交付 |
平台清冊、ERROR 派發及可信代理 client IP 決策已落於 §2/§3;建構子接線、mode guard、callback 診斷與最終 task 重驗已落於上節及 tenant source FEATURE_CHANGELOG.md 的 [Unreleased]。保留 production SUPER_ADMIN seed 既有能力、single 預設與 tenantless 邊界,不擴張為 root blanket bypass;不執行 Entity Diff snapshot、production migration 或恢復舊 signing keys/撤銷紀錄。FU-90 保持 ⭐、原加入日期及開放狀態。
Framework 發布與參考實作採用狀態(2026-10-14)
使用者明示「發布,確認 alpha.35」。appfuse-server 已發布至不可變 Maven releases repository:io.leandev.appfuse.webapp:appfuse-server:4.0.0-alpha.35,版本 commit 為 656479fe,alpha 不打 tag。JAR、sources JAR、POM 與 Gradle metadata 均經遠端下載核對;JAR/sources JAR SHA-256 與本機產物一致。P1/P2 的中性公開 API 及 credential context/pending activation 已包含於此版;4.0.0-alpha.34 不含本案新增能力。
發布前全量建置完成:1,751 tests 通過、58 個既有案例略過、0 failures/errors,含 50,000 列 PDF 的 128 MB heap 與 optional classpath 回歸。報告保留於 appfuse-server/build/reports/fu90-alpha35-preflight/;這些結果不替代 Q2 的真實 MySQL/正式網路/多節點/舊 artifact 回復證據。文檔站依同次 publish 流程同步部署,線上版本以 /actuator/info 的 build["appfuse-server"] 及 /server/changelog 的本版條目核對。
參考實作仍 pin 4.0.0-alpha.34;先前 tenant/tenantless 驗證使用 -PuseLocalFramework=true,不能視為已採用已發布的 Maven dependency。下一步須各自依升級流程採用 4.0.0-alpha.35,核對 adapter/新增 record components、constructor 與 skeleton 配套後重驗。V6 尚未對 production DB 執行;Office/Console 的 202 UI 採用與正式 tenant-domains 部署仍待交付,FU-93 繼續承接 skeleton 自動更新管道,FU-90 保持開放。
權衡、影響與第一版範圍
受控目錄讓 request、background 與登入協定可查同一位址;代價是維運需協調網域、代理及 IdP,變更接受重新登入。平台與租戶入口分離增加配置,但能明確保留 root 的跨租戶權界,避免透過 Host 暗中取得業務租戶身分。啟用寫入守衛與個別租戶隔離使單筆完整度錯誤不拖垮健康租戶的重啟/擴容,但維運必須處理告警與修復,不能只看 Server ready 就宣稱所有租戶皆可服務。整份設定覆寫使新網域上線需受控重啟;退役入口保留的是固定通知頁,舊登入連結不再自動登入。
第一版不建立自助網域註冊、DB domain 狀態機、DNS ownership 自動驗證、憑證自動申請、wildcard/alias、即時 reload 或跨域 SSO。這些範圍另案決策,不代表未知入口、tenant 綁定或既有啟用 Feature 的必要接線可以省略。
參考依據
- ADR-034:Server 對外位址:沿用 URI 組合、代理 path、交易隔離及 Feature Session ownership。
- ADR-016:原生租戶 discriminator、ADR-017:框架不擁有 Entity:Session 建構期隔離與 root 邊界;保留現行機制。
- ADR-023:auth 引擎、ADR-024:capability/feature/參考實作:中性引擎與應用 adapter 分工。
- ADR-013:ActingContext、ADR-021:代理同意、ADR-025:M2M、ADR-032:Actuator 授權:本案擴充入口核對,不另授權或取消其現行政策。
- RFC 9700 §2.1:OAuth redirect URI 精確核對。
- RFC 6265 §5.2.3:host-only Cookie。
- Spring Framework 7.0 Forwarded Headers:對齊 Boot 4.0.1 的 Framework 7.0 主線;實作仍核對實際 patch 版本。
- Spring Boot 4.0 Externalized Configuration:Map key 與 list/環境變數 binding 邊界,採明確 tenant-id 值保留租戶 ID。
變更歷史
| 日期 | 內容 |
|---|---|
| 2026-10-13 | 使用者同意整體建議方向;建立 ADR-036 Proposed 第 1 輪,承接 FU-90,細節待設計人審;未實作或驗收 |
| 2026-10-13 | 第 2 輪處置 R1-01~10:6 檔 7 處 resolver 盤點、OIDC 共用 registration/callback 矩陣、租戶清單 binding/雙向登錄/三 tier、固定 410 退役入口與中性詞彙;仍 Proposed,未實作或驗收 |
| 2026-10-13 | 第 3 輪處置 R2-01~03:未登錄啟用寫入守衛、啟動與 runtime 個別租戶隔離及營運復原;退役 filter 早於 Session,代理/容器 log 均脫敏;仍 Proposed,未實作或驗收 |
| 2026-10-13 | 第 3 輪 P0 verdict 通過,使用者明示 close;Q1–Q5 裁決落回並改 Accepted;完成 R3-01 正常待上線/停用不告警。結案只涵蓋設計,FU-90 接續 P1–P3/M1–M6,未實作、未發布 |
| 2026-10-13 | 使用者明示開始 P1;落地未發布 additive 原型,完成兩租戶/平台真實 browser 與背景 M1/M2/M3/M5 切片;P2/P3 和 FU-90 保持開放 |
| 2026-10-14 | 接續 P2:落地未發布 context SPI/受控 enable/持久 credential binding/M2M/一次性跨入口 acting、source ownership 與 V6 採用指南;本機 136 framework/129 tenant/11 tenantless、tenant browser 1 與兩型別 T1–T6 各 6 通過;FU-90 接續 P3,未 commit/發布/套用 production migration |
| 2026-10-14 | 接續 P3 本機安全矩陣:134 tenant/2 OSIV browser 通過;未知/偽造/非信任直連/兩 hop、retired 真實 pre-Session 與雙層日誌、JPA rollback、SQL drift 重啟、換網域/配置回復無 JWT/ledger 復活;四組 optional source compile/Boot/HTTP 通過。準備 change 人審;部署、MySQL、舊 artifact rollback 及 UI 採用證據仍待補,FU-90 不結案 |
| 2026-10-14 | 實作人審 R1-01~06 修訂:集中平台清冊與 ERROR dispatch、使用者裁決可信 proxy client IP、建構子/啟動 guard、狀態及 OIDC 診斷;162 tenant/2 OSIV browser/single 六個及四組 optional 重驗通過,進第 2 輪人審,未提交/發布 |
| 2026-10-14 | 實作 change 第 2 輪 verdict 通過,R1-01~06 全數處置且本輪無新意見;Q1 接受未發布成果並解鎖提交,Q2/Q3 保留正式部署/MySQL/多節點/舊 artifact 回復/202 UI 及 FU-93 下行邊界。使用者要求 close,結論落於本 ADR、Feature Changelog 與 FU-90;FU-90 不結案,未提交/發布/部署 |
| 2026-10-14 | 使用者確認結論落點並完成 close;接續將已通過人審的 5 模組/113 檔成果與本 ADR 納入同一提交。暫時簡報及展示副本已清理,原始驗證報告保留;尚未發布/部署,FU-90 接續正式環境與 UI 採用證據 |
| 2026-10-14 | 使用者指定發布 4.0.0-alpha.35;版本 commit 656479fe,Maven JAR/sources/POM/module 遠端驗證通過,不打 tag。發布前全量 1,751 通過/58 略過/0 失敗;文檔站依 publish 流程同步部署驗證,參考實作 pin 採用與正式環境/UI 證據仍待補,FU-90 不結案 |