---
title: "License 控管機制 — 需求討論稿 (FR-062)"
brand: "Guidant AI · **FR-062** License 控管機制"
eyebrow: "FR-062 · License Management — 需求討論稿 · 2026-08-08（v2）"
h1: "產品上線前的最後一塊地基：License 控管機制"
lede: "Guidant AI 即將以「雲端 SaaS」與「落地部署」兩種模式對外銷售。本文件提出完整的 License 控管設計：公司內部離線簽發、產品端驗章執法、按功能模組授權、可組態到期管線、序號開通與機器綁定。**十五項核心決策（D1–D15）已全數拍板，並於 v2 套入九項修訂**：到期政策改為可組態管線（出廠預設「到期 → 14 天寬限 → 永久唯讀」）、簽發端初版即做極簡 Web 後台、金鑰生命週期管理、Plan 套餐範本、補發換綁流程，以及依程式碼相依實證完成的「基礎包／加值包」模組分層。"
chips: [
  {text: "D1–D15 全數拍板（v2 含九項修訂）", kind: ok},
  {text: "出廠預設：到期 → 寬限 → 永久唯讀", kind: accent},
  {text: "基礎包＋4 加值包（程式碼實證）", kind: accent},
  {text: "待確認：Plan 套餐組合與命名", kind: warn},
  {text: "待確認：分層最終圈選", kind: warn}
]
footer: "FR-062 · License 控管機制 — 需求討論稿 · 2026-08-08 v2（套入九項拍板修訂＋BE 模組相依盤點）· v1 2026-08-08 初稿 · 模組盤點自 DEV 資料庫 public.capabilities 與 BE 程式碼相依分析（唯讀查詢，2026-08-08）"
---

## 實作後記 {#afterword nav="實作後記"}

::: {.callout .ok}
**本節為 2026-08-08~09 開發實作期（.1~.7 全七階段落地）後補記**，記錄五項討論稿定案之後、實作過程中發生的演進，格式一律「當時討論版 → 實作後 → 為什麼變」。既有討論內容一字不動，本節純附加。
:::

### 1. 照檔格式三次演進

**當時討論版**：只定案「Ed25519 離線簽章 license 檔」，格式為明文 JSON（v1）。

**實作後**：連續改了三次——
- **v2 payload 打包**（T-1.3）：user 驗收時提出「不想讓客戶隨手打開文字編輯器就看到照的內容」，改為信封 `{format_version, kid, payload, signature}`，`payload` 為原文 zlib 壓縮＋base64。
- **v3 `.license` armor 外皮**（T-1.5）：user 認為裸 `.json` 交付不夠專業，對齊業界（JetBrains／GitLab／Keygen）慣例改用自有副檔名＋PEM 風格文字塊包裝。
- **v2.1 `send_email` 補旗標**（T-7.3）：grace／readonly 兩段到期通知信要接線時發現格式只預留了 notify 段的 `send_email`，順勢把 grace／readonly 也補齊同一旗標，`from_dict` 對舊照容忍缺鍵。

**為什麼變**：三次都不是推翻決策，是把「防翻閱」的產品體感做扎實——**簽章防竄改（安全）跟打包防翻閱（體感）從頭到尾是兩件獨立的事**，文件不可把打包／armor 誤寫成「加密」。三次都趕在下一階段消費格式之前改完，沒有留下需要回溯相容多套格式的技術債。

### 2. Passphrase 環境變數化

**當時討論版**：私鑰以 passphrase 加密保存，簽發時人工輸入 passphrase 解密。

**實作後**：簽發站私鑰解密改優先讀 `.env` 的 `LICENSE_CENTER_KEY_PASSPHRASE` 環境變數，設定後簽發／展延／補發表單免手動輸入；表單欄位僅在未設環境變數時才顯示（保留 air-gapped 備援路徑）。

**為什麼變**：開發期簽發站部署在同一台機器，每次簽照都要手動貼 passphrase 太不順手，而且沒有增加真實風險——私鑰檔與 `.env` 本來就同機。**這是開發／內部部署階段的權宜做法，不是正式對外簽發的保管方式**——正式簽發仍須密碼管理器＋兩處站外加密備份，design.md §4.9 已明確標註此界線。

### 3. T-5.2 線上開通提前落地（不等雲端）

**當時討論版**：線上開通伺服器「掛雲端環境」，雲端仍在開發中，此任務排在後面、等雲端就緒才做。

**實作後**：user 點破「線上開通本體就是一條 HTTP 流程（產品端 ↔ 開通伺服器），local 模擬（localhost 對 localhost）跟真雲端唯一差別是網址」，裁定本任務現在做、不等雲端——把「開發＋全流程驗證」跟「公網部署」拆成兩件事，前者本棒完成，後者（TLS、網域、防火牆）留到雲端環境就緒後補驗。

**為什麼變**：機制本身不依賴雲端環境是否存在，只依賴「有個 HTTP 端點可以打」。硬把整個任務排到雲端就緒之後，等於把一個可以獨立驗證的功能無謂延後，錯失提前抓 bug 的機會。

### 4. 到期通知信收件人判定：三者聯集

**當時討論版**：到期通知信「通知租戶管理員」，未細寫怎麼判定誰是管理員。

**實作後**：DB 實查發現單一候選欄位（`is_super_admin`／`is_admin` 角色成員／`tenant.update` capability 持有者）任一單獨拿來判都有覆蓋殘缺（例如某租戶只有 capability 持有者收得到），改為三者聯集；聯集結果為空的租戶 fallback 寄到 root 管理員並記警告 log。

**為什麼變**：討論階段「通知管理員」聽起來是單一明確概念，實作時一查 DB 才發現租戶管理員的認定在系統裡本來就有三條平行路徑、彼此覆蓋不完整——不改成聯集會有租戶到期了但沒有任何人收到通知。

### 5. `TENANT_ADMIN_EXCLUDED_RESOURCE_TYPES` 不退役（推翻原定案）

**當時討論版**（D12）：此硬編租戶停用清單「將被 license 機制取代」，定案要退役。

**實作後**：T-4.3 實作者查證後拒絕退役，覆核 DB 實況後 user 接受——這份清單管的是**開通租戶當下**要不要預設給某個 UI 入口能力點（provisioning-time），license 管的是**這個租戶商務上有沒有買**某個模組（request-time）。兩個維度看起來像同一件事，實際各自獨立，license 取代不了這份清單的作用。改為兩者並存，design.md §3／§8 已同步（裁決見 CM-1126）。

**為什麼變**：這是本案唯一一項「討論階段的定案被實作查證推翻」的項目，值得記一筆——設計文件的判斷仰賴的是討論當下的認知，實作時對著真實程式碼與真實資料查證，才發現原本以為的「同一件事」其實是兩件事。

---

## 需求背景 {#why nav="背景"}

