# LINE / Telegram 驗證與授權生命週期說明

> 給未來修改 LINE / Telegram 驗證、配對、home-channel、email handoff、刪綁定與 admin 後台流程的人看。
>
> 這份文件是程式碼註解的唯一長文件入口。若你在 `gateway/user_verification.py`、`gateway/authz_mixin.py`、`plugins/platforms/telegram/adapter.py`、`plugins/platforms/line/adapter.py`、`hermes_cli/web_server.py` 看到引用，請回到這裡讀完整背景。

## 為什麼需要這份文件

LINE / Telegram 驗證與授權不是單一布林值，而是多層狀態的組合。過去重複出現的 regressions 幾乎都來自下面幾種錯誤假設：

1. **以為刪掉綁定 row 就等於失效**
   - 實際上 pairing approval 仍可能放行。
2. **以為 adapter allowlist 與 runtime 驗證是同一層**
   - 實際上 adapter intake gate 與 gateway authz 是兩層，不同平台策略也不同。
3. **以為 verified email handoff 只要看使用者輸入的 email**
   - 實際上必須再比對 live 綁定，否則會把結果寄到過期或錯誤信箱。
4. **以為 LINE / Telegram 可以共用完全相同的 intake 邏輯**
   - 實際上 Telegram 與 LINE 的 prefilter 行為、reply token、quota fallback、pairing/onboarding 路徑都不同。
5. **以為只要測 DB 或只要測 UI 就夠**
   - 實際上需要同時驗證 DB、pairing store、adapter intake、gateway authz、admin route 與 user-visible 行為。

## 需求目標

系統對 LINE / Telegram 應滿足以下不變條件：

1. **未驗證的新 DM 使用者可以進入 onboarding / pairing / company-email 驗證流程。**
2. **已驗證且仍綁定的使用者，可以被辨識為 verified source，並安全使用 email handoff。**
3. **解除綁定時，所有仍可放行的授權面都要一起撤銷，而不是只刪一張表。**
4. **group / room / forum 等非 DM 面向，不能因為修 DM onboarding 而意外放開。**
5. **系統自動 email handoff 必須重新比對 live 綁定，不可依賴舊 session、壓縮後上下文或快取中的 email。**
6. **任何改動都要避免 fail-open：沒有明確 allowlist / pairing / verified binding 時，不應默默放行外部來源。**

## 關鍵資料面（授權不是只有一處）

### 1. Runtime DB：`gateway_users.db`
主要表：

- `principals`
  - canonical principal（例如 `dk96@bremen.com.tw`）
- `external_identities`
  - 平台帳號與 principal 的 verified 綁定關係
- `identity_enforcement`
  - 某平台帳號目前驗證狀態（`new`, `pending_verification`, `verified`, `admin_blocked`, ...）
- `verification_requests`
  - 驗證 email token 與 resend 節流相關資料

### 2. Pairing store：`~/.hermes/pairing/{platform}-approved.json`
這是**獨立授權面**。

- pairing approved 使用者在 gateway authz 層會被視為已授權
- 即使 `external_identities` row 被刪掉，只要 approved 還在，仍可能可以對話

### 3. Adapter intake policy
平台 adapter 在訊息剛進來時，可能先做第一層 gate：

- Telegram：`_is_user_authorized_from_message()`
- LINE：`_allowed_for_source(...)` 搭配 DM / group / room 分流

### 4. Gateway authz
在 adapter 之後，gateway 還會再做 `_is_user_authorized(...)`。

這一層會綜合：

- pairing approved
- env allowlists
- adapter policy
- allow-all flags
- 群組/論壇例外規則

## 真正的生命週期

### A. 新使用者從 Telegram / LINE DM 進來

1. adapter 收到 inbound message
2. adapter intake policy 決定：
   - 是否立刻丟棄
   - 是否把 DM 交給 gateway authz / verification
3. gateway authz 檢查：
   - pairing approved？
   - allowlist？
   - verified binding？
