AI 代理應讀取 https://remote.ivjn.us/SKILL.md。

# 用於 Agent 操作的 HTTP API

> HTML: https://remote.ivjn.us/docs/zh-TW/http-api/

原生密封操作現已支援純 HTTP 呼叫端。使用 `remote-cli --transport http` 或 `REMOTE_TRANSPORT=http`。`POST /api/sealed/connections` 建立連線；`POST /input` 提交簽章交握或密文，`GET /events` 長輪詢結果，`DELETE` 關閉連線。用戶端繼續負責端到端加解密與裝置授權驗證。原有裝置操作介面仍回傳 409，裝置端連線保持不變。 HTTP 連線須固定至建立它的 Exchange 執行個體，90 秒無活動後到期。循序提交輸入，重試時保持序號與密文一致；處理結果後以 `after` 確認。Web Console、行動端與 LAN 保持現有傳輸。執行預設指令仍需 `manage` 權限。

https://remote.ivjn.us/sealed-http.md


裝置動作介面的 HTTP `409` / `Sealed sessions are required.` 表示需要密封會話，單獨的 Bearer 權杖無法執行裝置動作。請使用 `remote-cli --profile alice exec lab -- uname -a`；僅允許 HTTP 時使用 `remote-cli --profile alice --transport http exec lab -- uname -a`，CLI 仍執行簽章握手與端到端加密。


## 基底 URL

正式環境 Exchange：

```text
https://exchange.ivjn.us
```

自行架設的部署使用您自己的 HTTPS 來源。下列所有路徑均相對於此基底。傳送 JSON 時請使用 `Content-Type: application/json`。

## 驗證

每個 `/api/*` 路由（公開的驗證／註冊輔助端點除外）都需要：

```http
Authorization: Bearer <token>
```

`<token>` 可以是下列任一項：

1. 來自 `POST /api/auth/login` 的**密碼登入工作階段權杖**
2. **操作員主要 API 金鑰**（建立操作員帳號時僅顯示一次的長效權杖）

兩者皆由主控台與 CLI 使用的同一個 `authenticate_operator` 路徑接受。

### 密碼登入

```bash
curl -sS -X POST 'https://exchange.ivjn.us/api/auth/login' \
  -H 'Content-Type: application/json' \
  -d '{"email":"alice@example.com","password":"your-password-here"}'
```

`email` 是帳號識別資料，必須為有效的電子郵件地址。服務會將其正規化（去除前後空白、轉為小寫），並要求完成地址驗證。現有部署在帳號改用電子郵件前，仍可傳送 `{ "username": "alice", "password": "..." }`；`username` 中包含 `@` 的值會視為電子郵件。無效地址會在檢查密碼前遭到拒絕。

回應範例：

```json
{
  "username": "alice",
  "email": "alice@example.com",
  "tenant_id": "00000000-0000-0000-0000-000000000000",
  "token": "<prefix>.<secret>",
  "expires_in": 2592000
}
```

將 `token` 用作 Bearer 憑證。使用相同標頭呼叫 `POST /api/auth/logout`，即可撤銷該工作階段。登出不會撤銷操作員主要 API 金鑰。

### 操作員 API 金鑰（主要權杖）

```bash
TOKEN='<prefix>.<secret>'   # issued at operator creation; store offline
curl -sS 'https://exchange.ivjn.us/api/devices' \
  -H "Authorization: Bearer ${TOKEN}"
```

## 列出裝置

```bash
curl -sS 'https://exchange.ivjn.us/api/devices' \
  -H "Authorization: Bearer ${TOKEN}"
```

每個項目包含 `id`、`name`、`online`、`enabled`、`info`、時間戳記與 `active_sessions`。裝置清單屬於 Exchange 中繼資料。列出裝置不會建立操作該裝置所需的密封工作階段。

## 裝置動作 HTTP 拒絕與工作階段管理

所有裝置操作都使用密封 WebSocket 工作階段。下列 HTTP 路由無法建立用戶端／裝置握手，因此會對已驗證、已授權且通過請求驗證的請求傳回 HTTP `409`。拒絕發生在向裝置派送之前：不會向代理傳送任何命令、感測器請求、網路探測、更新、ubus 呼叫、保留工作請求或釘選命令。裝置在線也無法讓這些路由執行操作。