Guidant AI（GRC 合規管理平台）即將正式上線銷售，銷售模式有兩種：

::: grid2
::: {.card .ok}
#### 雲端 SaaS
客戶使用公司自營的雲端環境，多租戶共用同一套系統。授權由公司在系統管理後台直接指派，客戶無感、即改即生效。
:::
::: {.card .ok}
#### 落地部署（Host 版）
整套系統部署在客戶自己的機房或私有雲。產品離開公司掌控範圍，授權必須以「不可竄改的憑證檔」形式隨產品交付，並能離線驗證。
:::
:::

兩種模式都需要一套 License 控管機制，核心需求為：

1. **功能管控**——root tenant（總公司／維運租戶）以外的所有租戶，其可用功能均受 license 管控；未授權的模組不出現在選單、API 拒絕存取。
2. **簽發能力**——公司能產生 license 交付客戶，客戶註冊開通後生效。
3. **有效期管理**——支援授權起訖日與訂閱制到期，到期後有明確、漸進、可預期的降級行為，而非直接鎖死。

::: {.callout .decided}
**本文件的定位**

License 機制是「產品能不能收錢」的商業基礎設施，牽動簽發流程、合約條款、客服話術與續約營運。本文件將市場調查、十五項設計決策、安全信任模型、資料模型、系統接入點與階段拆分完整攤開，供決策層審閱。文中所有決策（D1–D15）均已於前期討論拍板；本版（v2）另套入九項後續拍板修訂，並將可販售模組清單依後端程式碼相依盤點改版為「基礎包／加值包」分層。
:::

## 市場調查摘要：業界怎麼做 {#market nav="市場調查"}

在定案前，我們調查了主流商用軟體的授權機制，重點結論如下。

| 廠商／產品 | 授權機制 | 對本案的啟示 |
|---|---|---|
| **GitLab EE** | 雙軌制：線上 activation code＋離線 signed license file（供 air-gapped 環境） | 落地部署必須支援離線授權檔；線上與離線是同一套驗證引擎的兩種交付方式 |
| **Elastic** | 簽章 JSON license；到期後自動降級至免費功能層，**不鎖資料** | 到期不直接鎖死是業界共識，避免客戶資料被扣押的觀感與合約糾紛 |
| **Atlassian Data Center** | 到期後擋「新增」，既有內容仍可使用 | 「唯讀降級」是成熟的到期緩衝設計 |
| **商用授權服務（Keygen、Cryptlex 等）** | 提供現成 license server、機器指紋綁定、floating seat 等 | 機器指紋與序號開通已是標準做法；但引入第三方服務會讓授權鏈依賴外部廠商，本案自建 |

業界共識可歸納為四點：

::: grid2
::: {.card .ok}
#### 簽發私鑰絕不進產品
產品端只放公鑰做驗證。私鑰留在公司內部，客戶拿到的產品裡沒有任何可以簽發授權的材料。
:::
::: {.card .ok}
#### 到期不直接鎖死
提醒 → 寬限 → 唯讀，漸進式收斂。直接鎖死會製造合約糾紛與惡劣的續約體驗；本案出廠預設的終態即為「永久唯讀」，不鎖登入。
:::
::: {.card .ok}
#### 防護分層
簽章防竄改 → 公鑰編譯進執行檔防替換 → 時鐘回撥偵測防倒轉系統時間 → 蓄意破解（直接改產品程式碼）靠合約與稽核約束。
:::
::: {.card .ok}
#### 管理平台可以很小
簽發端與產品端分離是必要的，但簽發端不必是完整網站——一個包在 CLI 簽發引擎之上的極簡 Web 後台（客戶總覽＋簽發表單）即可滿足營運，等經銷商等需求出現再演化。
:::
:::

## 決策定案表（D1–D15） {#decisions nav="決策定案"}

以下十五項決策已全數拍板，為本案設計的權威依據。**標示「v2 修訂」者為初稿後由決策者再拍板的修訂內容。**