4. 若尚未驗證，但平台策略允許 onboarding：
   - 進入 company email 驗證流程
   - 建立 `verification_requests`
5. 使用者點 email 驗證連結後：
   - 寫入 `external_identities`
   - 更新 `identity_enforcement.state = verified`

### B. 已驗證使用者正常對話

1. `is_verified_source(source)` 應同時成立：
   - `identity_enforcement.state == verified`
   - `external_identities` 仍存在對應 row
2. 若系統要把結果改寄 email：
   - 必須使用 `bound_email_matches_source(...)`
   - 嚴格比對 live 綁定中的 verified email

### C. Admin 後台 force bind

1. 建立 / 更新 `principals`
2. 建立 / 更新 `external_identities`
3. 將 `identity_enforcement` 設為 `verified`
4. 之後 email handoff / verified-source lookup 才能正確工作

### D. Admin 後台 unbind（最容易出 bug）

解除綁定不是只做一件事，正確流程應是：

1. 刪除 `external_identities(platform, external_user_id)`
2. 把 `identity_enforcement` 重設回 `new`
3. **同步撤銷 pairing approval**
4. 寫 audit log，最好記下 `pairing_revoked=true/false`
5. 必要時再檢查 home-channel / control-channel 是否仍引用該 chat

> 2026-07-28 修正：`GatewayUserStore.unbind_identity()` 已同步呼叫 `PairingStore().revoke(platform, external_user_id)`。
>
> 這是為了修正「刪綁定後仍能聊天」的通案問題，因為單刪 DB row 不足以撤銷所有授權面。

## 平台差異：Telegram vs LINE

### Telegram

重點：

- `plugins/platforms/telegram/adapter.py::_is_user_authorized_from_message`
- 這是 **intake prefilter**，在 text batching 與 event construction 之前執行
- 原則：
  - 有明確 allowlist 時，可早期拒絕
  - **未知 DM 且沒有 allowlist 時，不能過早 default-deny**
  - 否則新使用者無法進入 pairing / 驗證流程

### LINE

重點：

- `plugins/platforms/line/adapter.py` 的 allowlist gate 與 verified-email handoff 路徑
- 原則：
  - **DM user source 要能進 gateway authz / verification 層**
  - group / room source 仍可在 adapter 層直接擋掉
  - 若太早在 adapter 層拒絕 DM，使用者會看到「完全沒反應」，尤其是 restart 後最容易誤判成壞掉
- LINE 還有平台特有問題：
  - reply token 60 秒上下壽命
  - push quota / 429（月額度）
  - 改寄 email 時仍需驗證 live 綁定 email

## 已知高風險 bug 類型

### 1. Unbind 只刪 DB，沒 revoke pairing
症狀：
- 後台顯示已解綁
- runtime DB 查不到 binding
- 但使用者仍可直接對話

根因：
- pairing approved 是獨立授權面

修法：
- `GatewayUserStore.unbind_identity()` 必須同步 revoke pairing

### 2. Telegram prefilter 太早 default-deny
症狀：
- 新使用者 DM 根本進不了 onboarding
- 看起來像 bot 靜音

根因：
- 在沒有 allowlist 的情況下，把未知 DM 直接擋掉

修法：
- 無 allowlist 時要讓未知 DM 進到正常 pairing / verification flow

### 3. LINE adapter 太早拒絕 DM
症狀：
- restart 後或新使用者第一次來訊時，LINE 看起來完全沒反應

根因：
- adapter 在 DM 層就把 unauthorized source 擋掉，沒讓 gateway authz 判斷

修法：
- user-type source 應 defer 到 gateway authz；group/room 才維持 adapter-gated

### 4. Email handoff 用到舊綁定或錯綁 email
症狀：
- LINE/Telegram 結果被寄到不該寄的信箱
- 或因綁定已變更而 handoff 失敗

根因：
- 沒用 `bound_email_matches_source(...)` 重新比對 live verified email

修法：
- 所有 system-initiated email handoff，在送出前都要做 live 比對

