Skip to content

Commit ccfabb1

Browse files
Merge pull request #4 from easyvibecoding/feat/native-messaging-host
feat: NMH dual-path IPC — WebSocket primary + NMH fallback
2 parents c710cc6 + 6638ea2 commit ccfabb1

17 files changed

Lines changed: 660 additions & 91 deletions

File tree

CLAUDE.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ These are absolute rules — never violate them:
8787
- **React SPA input masking**: For dialog inputs in React/Vue SPAs, do NOT set `visibility: visible` after masking — the framework overwrites `input.value` but keeps our inline styles, exposing plaintext. Instead, keep inputs hidden by manifest CSS.
8888
- **AWS dual-key capture**: AWS has Access Key ID (`AKIA...`, DOM scan) + Secret Access Key (no prefix, 40-char base64, clipboard-only capture via `isAwsConsolePage()` in `handleClipboardText()`).
8989
- **Toast stacking**: Multiple consecutive captures show stacked toasts (each calculates top offset from existing toasts) instead of replacing the previous one.
90+
- **NMH dual-path IPC**: NativeMessagingHost supports both `get_config` (direct ipc.json read) and WS relay (`get_state`, `submit_captured_key`, `toggle_demo_mode`). WS relay uses `URLSessionWebSocketTask` short-lived connection (~20-60ms). clientType `"nmh"` — Core skips NMH in broadcast. `sendRequest()` in service-worker.ts tries WS first, then NMH fallback.
91+
- **NMH no plaintext queue**: When both WS and NMH fail for `submit_captured_key`, the key is NOT queued to `chrome.storage.local`. Storing plaintext API keys in browser storage violates security red line #1. The key is lost and must be re-captured.
92+
- **NMH isConnected semantics**: NMH relay success does NOT set `state.isConnected = true` — NMH is a one-shot relay, not a persistent connection. Popup shows "Connected (NMH)" via `connectionPath` field only, while WS reconnect continues independently.
93+
- **NMHInstaller**: Core auto-installs NMH binary + Chrome manifest from app bundle Resources on startup. Uses binary file size comparison for version checking. `install.sh` retained as manual fallback.
9094

9195
## Documentation
9296

docs/01-product-spec/implementation-status.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# 實作狀態追蹤
22

3-
> 最後更新:2026-03-17
3+
> 最後更新:2026-03-18
44
55
## 狀態圖例
66

@@ -19,7 +19,7 @@
1919
| KeychainService || store / retrieve / delete 完成 |
2020
| ClipboardEngine || copy + autoClear + detectKeys 完成 |
2121
| MaskingCoordinator || isDemoMode / activeContext / pattern 匹配完成 |
22-
| IPCServer (WebSocket) || handshake / state_changed / pattern_cache_sync / toggle_demo_mode |
22+
| IPCServer (WebSocket) || handshake / state_changed / pattern_cache_sync / toggle_demo_mode / nmh clientType |
2323
| HotkeyManager || `⌃⌥⌘D` toggle、`⌃⌥Space` hold 偵測、`⌃⌥[1-9]` paste、flagsChanged 監聽 |
2424
| Floating Toolbox (HUD) || NSPanel 浮動視窗、hold-to-search、Scheme B 鎖定、↑↓ 導航 |
2525
| ToolboxState (ViewModel) || 搜尋過濾、選取狀態、release/confirm/dismiss 邏輯 |
@@ -67,14 +67,16 @@
6767

6868
| 功能 | 狀態 | 備註 |
6969
|------|------|------|
70-
| Background Service Worker || WebSocket 連線、state 管理、reconnect |
71-
| Popup UI || 連線狀態、Demo Mode、Context、Patterns |
70+
| Background Service Worker || WebSocket 連線、NMH fallback 雙路分派、state 管理、reconnect |
71+
| Popup UI || 連線狀態(WebSocket/NMH/Offline)、Demo Mode、Context、Patterns |
7272
| Toggle Demo Mode || Popup → Background → Core → broadcast |
7373
| Content Script DOM masking || TreeWalker + CSS overlay + MutationObserver |
7474
| Content Script unmask || 退出 Demo Mode 恢復原始文字 |
7575
| Options 頁面 || Pattern cache 管理 + Dev IPC Config |
7676
| Dev IPC Config (workaround) || 替代 Native Messaging Host |
77-
| Native Messaging Host || Swift binary 未編譯部署 |
77+
| Native Messaging Host || get_config + WS relay(get_state / submit_captured_key / toggle_demo_mode) |
78+
| NMH 雙路 IPC || WS primary + NMH fallback,popup 顯示連線路徑 |
79+
| NMHInstaller(Core 自動安裝) || Core 啟動時從 bundle Resources 安裝 binary + manifest |
7880
| Active Key Capture || 4 層偵測:DOM scan → attribute → clipboard → platform selectors |
7981
| capture-patterns.ts (SSoT) || 11 平台 pattern 定義,單一檔案維護 |
8082
| Pre-hide anti-flash || 三層:manifest CSS → pre-hide.ts → instant MutationObserver |