| 編號 | 決策 | 內容 | 理由 |
|---|---|---|---|
| **D1**（v2 修訂） | 簽發端與管理端分工 | **簽發端**＝公司內部簽發工具：底層為 CLI 簽發引擎（Ed25519 私鑰簽章＋發照紀錄表），**初版即在其上做一個極簡 Web 後台**（客戶總覽頁＋簽發／換發表單頁，詳見〈簽發端形態〉節）；**管理端**＝產品內 root tenant 系統管理後台（上傳、驗章、指派、各租戶授權狀態總覽）。不另建獨立管理網站 | Web 後台讓非工程師（業務、維運）可自行發照與掌握客戶到期全貌；CLI 保留為底層引擎，air-gapped 簽發或緊急狀況仍可命令列操作。管理功能放主站的理由見 D15 |
| **D2** | 授權對象 | License 綁「頂層客戶租戶」，**不限使用者人數（seat）**；但格式預留 quota 欄位（max_users、max_projects 等），初版一律填不限 | 現階段以模組計價，seat 計價留為未來商業槓桿；格式先預留可免日後換版 |
| **D3** | 授權粒度 | 按**功能模組**授權，模組粒度沿用既有權限系統的 `capability.resource_type`；模組的商業打包採「基礎包＋加值包」分層（見〈附表：模組分層表〉），簽發端另以 Plan 套餐範本組合（見〈Plan 套餐範本〉節） | 直接複用權限系統既有的資源分類，執法時 license 與 capability 兩層守門對齊同一套語彙，無需另建對照 |
| **D4** | 簽章與驗證 | **Ed25519 離線簽章** license 檔，公鑰**編譯進後端程式**（不放設定檔、不放資料庫），並採 **kid＋公鑰列表**機制支援金鑰輪替（見〈金鑰生命週期管理〉節）。SaaS 與 Host 版共用同一驗證引擎：SaaS 由 root tenant 後台線上指派（內部同樣產照，客戶無感、即改即生效）；Host 版的照＝合約，出貨定案，續約換新照 | 公鑰放設定檔或資料庫等於讓落地客戶可自行替換；編譯進執行檔是業界標準防線。單一驗證引擎確保兩種銷售模式行為一致 |
| **D5**（v2 修訂） | 到期政策 | **可組態到期管線**：提醒（notify）→ 寬限（grace）→ 唯讀（readonly）→ 鎖定（lockout）四段，**每段各帶 `{enabled, days}` 開關與天數，整組 `expiry_policy` 簽進照內**，橫幅與 email 通知也是每段的旗標。**出廠預設：notify 30 天、grace 14 天、readonly 啟用且為終態、lockout 預設關閉**——即預設行為是「到期 → 14 天寬限 → 永久唯讀（可登入、可看、可下載匯出檔案，擋全部寫入）」，不鎖登入。**產品端無任何天數設定** | 天數放產品端＝落地版客戶可竄改設定拉長寬限，且形成雙真相衝突。簽進照裡＝單一真相來源，改預設值只影響之後簽的新照。lockout 段格式保留、預設停用：未來政策變嚴時發照打開即可，不必改版 |
| **D6** | 換發模式 | **Replace 換發制**：一個租戶任一時刻只有一張現行照；續約、加購、展延一律重簽整張新照替換，舊照留歷史紀錄（稽核可查）。不採 append 疊加 | 多張照疊加會出現「模組 A 三月到期、模組 B 六月到期」的混合到期日，狀態機被迫 per-module 化，客服話術與客戶理解成本都會失控 |
| **D7** | 簽發權集中 | 只有公司內部能簽發（私鑰不出門）；未來經銷商也透過公司簽發，需計算抽佣——發照紀錄表**預留經銷商與抽佣欄位** | 私鑰外流等於授權體系瓦解；經銷商模式的商業結構（抽佣）先在資料上預留，流程日後再議 |
| **D8**（v2 修訂） | 租戶樹效力 | 照綁**頂層客戶租戶**，效力涵蓋其下全部子孫租戶；照內帶 `max_sub_tenants` 子租戶數上限（商業計價槓桿），**預設值定為 5**。平行頂層租戶只有 root tenant 能開；客戶可在額度內自行開設子租戶 | 客戶內部組織（子公司、部門）自然映射為子租戶，授權隨樹繼承最直覺；子租戶上限提供「集團版 vs 單公司版」的計價空間 |
| **D9** | 機器綁定 | 綁**機器指紋**：開通時取得並綁定該部署主機；續約換照時機器不變即不必重跑開通。照檔遺失與換機另有補發／換綁流程（見〈補發與換綁流程〉節） | 防止一張照複製到多套部署；續約免重開通降低客戶摩擦 |
| **D10** | 開通方式 | **序號開通制，線上與離線雙路都做**（實作各開 case）：簽發產出「license 檔＋開通序號（短碼）」→ 客戶系統首次登入偵測無照，導向開通頁 → **線上開通**：輸入序號打公司開通伺服器（**部署位置已定：掛雲端環境**），自動下載照並回傳機器指紋；**離線開通**：開通頁顯示本機機器碼，客戶把序號＋機器碼交給公司，公司簽出綁定該機的照回寄，客戶上傳 → 開通成功回寫發照紀錄。**序號／機器碼開通流程僅 Host 版適用**；SaaS 的送照管道是 root tenant 後台指派，零客戶動作（見〈開通流程〉節的兩模式對照） | 線上開通體驗最好，但落地客戶常在封閉網路，離線路徑不可省。雙路共用同一張照與同一套驗證，只差交付方式 |
| **D11** | 照型分類 | License 帶 `type` 欄位：`formal`（正式）／`trial`（試用）／`extension`（臨時展延）／`transitional`（過渡）。**extension** 應對「訂單行政流程未回簽但服務不可中斷」——抓原照只改到期日快速簽發。紀錄表標記型態，試用與展延**不混入營收認列** | 實務上續約回簽常晚於服務到期日，沒有展延機制就會出現「合約在走流程、客戶系統先鎖住」的事故；型態標記讓財務端能區分正式營收與行政性簽發 |
| **D12**（v2 修訂） | 可販售模組分層 | 以 BE 程式碼相依盤點為據，模組分為**基礎包**（隨基本授權附）與**四個加值包**（問卷／檢測工具／雲端整合／AI 儀表板），死模組與舊架構殘留自販售清單移除，平台級模組不販售（見〈附表：模組分層表〉）。現行硬編的租戶停用清單 `TENANT_ADMIN_EXCLUDED_RESOURCE_TYPES` 將被 license 機制取代 | 逐模組圈選在工程上不成立——多數模組彼此 hard 相依，拆開賣會得到不能動的系統。分層邊界以程式碼實證劃定，加值包挑的是「關掉之後系統仍完整」的模組 |
| **D13** | 到期通知 | **系統內橫幅＋email 通知租戶管理員，兩者都做**；v2 修訂後兩者均為 `expiry_policy` 各段的旗標，簽進照內 | 橫幅保證登入必見，email 覆蓋不常登入的管理員；到期是收入事件，通知不嫌多 |
| **D14** | 既有租戶過渡 | 本功能上線的 migration **自動為所有既有頂層租戶（root 除外）產生過渡照**：全模組、效期＝上版日＋90 天、`type: transitional`；內部環境（DEV／STG）發長效內部照。**執法邏輯第一天即全量生效，無任何特例分支** | 「既有租戶暫時豁免」的特例分支是技術債溫床且測不到執法路徑；發過渡照讓所有租戶第一天就走同一條授權管線，90 天內完成正式照替換 |
| **D15**（併入 D1） | 管理功能放主站 | License 管理頁放在產品內 root tenant 系統管理後台，不另建站 | 三個理由：①Host 版客戶機器上只有產品本身，別無他處可放；②SaaS 復用同一批頁面，成本最低；③root tenant 後台（platform-admin 權限軸）本就是特權維運功能的家 |

## 整體架構 {#architecture nav="整體架構"}

```{.mermaid cap="圖 1 — 整體架構：公司簽發端（Web 後台＋CLI 引擎）與產品端的分工與交付物"}
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
flowchart LR
  subgraph issuer["公司簽發端（內部，持有私鑰）"]
    WEB["極簡 Web 後台<br/>客戶總覽頁＋簽發/換發表單<br/>Plan 套餐一鍵帶入"]
    CLI["簽發引擎 CLI<br/>（Ed25519 私鑰簽章）"]
    LEDGER[("發照紀錄表<br/>license_issuance<br/>訂單編號／客戶代碼／序號／型態<br/>經銷商·抽佣欄位預留")]
    ACT["線上開通伺服器<br/>（掛雲端環境）"]
    WEB --> CLI
    CLI --> LEDGER
    ACT --> LEDGER
  end

  subgraph deliver["交付物"]
    LIC["license 檔<br/>（簽章 JSON）"]
    SN["開通序號<br/>（短碼，Host 版）"]
  end

  subgraph product["產品端（SaaS ／ Host 版共用）"]
    ADMIN["root tenant 系統管理後台<br/>上傳／驗章／指派／狀態總覽"]
    ENGINE["License 驗證引擎<br/>公鑰列表（kid 查表）編譯進後端"]
    STORE[("tenant_licenses<br/>現行照＋歷史照")]
    GUARD["授權守門<br/>common/authz 第六軸<br/>＋唯讀 gate＋選單/任務型態過濾"]
    CRON["每日排程<br/>到期狀態機檢查"]
    ADMIN --> ENGINE
    ENGINE --> STORE
    STORE --> GUARD
    CRON --> STORE
  end

  CLI --> LIC
  CLI --> SN
  LIC --> ADMIN
  SN -->|"線上開通（Host）"| ACT
  ACT -->|"自動下載照＋回傳機器指紋"| ADMIN
```

