從 local-tool 複製出獨立的「visionA Agent」桌面應用(A3 純橋樑: tunnel client + 配對 UI + 設定,不開 HTTP port、不做本機裝置/推論 UI)。 Bundle ID 與 local-tool 不同(com.innovedus.visiona-agent vs visiona-local), 雙 app 可共存。fork 後不主動 sync,需要時手動 cherry-pick。 Backend / Wails Go(AB1-AB13): - internal/tunnel:6 狀態機(Idle/Connecting/Connected/Reconnecting/Failed/Stopped) + Pair/Unpair/Reconnect/Disconnect binding + ClientHooks event - internal/auth:encrypted file token store(AES-GCM + scrypt + machineID fallback salt + 13 tests) - internal/config:YAML validation + atomic write + 11 tests - internal/log:ring buffer + ExportLog 升級 zip - visionA-backend /api/pairing/exchange:SessionTokenStore + 17 new tests - 三平台 build 驗證(macOS DMG 160 MB / Windows EXE / Linux AppImage) - end-to-end 5 milestone 全綠(pairing → tunnel → forward → reuse 防護 → tunnel drop failover) Frontend / Next.js(AF1-AF7,沿用 visionA-frontend 基礎): - AppShell + Header + TabNav(StatusView / PairView / SettingsView 三 tab) - ConnectionStatusBadge 5 種狀態 - TokenInput regex 驗證 + 7 種錯誤 + 0.5s auto-switch 到狀態頁 - 設定頁 4 區塊(含重新配對 AlertDialog) - agent-api.ts 封裝 Wails bindings(mock/real 雙實作)+ 90 tests Phase 0.7 review-driven fix(Round 2): - A1 Session fixation 防護(RotateSessionID) - A3 mock pairing 預設改 false(必須明確 opt-in)+ startup log - A4 Pair 失敗後 state 清理矩陣(exchange/Save/Start fail 各自終態) - A5 Pair/Unpair/Reconnect lifecycleMu + 50 goroutine race test - F1 重新配對次按鈕 / F2 PairView Esc cancel / F3 Wails BrowserOpenURL / F4 Settings draft 持久 + 未儲存 badge 驗證:agent backend go test -race -count=3 ./... 4 packages 全綠 / agent frontend pnpm test 119 tests 全綠 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
94 lines
3.8 KiB
Go
94 lines
3.8 KiB
Go
// tokenstore.go — Session Token 儲存介面(AB5 範圍)。
|
||
//
|
||
// 本檔只定義 interface 與一個測試用的 MemoryTokenStore。真正的 OS Keychain /
|
||
// encrypted file 實作留到 AB7(見 ADR-009、TDD §9)。AB7 完成後,只要把 Wails
|
||
// app 啟動時建立的 TokenStore 換成 keychain 版,Manager 不用動。
|
||
//
|
||
// 設計原則(對齊 §9 Token 儲存策略):
|
||
//
|
||
// - Session Token 是長效憑證(雛形無 TTL,Phase 1 = 90 天),洩漏等於整台
|
||
// 電腦被遠端控制;AB7 之前用 MemoryTokenStore 只保證 process 內不落檔。
|
||
// - 介面故意極簡(Save / Load / Delete),避免 AB7 換實作時 Manager 要改。
|
||
// - Load 在「尚未儲存」時回傳空字串 + nil error(非 error 情境),與
|
||
// os.IsNotExist 語義對齊,讓呼叫端用 `if token == ""` 判斷更直覺。
|
||
// - 任何 I/O / 加解密錯誤才回 error(AB7 檔案讀取失敗、keychain 拒絕等)。
|
||
//
|
||
// Thread safety:
|
||
//
|
||
// - Manager 可能在 main goroutine(Pair)與 reconnect goroutine(401 清 token)
|
||
// 同時觸碰,實作必須 thread-safe。MemoryTokenStore 已內建 sync.RWMutex。
|
||
package tunnel
|
||
|
||
import "sync"
|
||
|
||
// TokenStore 是 Session Token 的持久化介面。
|
||
//
|
||
// AB7 會提供三個平台實作:
|
||
// - macOS:go-keychain(Keychain Services)
|
||
// - Windows:wincred(Credential Manager)
|
||
// - Linux:secret-service 或 encrypted file fallback
|
||
//
|
||
// 雛形(AB5 / AB6)先用 MemoryTokenStore,重啟 Wails app 會遺失 token、需要
|
||
// 重新配對——這是雛形可接受的代價。
|
||
//
|
||
// 介面契約(所有實作必須遵守):
|
||
//
|
||
// - Save(token) 覆蓋既有值。**Save("") 等同 Delete()**:傳入空字串 token
|
||
// 視為「清除已儲存值」,不留下空字串條目,與 Delete() 行為等價。
|
||
// 這個契約讓呼叫端在「不確定 token 是空還是要清掉」的場景可以統一走 Save,
|
||
// 而不必先檢查 token 是否為空。
|
||
// - Load() 在「從未儲存」或「已 Delete」時回傳 ("", nil),**非 error**。
|
||
// 對齊 os.IsNotExist 語義,呼叫端用 `if token == ""` 即可判斷未儲存。
|
||
// 僅 I/O / 解密失敗(AB7 keychain 拒絕、檔案損壞等)才回 non-nil error。
|
||
// - Delete() 是 idempotent:不存在時視為成功(不回 error)。
|
||
// - 所有方法必須 thread-safe(Manager 會在 main / reconnect goroutine 同時呼叫)。
|
||
type TokenStore interface {
|
||
// Save 儲存 Session Token。覆蓋既有值。
|
||
// Save("") 等同 Delete()(見介面 doc 「介面契約」第 1 點)。
|
||
Save(token string) error
|
||
|
||
// Load 回傳已儲存的 Session Token;未儲存時回傳 ("", nil)。
|
||
// 只有 I/O / 解密錯誤才回 non-nil error。
|
||
Load() (string, error)
|
||
|
||
// Delete 清除儲存的 token。不存在時視為成功(idempotent)。
|
||
Delete() error
|
||
}
|
||
|
||
// MemoryTokenStore 是測試與 AB5/AB6 雛形用的 in-memory 實作。
|
||
// AB7 完成後會被平台 keychain 實作取代。
|
||
//
|
||
// ⚠️ 不持久化:process 結束 token 就消失。
|
||
type MemoryTokenStore struct {
|
||
mu sync.RWMutex
|
||
token string
|
||
}
|
||
|
||
// NewMemoryTokenStore 建立一個空的 in-memory TokenStore。
|
||
func NewMemoryTokenStore() *MemoryTokenStore {
|
||
return &MemoryTokenStore{}
|
||
}
|
||
|
||
// Save 儲存 token;空字串視為清除(對齊 TokenStore 介面語義)。
|
||
func (s *MemoryTokenStore) Save(token string) error {
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
s.token = token
|
||
return nil
|
||
}
|
||
|
||
// Load 回傳目前儲存的 token;未儲存回 ("", nil)。
|
||
func (s *MemoryTokenStore) Load() (string, error) {
|
||
s.mu.RLock()
|
||
defer s.mu.RUnlock()
|
||
return s.token, nil
|
||
}
|
||
|
||
// Delete 清除儲存的 token。idempotent。
|
||
func (s *MemoryTokenStore) Delete() error {
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
s.token = ""
|
||
return nil
|
||
}
|