docs/02-technical-architecture/chrome-extension-architecture.md

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Chrome Extension 架構
22

3-
> 狀態:✅ 核心功能完成(WebSocket 連線、Content Script masking、Popup UI)
4-
> 尚未完成:Native Messaging Host 部署、Smart Extract
3+
> 狀態:✅ 核心功能完成(WebSocket 連線、NMH 雙路 IPC、Content Script masking、Popup UI)
4+
> 尚未完成:Smart Extract
55
66
---
77

@@ -43,6 +43,7 @@
4343
**職責**
4444
- 維持與 Swift Core 的 WebSocket 連線(含指數退避重連)
4545
- 透過 Native Messaging Host 取得 IPC 連線資訊
46+
- **雙路分派**:WS primary → NMH fallback(get_state / submit_captured_key / toggle_demo_mode)
4647
- 接收 Core 事件並轉發至 Content Scripts
4748
- 回應 Popup 的狀態查詢
4849
- 持久化 pattern cache 至 `chrome.storage.local`
@@ -53,6 +54,12 @@
5354
3. 發送 handshake(clientType: 'chrome', token, version)
5455
4. 收到 success → 標記 connected,開始接收事件
5556

57+
**雙路分派(Dual-path Dispatch)**
58+
- `sendRequest(action, payload)` 統一入口
59+
- WS 連線中 → 透過 WS 發送(含 request-response correlation + 5s timeout)
60+
- WS 斷線且 action 為 relay 清單 → 透過 NMH fallback
61+
- 雙路皆失敗 → log warning(不將明文 key 存入 chrome.storage,遵守安全紅線)
62+
5663
**Dev Fallback**
5764
- Native Host 不可用時,從 `chrome.storage.local` 讀取手動設定的 port/token
5865
- Options 頁面提供 Dev IPC Config 輸入介面
@@ -108,11 +115,12 @@ chrome.runtime.sendMessage({ type: 'get_state' }, (response) => {
108115
### Popup (`popup.ts` + `popup.html`)
109116

110117
**顯示資訊**
111-
- Connection 狀態(綠點 Connected / 紅點 Offline)
118+
- Connection 狀態與路徑(綠點 WebSocket / 藍點 NMH / 紅點 Offline)
112119
- Mode(Normal / Demo)
113120
- Active Context 名稱
114121
- Pattern 數量
115122
- Toggle Demo Mode 按鈕
123+
- Capture Mode 控制
116124

117125
### Options (`options.ts` + `options.html`)
118126

@@ -126,45 +134,68 @@ chrome.runtime.sendMessage({ type: 'get_state' }, (response) => {
126134

127135
## Native Messaging Host
128136

129-
### 架構
137+
### 架構(雙路 IPC)
138+
139+
NMH 同時支援兩種模式:**config 查詢****WS relay**
130140

131141
```
132142
Chrome Extension → chrome.runtime.sendNativeMessage('com.demosafe.nmh', ...)
133143
↓ stdin (4-byte length prefix + JSON)
134144
NativeMessagingHost (Swift binary)
135-
↓ 讀取 ~/.demosafe/ipc.json
145+
├─ action: get_config → 讀取 ~/.demosafe/ipc.json → 回傳 {port, token}
146+
└─ action: get_state / submit_captured_key / toggle_demo_mode
147+
→ 讀取 ipc.json 取得 port/token
148+
→ 建立短暫 WS 連線至 Core(URLSessionWebSocketTask)
149+
→ handshake (clientType: "nmh") → 送 request → 收 response → 斷線
150+
→ stdout 回傳 Core 的 response
136151
↓ stdout (4-byte length prefix + JSON)
137-
Chrome Extension ← { port: 55535, token: "..." }
152+
Chrome Extension ← response
138153
```
139154

155+
**WS Relay 特性**
156+
- 短暫連線:connect → handshake → 1 request → 1 response → close(~20-60ms)
157+
- clientType `"nmh"`:Core 不會對 NMH 連線推送 events(broadcast 時跳過)
158+
- 5 秒 timeout,失敗回傳 `{"error": "core_unreachable" | "auth_failed" | "timeout"}`
159+
- 會自動跳過 handshake 後 Core 推送的 event messages(如 pattern_cache_sync)
160+
161+
### 支援的 Actions
162+
163+
| Action | 模式 | 說明 |
164+
|--------|------|------|
165+
| `get_config` | 直讀 ipc.json | 回傳 `{port, token}`,原有行為 |
166+
| `get_state` | WS relay | 回傳 isDemoMode、activeContext、patternCacheVersion |
167+
| `submit_captured_key` | WS relay | 提交截取到的 API key |
168+
| `toggle_demo_mode` | WS relay | 切換 Demo Mode |
169+
140170
### 安裝路徑
141171

142172
| 檔案 | 路徑 |
143173
|------|------|
144-
| Swift binary | `/Applications/DemoSafe.app/Contents/Helpers/demosafe-nmh` |
174+
| Swift binary | `~/.demosafe/bin/demosafe-nmh` |
145175
| Host manifest | `~/Library/Application Support/Google/Chrome/NativeMessagingHosts/com.demosafe.nmh.json` |
146176

177+
### 自動安裝(NMHInstaller)
178+
179+
Core 啟動時自動檢查並安裝 NMH:
180+
1. 檢查 `~/.demosafe/bin/demosafe-nmh` 是否存在(比對 binary size)
181+
2. 檢查 Chrome NMH manifest 是否存在且 allowed_origins 正確
182+
3. 缺少或版本不符 → 從 app bundle Resources 複製 binary + 寫入 manifest
183+
4. `install.sh` 保留作為手動備援
184+
147185
### Host Manifest (`com.demosafe.nmh.json`)
148186

149187
```json
150188
{
151189
"name": "com.demosafe.nmh",
152-
"description": "DemoSafe Native Messaging Host",
153-
"path": "/Applications/DemoSafe.app/Contents/Helpers/demosafe-nmh",
190+
"description": "DemoSafe Native Messaging Host — relay for Chrome Extension",
191+
"path": "/Users/<user>/.demosafe/bin/demosafe-nmh",
154192
"type": "stdio",
155193
"allowed_origins": [
156194
"chrome-extension://ACTUAL_EXTENSION_ID/"
157195
]
158196
}
159197
```
160198

161-
### 部署步驟(尚未自動化)
162-
163-
1. 編譯 `native-host/NativeMessagingHost.swift` 為 binary
164-
2. 放置至 `/Applications/DemoSafe.app/Contents/Helpers/demosafe-nmh`
165-
3.`com.demosafe.nmh.json` 複製至 NativeMessagingHosts 目錄
166-
4. 替換 `EXTENSION_ID_HERE` 為實際 Chrome Extension ID
167-
168199
---
169200

170201
## Content Script 目標網站

docs/02-technical-architecture/extension-boundaries.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,20 +60,22 @@
6060
### 連線架構
6161

6262
- Background Service Worker 維護與 Core 的 WebSocket 連線
63-
- Native Messaging Host(Swift helper)讀取 `ipc.json` 以輔助探索
63+
- Native Messaging Host(Swift helper)提供 config 查詢 + WS relay 雙路備援
6464

6565
### Native Messaging Host 規格
6666

67-
Chrome Extension 無法直接讀取檔案系統(`~/.demosafe/ipc.json`),因此需要 Native Messaging Host 作為橋接
67+
Chrome Extension 無法直接讀取檔案系統(`~/.demosafe/ipc.json`),因此需要 Native Messaging Host 作為橋接。NMH 同時作為 WS 斷線時的 fallback relay。
6868

6969
| 項目 | 說明 |
7070
|------|------|
71-
| 實作語言 | Swift(macOS helper binary) |
72-
| 安裝位置 | `~/Library/Application Support/Google/Chrome/NativeMessagingHosts/com.demosafe.nmh.json` |
73-
| 職責 | 讀取 `ipc.json` → 回傳 `{port, token}` 給 Background Service Worker |
71+
| 實作語言 | Swift(macOS helper binary,standalone swiftc 編譯,~93KB|
72+
| 安裝位置 | binary: `~/.demosafe/bin/demosafe-nmh`,manifest: `~/Library/Application Support/Google/Chrome/NativeMessagingHosts/com.demosafe.nmh.json` |
73+
| 職責 | (1) 讀取 `ipc.json` → 回傳 `{port, token}`;(2) WS relay:get_state / submit_captured_key / toggle_demo_mode |
7474
| 通訊協定 | Chrome Native Messaging(stdin/stdout JSON) |
75-
| 觸發時機 | Extension 啟動時呼叫一次取得連線資訊;Core 重啟後重新呼叫 |
76-
| 安全性 | manifest 限定 `allowed_origins` 僅允許本 Extension ID |
75+
| WS Relay | 短暫連線(connect → handshake → 1 request → 1 response → close),clientType: `"nmh"`,5s timeout |
76+
| 觸發時機 | Extension 啟動時取連線資訊;WS 斷線時 fallback relay |
77+
| 安裝方式 | Core 啟動時 NMHInstaller 自動安裝;`install.sh` 手動備援 |
78+
| 安全性 | manifest 限定 `allowed_origins`;NMH 不儲存任何資料,僅轉發 |
7779

7880
---
7981

docs/05-ipc-protocol/protocol-spec.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636

3737
| Action | Payload | Response |
3838
|--------|---------|----------|
39-
| `handshake` | `clientType`, `token`, `version` | port, pid, patternCache version, 連線狀態 |
39+
| `handshake` | `clientType``vscode` / `chrome` / `accessibility` / `nmh`, `token`, `version` | port, pid, patternCache version, 連線狀態 |
4040
| `get_state` | (無) | isDemoMode, activeContext, patternCache, version |
4141
| `request_paste` | `keyId` | status (`success` \| `denied` \| `offline`) |
4242
| `request_paste_group` | `groupId`, `fieldIndex`(可選) | status, groupId |
@@ -159,11 +159,25 @@ Core 維護 `patternCacheVersion`,每次 pattern 變更時遞增。Extension
159159
160160
---
161161

162+
## NMH Relay(Native Messaging Host 短暫連線)
163+
164+
clientType `"nmh"` 為 Chrome Native Messaging Host 透過短暫 WS 連線轉發請求。
165+
166+
| 特性 | 說明 |
167+
|------|------|
168+
| 連線生命週期 | connect → handshake → 1 request → 1 response → close(~20-60ms) |
169+
| 支援的 action | `get_state``submit_captured_key``toggle_demo_mode` |
170+
| broadcast 排除 | Core broadcast events 時自動跳過 `.nmh` clients |
171+
| 使用場景 | Chrome Extension WS 斷線時的 fallback 路徑 |
172+
173+
---
174+
162175
## 安全約束
163176

164177
| 規則 | 說明 |
165178
|------|------|
166179
| WebSocket 僅 127.0.0.1 | 禁止綁定外部介面 |
167180
| Handshake 認證 | 需要 `ipc.json` 中的 token |
168181
| **明文永不經過 IPC** | 僅遮蔽表示和參照流過網路 |
182+
| **明文不存入 chrome.storage** | submit_captured_key 失敗時不 queue,遵守安全紅線 |
169183
| `ipc.json` 權限 600 | 僅限使用者讀寫 |

docs/en/01-product-spec/implementation-status.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Implementation Status Tracking
22

3-
> Last updated: 2026-03-17
3+
> Last updated: 2026-03-18
44
55
## Status Legend
66

@@ -19,7 +19,7 @@
1919
| KeychainService || store / retrieve / delete completed |
2020
| ClipboardEngine || copy + autoClear + detectKeys completed |
2121
| MaskingCoordinator || isDemoMode / activeContext / pattern matching completed |
22-
| IPCServer (WebSocket) || handshake / state_changed / pattern_cache_sync / toggle_demo_mode |
22+
| IPCServer (WebSocket) || handshake / state_changed / pattern_cache_sync / toggle_demo_mode / nmh clientType |
2323
| HotkeyManager || `⌃⌥⌘D` toggle, `⌃⌥Space` hold detection, `⌃⌥[1-9]` paste, flagsChanged listener |
2424
| Floating Toolbox (HUD) || NSPanel floating window, hold-to-search, Scheme B lock, ↑↓ navigation |
2525
| ToolboxState (ViewModel) || Search filtering, selection state, release/confirm/dismiss logic |
@@ -67,14 +67,16 @@
6767

6868
| Feature | Status | Notes |
6969
|---------|--------|-------|
70-
| Background Service Worker || WebSocket connection, state management, reconnect |
71-
| Popup UI || Connection status, Demo Mode, Context, Patterns |
70+
| Background Service Worker || WebSocket connection, NMH fallback dual-path dispatch, state management, reconnect |
71+
| Popup UI || Connection status (WebSocket/NMH/Offline), Demo Mode, Context, Patterns |
7272
| Toggle Demo Mode || Popup → Background → Core → broadcast |
7373
| Content Script DOM masking || TreeWalker + CSS overlay + MutationObserver |
7474
| Content Script unmask || Restore original text when exiting Demo Mode |
7575
| Options page || Pattern cache management + Dev IPC Config |
7676
| Dev IPC Config (workaround) || Alternative to Native Messaging Host |
77-
| Native Messaging Host || Swift binary not compiled/deployed |
77+
| Native Messaging Host || get_config + WS relay (get_state / submit_captured_key / toggle_demo_mode) |
78+
| Dual-path IPC (NMH fallback) || WS primary + NMH fallback, popup shows connection path |
79+
| NMHInstaller (Core auto-install) || Core installs binary + manifest from bundle Resources on startup |
7880
| Active Key Capture || 4-layer detection: DOM scan → attribute → clipboard → platform selectors |
7981
| capture-patterns.ts (SSoT) || 11 platform pattern definitions, single file maintenance |
8082
| Pre-hide anti-flash || 3 layers: manifest CSS → pre-hide.ts → instant MutationObserver |

0 commit comments

Comments
 (0)