要點：

- **私鑰只存在於左側**。產品端（右側）只有公鑰列表與驗證邏輯，沒有任何簽發能力。
- SaaS 與 Host 版是**同一個右側**：SaaS 的「指派」動作在公司自營環境的 root tenant 後台完成（內部同樣走簽發產照），Host 版則由客戶自行上傳照——驗證引擎、守門、狀態機完全共用。
- 發照紀錄表是簽發端唯一的持久資料，含**訂單編號與客戶代碼**，供財務對帳與稽核。

### 簽發端形態：極簡 Web 後台（初版即做，D1 修訂） {#issuer-web}

初版即做一個公司內部小型 Web 應用，包在 CLI 簽發引擎之上（Web 是殼、CLI 是引擎），兩個頁面：

::: grid2
::: {.card .ok}
#### 客戶總覽頁
每個客戶一列：客戶代碼／名稱／訂單編號／照型態／Plan／模組集／到期日／**推算狀態**／開通狀態。依臨期排序並上色，維運與業務打開就知道「誰快到期了」。
:::
::: {.card .ok}
#### 簽發／換發表單頁
選 Plan 套餐一鍵帶入模組集，可再逐模組微調；填效期產照＋序號；展延（extension）一鍵完成——抓原照只改到期日。
:::
:::

三點誠實揭露：

- **Host 版客戶顯示的是「按合約推算」狀態，不是即時實況**——照在客戶機房裡跑，簽發端只能按到期日與 `expiry_policy` 推算目前應處階段。有網路的 Host 客戶可經回報端點回心跳，補一欄「實際狀態」；SaaS 租戶則為真即時（照就在自家平台 DB）。
- **CLI 保留為底層引擎**：air-gapped 簽發、緊急搶修時仍可純命令列操作，Web 掛了不影響簽發能力。
- **成本影響**：相較純 CLI，簽發端（階段 6）工時約 **2–3 倍**，換取的是非工程師可自行操作發照與續約營運，並成為未來經銷商後台的地基。

## 信任模型與安全設計 {#security nav="安全設計"}

License 機制的價值取決於「客戶繞不繞得過去」。本節誠實攤開防線與殘餘風險。

### 防線一：竄改照內容——驗章失敗即鎖定

客戶若直接編輯 license 檔（改到期日、開模組），Ed25519 驗章立即失敗，**整張照無效**，系統進入鎖定狀態（無有效照＝無授權，與到期管線的可組態終態無關），並且：

- **本地留證**：記錄事件時間與檔案指紋，air-gapped 環境由維運進場時查看。
- **有網路則回報**公司端點。

原則：**能通知就通知，不能通知就鎖——鎖了客戶自然會回來找**。

### 防線二：破解管理員帳號也無法自行簽發

產品內**沒有私鑰、沒有簽發程式碼**，只有「收照櫃台」（上傳與驗證）。即使客戶取得產品最高權限帳號，能做的也只是上傳一張照——而照必須由公司私鑰簽出。SaaS 的線上指派功能只存在於公司自營環境的 root tenant，不隨 Host 版出貨。

### 防線三：時鐘回撥偵測

落地客戶可能把系統時間倒轉來延長效期。對策：

- 系統持續記錄「見過的最大時間戳」，偵測到時間倒退即判定異常。
- license 的簽發時間不可在未來（簽發時間晚於當下系統時間即拒收）。

### 殘餘風險揭露

::: {.callout .warn}
**蓄意破解防不完，這是所有落地軟體的共同事實**

客戶若直接修改產品程式碼（patch 掉驗證邏輯），任何技術防線都擋不住——GitLab、Atlassian 等一線廠商的落地版同樣如此。這一層的約束手段是**合約與稽核**：授權合約明訂禁止反向工程與規避授權，技術上留存的竄改證據（防線一）作為舉證材料。本案的技術防線目標是「讓正常客戶不會誤觸、讓一般管理員無法繞過」，不是「數學上不可破解」。
:::

## 金鑰生命週期管理 {#keys nav="金鑰管理"}

私鑰是整套授權體系的根，本節定義保管、輪替與應變。

### 私鑰保管與備份紀律

- 私鑰檔以 **passphrase 加密**保存，加密備份**至少兩處**：公司密碼管理器／保險庫一份＋離線媒介（如保險櫃內的隨身碟）一份。
- 發照紀錄表 `license_issuance` 同步備份——它是「發過哪些照」的唯一權威紀錄，遺失等於失去對帳與補發依據。

### 簽發機器壞了怎麼辦

私鑰是**檔案**，不綁機器。簽發機器故障時，換一台機器裝上簽發工具、還原私鑰備份即恢復簽發能力；**照的驗證完全不看簽發機器**，已交付客戶的照不受任何影響。

### kid＋公鑰列表機制（初版就做）

::: {.callout .decided}
**為什麼初版就要做金鑰輪替的地基**

若產品端只編譯一把公鑰，日後換鑰＝逼**全部**客戶同時升級產品版本，商業上不可行。因此初版即採：

- 產品端編譯進**公鑰列表** `[{kid, pubkey}]`（不是單一把）。
- 照內帶 `kid` 欄位，驗章時**按 kid 查表**取對應公鑰。

輪替時：新版產品帶**新舊雙鑰**過渡，新照用新鑰簽、存量舊照仍能驗，客戶按自己的升級節奏走，無需同步換照。
:::

### 輪替劇本與洩漏應變

| 情境 | 處置 |
|---|---|
| **計畫性輪替**（例如每 2–3 年） | 產鑰新 kid → 新版產品公鑰列表加入新鑰（保留舊鑰）→ 之後新照改用新鑰簽 → 存量照到期換發時自然轉到新鑰 → 舊鑰無存量照後，下一版移除 |
| **私鑰洩漏**（急件） | 立即產新鑰；緊急出版**只帶新鑰**的產品版本（＝吊銷舊鑰）；**換發全部現行照**（新鑰重簽）並通知客戶升級。發照紀錄表就是「哪些照要換發」的清單 |

### 誠實揭露：備份紀律是必須品

::: {.callout .crit}
**私鑰遺失且無任何備份＝無法再簽發新照與續約**

已發出去的照不受影響（各自跑到到期日），但公司將無法簽新照、無法續約、無法展延——等同授權營運中斷，只能換新鑰並要求全部客戶換照升級。這不是可以事後補救的風險，**上述兩處備份紀律是本案的營運前提，不是建議**。
:::

## License 檔欄位定義 {#fields nav="欄位定義"}

License 檔為簽章 JSON，欄位分五組：