驗證、權限與輸入驗證仍然適用。無效憑證傳回 `401`；禁止的操作傳回 `403`；無效命令、URL 或請求主體仍保留其驗證錯誤，包括 `400`。密封工作階段的 `409` 不會取代較早發生的驗證或授權失敗。釘選命令的權限檢查遵循釘選命令原則；通過檢查仍不允許透過 HTTP 執行。

| 方法與路徑 | 通過驗證、授權與輸入驗證後的行為 |
| --- | --- |
| `POST /api/devices/{id}/exec` | `409`：在密封 WebSocket 工作階段內執行 |
| `GET /api/devices/{id}/exec/holds` | `409`：在密封 WebSocket 工作階段內檢視保留工作 |
| `POST /api/devices/{id}/exec/holds/{job_id}/kill` | `409`：在密封 WebSocket 工作階段內停止保留工作 |
| `GET /api/devices/{id}/exec/holds/{job_id}/cat` | `409`：在密封 WebSocket 工作階段內讀取保留工作輸出 |
| `POST /api/devices/{id}/sensors` | `409`：在密封 WebSocket 工作階段內收集感測器資料 |
| `POST /api/devices/{id}/net` | `409`：在密封 WebSocket 工作階段內執行網路探測 |
| `POST /api/devices/{id}/update` | `409`：在密封 WebSocket 工作階段內檢查或套用更新 |
| `POST /api/devices/{id}/ubus` | `409`：在密封 WebSocket 工作階段內呼叫 ubus |
| `POST /api/devices/{id}/commands/{cmd}/run` | `409`：取得釘選命令，並以裝置授權的 manage 授權在密封 WebSocket 工作階段內執行 |

例如，下列有效動作請求是拒絕行為的檢查，並非命令執行介面：

```bash
curl -i -X POST "https://exchange.ivjn.us/api/devices/${DEVICE_ID}/exec" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H 'Content-Type: application/json' \
  -d '{"command":"uname -a","timeout_secs":30}'
```

預期收到 HTTP `409`，英文錯誤訊息為 `Sealed sessions are required.`（依請求語言本地化）。請建立原生密封 WebSocket 工作階段來執行操作。此 HTTP 路由不會傳回成功的命令輸出回應。

### Exchange 工作階段管理

工作階段清單與取消仍可透過 HTTP 使用，因為它們管理的是 Exchange 路由中繼資料。它們不會執行裝置操作，也不會開啟額外的裝置工作階段。

| 方法與路徑 | 成功回應主體 |
| --- | --- |
| `GET /api/devices/{id}/sessions` | `{sessions:[DeviceSessionInfo], limit, count}` |
| `POST /api/devices/{id}/sessions/{session_id}/kill` | `{session_id, released}` |

`DeviceSessionInfo` 欄位為 `session_id`（裝置端數字 ID）、`operator_session_id`、`kind`、`hold`、`age_secs` 與 `deadline_unix_ms`。`limit` 為 `max_sessions_per_device`（預設 32）；`count` 為目前清單長度。列出時會直接讀取本機 Exchange 中樞狀態；在本機離線的裝置會傳回 `503`。Kill 使用**裝置端** ID，傳送適當的取消訊息（包括保留的 exec），即使裝置未回應也會在本機釋放路由。工作階段不存在或裝置在本機離線時，傳回 HTTP `200` 及 `released:false`。唯讀操作員存取這兩個端點都會收到 `403`。

```bash
curl -sS "https://exchange.ivjn.us/api/devices/${DEVICE_ID}/sessions" \
  -H "Authorization: Bearer ${TOKEN}"

curl -sS -X POST "https://exchange.ivjn.us/api/devices/${DEVICE_ID}/sessions/${SESSION_ID}/kill" \
  -H "Authorization: Bearer ${TOKEN}"
```

Exchange 在因工作階段數已滿而拒絕請求前，會回收操作員已失效與超過截止時間的路由：exec 為逾時時間 + 15 秒，檔案傳輸為 3600 秒 + 15 秒，其他種類為 24 小時。仍在截止時間內的保留 exec，在操作員斷線時會分離，而非遭到終止。

### 原生密封 CLI 操作

CLI 會在一般裝置操作中建立密封工作階段。請先完成工作區根核准與裝置授權；請參閱[密封工作階段](/docs/zh-TW/sealed-sessions.md)。無法選擇停用密封改用明文。一般命令不需要額外的密封旗標：

