Files
bear-cli/docs/commands.md
T

631 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Bear CLI 指令規劃(Command Spec)
> 狀態:P2 PAT 模式 MVP 已實作(見第 9 節);Device Flow 待伺服器端 P1。
> 範圍:本文件為指令介面規格;App 管理指令(`bear apps` CRUD)另見 #5 規劃、個人自助指令(`bear profile` 等)另見 #11 規劃。
> 登入採用的 OIDC Device Authorization Grant(RFC 8628)細節,見 bear 倉庫 `docs/cli-feature-plan.md`(issue #17)。
> 另支援以**個人存取權杖(PAT)**登入(bear 已於 profile 頁提供 PAT 建立/列表/撤銷,`/userinfo` 接受 PAT 作為 Bearer)。
---
## 1. 命名與呼叫慣例
- 可執行檔名:`bear`(Linux 終端機呼叫 `bear <command>`)。
- 指令採「動詞在前」的扁平結構,不設多層子指令(首版維持簡單,後續需要再擴充)。
- 全部指令支援 `--json` 輸出機器可讀結果;人類可讀輸出為預設。
- 輸出語言跟隨系統 Locale,優先繁體中文(zh_TW),英文(en)為後備。
---
## 2. 全域選項(Global Options)
| 選項 | 型別 | 預設 | 說明 |
|------|------|------|------|
| `--issuer URL` | string | `https://alterminal.com` | Bear(OIDC Provider)位址;可被設定檔覆寫 |
| `--config PATH` | string | `~/.config/bear/config.json` | 設定檔路徑 |
| `--json` | bool | `false` | 機器可讀輸出(JSON) |
| `-v, --verbose` | bool | `false` | 顯示詳細日誌(含 HTTP 請求摘要,但不含 token) |
| `-h, --help` | bool | — | 顯示說明 |
| `--version` | bool | — | 顯示版本後離開 |
優先序:CLI flag > 環境變數(`BEAR_ISSUER`、`BEAR_TOKEN`)> 設定檔 > 內建預設。
> `BEAR_TOKEN` 為一次性/CI 用的個人存取權杖(PAT);提供後優先於本機憑證檔,但不寫入憑證檔。
---
## 3. 指令詳述
### 3.1 `bear login`
登入 Bear,取得並保存憑證。預設走 **Device Flow**;亦可用 `--token` 直接以**個人存取權杖(PAT)**登入。
```
bear login [--scope "openid profile email"] [--client-id ID]
bear login --token <PAT>
```
| 選項 | 預設 | 說明 |
|------|------|------|
| `--scope` | `openid profile email` | Device Flow 請求的 scope(空白分隔) |
| `--client-id` | 設定檔內值 | OIDC client id |
| `--token <PAT>` | — | 直接以個人存取權杖登入(跳過 Device Flow) |
**行為(Device Flow,未給 `--token`)**:
1. 讀取設定 → 向 `{issuer}/.well-known/openid-configuration` 取 `device_authorization_endpoint`(**動態發現,不寫死網址**)。
2. `POST /device_authorization`(`client_id` + `scope`)。
3. 終端印出:
```
請在瀏覽器開啟:https://alterminal.com/device
輸入代碼:ABCD-EFGH
```
若終端支援,同步印出 QR(掃碼直達 `verification_uri_complete`)。
4. 以 `interval` 週期輪詢 `POST /token`(`grant_type=device_code`):
- `authorization_pending` → 繼續等待;
- `slow_down` → 加大輪詢間隔;
- `access_denied` → 中止並提示;
- 成功 → 寫入憑證檔(0600),印出 `已登入:<email>`。
**行為(PAT,給 `--token <PAT>`)**:
1. 以該 PAT 作為 Bearer 呼叫 `GET /userinfo` 驗證可用性並取得身分。
2. 成功 → 將 PAT 直接寫入憑證檔(0600),印出 `已登入:<email>`(無 refresh token)。
3. 失敗(401/無效或已撤銷)→ 提示 PAT 無效,不寫入任何資料。
**退出碼**:`0` 成功;`3` PAT 無效或已撤銷;`4` 等待逾時(`expires_in`);`5` 使用者拒絕;`6` 網路/伺服器錯誤。
**`--json` 輸出範例(成功)**:
```json
{"ok": true, "email": "alice@example.com", "expires_in": 3600}
```
PAT 模式下 `expires_in` 依權杖到期日回傳;無到期日則省略該欄位。
---
### 3.2 `bear whoami`
顯示目前登入身分(`GET /userinfo`,Bearer access token)。
```
bear whoami
```
**行為**:讀取憑證——Device Flow 讀取快取的 access token(過期則先用 refresh token 輪轉換新);PAT 直接使用該 token——呼叫 `/userinfo`,印出 claims(name、email、picture 等)。`/userinfo` 同時接受 OIDC access token 與 PAT 作為 Bearer。
**退出碼**:`0` 成功;`3` 未登入。
**輸出範例**:
```
sub : 0190...uuid
name : Alice Chen
email : alice@example.com
picture : https://...
```
---
### 3.3 `bear token [--refresh]`
印出 access token 到 stdout,供 pipe 給其他工具(如 `curl`、腳本)。
```
bear token [--refresh]
```
| 選項 | 說明 |
|------|------|
| `--refresh` | 強制先以 refresh token 輪轉換新 access token |
**行為**:
- 若無 `--refresh` 且快取的 access token 未過期 → 直接印出。
- 若過期(或指定 `--refresh`)→ 以 refresh token 向 `/token` 換新,更新快取後印出。
- **PAT 模式**:直接印出該 PAT(無 refresh token;`--refresh` 為無效選項,報用法錯誤 `2`)。
- 輸出**只含 token 本體**(單行、無前綴),方便 pipe;診斷訊息一律走 stderr。
**退出碼**:`0` 成功;`3` 未登入;`1` refresh 失敗(token 已撤銷或過期,需重新 login)。
---
### 3.4 `bear logout`
撤銷 refresh token 並清除本機憑證。
```
bear logout
```
**行為**:
- Device Flow:呼叫 `POST /revoke`(撤銷 refresh token),無論伺服器回應如何,皆刪除本機憑證檔。
- PAT:僅刪除本機憑證檔(PAT 的撤銷需於網頁 `/profile/tokens` 進行,CLI 不提供撤銷)。
**退出碼**:`0` 成功;`6` 伺服器呼叫失敗(但本機憑證已清除)。
---
### 3.5 `bear status`
顯示登入狀態與 token 剩餘效期(不呼叫網路,純本機判定)。
```
bear status
```
**行為**:讀取本機憑證,印出登入與否、憑證類型(Device Flow/PAT)、access token 剩餘秒數、refresh token 是否存在。
**退出碼**:`0` 已登入;`3` 未登入。
**輸出範例(Device Flow)**:
```
已登入:alice@example.com
憑證類型:Device Flow
access token 剩餘:3210 秒
refresh token:有
issuer:https://alterminal.com
```
**輸出範例(PAT,無到期日)**:
```
已登入:alice@example.com
憑證類型:Personal Access Token
access token 剩餘:無到期
refresh token:無
issuer:https://alterminal.com
```
---
### 3.6 `bear apps`:App 管理指令群(P3,規劃中)
> 依賴:bear 伺服器端 **App 管理 JSON API**(alterminal/bear#28,資源前綴 `/api/v1/apps`,PAT Bearer 認證、管理操作限 admin)。API 落地前本節僅為規格。
`bear apps` 是「資源群組」指令:`bear apps <動詞>`。這是第 1 節「扁平結構」原則的首個例外——App 的管理操作(列表、檢視、建立、更新、輪轉 secret、啟停用)若攤平命名會造成指令名爆炸(`bear apps-create`…),故收斂為單一群組;未來同類資源管理指令比照。
```
bear apps list [選項]
bear apps show <client-id>
bear apps create [選項]
bear apps update <client-id> [選項]
bear apps rotate-secret <client-id>
bear apps toggle <client-id>
```
**認證與授權(群組共通)**:
- 一律使用既有 PAT 憑證(`bear login --token` 登入,或 `BEAR_TOKEN` 環境變數)作為 Bearer 呼叫 API;不提供其他登入方式。
- 管理操作僅限 admin 角色帳號。`401`(token 無效/已撤銷)→ 退出碼 `3`,提示重新 `bear login`;`403`(非 admin)→ 退出碼 `8`,明確提示「此操作需 admin 權限」,不重試。
- **`client_secret` 只在 `create`/`rotate-secret` 成功當下一次性顯示**於 stdout,不寫入憑證檔、log、`--verbose` 輸出或版本庫;`list`/`show`/`update`/`toggle` 的回應永遠不含 secret(API 端保證)。
- App 識別一律以 `client_id`(人類可讀、唯一),不暴露資料庫 UUID。若 API 路徑參數僅接受 UUID,由 CLI 先以 list 比對解析(見第 7 節開放問題 6)。
**群組共通退出碼**:
| 碼 | 語意 |
|----|------|
| `0` | 成功 |
| `1` | 目標不存在或驗證失敗(HTTP 404/422;含欄位格式錯誤、`client_id` 重複) |
| `2` | 用法錯誤(缺必選參數、列舉值非法、PKCE 未綁 `--jwk-id` 等,由 CLI 本地檢查) |
| `3` | 未登入或憑證無效(HTTP 401) |
| `6` | 網路/伺服器錯誤 |
| `8` | 權限不足(HTTP 403,管理操作僅限 admin) |
#### 3.6.1 `bear apps list`
列出 App 清單。
```
bear apps list [--visibility public|internal] [--status active|inactive]
[--page N] [--per-page N] [--json]
```
| 選項 | 預設 | 說明 |
|------|------|------|
| `--visibility` | 全部 | 過濾 `public` 或 `internal` |
| `--status` | 全部 | 過濾 `active` 或 `inactive` |
| `--page` | `1` | 分頁頁碼(對應 API `page`) |
| `--per-page` | `20` | 每頁筆數(對應 API `per_page`,上限同伺服器) |
**行為**:`GET /api/v1/apps`(Bearer PAT),套用過濾與分頁;人類可讀輸出為表格,依 `client_id` 排序。
```
CLIENT ID TITLE VISIBILITY STATUS METHOD SCOPES
my-app My App public active client_secret openid, profile
internal-tool Internal Tool internal inactive PKCE openid
```
**`--json` 輸出範例**:
```json
{"ok": true, "page": 1, "per_page": 20, "apps": [{"client_id": "my-app", "title": "My App", "url": "https://example.com", "method": "client_secret", "status": "active", "visibility": "public", "scopes": ["openid", "profile"], "redirect_urls": ["https://example.com/callback"], "post_logout_redirect_uris": [], "sub": "id", "jwk_id": null, "created_at": "2026-09-07T00:00:00Z", "updated_at": "2026-09-07T00:00:00Z"}]}
```
#### 3.6.2 `bear apps show <client-id>`
顯示單一 App 完整資訊。
**行為**:`GET /api/v1/apps/{id}`(以 client-id 解析);印出全部欄位(同 create 選項集合,另含 `status`、`created_at`/`updated_at`、`jwk_id`);`client_secret` 永不回傳。
**輸出範例**:
```
client_id : my-app
title : My App
url : https://example.com
method : client_secret
visibility : public
status : active
scopes : openid, profile
redirect_urls : https://example.com/callback
post_logout_redirect_uris : (無)
sub : id
jwk_id : (無)
created_at : 2026-09-07T00:00:00Z
updated_at : 2026-09-07T00:00:00Z
```
**`--json` 輸出範例**:`{"ok": true, "app": {…同 list 之單一物件…}}`
#### 3.6.3 `bear apps create`
建立 App。`method=client_secret` 且未給 `--secret` 時,由伺服器自動產生並**一次性**回傳明文。
```
bear apps create --client-id ID --url URL --title TITLE
[--method client_secret|PKCE] [--visibility public|internal]
[--redirect-url URL]… [--post-logout-redirect-uri URI]…
[--scope SCOPE]… [--sub FIELD] [--jwk-id ID] [--secret SECRET] [--json]
```
| 選項 | 預設 | 說明 |
|------|------|------|
| `--client-id` | (必填) | 唯一識別碼;英數字與連字號/底線 |
| `--url` | (必填) | 絕對 URI(http/https 或自訂 scheme 如 `myapp://`) |
| `--title` | (必填) | 顯示名稱 |
| `--method` | `client_secret` | `client_secret` 或 `PKCE`;`PKCE` 必須給 `--jwk-id` |
| `--visibility` | `internal` | `public` 或 `internal` |
| `--redirect-url` | `[]` | 可重複給予,附加為允許的 redirect URL 清單 |
| `--post-logout-redirect-uri` | `[]` | 可重複給予,登出後跳轉 URI 清單 |
| `--scope` | `["openid"]` | 可重複給予,允許的 scope 清單 |
| `--sub` | `id` | 作為 `sub` claim 的欄位名 |
| `--jwk-id` | — | 綁定的 JWK id(`method=PKCE` 時必填) |
| `--secret` | 自動產生 | 自備 client secret;省略則由伺服器產生並一次性回傳 |
**行為**:`POST /api/v1/apps`(Bearer PAT)。成功後印出 App 摘要與一次性 secret 區塊:
```
App 已建立:my-app(public/active)
client_secret(只顯示這一次,請立即保存):
4f9c1d2e-…
```
**`--json` 輸出範例**:`{"ok": true, "app": {…}, "client_secret": "4f9c1d2e-…"}`(secret 同樣只出現這一次。)
#### 3.6.4 `bear apps update <client-id>`
更新既有 App。**只更新有給的欄位**(部分更新);未給的欄位保持不變。`client_id` 不可變更(伺服器 update 不允許);`status` 不在此改(用 `toggle`)。
```
bear apps update <client-id> [--url URL] [--title TITLE]
[--method client_secret|PKCE] [--visibility public|internal]
[--redirect-url URL]… [--post-logout-redirect-uri URI]…
[--scope SCOPE]… [--sub FIELD] [--jwk-id ID] [--json]
```
**行為**:`PUT /api/v1/apps/{id}`(Bearer PAT),只送有給的欄位。清單類欄位(`--redirect-url`、`--post-logout-redirect-uri`、`--scope`)為**整組覆寫**語意:有給即取代整份清單。成功後印出更新後的 App 摘要(同 `show` 的精簡版)。422 驗證失敗時逐欄位列出錯誤。
#### 3.6.5 `bear apps rotate-secret <client-id>`
輪轉 client secret。舊 secret 立即失效。
**行為**:`POST /api/v1/apps/{id}/rotate-secret`(Bearer PAT)。成功後一次性顯示新 secret(提示同 create)。`method=PKCE` 的 App 不使用 client secret → 退出碼 `1`,提示該 App 無 secret 可輪轉。
**`--json` 輸出範例**:`{"ok": true, "client_secret": "9a7b3c…"}`(secret 只出現這一次)
#### 3.6.6 `bear apps toggle <client-id>`
切換 App 狀態:`active ↔ inactive`。
**行為**:`POST /api/v1/apps/{id}/toggle`(Bearer PAT)。成功後印出新狀態,如 `my-app:active → inactive`。
**`--json` 輸出範例**:`{"ok": true, "app": {"client_id": "my-app", "status": "inactive"}}`
---
### 3.7 `bear profile`:個人自助指令群(P3,規劃中)
> 依賴:bear 伺服器端**個人自助 JSON API**(alterminal/bear#35,資源前綴 `/api/v1/profile`,PAT Bearer 認證、**任何有效 PAT 帳號**可用,不限 admin)。API 落地前本節僅為規格;端點細節以 #35 定案為準。
比照 `bear apps` 的「資源群組」例外:個人自助操作若攤平命名同樣會造成指令名爆炸(`bear password-change`…),收斂為 `bear <資源> <動詞>` 的六個子群組。頭像(picture)檔案上傳需要 multipart 與本機圖片,終端體驗差,**不在本期範圍**(#35 有對應端點,未來有需要再評估);如需更換頭像,可自行上傳圖片後以 `profile set --picture URL` 設定。
```
bear profile show [--json]
bear profile set [選項] [--json]
bear password change
bear email change --new EMAIL [--json]
bear sessions list|revoke <id>|revoke-others [--json]
bear tokens list|create|revoke <id> [選項] [--json]
bear mfa status|setup|disable|recovery-codes [--json]
```
**認證與授權(群組共通)**:
- 一律使用既有 PAT 憑證(`bear login --token` 登入,或 `BEAR_TOKEN` 環境變數)作為 Bearer 呼叫 API;不提供其他登入方式。
- 與 `apps` 群組不同,個人自助操作**任何有效 PAT 帳號皆可**(非 admin 限定)。`401`(token 無效/已撤銷)→ 退出碼 `3`,提示重新 `bear login`;`403` → 退出碼 `8`(正常情況不應出現,保留對應);`404`/`422` → 退出碼 `1`;網路/伺服器錯誤 → `6`。退出碼沿用 §4 總表,群組共通部分同 §3.6 表。
- **敏感輸入一律互動提示讀取且不回顯**:目前密碼、新密碼、email 驗證碼、MFA 驗證碼。不接受命令列明文參數(不進 shell history、不出現在 `ps`)。非 TTY 環境執行需要互動輸入的指令 → 退出碼 `2`,提示需在終端機執行。
- **一次性明文**(`tokens create` 的 PAT、`mfa setup`/`recovery-codes` 產生的 recovery codes)只於成功當下輸出於 stdout,不寫入憑證檔、log、`--verbose` 輸出或版本庫(同 `apps create` 的 `client_secret` 處理)。
- 密碼與 email 變更不因 CLI 放寬:沿用伺服器端現行密碼驗證與 email 雙向驗證碼流程,CLI 只是通道。
#### 3.7.1 `bear profile show`
顯示完整個人資料。
**行為**:`GET /api/v1/profile`(Bearer PAT),印出全部 OIDC profile 欄位(name、given_name、family_name、nickname、preferred_username、picture、profile、website、gender、birthdate、zoneinfo、locale、phone_number、address_*、email、email_verified、totp_enabled 等),格式同 `whoami` 的 `key : value`。
**輸出範例**:
```
sub : 0190...uuid
email : alice@example.com
email_verified: true
name : Alice Chen
given_name : Alice
family_name : Chen
nickname : ali
locale : zh_TW
zoneinfo : Asia/Taipei
mfa_enabled : true
```
**`--json` 輸出範例**:`{"ok": true, "profile": {…同上欄位…}}`
#### 3.7.2 `bear profile set [選項]`
更新個人資料。**只更新有給的欄位**(部分更新,同 `apps update` 語意);`email` 不可由此改(走 `bear email change`),`sub`、`email_verified`、`totp_enabled` 唯讀。
```
bear profile set [--name NAME] [--given-name NAME] [--family-name NAME]
[--nickname NAME] [--preferred-username NAME]
[--profile URL] [--picture URL] [--website URL]
[--gender male|female|other] [--birthdate YYYY-MM-DD]
[--zoneinfo TZ] [--locale LOC] [--phone-number E164]
[--phone-verified] [--address-formatted TEXT]
[--address-street-address TEXT] [--address-locality TEXT]
[--address-region TEXT] [--address-postal-code TEXT]
[--address-country TEXT] [--json]
```
| 選項 | 說明 |
|------|------|
| `--name` 等 20 個欄位選項 | 對應網頁 `/profile/edit` 表單的同一欄位集合(`profile_changeset` 的 `profile_fields`) |
| `--gender` | 列舉 `male`/`female`/`other`(非法值 → 退出碼 `2`) |
| `--birthdate` | `YYYY-MM-DD` |
| `--phone-verified` | 布林旗標(設定 `phone_number_verified`;清除不在本期,避免自我標記已驗證的濫用——**待 #35 確認是否開放**) |
| URL 類(`--profile`/`--picture`/`--website`) | 需 `http(s)://` 開頭,否則 `422` → 退出碼 `1` |
**行為**:`PUT /api/v1/profile`(Bearer PAT),只送有給的欄位。成功後印出更新後摘要;`422` 驗證失敗時逐欄位列出錯誤(退出碼 `1`)。`preferred_username` 與他人重複 → `422`,退出碼 `1`。
**`--json` 輸出範例**:`{"ok": true, "profile": {…更新後完整物件…}}`
#### 3.7.3 `bear password change`
變更密碼。**無任何命令列參數**(密碼一律互動輸入)。
**行為**:
1. 互動提示(不回顯)依序讀取:`目前密碼`、`新密碼`、`確認新密碼`。
2. 兩次新密碼不一致 → 退出碼 `2`(本地檢查),不發請求。
3. `POST /api/v1/profile/password`(Bearer PAT,body 帶 `current_password`/`new_password`)。
4. 成功 → 印出 `密碼已變更`;現行密碼錯誤 → 退出碼 `1`;新密碼不符伺服器強度規則 → `422`,退出碼 `1`。
比照網頁流程,變更密碼**不**自動撤銷其他 session;需要時另用 `bear sessions revoke-others`。
#### 3.7.4 `bear email change --new EMAIL`
變更 email(雙向驗證碼流程)。驗證碼一律互動輸入,不接受命令列參數。
| 選項 | 說明 |
|------|------|
| `--new EMAIL` | (必填)新 email 位址 |
**行為**:
1. `POST /api/v1/profile/email/request-codes`(Bearer PAT,`new_email`)→ 伺服器向**現有與新**信箱各寄一組驗證碼。
2. 終端印出 `驗證碼已寄至 alice@example.com 與 new@example.com`,互動提示(不回顯)讀取:`現有信箱驗證碼`、`新信箱驗證碼`。
3. `POST /api/v1/profile/email`(`current_code`/`new_code`)。
4. 成功 → 印出 `email 已更新:new@example.com`,並提醒 `bear login` 用的 PAT 不受影響(PAT 不綁 email)。
錯誤:`same_email`/`email_taken`/`invalid_email` → 退出碼 `1`;驗證碼錯誤或逾期 → 退出碼 `1`,提示重新執行(會重寄驗證碼);伺服器限流(`429`)→ 退出碼 `1`,提示稍後再試。
**`--json` 輸出範例(成功)**:`{"ok": true, "email": "new@example.com"}`
#### 3.7.5 `bear sessions`
管理**網頁 session**(裝置登入狀態);與 PAT 無關——撤銷 session 不影響 PAT,PAT 的撤銷用 `bear tokens revoke`。CLI 以 PAT 呼叫 API,本身沒有「目前 session」,清單不標記目前項。
- **`bear sessions list`**:`GET /api/v1/profile/sessions`(Bearer PAT),表格輸出,新到舊排序:
```
ID IP USER AGENT CREATED LAST SEEN EXPIRES
0192a1b0-… 203.0.113.10 Mozilla/5.0 (X11; Linux…) 2026-09-07 10:00:00Z 2026-09-08 09:00:00Z 2026-09-21 10:00:00Z
```
(`user_agent` 截斷至欄寬;`--json` 輸出完整值。)
- **`bear sessions revoke <id>`**:`POST /api/v1/profile/sessions/{id}/revoke`。成功印出 `session 已撤銷`;不存在或非本人 → `404`,退出碼 `1`。
- **`bear sessions revoke-others`**:`POST /api/v1/profile/sessions/revoke-others`。成功印出 `已撤銷 N 個其他 session`。
`<id>` 為 session UUID(取自 `list` 輸出;不接受前綴比對)。
**`--json` 輸出範例**:`{"ok": true, "sessions": […]}`/`{"ok": true, "revoked": 3}`
#### 3.7.6 `bear tokens`
管理 PAT。**token 明文絕不出現在 `list`**(伺服器只存 SHA-256 雜湊)。
- **`bear tokens list`**:`GET /api/v1/profile/tokens`(Bearer PAT),新到舊排序:
```
ID NAME SCOPE CREATED EXPIRES LAST USED STATUS
0192a1c0-… ci-token openid, profile 2026-09-01 00:00:00Z 2026-10-01 00:00:00Z 2026-09-08 08:00:00Z active
0192a1b0-… old-laptop openid, profile 2026-08-01 00:00:00Z (無到期) (未使用) revoked
```
`STATUS`:`active`/`expired`/`revoked`(CLI 依 `expires_at`/`revoked_at` 本地判定)。
- **`bear tokens create --name NAME [--expires-in DAYS|never] [--scope SCOPES]`**:`POST /api/v1/profile/tokens`(Bearer PAT)。成功後一次性顯示:
```
PAT 已建立:ci-token(30 天後到期)
token(只顯示這一次,請立即保存):
9f3a…(Base64url,43 字元,無前綴)
```
| 選項 | 預設 | 說明 |
|------|------|------|
| `--name` | (必填) | 顯示名稱;空白或重複名稱依伺服器規則 `422` → 退出碼 `1` |
| `--expires-in` | `30` | 天數(正整數)或 `never`(永不過期) |
| `--scope` | `openid profile email` | 空白分隔的 scope 清單(同伺服器預設) |
`--json` 輸出範例:`{"ok": true, "token": {…list 之單一物件…}, "raw": "9f3a…"}`(`raw` 只出現這一次。)
提醒:新 PAT 要使用須另以 `bear login --token` 登入(或設 `BEAR_TOKEN`)。
- **`bear tokens revoke <id>`**:`POST /api/v1/profile/tokens/{id}/revoke`(`<id>` 為 UUID)。成功印出 `PAT 已撤銷:<name>`;若撤銷的是**目前憑證用的 PAT**,後續指令會得到 `401`(退出碼 `3`),屆時重新 `bear login`;CLI 在撤銷成功後偵測到 `id` 對應本機憑證時,主動提示執行 `bear logout` 清除本機憑證。
#### 3.7.7 `bear mfa`
TOTP 兩因子管理(WebAuthn/passkey 不在範圍)。
- **`bear mfa status`**:讀 `GET /api/v1/profile` 的 MFA 欄位,印出 `MFA(TOTP):已啟用/未啟用`。
- **`bear mfa setup`**:兩步互動流程——
1. `POST /api/v1/profile/mfa/setup`(無 code,Bearer PAT)→ 回傳 Base32 `secret` 與 `otpauth://` URI。終端印出 URI、secret,並在本機產生 ASCII QR(掃碼加入驗證器;QR 由 CLI 本地生成,不經伺服器)。
2. 互動提示(不回顯)讀取 6 位碼 → `POST /api/v1/profile/mfa/setup`(帶 code)確認。
3. 成功 → **一次性**顯示 recovery codes(提示同 PAT:只顯示這一次)。
已啟用 MFA 再執行 → 退出碼 `1`,提示已啟用。驗證碼錯誤 → 退出碼 `1`,可重試(不重產 secret)。
- **`bear mfa disable`**:互動提示(不回顯)讀取目前的 TOTP 碼或 recovery code → `POST /api/v1/profile/mfa/disable`。成功印出 `MFA 已停用`;碼錯誤 → 退出碼 `1`,未停用。
- **`bear mfa recovery-codes`**:互動提示(不回顯)讀取目前的 TOTP 碼或 recovery code → `POST /api/v1/profile/mfa/recovery-codes`。成功後**一次性**顯示整組新的 recovery codes(舊組全部失效)。
**`--json` 輸出範例**:`{"ok": true, "totp_enabled": true}`;setup 確認成功:`{"ok": true, "recovery_codes": ["…", "…"]}`(只出現這一次)。
---
## 4. 退出碼總表
| 碼 | 語意 |
|----|------|
| `0` | 成功 |
| `1` | 一般錯誤(含 refresh 失敗、憑證檔讀寫失敗) |
| `2` | 用法錯誤(未知指令/參數) |
| `3` | 未登入(需先 `bear login`) |
| `4` | 授權等待逾時(authorization_pending 超過 `expires_in`) |
| `5` | 授權被拒絕(access_denied) |
| `6` | 網路/伺服器錯誤 |
| `7` | 設定檔/憑證檔格式損毀 |
| `8` | 權限不足(HTTP 403;App 管理操作僅限 admin) |
---
## 5. 設定檔與憑證檔
| 檔 | 路徑 | 內容 | 權限 |
|----|------|------|------|
| 設定 | `~/.config/bear/config.json` | `issuer`、`client_id`、`scope` | 0644 |
| 憑證 | `~/.local/state/bear/credentials.json` | Device Flow:`refresh_token`+快取 `access_token`/`expires_at`;PAT:`access_token`(無 refresh) | **0600** |
- **Device Flow 只以 refresh token 為主**:access token 短命快取,過期以 refresh token 輪轉換新(沿用 Bear 既有 refresh rotation + 重用偵測)。
- **PAT 直接存 access token**:無 refresh token、無輪轉;效期由權杖本身的 `expires_at` 決定(可無到期日)。
- 寫入憑證檔務必 0600,且以 `O_CREAT|O_EXCL` 防止 symlink 攻擊。
- 未來可擴充系統 keyring(libsecret / Keychain),避免明文檔。
---
## 6. 錯誤處理與安全
- `authorization_pending`、`slow_down`、`access_denied`、`invalid_grant` 依 RFC 8628 §3.5 顯示對應中文提示。
- 所有診斷訊息走 stderr;`--json` 模式將錯誤以 `{"ok": false, "error": "...", "code": N}` 輸出。
- token、user_code、device_code 一律不入 log、不入 stdout(除 `bear token` 之目的外)、不入版本庫。
- `client_secret`(App 管理)僅於 `apps create`/`apps rotate-secret` 成功當下一次性輸出;`403` 顯示「此操作需 admin 權限」並以退出碼 `8` 結束,不重試。
- 個人自助的敏感輸入(密碼、email/MFA 驗證碼)一律互動提示、不回顧、不接受命令列參數;一次性明文(`tokens create` 的 PAT、`mfa` 的 recovery codes)僅於成功當下輸出(見 §3.7)。
- 網路層一律使用 `Req`,遵循 bear 倉庫 HTTP 客戶端慣例。
---
## 7. 開放問題(待確認)
1. CLI 的 OIDC client 採新 `method: "device"`(client auth 為 `none`)或沿用 `PKCE`+JWK?傾向前者(見 bear `cli-feature-plan.md` §5.5)。
2. 發行格式:escript(目標機需 Erlang)或 self-contained release(bakeware/burrito)?
3. `bear apps list --visibility public` 未來是否開放非 admin(僅公開/active 清單,同網頁 Launch)?目前規劃為 admin 限定。
4. 是否支援 `offline_access` scope,或沿用現行 refresh 輪轉即可?
5. 是否在 CLI 提供「建立/撤銷 PAT」?原規劃傾向 CLI 只「使用」不「管理」;#35 的 PAT 端點落地後,`bear tokens` 已納入 §3.7.6 規格(管理自己帳號的 PAT),此題視為已解。
6. App 管理 API 的路徑參數最終採 UUID 或 `client_id`(alterminal/bear#28 規格為 `{id}`)?若為 UUID,CLI 需先以 list 解析 `client_id` → UUID,或伺服器提供以 `client_id` 直取的端點。
7. `bear profile set --phone-verified`(自我標記電話已驗證)是否開放?#35 需決定 API 是否接受該欄位;不開放則 CLI 移除此選項。
8. `mfa setup` 的 `secret`/`otpauth://` 屬敏感資料,#35 的 API 回應是否應限制僅 setup 流程期間回傳(未確認前不重送)?
---
## 8. 對應關係
| 本指令 | 對應 OIDC/Bear 端點 |
|--------|----------------------|
| `login` | `/.well-known/openid-configuration` → `POST /device_authorization` → `POST /token`(device_code) |
| `login --token <PAT>` | `GET /userinfo`(Bearer PAT 驗證身分,無 OIDC 端點) |
| `whoami` | `GET /userinfo`(接受 OIDC access token 或 PAT) |
| `token --refresh` | `POST /token`(refresh_token,僅 Device Flow) |
| `logout` | `POST /revoke`(僅 Device Flow;PAT 僅本機刪除,撤銷走網頁 `/profile/tokens`) |
| `apps list` | `GET /api/v1/apps`(Bearer PAT;分頁 `page`/`per_page`) |
| `apps show` | `GET /api/v1/apps/{id}` |
| `apps create` | `POST /api/v1/apps` |
| `apps update` | `PUT /api/v1/apps/{id}` |
| `apps rotate-secret` | `POST /api/v1/apps/{id}/rotate-secret` |
| `apps toggle` | `POST /api/v1/apps/{id}/toggle` |
| `profile show` | `GET /api/v1/profile` |
| `profile set` | `PUT /api/v1/profile` |
| `password change` | `POST /api/v1/profile/password` |
| `email change` | `POST /api/v1/profile/email/request-codes` → `POST /api/v1/profile/email` |
| `sessions list` | `GET /api/v1/profile/sessions` |
| `sessions revoke <id>` | `POST /api/v1/profile/sessions/{id}/revoke` |
| `sessions revoke-others` | `POST /api/v1/profile/sessions/revoke-others` |
| `tokens list` | `GET /api/v1/profile/tokens` |
| `tokens create` | `POST /api/v1/profile/tokens` |
| `tokens revoke <id>` | `POST /api/v1/profile/tokens/{id}/revoke` |
| `mfa status` | `GET /api/v1/profile`(讀 MFA 欄位) |
| `mfa setup` | `POST /api/v1/profile/mfa/setup`(先無 code 取 secret,再帶 code 確認) |
| `mfa disable` | `POST /api/v1/profile/mfa/disable` |
| `mfa recovery-codes` | `POST /api/v1/profile/mfa/recovery-codes` |
> `apps` 群組依賴 alterminal/bear#28 的 App 管理 JSON API(PAT Bearer、admin 限定);`profile` 系列依賴 alterminal/bear#35 的個人自助 JSON API(PAT Bearer、任何有效帳號)。
---
## 9. 現行實作狀態(PAT 模式 MVP,issue #6)
目前實作為 **PAT 模式 MVP**(Elixir + Req、mix escript),與上方 Device Flow 規格的差異:
| 指令 | 原規格(Device Flow) | 現行實作(PAT 模式) |
|------|----------------------|----------------------|
| `login` | 裝置碼流程 | `--token <PAT>` 驗證 `GET /userinfo` 後保存(`-` 可從 stdin 讀);未給 `--token` 時提示 Device Flow 尚未開放(退出碼 2) |
| `whoami` | `GET /userinfo`(refresh 輪轉) | `GET /userinfo`(Bearer PAT,無輪轉);`BEAR_TOKEN` 提供時優先使用 |
| `token` | refresh token 輪轉換新 | 直接印出 PAT;`--refresh` 為用法錯誤(退出碼 2);`BEAR_TOKEN` 提供時優先印出 |
| `logout` | `POST /revoke`(撤銷 refresh token) | 僅清除本機憑證;PAT 需至網頁 `/profile/tokens` 撤銷 |
| `status` | 顯示 access token 剩餘秒數 | 純本機判定:顯示模式(PAT/BEAR_TOKEN 環境變數)與 issuer |
| `apps` | P3 暫緩 | 未實作;App 管理指令規格見 #5,伺服器端 API 見 alterminal/bear#28 |
| `profile`/`password`/`email`/`sessions`/`tokens`/`mfa` | P3 暫緩 | 未實作;個人自助指令規格見 #11(§3.7),伺服器端 API 見 alterminal/bear#35 |
退出碼差異:`login`/`whoami` 的 token 無效(401)依 §3.1 為 `3`(本節實作一致)。
憑證檔結構(PAT 模式,0600、原子寫入、O_EXCL):
```json
{"issuer": "https://alterminal.com", "access_token": "<PAT>", "email": "..."}
```
建置:
```bash
mix deps.get && mix test && mix escript.build # 產生單檔可執行 bear
```