| 分組 | 欄位 | 說明 |
|---|---|---|
| **識別** | `license_id` | 照的唯一識別碼 |
| | `customer_code` | 客戶代碼（對齊發照紀錄表） |
| | `order_no` | 訂單編號（財務對帳鏈結） |
| | `issued_to` | 客戶名稱 |
| | tenant 綁定 | 綁定的頂層客戶租戶識別 |
| | `type` | `formal` ／ `trial` ／ `extension` ／ `transitional` |
| | `issuer` | 發照人 |
| **時間** | `issued_at` | 簽發時間（不可在未來） |
| | `starts_at` ／ `expires_at` | 效期起訖 |
| | `expiry_policy.notify` | `{enabled, days_before, show_banner, send_email}`——到期前提醒段（出廠預設：啟用、30 天、橫幅＋email） |
| | `expiry_policy.grace` | `{enabled, days, show_banner}`——寬限段（出廠預設：啟用、14 天） |
| | `expiry_policy.readonly` | `{enabled, days?}`——唯讀段；`days` 省略或 readonly 為終態時無期限（出廠預設：啟用、終態） |
| | `expiry_policy.lockout` | `{enabled}`——鎖定段（出廠預設：**停用**；格式保留，未來政策變嚴時發照打開） |
| **功能** | `modules` | `{resource_type: bool}` 對映，**展開後的模組清單**（產品端只認這個） |
| | `plan` | Plan 套餐名稱標籤（如 `professional`）；純標示用，產品端不據此執法 |
| | `limits` | `{max_sub_tenants（預設 5）, max_users(預留), max_projects(預留)}` |
| **綁定** | `machine_fingerprint` | 機器指紋（開通後綁定；SaaS 不綁，留空） |
| | `deployment_mode` | `saas` ／ `host`——同一 schema 同一引擎，僅以此欄位區分（host 有指紋＋序號、saas 空） |
| **完整性** | `kid` | 簽章金鑰識別碼，驗章時按 kid 查產品端公鑰列表 |
| | `signature` ＋ `alg` | Ed25519 簽章與演算法標記 |

## Plan 套餐範本（簽發端概念） {#plans nav="Plan 套餐"}

Plan 是**簽發端的範本**，不是產品端的概念：發照時選 Plan 一鍵帶入模組集，可再逐模組微調後簽出。

- **照內存的是展開後的模組清單**＋`plan` 名稱標籤欄；**產品端只認模組清單、不認 Plan**。
- 因此套餐內容日後調整**只影響未來發的照**，已發照完全不受影響——與 `expiry_policy` 天數預設值同一哲學：範本改動不回溯存量。

Plan 草案（供決策者與業務修改的起點）：

| Plan | 內容 |
|---|---|
| **Basic** | 基礎包 |
| **Professional** | 基礎包＋問卷包＋雲端整合包 |
| **Enterprise** | 全模組（基礎包＋全部四個加值包） |

## 開通流程 {#activation nav="開通流程"}

### SaaS 與 Host 版的送照管道對照

序號／機器碼開通流程**僅 Host 版適用**。兩種模式對照如下：

| | **SaaS** | **Host 版** |
|---|---|---|
| 送照管道 | root tenant 後台指派：簽發引擎內部產照**直接寫入租戶**，客戶零動作、即改即生效 | 交付 license 檔＋開通序號，客戶走線上或離線開通 |
| 機器綁定 | 不綁機器指紋（照存平台 DB） | 開通時綁定機器指紋（D9） |
| 照格式 | **同一 schema、同一簽發引擎**，`deployment_mode: saas`（指紋與序號留空） | 同一 schema，`deployment_mode: host`（含指紋＋序號） |
| 照的存放 | 簽發端紀錄表存全部（以 `deployment_mode` 篩選）；SaaS 平台 DB 只有 SaaS 租戶的照 | 各 Host 環境只有自己那一張——存放天然分離，無互見風險 |

```{.mermaid cap="圖 2 — 送照時序：SaaS 內部指派＋Host 版線上／離線開通（D10）"}
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
sequenceDiagram
    participant C as 客戶（管理員）
    participant P as 產品端（開通頁）
    participant S as 公司開通伺服器
    participant I as 公司簽發端（Web 後台＋CLI）

    rect rgb(226, 240, 241)
    Note over I,P: 路徑 0 — SaaS：後台指派（客戶零動作）
    I->>I: root tenant 後台指派 → 簽發引擎內部產照
    I->>P: 直接寫入該租戶（不綁機器指紋）
    P->>P: 驗章 → 即改即生效
    end

    Note over I: Host 版：簽約後簽發產出 license 檔＋開通序號
    I->>C: 交付開通序號（短碼）

    rect rgb(238, 242, 243)
    Note over C,S: 路徑 A — Host 線上開通
    C->>P: 首次登入，系統偵測無照 → 導向開通頁
    C->>P: 輸入開通序號
    P->>S: 序號＋本機機器指紋
    S->>I: 核對序號 → 簽出綁定該機的照
    S-->>P: 回傳 license 檔（自動下載安裝）
    P->>P: 驗章 → 生效
    S->>I: 回寫紀錄：已開通＋機器指紋＋時間
    end

    rect rgb(251, 252, 252)
    Note over C,I: 路徑 B — Host 離線開通（封閉網路）
    C->>P: 首次登入 → 開通頁顯示本機機器碼
    C->>I: 將序號＋機器碼交付公司（郵件等既有管道）
    I->>I: 簽出綁定該機器碼的照
    I->>C: 回寄 license 檔
    C->>P: 上傳 license 檔
    P->>P: 驗章＋核對機器指紋 → 生效
    Note over I: 人工回寫紀錄：已開通＋機器指紋＋時間
    end
```

Host 兩路共用同一張照格式與同一套驗證引擎，差別只在交付方式。機器綁定（D9）在開通當下完成；日後續約換照，機器不變即不必重跑開通。線上開通伺服器**掛雲端環境**（決策者已定；雲端環境仍在開發中，不擋本案——離線路徑先行，見階段拆分）。

## SaaS 後台管理操作 {#saas-ops nav="SaaS 管理操作"}

root tenant 後台對 SaaS 租戶可執行的授權操作：

| 操作 | 說明 |
|---|---|
| 指派／變更方案 | 選 Plan 或逐模組調整，內部產照直接寫入，**即改即生效** |
| 展延／縮短效期 | 重簽新照替換（D6 replace 制） |
| **手動立即停權** | **SaaS 獨有能力**——欠費等情況直接把租戶切唯讀或停用，**不等到期日**。Host 版做不到此事（照在客戶手上，只能等它到期） |
| 檢視單一租戶狀態 | 現處階段、下一階段時點、模組集 |

## 補發與換綁流程 {#reissue nav="補發與換綁"}

Host 版照檔與機器綁定的兩種例外情境（SaaS 無此問題，照存平台 DB）：