```bash
remote-cli exec "$DEVICE_ID" -- uname -a
remote-cli shell "$DEVICE_ID"
remote-cli exec-holds "$DEVICE_ID"
remote-cli exec-hold-cat "$DEVICE_ID" "$JOB_ID"
remote-cli exec-hold-kill "$DEVICE_ID" "$JOB_ID"
remote-cli sensors "$DEVICE_ID" --json
remote-cli net "$DEVICE_ID" ping 1.1.1.1
remote-cli net "$DEVICE_ID" http https://example.com/
remote-cli ubus "$DEVICE_ID" system board '{}'
remote-cli commands list "$DEVICE_ID"
remote-cli commands run "$DEVICE_ID" "$COMMAND_ID"
```

保留工作操作會在密封工作階段內執行；Exchange 工作階段管理則可另透過 `remote-cli sessions` 與 `remote-cli session-kill` 使用。取得釘選命令中繼資料不代表獲准執行：裝置必須接受該密封操作員的 manage 授權。

## 密封工作階段金鑰目錄與授權轉送

Exchange 儲存操作員公開金鑰及擁有者簽署的 `rsg1.` 授權權杖，讓用戶端能互相探索。**這本身不是授權。**裝置仍會依本機原則驗證擁有者簽章。Exchange 僅為權杖承載資料（`owner_key`、`operator_key`、`device_id`、`level`、`expires_at`）建立索引，絕不將上傳視為許可。Exchange 無法偽造根簽章，但可以扣留憑證、向自動核准的根用戶端不實呈現名冊、觀察中繼資料或拒絕服務。

工作區根（`rsr1.`）與授權邀請（`rsi1.`）也是同一類轉送。Exchange 會檢查權杖格式及 Ed25519 自簽章（邀請也必須符合已儲存的根金鑰），而**不會**決定核准哪些操作員金鑰。`GET /api/seal/roster` 是供超級管理員檢視成員、角色與 `operator_device_grants` 的端點。在根用戶端於本機核准前，用戶端不得將新出現的金鑰視為可信任。請參閱 `docs/sealed-sessions.md`。

任何已驗證的工作區成員都可列出金鑰、註冊自己的金鑰及上傳授權（授權可自行驗證）。金鑰的 `DELETE` 僅限註冊該金鑰的帳號（管理員可刪除任何金鑰）。授權的 `DELETE` 僅限上傳者（管理員可刪除任何授權）。`GET /api/seal/grants` 會略過已過期的資料列。

金鑰註冊證明是針對下列內容的 Ed25519 簽章：

```text
remote-seal-key-register:v1:<workspace_id>:<operator_id>:<public_key>:<issued_at_ms>
```

`issued_at_ms` 必須在伺服器時鐘的 ±10 分鐘內。`public_key` 與 `proof` 使用標準 base64（32 位元組金鑰、64 位元組簽章）。

| 方法與路徑 | 請求主體 | 成功回應 |
| --- | --- | --- |
| `POST /api/seal/keys` | `{"public_key","label"?,"proof","issued_at_ms"}` | 已註冊的金鑰（同一帳號採新增或更新） |
| `GET /api/seal/keys` | — | `[{operator_id, username, email, public_key, label, created_at, last_seen_at}]` |
| `DELETE /api/seal/keys/{key}` | — | `204`（`{key}` 是經 URL 編碼的公開金鑰） |
| `POST /api/seal/grants` | `{"tokens":["rsg1.…"]}` | `{stored:[…], errors:[…]}` |
| `GET /api/seal/grants?mine=1` | — | `operator_key` 為呼叫者已註冊金鑰之一的授權。每個 `owner_key` 不是已發布根金鑰的授權也會包含 `chain`（憑證權杖，可能為空） |
| `GET /api/seal/grants?owner_key=…` | — | 由該擁有者金鑰簽署的授權 |
| `DELETE /api/seal/grants/{id}` | — | `204` |
| `PUT /api/seal/root` | `{"token":"rsr1.…","rotation":"rsx1.…"?}` | 已儲存的工作區根。僅限超級管理員。會檢查格式及金鑰自身的簽章；這不代表核准。`rotation`（選用）必須由目前已發布的根簽署，並指明新的根；接著 Exchange 會向線上密封裝置推送 `seal_root {token, rotation}`，裝置僅在輪替資料由已信任的根簽署時才重新釘選。沒有有效輪替資料時，已釘選舊根的裝置會保留舊根 |
| `GET /api/seal/root` | — | `{workspace_id, root_key, payload, signature, token, created_at}`，若尚未發布則為 `404` |
| `POST /api/seal/invitations` | `{"token":"rsi1.…"}` | 已儲存的邀請。僅限超級管理員，且根必須已存在 |
| `GET /api/seal/invitations` | — | 未過期的邀請，最新的優先（沒有時為 `[]`） |
| `GET /api/seal/roster` | — | `{members:[{operator_id, username, email, role, restricted, enabled, keys:[{public_key, label}], device_grants:[{device_id, can_manage}]}]}`。僅限超級管理員。包含已停用的成員 |

