- lib/bear_cli/self_service.ex:六個子群組(profile show/set、password change、 email change、sessions list/revoke/revoke-others、tokens list/create/revoke、 mfa status/setup/disable/recovery-codes),對接 alterminal/bear#38 的 /api/v1/profile* JSON API(PAT Bearer、任何有效帳號) - 敏感輸入(密碼/驗證碼)互動提示讀取,非 TTY → 退出碼 2; 一次性明文(PAT 明文、recovery codes)僅於成功當下輸出 - mfa setup:otpauth URI 本地生成 ASCII QR(新增 eqrcode 依賴), 確認走 /mfa/setup/confirm(依 #38 實作) - Api 模組新增 profile 系列端點與 429 分流;CLI 分派接上六個指令群組 - docs/commands.md §3.7 對齊 #38 實際 API(sessions 欄位、tokens create 無 --scope、mfa setup/confirm、email 需重帶 new_email);§7/§8/§9 與 README 同步更新 - 測試 50 例(fake API 注入):解析、輸出、退出碼、互動輸入、錯誤分流; mix precommit 全綠(114 passed)
33 KiB
Bear CLI 指令規劃(Command Spec)
狀態:P2 PAT 模式 MVP 已實作(見第 9 節);Device Flow 待伺服器端 P1。 範圍:本文件為指令介面規格;App 管理指令(
bear appsCRUD)另見 #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):
-
讀取設定 → 向
{issuer}/.well-known/openid-configuration取device_authorization_endpoint(動態發現,不寫死網址)。 -
POST /device_authorization(client_id+scope)。 -
終端印出:
請在瀏覽器開啟:https://alterminal.com/device 輸入代碼:ABCD-EFGH若終端支援,同步印出 QR(掃碼直達
verification_uri_complete)。 -
以
interval週期輪詢POST /token(grant_type=device_code):authorization_pending→ 繼續等待;slow_down→ 加大輪詢間隔;access_denied→ 中止並提示;- 成功 → 寫入憑證檔(0600),印出
已登入:<email>。
行為(PAT,給 --token <PAT>):
- 以該 PAT 作為 Bearer 呼叫
GET /userinfo驗證可用性並取得身分。 - 成功 → 將 PAT 直接寫入憑證檔(0600),印出
已登入:<email>(無 refresh token)。 - 失敗(401/無效或已撤銷)→ 提示 PAT 無效,不寫入任何資料。
退出碼:0 成功;3 PAT 無效或已撤銷;4 等待逾時(expires_in);5 使用者拒絕;6 網路/伺服器錯誤。
--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 已上線(PR alterminal/bear#29),本節指令群已實作(issue #10);--visibility/--status過濾目前由 CLI 全量列舉後本地套用(伺服器 list 端點尚無對應查詢參數)。
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 輸出範例:
{"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 已上線(PR alterminal/bear#38),本節指令群已實作(issue #11);端點細節以 #38 實作為準。
比照 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
變更密碼。無任何命令列參數(密碼一律互動輸入)。
行為:
- 互動提示(不回顯)依序讀取:
目前密碼、新密碼、確認新密碼。 - 兩次新密碼不一致 → 退出碼
2(本地檢查),不發請求。 POST /api/v1/profile/password(Bearer PAT,body 帶current_password/new_password/password_confirmation;CLI 以兩次輸入一致的新密碼填password_confirmation)。- 成功 → 印出
密碼已變更;現行密碼錯誤 → 退出碼1;新密碼不符伺服器強度規則 →422,退出碼1。
比照網頁流程,變更密碼不自動撤銷其他 session;需要時另用 bear sessions revoke-others。
3.7.4 bear email change --new EMAIL
變更 email(雙向驗證碼流程)。驗證碼一律互動輸入,不接受命令列參數。
| 選項 | 說明 |
|---|---|
--new EMAIL |
(必填)新 email 位址 |
行為:
POST /api/v1/profile/email/request-codes(Bearer PAT,new_email)→ 伺服器向現有與新信箱各寄一組驗證碼。- 終端印出
驗證碼已寄至現有信箱與 <new>,互動提示(不回顧)讀取:現有信箱驗證碼、新信箱驗證碼。 POST /api/v1/profile/email(new_email/current_code/new_code;API 版需再帶一次new_email,CLI 自動帶入)。- 成功 → 印出
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
0192a1b0-… 203.0.113.10 Mozilla/5.0 (X11; Linux…) 2026-09-07 10:00:00Z 2026-09-08 09:00:00Z
(user_agent 截斷至欄寬;--json 輸出完整值。API 回應欄位為 id/ip/user_agent/created_at/last_seen_at,不含 expires_at。)
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 (無到期) (未使用) expired
STATUS:active/expired(CLI 依 expires_at 本地判定)。API 的 list 端點只回未撤銷的 token,故 revoked 不會出現在清單。
bear tokens create --name NAME [--expires-in DAYS|never]:POST /api/v1/profile/tokens(Bearer PAT,bodyname/expires_in;scope 由伺服器給預設openid profile email,API 不接受指定)。成功後一次性顯示:
PAT 已建立:ci-token(30 天後到期)
token(只顯示這一次,請立即保存):
9f3a…(Base64url,43 字元,無前綴)
| 選項 | 預設 | 說明 |
|---|---|---|
--name |
(必填) | 顯示名稱;空白或重複名稱依伺服器規則 422 → 退出碼 1 |
--expires-in |
30 |
天數(正整數)或 never(永不過期) |
--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 已撤銷;若撤銷的是目前憑證用的 PAT,後續指令會得到401(退出碼3),屆時重新bear login(本機憑證檔未存 token id,CLI 無法預先比對,僅能提示此退路)。
3.7.7 bear mfa
TOTP 兩因子管理(WebAuthn/passkey 不在範圍)。
-
bear mfa status:讀GET /api/v1/profile的mfa_enabled欄位,印出MFA(TOTP):已啟用/未啟用。 -
bear mfa setup:兩步互動流程——POST /api/v1/profile/mfa/setup(無 body,Bearer PAT)→ 回傳 Base32secret與otpauth://URI。終端印出 URI、secret,並在本機產生 ASCII QR(掃碼加入驗證器;QR 由 CLI 本地生成,不經伺服器;--json模式此步資訊走 stderr,stdout 留給最終結果)。- 互動提示(不回顧)讀取 6 位碼 →
POST /api/v1/profile/mfa/setup/confirm(code)確認。 - 成功 → 一次性顯示 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. 開放問題(待確認)
- CLI 的 OIDC client 採新
method: "device"(client auth 為none)或沿用PKCE+JWK?傾向前者(見 bearcli-feature-plan.md§5.5)。 - 發行格式:escript(目標機需 Erlang)或 self-contained release(bakeware/burrito)?
bear apps list --visibility public未來是否開放非 admin(僅公開/active 清單,同網頁 Launch)?目前規劃為 admin 限定。- 是否支援
offline_accessscope,或沿用現行 refresh 輪轉即可? - 是否在 CLI 提供「建立/撤銷 PAT」?原規劃傾向 CLI 只「使用」不「管理」;#35 的 PAT 端點落地後,
bear tokens已納入 §3.7.6 規格(管理自己帳號的 PAT),此題視為已解。 - App 管理 API 的路徑參數最終採 UUID 或
client_id(alterminal/bear#28 規格為{id})?若為 UUID,CLI 需先以 list 解析client_id→ UUID,或伺服器提供以client_id直取的端點。 bear profile set --phone-verified(自我標記電話已驗證)是否開放?#35 已定案:API 接受該欄位(profile_changeset的profile_fields含phone_number_verified),CLI 保留--phone-verified旗標(僅設定,清除不在本期)。mfa setup的secret/otpauth://屬敏感資料,#35 的 API 回應是否應限制僅 setup 流程期間回傳(未確認前不重送)?#38 實作為 setup 回應一次性回傳,未確認前重呼 setup 會重產 secret;CLI 依此實作(第二步走/mfa/setup/confirm)。
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(取 secret)→ POST /api/v1/profile/mfa/setup/confirm(帶 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 暫緩 | ✅ 已實作(issue #10):list/show/create/update/rotate-secret/toggle,對接 alterminal/bear#28 的 /api/v1/apps JSON API(PAT Bearer、admin 限定)。client_id → UUID 由 CLI 以 list 解析;一次性 client_secret 僅於 create/rotate-secret 成功當下輸出 |
profile/password/email/sessions/tokens/mfa |
P3 暫緩 | ✅ 已實作(issue #11):profile show/set、password change、email change、sessions list/revoke/revoke-others、tokens list/create/revoke、mfa status/setup/disable/recovery-codes,對接 alterminal/bear#38 的 /api/v1/profile* JSON API(PAT Bearer、任何有效帳號)。敏感輸入互動提示;一次性明文(PAT 明文、recovery codes)僅於成功當下輸出;mfa setup 的 ASCII QR 由本機 eqrcode 生成 |
退出碼差異:login/whoami 的 token 無效(401)依 §3.1 為 3(本節實作一致)。
憑證檔結構(PAT 模式,0600、原子寫入、O_EXCL):
{"issuer": "https://alterminal.com", "access_token": "<PAT>", "email": "..."}
建置:
mix deps.get && mix test && mix escript.build # 產生單檔可執行 bear