::: grid2
::: {.card .ok}
#### 情境 A：照檔弄丟（機器沒變）
發照紀錄表存有完整照內容，**一鍵重出同一張照**（同內容、同機器指紋）交付客戶。紀錄表記「補發」事件，**不影響營收報表**（不是新簽）。
:::
::: {.card .warn}
#### 情境 B：換機（機器指紋變了）
客戶新機走一次開通流程取得**新機器碼** → 公司簽出**同內容、改綁新機**的照 → 紀錄表記「換綁」事件，舊指紋留歷史並**作廢**。
:::
:::

防複製政策：

- 換綁**人工受理**——受理時確認舊機已停用，不提供自助換綁。
- **同一客戶頻繁換綁是警訊**（可能在複製部署），簽發端總覽頁對高頻換綁客戶標記提示。

## 到期狀態機（可組態管線） {#expiry nav="到期狀態機"}

到期後的行為不再是寫死的四段，而是一條**可組態管線**：notify → grace → readonly → lockout 四段各自可獨立開關、各帶天數，整組 `expiry_policy` 簽進照內（D5 修訂）。

```{.mermaid cap="圖 3 — 可組態到期管線（D5 修訂）：實線為出廠預設路徑（終態＝永久唯讀），lockout 段預設停用"}
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
stateDiagram-v2
    [*] --> valid: 照生效
    valid --> notice: 距到期 ≤ notify.days_before（預設 30）
    notice --> grace: 到期日過（grace 啟用）
    notice --> readonly: 到期日過（grace 停用時直接唯讀）
    grace --> readonly: 寬限天數用盡（預設 14）
    readonly --> locked: lockout 啟用且 readonly.days 用盡<br/>（出廠預設 lockout 停用，此轉換不會發生）

    notice --> valid: 換發新照
    grace --> valid: 換發新照
    readonly --> valid: 換發新照
    locked --> valid: 管理員上傳新照

    valid: valid — 正常服務
    notice: notice — 全功能＋到期提醒（橫幅／email 依照內旗標）
    grace: grace — 全功能＋醒目警告
    readonly: readonly — 可登入·可看·可下載匯出，擋全部寫入（預設為終態）
    locked: locked — 鎖登入，僅管理員可進上傳頁（預設停用，可組態）
```

### 各段開關的語意

| 組態 | 行為語意 |
|---|---|
| 關閉 `grace` | 到期日一過**直接進唯讀**，無寬限 |
| 關閉 `lockout`（**出廠預設**） | 唯讀為**終態**——客戶永遠可登入、查看、下載匯出資料，只是不能寫 |
| 關閉 `readonly`（且 lockout 開） | 寬限用盡直接鎖定（嚴格政策組合） |
| `readonly.days` 省略 | 唯讀無期限（readonly 為終態時的自然寫法） |
| **全部關閉** | 形同永久照——簽發工具會**警示**（防誤簽），但不禁止（內部長效照即為此組態） |
| 各段 `show_banner` ／ notify 的 `send_email` | UI 橫幅與 email 通知逐段可控，一樣簽進照內 |

設計要點：

- **出廠預設（決策者已定）：notify 30 天 → grace 14 天 → readonly 啟用且為終態、lockout 停用**。即預設行為是「到期 → 14 天寬限 → 永久唯讀（可登入、可看、可下載匯出檔案，擋全部寫入）」，**不鎖登入**。lockout 段格式保留、預設停用——未來政策變嚴，發照時打開即可，**不必改版**。
- **整組 `expiry_policy` 簽在照裡**，產品端沒有任何可調設定。簽發端維護全系統統一預設值，改預設值只影響之後簽的新照，**舊照按自己照裡的政策跑完自然收斂**——不存在「改一個設定影響所有存量客戶」的風險，也杜絕落地版被客戶竄改天數。
- **locked（若啟用）不是死路**：管理員永遠可以進 License 上傳頁上傳新照（防死鎖），登入端點與上傳端點在唯讀與鎖定狀態下均豁免。
- 狀態轉換由**每日排程**檢查驅動（複用既有 APScheduler 機制），任何狀態換發新照即回到 valid。

## 端到端流程 {#e2e nav="端到端流程"}

```{.mermaid cap="圖 4 — 端到端：簽約 → 簽發 → 交付 → 開通 → 使用 → 到期 → 續約換發（D6 replace 制；到期段依出廠預設，終態＝永久唯讀）"}
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
flowchart LR
  A["① 簽約<br/>訂單成立<br/>（訂單編號·客戶代碼）"] --> B["② 簽發<br/>Web 後台選 Plan 產照＋序號<br/>寫入發照紀錄表"]
  B --> C["③ 交付<br/>Host：照＝合約隨貨<br/>SaaS：後台直接指派"]
  C --> D["④ 開通（Host）<br/>線上或離線<br/>綁定機器指紋"]
  D --> E["⑤ 使用<br/>模組守門·子租戶額度<br/>每日狀態檢查"]
  E --> F["⑥ 到期<br/>notice → grace → readonly<br/>（出廠預設終態＝永久唯讀）"]
  F --> G["⑦ 續約換發<br/>重簽整張新照替換<br/>舊照留歷史"]
  G --> E
  F -.->|"訂單未回簽<br/>但服務不可中斷"| H["臨時展延照<br/>type: extension<br/>只改到期日"]
  H --> E
```

## 資料模型草案 {#data nav="資料模型"}

兩端各一張表起步，刻意極簡：

### 簽發端：發照紀錄表 `license_issuance`

公司內部持有，是財務對帳與稽核的權威紀錄，也是 Web 後台客戶總覽頁的資料源。

| 欄位群 | 欄位 | 說明 |
|---|---|---|
| 商務識別 | **`order_no`（訂單編號）**、**`customer_code`（客戶代碼）**、客戶名 | 決策層明確要求必含前兩欄，對帳鏈結 |
| 授權內容 | tenant 識別、開通序號、`type`、`deployment_mode`、Plan 標籤、模組集、`expiry_policy`、到期日 | 簽進照裡的內容留檔；補發（情境 A）即據此重出同一張照 |
| 開通狀態 | 機器指紋（開通後回寫）、開通時間、換綁歷史（舊指紋作廢紀錄） | 線上路徑自動回寫、離線路徑人工回寫；頻繁換綁於總覽頁標記 |
| 通路（預留） | 經銷商欄位、抽佣欄位 | D7：未來經銷商模式的資料先留位 |
| 稽核 | 發照人、時間戳、事件類別（新簽／換發／展延／補發／換綁） | |

### 產品端：`tenant_licenses`