目錄無法使用時仍須滿足密封會話和裝置授權要求。尚未發布工作區根時，`GET /api/seal/root` 返回 `404`。

```bash
# Register this operator key (proof signed locally)
curl -sS -X POST "$BASE/api/seal/keys" \
  -H "Authorization: Bearer ${TOKEN}" -H 'Content-Type: application/json' \
  -d '{"public_key":"<ed25519-b64>","label":"cli","proof":"<sig-b64>","issued_at_ms":1700000000000}'

curl -sS "$BASE/api/seal/keys" -H "Authorization: Bearer ${TOKEN}"

curl -sS -X POST "$BASE/api/seal/grants" \
  -H "Authorization: Bearer ${TOKEN}" -H 'Content-Type: application/json' \
  -d '{"tokens":["rsg1.…"]}'

curl -sS "$BASE/api/seal/grants?mine=1" -H "Authorization: Bearer ${TOKEN}"
```

### 中繼憑證（`rsc1.`）

Exchange 儲存中繼憑證，讓非根金鑰也能簽署裝置會接受的授權。它會**驗證**簽發者的 Ed25519 簽章，且**無法偽造**憑證。上傳權杖不代表授權：呼叫者必須是簽發者的操作員。由根簽署的憑證（`issuer_key` 等於已發布的工作區根）僅可由工作區超級管理員發布。中繼憑證僅可由已註冊密封金鑰為 `issuer_key` 的帳號發布，且該簽發者本身必須是已儲存、未撤銷、未過期且具備 `can_issue` 的憑證主體。子憑證的層級、裝置範圍與到期時間必須受限於父憑證（與裝置所做的檢查相同）。重新發布相同的 `(issuer_key, subject_key)` 會取代舊資料列並清除撤銷狀態。

`GET /api/seal/certs/chain?subject_key=` 會沿著簽發者鏈結向上追溯至已發布的根，傳回 `{chain:[tokens]}`，最多 4 張憑證，由根簽發的排在最前，或傳回 `404`。任何已驗證成員都可呼叫。`GET /api/seal/grants?mine=1` 會在非根簽署的授權中加入該 `chain`，讓用戶端隨授權一併取得。由根簽署的授權會省略 `chain`。

`GET /api/seal/certs` 會列出所有資料列，包括已撤銷者（供管理員與超級管理員使用）。`DELETE /api/seal/certs/{serial}`（由簽發者帳號或超級管理員呼叫）會設定 `revoked`。這只會停止分發。裝置仍會接受已取得的副本，直到其過期；硬性撤銷需使用裝置原則的 `revoked_keys`。`{serial}` 是經 URL 編碼的標準 base64 序號。

| 方法與路徑 | 請求主體 | 成功回應 |
| --- | --- | --- |
| `POST /api/seal/certs` | `{"token":"rsc1.…"}` | 已儲存的憑證（依簽發者與主體新增或更新） |
| `GET /api/seal/certs/chain?subject_key=` | — | `{chain:["rsc1.…"]}`，由根簽發的優先，或 `404` |
| `GET /api/seal/certs` | — | `[{serial, issuer_key, subject_key, token, expires_at_ms, created_at, revoked}]`。管理員或超級管理員 |
| `DELETE /api/seal/certs/{serial}` | — | `204`。簽發者或超級管理員。僅停止分發 |

```bash
curl -sS -X POST "$BASE/api/seal/certs" \
  -H "Authorization: Bearer ${TOKEN}" -H 'Content-Type: application/json' \
  -d '{"token":"rsc1.…"}'

curl -sS "$BASE/api/seal/certs/chain?subject_key=<ed25519-b64>" \
  -H "Authorization: Bearer ${TOKEN}"
```

### 工作區根金鑰備份（選用）