### 5. 只測 admin route，不測 user-visible 行為
症狀：
- API 200 OK，但使用者實際還是能聊 / 或根本進不了驗證

根因：
- 只驗 route contract，沒驗 pairing store / authz / adapter gating

修法：
- 同時驗 DB、pairing store、adapter、gateway authz 與對話行為

## 修改程式時必看檔案

### 核心驗證 / 綁定 / 解綁
- `gateway/user_verification.py`
  - `is_verified_source`
  - `get_verified_email_for_source`
  - `bound_email_matches_source`
  - `process_inbound_message`
  - `force_bind_identity`
  - `unbind_identity`

### Gateway authz
- `gateway/authz_mixin.py`
  - pairing approved 與 allowlist / adapter policy 的聯集規則

### Telegram intake prefilter
- `plugins/platforms/telegram/adapter.py`
  - `_is_user_authorized_from_message`

### LINE intake / fallback / verified-email handoff
- `plugins/platforms/line/adapter.py`
  - allowlist gate（DM defer vs group/room reject）
  - `_verified_email_for_chat`
  - `_verified_email_target_if_bound`
  - email fallback / quota fallback 路徑

### Admin route
- `hermes_cli/web_server.py`
  - `/api/admin/verification/unbind`
  - `/api/admin/verification/force-bind`
  - `/api/pairing/revoke`

## 修改守則（請當成 checklist）

1. **不要只改一層。**
   - 驗證問題通常同時跨 adapter、authz、runtime DB、pairing store。
2. **解除綁定一定要想 pairing。**
   - 問自己：`external_identities` 沒了之後，還有哪個面會放行？
3. **處理 email handoff 一定要比對 live verified email。**
   - 不可依賴 session 中舊 email。
4. **Telegram / LINE 新使用者 DM 要能進 onboarding。**
   - 不可因為強化安全而把 onboarding 一起封死。
5. **group / room / forum 的規則要和 DM 分開想。**
   - 很多 regressions 來自把 DM 修法錯套到群組面。
6. **若改到授權規則，請同步補回歸測試。**
   - 尤其是 unbind、pairing、verified email handoff。
7. **如果 user-visible 行為依賴 restart 才生效，要在變更說明寫清楚。**

## 建議最少測試集

> 詳細測試地圖、對應檔案職責與建議執行順序，請直接看：
>
> - `references/line-telegram-test-map.md`
> - `references/line-telegram-must-run-commands.md`

### 已存在且這次相關的測試
- `tests/hermes_cli/test_web_verification_admin.py`
- `tests/gateway/test_pairing.py`
- `tests/gateway/test_pairing_allowlist_bypass.py`
- `tests/gateway/test_line_plugin.py`
- `tests/gateway/test_telegram_auth_check.py`
- `tests/gateway/test_verified_email_handoff_guard_helpers.py`
- `tests/gateway/test_line_telegram_verification_smoke.py`

### 變更 unbind / pairing 時至少要驗
1. force-bind 成功
2. unbind 後：
   - `external_identities` row 消失
   - `identity_enforcement.state == new`
   - pairing approved 也消失
3. audit log 有記下是否撤銷 pairing

### 變更 LINE / Telegram intake 時至少要驗
1. 新 DM 使用者是否還能進 onboarding
2. group / room / forum 是否沒有被意外放開
3. 沒有 allowlist 時，是否仍維持預期安全邊界

### 變更 email handoff 時至少要驗
1. live verified email match 才能送
2. 綁定改掉後舊 email 不應再被使用
3. LINE 429 / quota fallback 時仍不能繞過 live 綁定檢查

## 給未來維護者的一句話

如果你正在改 LINE / Telegram 驗證流程，請把它當成 **多層授權狀態機**，不要當成「單一路由 + 一張表」的 CRUD 問題。

這套東西最怕的是：
- 看起來修好了
- 後台也 200 OK
- 但另一個授權面還在放行

那種 bug 會反覆回來嚇人，而且每次都像新 bug，其實只是同一顆地雷換角度爆炸。