| 欄位群 | 欄位 | 說明 |
|---|---|---|
| 綁定 | `tenant_id` | 頂層客戶租戶 |
| 照本體 | license 原文 | 逐次驗章的依據（不信任解析快取） |
| 快取 | 解析後欄位快取 | 模組集、到期日、`expiry_policy` 等，供守門快速讀取 |
| 狀態 | `valid` ／ `notice` ／ `grace` ／ `readonly` ／ `locked` | 每日排程更新；哪些狀態可達由照內 `expiry_policy` 決定 |
| 歷史 | `is_current` 旗標（或歷史表） | D6：舊照留存供稽核 |
| 稽核 | 上傳人、上傳時間 | |

產品端狀態機由**每日排程**檢查（複用 `core/scheduler.py` 既有 APScheduler cron 樣板，比照 `framework_parse_job_cleanup` 的寫法）。

## 系統接入點盤點 {#touchpoints nav="系統接入點"}

本案不是從零長出一套機制，而是掛進既有骨架。逐點盤點如下：

| 元件 | 現況 | 本案動作 |
|---|---|---|
| `common/authz/` | FR-048 建立的統一授權守門，現有五軸（platform-admin ／ super-admin ／ project-role ／ capability ／ signed-token） | **新增第六軸 `license.py`**：照抄 `capability.py` 的 guard＋decorator＋`flask.g` memoize 樣板；root tenant 經 `viewer_is_platform_admin()` 無條件豁免 |
| `capability.resource_type` | 權限系統的資源分類（見附表分層盤點） | 作為 license 模組粒度來源（D3）；現行硬編停用清單 `TENANT_ADMIN_EXCLUDED_RESOURCE_TYPES`（`app/auth/service/tenant_provisioning_service.py`）**將被 license 機制取代** |
| `api/grc` ／ `api/project` capability 守門 | **目前零 capability 守門**（技術債，見附表 callout） | license 執法前**必須補守門**，否則「沒買 project」API 層擋不住 |
| 任務型態選單 | `GrcJobType` enum 寫死（`common/enum/grc_job_type_enum.py`＋`api/grc/serializers/job.py`） | 任務型態下拉**按租戶授權動態過濾**——沒買問卷包的租戶不可選到問卷任務 |
| ui_route 選單過濾 | 選單項由 `ui_routes` 動態組出（`api/auth/routes/ui_route_route.py` 為樣板） | 未授權模組的選單項**直接隱藏**，使用者不會看到點了才報錯的按鈕 |
| `core/scheduler.py` | 既有 APScheduler，已有每日 cron 前例（`framework_parse_job_cleanup`） | 新增每日 cron 檢查各租戶照的到期狀態轉換 |
| 唯讀執法 | 無 | readonly 狀態下**全域攔截 POST／PUT／PATCH／DELETE 回 403**；License 上傳端點與登入端點豁免（防死鎖）；下載匯出類 GET 不受影響 |
| config | 現有 ENV class 只有基礎設施參數，無部署形態語意 | 新增 **`DEPLOYMENT_MODE`**（`saas` ／ `host`）概念，供驗證引擎區分行為（如機器指紋是否必綁） |

## 使用情境 {#scenarios nav="使用情境"}

以下以虛構客戶「宏遠科技」（customer_code：`HY-001`）走五個情境，說明機制在真實營運中的樣貌。

### 情境一：落地版發照與開通

宏遠採購 Host 版 Professional 方案，訂單編號 `SO-2026-0142`。業務結單後，維運在簽發 Web 後台的簽發表單選 Plan「Professional」（自動帶入基礎包＋問卷包＋雲端整合包）、填效期一年，產出：license 檔（綁宏遠頂層租戶、展開後的模組集與出廠預設 `expiry_policy` 簽入照內）＋開通序號，發照紀錄表自動新增一筆（含訂單編號與客戶代碼）。宏遠機房是封閉網路，走**離線開通**：IT 人員首次登入被導向開通頁，抄下畫面上的機器碼，連同序號寄回公司；公司簽出綁定該機的照回寄；IT 上傳，驗章通過即全功能生效。發照紀錄表回寫「已開通＋機器指紋＋時間」。

### 情境二：到期全過程（出廠預設政策）

宏遠的照 2027-08-31 到期，假設未續約，依出廠預設 `expiry_policy`（notify 30／grace 14／readonly 終態／lockout 停用），時間軸如下：

| 日期 | 狀態 | 系統行為 |
|---|---|---|
| 2027-08-01 | `notice`（到期前 30 天） | 橫幅出現「授權將於 8/31 到期」＋email 通知租戶管理員 |
| 2027-09-01 | `grace`（寬限 14 天） | 全功能照常，橫幅升級為醒目警告「授權已到期，9/14 後進入唯讀」 |
| 2027-09-15 起 | `readonly`（**終態**） | 可登入、可查看、可下載匯出資料；所有寫入操作被擋（403）。lockout 段預設停用，**不會進一步鎖登入** |

過程中任一天完成續約、上傳新照，立即回到 `valid`。客戶資料自始至終可登入查看與匯出，不存在「資料被扣押」的爭議空間；若未來對特定客群需要更嚴格的政策，發照時啟用 lockout 段即可。

### 情境三：公司調整到期政策預設值

2027 年公司決定把寬限期從 14 天改為 7 天。維運只改簽發端的 `expiry_policy` 預設值——**之後簽的新照**帶 grace 7 天；宏遠等存量客戶手上的照仍是 14 天，按原條件跑到換照為止。沒有任何存量客戶的行為在無預警下改變，也沒有產品端設定要同步。

### 情境四：臨時展延（訂單未回簽）

宏遠 2027 年續約，採購流程卡在用印，回簽日眼看晚於 8/31 到期日。業務向維運申請**展延照**：Web 後台展延一鍵——抓原照內容、只把到期日改為 9/30、`type: extension` 簽出。宏遠上傳後服務不中斷；紀錄表標記為展延、不計入營收。正式訂單回簽後，再簽正式新照替換。

### 情境五：客戶偷改照被鎖

宏遠某工程師嘗試直接編輯 license 檔，把到期日改成 2099 年。下一次驗證時 Ed25519 簽章比對失敗，**整張照判定無效**，系統進入鎖定並在本地記錄事件（時間、檔案指紋）；該環境若可連外即回報公司端點。宏遠管理員發現系統鎖定後聯繫公司，維運調出竄改留證，依合約處理，重新提供正確的照恢復服務。（此為「無有效照」的鎖定，與到期管線的可組態終態無關。）

## 階段拆分（可獨立驗證、隨時可中斷） {#phases nav="階段拆分"}

::: {.callout .decided}
**拆分原則（決策者明確要求）**

每階段自帶**可觀察產出與人工測試素材**——測試資料由前一階段產出，不依賴任何未完成的後續階段；**階段 1–3 對現有系統零侵入**，中途暫停不留任何風險；**執法最後才進場，且帶環境開關可整體關閉**。任何一個階段結束都是一個安全的暫停點。
:::