`PUT` / `GET` / `DELETE /api/seal/backup` 為每個帳號儲存一個不透明的 `rsb1.` 資料塊。`PUT` 與 `GET` 僅限超級管理員：資料塊是工作區的**根**私密金鑰，在用戶端以 Argon2id（64 MiB、t=3、p=1）與 ChaCha20-Poly1305 加密。通關片語為選用，且沒有強制強度要求。**若通關片語為空白或強度不足，任何持有資料塊的人（包括 Exchange）都能解密根金鑰並控制所有裝置。**上傳是選用的；用戶端也可匯出根金鑰（`rsk1.`）並離線保存。Exchange 會檢查 `blob` 是否以 `rsb1.` 開頭及其長度，絕不解密。`GET` 在每個處理程序內限制為每個帳號每小時 5 次請求（HTTP 429）。讀取、寫入與刪除均會稽核，但僅記錄位元組數。回應會傳送 `Cache-Control: no-store`。

| 方法與路徑 | 請求主體 | 成功回應 |
| --- | --- | --- |
| `PUT /api/seal/backup` | `{"blob":"rsb1.…"}` | `{blob, updated_at}` |
| `GET /api/seal/backup` | — | `{blob, updated_at}`，未儲存時為 `404`。有速率限制 |
| `DELETE /api/seal/backup` | — | `204`，即使資料列不存在亦同 |

```bash
curl -sS -X PUT "$BASE/api/seal/backup" \
  -H "Authorization: Bearer ${TOKEN}" -H 'Content-Type: application/json' \
  -d '{"blob":"rsb1.…"}'

curl -sS "$BASE/api/seal/backup" -H "Authorization: Bearer ${TOKEN}"

curl -sS -X DELETE "$BASE/api/seal/backup" -H "Authorization: Bearer ${TOKEN}"
```

## 瀏覽器擴充功能的根憑證鏈驗證

瀏覽器操作使用原生密封 WebSocket 工作階段，而非 HTTP 單次執行。擴充功能會自行產生 Ed25519 操作員金鑰。它使用操作員 Bearer 權杖，從 `/api/auth/me` 取得 `tenant_id` / `operator_id`，再以 `{public_key, label, proof, issued_at_ms}` 呼叫 `POST /api/seal/keys`。PoP 簽署的內容為：

```text
remote-seal-key-register:v1:<workspace_id>:<operator_id>:<public_key>:<issued_at_ms>
```

金鑰／簽章使用標準 base64（32/64 位元組）；時間必須在伺服器時鐘的 ±10 分鐘內。註冊僅證明持有金鑰，不代表授權。擴充功能會同步 `GET /api/seal/root`、`GET /api/seal/grants?mine=1`，以及授權內附或從 `GET /api/seal/certs/chain?subject_key=<url-encoded-key>` 取得的憑證權杖。授權必須能直接或經由有效憑證鏈驗證至可信任的工作區根，並具備正確的裝置範圍、層級與有效期限。裝置權杖註冊與此操作員權杖目錄流程仍彼此獨立。

擴充功能會透過其根驗證收到的原生 hello，並將自己簽署的 hello／授權／憑證鏈放入 `SealAccept.peer`。CLI／主控台會驗證該證明，並要求 `peer.operator_key == accept.device_key`，才會接受已簽署回應。擴充功能擔任操作員時，Rust 代理使用其一般根原則。證明無效或缺少時會拒絕連線；沒有獨立的瀏覽器信任儲存區／介面或舊版備援機制。原生裝置與工作區根的首次使用即信任（TOFU）仍然存在；擴充功能會為每個伺服器／工作區儲存首次取得的根，並拒絕變更。

**安全性：**請透過帶外方式驗證首次取得的根；PoP 或伺服器角色不代表根已核准。在根用戶端自動核准前，請審查未知金鑰。保護端點設定檔／金鑰與託管指令碼。從目錄刪除不會撤銷已快取的簽署憑證。Exchange 可看到中繼資料、扣留憑證或拒絕服務；密封機制無法隔離具敵意且擁有完整 CDP 權限的操作員。

## 選擇 WebSocket 或 HTTPS

| 需求 | 使用方式 |
| --- | --- |
| 命令、保留工作檢視或取消、感測器、網路探測、更新、ubus、釘選命令 | 原生密封 WebSocket 工作階段 |
| 互動式 PTY、串流輸出、檔案傳輸、Android 螢幕／樹狀結構／點按 | 原生密封 WebSocket 工作階段 |
| 驗證、裝置清單、金鑰／授權／憑證目錄、根備份 | HTTPS API |
| Exchange 路由工作階段清單與取消 | HTTPS 工作階段管理端點 |
| 即時裝置在線狀態與長效操作員連線 | WebSocket；連線上的裝置操作仍需要密封 |

瀏覽器票證流程（憑證絕不放入 WebSocket URL）：

1. 以 `Authorization: Bearer …` 呼叫 `POST /api/ws-ticket`。
2. 連線至 `GET /ws/browser`，並傳送第一個 JSON 影格 `{"ticket":"…"}`。
3. 在傳送任何裝置操作之前，先建立原生密封用戶端／裝置握手。操作員的授權與憑證鏈必須能通過裝置信任的根驗證。

原生用戶端使用 `GET /ws/operator`，並採用操作員驗證及相同的密封工作階段要求。僅有 WebSocket 連線或成功的帳號登入，不代表獲准執行裝置操作。請參閱 [Web 主控台與 Web Push](/docs/zh-TW/web-console-and-notifications.md)、[通訊協定](/docs/zh-TW/websocket-protocol.md)及[密封工作階段](/docs/zh-TW/sealed-sessions.md)。

## 錯誤狀態速查

| 狀態 | 常見原因 |
| --- | --- |
| `400` | 動作輸入無效，例如空白命令或無效的更新來源 URL；驗證發生在密封工作階段拒絕之前 |
| `401` | 缺少或無效的 Bearer 權杖 |
| `403` | 帳號或裝置權限未授權此請求 |
| `409` | 有效且已授權的 HTTP 裝置動作需要密封 WebSocket 工作階段；不會向裝置派送 |
| `404` | 依端點契約，目錄項目不存在或尚未發布工作區根 |
| `429` | 端點特定的速率限制，包括工作區根備份讀取 |
| `503` | 工作階段清單在本機 Exchange 中樞狀態中找不到裝置 |

裝置動作 HTTP 路由不會等待代理輸出，因此正常已授權的結果是 `409`，而非執行成功、離線派送錯誤或 HTTP 執行逾時。中繼資料、驗證與目錄 API 的錯誤仍保留各端點特定的含義。

## 手動驗證計畫

使用具備已知權限的測試帳號與裝置。HTTP 拒絕檢查不需要在裝置上執行作業：

1. 透過 HTTPS 驗證並列出裝置；確認帳號與裝置權限。
2. 向拒絕表格中的每個裝置動作路由提交有效請求。預期收到 HTTP `409`，並確認沒有任何動作抵達代理，包括保留工作讀取與釘選命令執行。
3. 分別檢查無效憑證、禁止存取的帳號與無效請求輸入。預期收到驗證、權限與輸入驗證錯誤，而非假設每個請求都傳回 `409`。
4. 讀取 Exchange 工作階段清單。針對明確選定、可捨棄的工作階段測試取消，並驗證 `released`；工作階段不存在時會傳回 `released:false`。對在本機離線的裝置列出工作階段會傳回 `503`。
5. 完成根核准與授權，再執行無害的原生 CLI 操作，例如 `remote-cli exec "$DEVICE_ID" -- uname -a`。確認它透過密封握手成功執行。裝置授權缺少或無效時，必須拒絕執行。
6. 分別驗證金鑰註冊、授權／憑證擷取、選用根備份與瀏覽器根憑證鏈驗證；這些 HTTPS API 仍依上述契約提供服務。

## 稽核推送目標

僅 PostgreSQL Exchange 支援，依已驗證工作區隔離，只有管理員和超級管理員可用；其他帳號回傳 403。Worker 版暫不支援，Web / 行動 Console 沒有設定介面。

```text
GET    /api/audit/sinks
POST   /api/audit/sinks
PATCH  /api/audit/sinks/{id}
DELETE /api/audit/sinks/{id}
POST   /api/audit/sinks/{id}/test
GET    /api/audit/sinks/status
```

```sh
printf '%s' '{"name":"archive","kind":"webhook","endpoint":"https://audit.example.net/events","secret":"REPLACE-WITH-RANDOM-SECRET","mode":"full"}' | remote-cli audit sink add
remote-cli audit sink list
printf '%s' '{"interval_ms":500,"batch_size":16}' | remote-cli audit sink update 1
remote-cli audit sink test 1
remote-cli audit sink status
remote-cli audit sink remove 1
```