| 階段 | 內容 | 人工測法 | 依賴 |
|---|---|---|---|
| **階段 1** | 簽發端獨立先行：Ed25519 產鑰／簽章引擎＋CLI 產照＋發照紀錄表 | 跑 CLI 產照、驗簽章、查紀錄表。**完全不碰主產品** | — |
| **階段 2** | 產品端驗照與狀態顯示（**只讀不執法**）：`tenant_licenses` 表＋上傳／驗章＋狀態頁 | 拿階段 1 的照上傳，看解析與狀態顯示。**不影響既有任何功能** | 階段 1（要有照可上傳） |
| **階段 3** | 狀態機排程與橫幅（**仍不擋操作**）：每日 cron＋階段推算＋橫幅顯示 | 發一張短效照（2 天）觀察狀態轉換與橫幅變化 | 階段 1＋2 |
| **階段 4** | **執法進場**（帶環境開關可整體關閉）：authz 第六軸守門＋`api/grc`／`api/project` 補 capability 守門＋唯讀 gate＋任務型態動態過濾＋選單過濾 | 測試租戶驗「擋／不擋」、開關切換前後行為對照 | 階段 1–3 |
| **階段 5** | 開通流程：**離線開通先行（case A）**、線上開通後續（case B——開通伺服器掛雲端環境，決策者已定；雲端仍在開發中，不擋本案） | 走一輪機器碼→簽照→上傳→綁定；線上路徑待雲端就緒後補測 | 階段 1＋2（要有照與序號可開通） |
| **階段 6** | 簽發 Web 後台（包在階段 1 的 CLI 引擎上）：客戶總覽頁＋簽發／換發表單頁 | 用總覽頁核對既有發照紀錄、走表單簽一張照與 CLI 結果比對 | 階段 1（引擎與紀錄表） |
| **階段 7** | email 通知＋過渡照 migration（D14，**上版前最後執行**） | 短效照觸發通知信；DEV 先跑 migration 驗過渡照產出 | 階段 3（狀態機事件）；migration 為上版動作 |

## 附表：模組分層表（D12 修訂） {#modules nav="模組分層表"}

v1 的逐模組圈選表已廢止。本次依 **BE 程式碼相依盤點**（2026-08-08，唯讀分析）改為「基礎包／加值包」分層——分層邊界不是商業直覺，而是程式碼實證：**基礎包缺一系統不成立，加值包關掉系統仍完整**。

### 基礎包（隨基本授權附）

缺一系統不成立、或屬支撐資料／一般營運的模組，共 21 個：

`project`、`module-frame`、`compliance-framework`、`workflow`、`flow_template`、`audit`、`device`、`information-system`、`dashboard`、`report`、`project-summary-report`、`user`、`role`、`department`、`tenant`、`bulletin`、`bulletin-list`、`feedback`、`feedback-view`、`notify_config`、`storage-config`

關鍵相依證據（為何不能拆開賣）：

| 相依事實 | 程式碼座標 |
|---|---|
| 開專案 hard 依賴 module-frame 三件組 clone——沒有 module-frame 就開不了專案 | `app/project/service/project_start_app_service.py:335-344` |
| flow engine（workflow／flow_template）是任務執行骨幹，缺了專案是空殼 | `app/grc/service/prep_job_generation_service.py` |
| device／information-system 是 SSP 受評範圍的支撐資料 | `app/grc/service/project_service.py:299-316` |
| dashboard SQL 串 6 個模組的資料，缺一歸零 | `infra/grc/repository/grc_dashboard_repo_impl.py:185-201` |

### 加值模組（4 包，邊界經程式碼實證）

| 加值包 | 內含 resource_type | 邊界實證 |
|---|---|---|
| **問卷包** | `survey` | 解鎖專案的問卷型任務。`job_service` 對 survey 為 hard 相依但方向單純——未授權時把任務型態選單過濾掉即可，不影響其他任務型態 |
| **檢測工具包** | `remote-agent-manage`＋`detection-profile`＋`plugin`（**三合一，不拆賣**） | detection 任務鏈三者 hard 綁定（`app/grc/service/job_service.py:131-135`、`detection_orchestration_service.py`），拆開任一都是壞的半套 |
| **雲端整合包** | `cloud_integration` | **全系統邊界最乾淨**：Drive 為 best-effort 同步層，證據上傳有本地保底（`app/flow_engine/service/job_evidence_service.py:85-88`），關掉零影響 |
| **AI 儀表板包** | `ai-dashboard` | 技術獨立，registry 為靜態表；未授權時模組呈空圖表，需在選單／入口過濾 |

### 自販售清單移除

| resource_type | 移除理由 |
|---|---|
| `resource` | **死模組**——無任何程式引用、已停用、報告腳本已標記刪除 |
| `cruise-project` | 舊架構殘留，非現行產品功能 |

### 平台級（維持不販售）

`issue-integrate-config`、`ldap-config`、`smtp-config`、`log`、`system-menu`、`system_config` 共 6 個，`is_platform` 全標記，僅 root tenant 使用。

### 販售前必補技術債

::: {.callout .crit}
**🔴 以下四項是 license 執法的前置工程，屬本案工程範圍的一部分**

1. **`api/grc` 與 `api/project` 目前零 capability 守門**——意即「沒買 project」在 API 層根本擋不住，只是選單看不到。license 執法必須先把守門補齊（納入階段 4）。
2. **`GrcJobType` enum 寫死**（`common/enum/grc_job_type_enum.py`＋`api/grc/serializers/job.py:173-177,209-213`）——任務型態下拉需按租戶授權**動態過濾**，否則沒買問卷包的租戶選得到問卷任務，選了會卡死在 `SURVEY_INCOMPLETE` 永遠完成不了。
3. **`job_import_service.py:31` 的 `VALID_JOB_TYPES` 與 enum 不同步**（缺 `detection_tool`）——既有小 bug，本案順手修。
4. **workflow resource_type 語意釐清**：現行租戶停用清單停掉的是**舊 UI 入口的能力點**，flow engine 引擎本身照常運作——license 切分時**不可誤把引擎關掉**（引擎屬基礎包骨幹）。
:::

## 待確認事項 {#open nav="待確認"}

::: {.callout .pending}
**留待決策的三件事**

1. **Plan 套餐組合與命名**：Basic／Professional／Enterprise 草案（見〈Plan 套餐範本〉節）為工程端起點，組合內容與命名請決策者與業務最終確認。
2. **基礎包／加值包分層最終圈選**：分層邊界已依程式碼相依實證劃定（見附表），商業面是否照此打包、加值包是否再合併或拆分，待最終確認。
3. **過渡照效期**：現定上版日＋90 天（D14），數字可調，於階段 7 migration 前定案即可。

已定案、自待確認清單移除：子租戶上限預設值＝**5**（D8）；lockout 段**預設關閉**、唯讀為出廠終態（D5）；線上開通伺服器**掛雲端環境**（D10）。
:::