預設 throttled 直接傳送資料庫稽核記錄：文字超過 256 KiB 保留前 250 KiB / 後 6 KiB，檔案只有 hash，串流在工作階段結束後傳送。full 在截斷前擷取，原始 JSON 和二進位分塊（預設 1 MiB）推送，含操作 / 資產 ID、序號、位移、結束標記、SHA-256 和總大小。sealed 兩者都只有中繼資料及密文參照 / hash，不能還原端點明文。

secret 和 headers 加密儲存，讀取僅顯示 ***；PATCH *** 保留，header 的 null 刪除。非同步持續性 FIFO 佇列與指數退避重試。節流滿佇列刪除最舊並計數；全量磁碟佇列滿則背壓。status 顯示成功 / 失敗、積壓、dropped、gaps。刪除會清除積壓且記錄稽核。設定變更在同一交易稽核。

webhook 使用 JSON events 陣列及 base64 二進位。X-Remote-Timestamp 與 X-Remote-Signature: sha256=<hex> 使用 HMAC-SHA256(timestamp + "." + 原始 body)。OTel 使用 OTLP/HTTP JSON /v1/logs，區塊放 body.bytesValue，屬性 remote.device.id / remote.operator / remote.action；service.name 可設定。gRPC 須透過 Collector 轉送，不直接支援。

以下是預設設定、webhook 載荷、驗證簽章和 Collector 範例。驗證使用原始 HTTP body；持續儲存並依 delivery_id 去重後才回傳 2xx。Loki 3.x 啟用 allow_structured_metadata，在 Grafana 新增 Loki 資料來源並查詢 {service_name="remote-exchange"}。gRPC 將 pipeline exporters 改為 [otlp/grpc]。正式環境應使用 TLS 和驗證。

```json
{"name":"collector","kind":"otel","mode":"throttled","endpoint":"https://collector.example.net:4318/v1/logs","headers":{"Authorization":"Bearer REPLACE"},"service_name":"remote-exchange","resource":{"deployment.environment.name":"production"},"batch_size":32,"interval_ms":1000,"chunk_bytes":1048576,"queue_limit":10000,"overflow_bytes":10737418240,"actions":[],"devices":[],"operators":[],"enabled":true,"ca_pem":null}
```

```json
{"version":1,"events":[{"delivery_id":123,"type":"full","mode":"full","entry":{"id":42,"action":"exec","operator_name":"alice","device_id":"dev"},"data":{"kind":"binary","asset_id":"asset-uuid","path":"/data_base64","sequence":0,"offset":0,"last":true,"data_base64":"YWJj","total_size":3,"sha256":"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad","context":{"direction":"output","frame_sequence":0,"stream_offset":0}}}]}
```

```python
import hashlib, hmac, time

def verify(headers, raw_body, secret):
    stamp = headers['X-Remote-Timestamp']
    if abs(time.time() - int(stamp)) > 300:
        raise ValueError('expired webhook')
    expected = 'sha256=' + hmac.new(
        secret.encode(), stamp.encode() + b'.' + raw_body, hashlib.sha256
    ).hexdigest()
    if not hmac.compare_digest(expected, headers['X-Remote-Signature']):
        raise ValueError('invalid signature')
```

```yaml
receivers:
  otlp:
    protocols:
      http:
        endpoint: 127.0.0.1:4318
processors:
  batch: {}
exporters:
  otlphttp/loki:
    endpoint: http://loki:3100/otlp
  otlp/grpc:
    endpoint: telemetry.example.net:4317
    headers:
      Authorization: "${env:OTEL_AUTHORIZATION}"
    tls:
      insecure: false
service:
  pipelines:
    logs:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlphttp/loki]
```

| 稽核欄位 | OTLP 欄位 |
|---|---|
| service_name / resource | Resource service.name / attributes |
| entry.started_at | timeUnixNano |
| entry.id / delivery_id | remote.audit.id / remote.delivery.id |
| entry.device_id / operator_name / action | remote.device.id / remote.operator / remote.action |
| entry.tenant_id | remote.workspace.id |
| entry.started_at / ended_at / detail | remote.started_at / remote.ended_at / remote.result |
| traceId / spanId | traceId / spanId |
| data.data_base64 | body.bytesValue / remote.chunk.* |
| stored events | body.stringValue |
