管理服務帳號
誰: 部門管理者,針對自己目前所屬的部門,路徑為 /service-accounts…。公司管理者可透過鏡像路徑 /company/departments/{department_id}/service-accounts… 管理公司內任何存活部門(請求與回應相同;department_id 不存在、已刪除或屬於其他公司 → 404 Department not found)。部門成員無法列出或讀取服務帳號,服務帳號呼叫者會被拒絕。路徑相對於模組基底。{id} 是 UUID(其他值 → 422)。概念:服務帳號。
路由
| 路由 | Body | 成功 | no-store |
|---|---|---|---|
POST /service-accounts | 無 | 201 ServiceAccountView | 否 |
GET /service-accounts?limit=&cursor= | — | 200 ServiceAccountListResponse | 否 |
GET /service-accounts/{id} | — | 200 ServiceAccountDetail | 是 |
POST /service-accounts/{id}/credential/reset | 無 | 200 ServiceAccountResetResponse | 是 |
DELETE /service-accounts/{id} | — | 200 ServiceAccountDeleteResponse | 是 |
PUT /service-accounts/{id}/chatrooms | ServiceAccountChatroomsRequest | 200 ServiceAccountChatroomsResult | 是 |
GET /service-accounts/{id}/chatroom-candidates?limit=&cursor=&q= | — | 200 ServiceAccountChatroomCandidates | 是 |
沒有綁定路由:綁定是 Worker 操作設定中的欄位(見下方「綁定到操作」)。這些路由沒有路由專屬的 413。
建立
POST /service-accounts
→ 201 {
"id": "…", "company_id": "…", "department_id": "…",
"username": "EdgeWorkerSA-20261008T123456Z", "nickname": "EdgeWorkerSA-20261008T123456Z",
"email": "sa-…@service-account.invalid",
"role_id": 0, "auth_source": "service_account", "is_service_account": true,
"enabled": true, "is_verified": true, "created_at": "…"
}| 欄位 | 限制 |
|---|---|
id、company_id、department_id | UUID |
username | ^EdgeWorkerSA-[0-9]{8}T[0-9]{6}Z(?:-(?:[2-9]|[1-9][0-9]))?$,≤ 32 個字元 |
nickname | 1–64 個字元(等於 username) |
email | ^sa-[0-9a-f-]{36}@service-account\.invalid$,≤ 128 個字元 |
role_id / auth_source / is_service_account | 一律 0 / "service_account" / true |
enabled、is_verified | 布林值(true) |
created_at | 時間戳記 |
新帳號還無法登入:首次綁定時才會取得憑證。目前這個 UTC 秒的 99 個名稱都被占用 → 409 Service account name is unavailable;一秒後重試。
列出
GET /service-accounts?limit=100
→ { "items": [ { …ServiceAccountView… } ], "next_cursor": "100" }limit 1–100(預設 100)。cursor 是上一頁的 next_cursor:最多 7 位數的十進位位移量(否則為 422 驗證錯誤),≤ 1 000 000(否則 422 Invalid cursor)。依 created_at、再依 id 遞增排序;最後一頁的 next_cursor 為 null。列表只讀取資料庫。
讀取單一帳號
GET /service-accounts/{id}
→ {
…ServiceAccountView fields…,
"rooms": [ { "id": "…", "name": "Field ops", "department_id": "…", "department_name": "Support", "is_member": true } ],
"rooms_truncated": false,
"bound_operations": ["syncProfile"],
"credential": { "present": true, "updated_at": "…" }
}rooms:帳號所屬的存活聊天室,最多 500 個,由舊到新;超過時rooms_truncated: true。未命名聊天室的name為"",department_name≤ 32 個字元(未知時為"")。bound_operations:至多 64 個操作 id(已排序),其在線上 release 中的有效綁定為此帳號(自己的綁定或經由部門預設)。除非線上 release 支援服務帳號憑證,否則為[]。credential:Worker 未部署時(沒有 Worker、不是active/paused、沒有目前的deployed修訂版)為null;線上 release 早於服務帳號支援,或 Worker 未持有此帳號的憑證時為{"present": false, "updated_at": null};Worker 持有時為{"present": true, "updated_at": …}(updated_at= Worker 儲存目前憑證的時間)。- 無法讀取 Worker 狀態時 → 503
Department Worker service-account credentials are unavailable、Department Worker operation settings are unavailable或Deployed department Worker contract is unavailable。
聊天室
帳號透過 TeamSync API 能讀寫的範圍,等同其部門中一位位於這些聊天室的 role-0 成員,所以聊天室成員資格就是它的資料存取權。PUT /service-accounts/{id}/chatrooms 接受 {"add": [...], "remove": [...]}(聊天室 UUID,各 ≤ 100 個,不可重疊,否則為 422 驗證錯誤,其 msg 含 add and remove must be disjoint),並逐一獨立處理每個聊天室:results 列出 chatroom_id 與 status(added、already_member、removed、not_member、not_found、forbidden;前四種另帶 department_id/department_name),member_room_count 是處理後帳號所屬的聊天室數。forbidden 表示你無法管理該聊天室:部門管理者可使用自己部門的聊天室與自己建立的聊天室,公司管理者可使用公司內任何聊天室。GET /service-accounts/{id}/chatroom-candidates 列出你能管理的聊天室並附上 is_member,分頁方式同列表(q ≤ 128 個字元,依名稱篩選)。這兩個路由是把服務帳號加入聊天室的唯一途徑。
綁定到操作
前提:Worker 的線上 release 以 builder 政策 department-worker-project-v2.9 或更新版本建置(重新上傳專案並部署一次;否則 409 service_account_credentials_require_supported_release),且部署環境的 HOST 是單純的 HTTPS origin(否則 503 api_base_unconfigured)。
先讀取目前的設定,再連同綁定送回。列表必須列出線上 manifest 的每個操作(422 operation_settings_incomplete)。每個操作的 access 請照原樣送回,因為 access: null 代表「繼承 Worker 全域政策」。完整規則:管理逐操作存取。
GET /worker/operation-settings
→ { "state": "reconciled", "version_id": "…", "revision": 4, "default_service_account_id": null, "default_service_account": null,
"operations": [ { "operation_id": "health", "access": null, "service_account_id": null, … },
{ "operation_id": "syncProfile", "access": null, "service_account_id": null, … } ] }
PUT /worker/operation-settings
{ "base_revision": 4,
"operations": [ { "operation_id": "health", "access": null },
{ "operation_id": "syncProfile", "access": null, "service_account_id": "…" } ] }
→ …, "operations": [ …, { "operation_id": "syncProfile", "service_account_id": "…",
"effective_service_account": { "id": "…", "username": "EdgeWorkerSA-20261008T123456Z", "source": "operation", "live": true }, … } ]- 每個操作的
service_account_id,以及供所有沒有自己綁定之操作使用的default_service_account_id。省略欄位即保留目前的綁定;null會清除它(操作改為繼承預設;預設被清除則不綁定任何帳號)。 - 設定表變更之前,後端會為 Worker 尚未持有的每個綁定帳號交付憑證。任一交付失敗時,設定表不會變更:503
Department Worker service-account credentials are unavailable、409service_account_limit_exceeded(每個 Worker 256 個憑證)、503service_account_verifier_unavailable,或 503worker_release_unavailable(請重試)。 - 同樣的欄位也可放在
POST /worker/deployments的operation_settings區塊,此時只有列出的操作會變更。之後的部署即使不帶此區塊,也會延續存續操作的綁定與預設。
端到端範例
- 提交並部署一個修訂版,讓線上 release 達到 builder 政策 v2.9 或更新版本(提交修訂版、部署與輪詢)。
POST /service-accounts→ 記下id。PUT /service-accounts/{id}/chatrooms,加入該操作需要的聊天室。PUT /worker/operation-settings,把syncProfile綁定到id(見上文)。GET /service-accounts/{id}此時顯示"credential": {"present": true, …}與"bound_operations": ["syncProfile"]。- handler 以該帳號身分呼叫 TeamSync API:
export async function syncProfile({ teamsync }) {
try {
const res = await fetch(`${teamsync.apiBase}/private/user/me`, {
headers: await teamsync.authHeaders(),
})
return Response.json(await res.json(), { status: res.status })
} catch (error) {
if (error?.name === "TeamSyncServiceAccountError") {
return Response.json({ code: error.code }, { status: 503 })
}
throw error
}
}各 code 值(service_account_not_bound、operation_settings_pending、service_account_unavailable、service_account_sign_in_failed、service_account_rate_limited)的說明見服務帳號。Edge Workers 成員路由會拒絕該帳號的 token(403 Service accounts cannot invoke Edge Workers),管理者路由則回應 403 Insufficient permissions.,所以它無法呼叫或管理 Worker。
重設憑證
POST /service-accounts/{id}/credential/reset
→ { "delivery": "delivered" } # or not_deployed | unsupported_release | not_bound | failed舊機密在每一種結果下都會失效;新機密不會顯示給任何人。delivered 表示 Worker 已持有新機密。not_deployed、unsupported_release 與 not_bound 表示沒有任何東西使用此帳號,舊機密僅被撤銷。failed 表示無法讀取 Worker 的憑證狀態,或交付失敗、結果不確定:舊機密照樣撤銷(只要曾嘗試交付新機密,新機密即已提交),綁定的操作可能登入失敗,直到你再次重設並得到 delivered。交付問題永遠不會以獨立的狀態碼出現。
409 {"code":"deployment_in_progress","message":"Another Worker settings mutation or deployment is in progress"} 只在部門的設定鎖被持有時出現:由操作設定寫入、部署暫存、另一個重設或服務帳號刪除持有(鎖的存活時間 600 秒)。只有線上 release 支援服務帳號憑證時,重設才會取得這個鎖;進行中部署的其餘階段不會擋住它。鎖儲存失敗時為 503 Operation-settings mutation lock is unavailable。
移除或替換可疑的 Worker 程式碼之後,請重設該 Worker 用過的每一個服務帳號;維運人員輪替 JWT_SECRET_KEY 之後也一樣。release 中的程式碼能觀察到它所用帳號的機密;被截取的機密在下一次重設或刪除前都有效。部署永遠不會輪替機密。
刪除
DELETE /service-accounts/{id}
→ { "deleted": true, "unbound_operations": ["syncProfile"] }帳號立即停止運作:它的 JWT 無法再解析,成員資格與 refresh token 都被移除。第二次 DELETE → 404。即使移除 Worker 中的憑證副本失敗或鎖正被占用,回應仍是 200。
unbound_operations(≤ 64,已排序)列出有效綁定為此帳號的線上操作(沒有支援服務帳號的線上 release,或無法讀取其綁定時為 [])。它們的綁定不會被移除:在你重新綁定之前一律失敗即關閉(fail closed)。刪除前先查看詳細資料中的 bound_operations;刪除後,送出 PUT /worker/operation-settings,在 service_account_id(或 default_service_account_id)中指名另一個存活帳號,或送 null 改用預設。重新送出已刪除的 id → 422 service_account_not_found;省略該欄位則保留失效的綁定。
錯誤
| 狀態 | detail | 位置 |
|---|---|---|
| 400 | Cannot modify yourself. | 階層檢查(防禦性) |
| 403 | Insufficient permissions. | role 低於管理者門檻;服務帳號 token 在這些路由上得到此回應(role 0) |
| 403 | Service accounts cannot invoke Edge Workers | 服務帳號 token 呼叫 Edge Workers 成員路由(例如 GET /worker、呼叫) |
| 403 | A live manager identity is required | 執行操作的管理者已不存在 |
| 404 | Service account not found | 不存在、已刪除、他部門或一般使用者的 id |
| 404 | Department not found | 公司鏡像路徑;建立期間部門被刪除 |
| 409 | Service account name is unavailable | 建立 |
| 409 | {"code":"deployment_in_progress", …} | 鎖被持有時的重設、操作設定寫入、部署暫存 |
| 409 | {"code":"service_account_credentials_require_supported_release", …} | 在 v2.9 之前的 release 上綁定;持有憑證時降版 |
| 409 | {"code":"service_account_limit_exceeded","message":"Worker service-account credential limit was reached"} | 綁定第 257 個帳號 |
| 409 | Service-account credential delivery is stale | 綁定交付衝突 |
| 422 | Invalid cursor(7 位數且大於 1 000 000 的 cursor);其他錯誤參數及重疊的 add/remove 得到驗證錯誤清單 | 列表、候選聊天室、聊天室 |
| 422 | [{"loc": ["body", "default_service_account_id"], "msg": "service_account_id is not a live service account of this department", "type": "service_account_not_found"}],對操作則為同一筆內的 "loc": ["body", "operations", <i>, "service_account_id"] | 操作設定或部署的綁定欄位;每個出問題的欄位各一筆;<i> 是 operations 清單中從 0 起算的位置(部署時這些欄位在 operation_settings 底下,但 loc 維持如上) |
| 503 | Department Worker service-account credentials are unavailable | 讀取詳細資料、綁定交付 |
| 503 | {"code":"api_base_unconfigured","message":"This deployment's HOST is not a bare HTTPS origin"} | 綁定 |
| 503 | {"code":"service_account_verifier_unavailable","message":"Service-account credentials cannot be verified by this deployment"} | 綁定(重設回報 failed) |
| 503 | Operation-settings mutation lock is unavailable | 重設、綁定 |
detail 可能是字串或 {code, message} 物件。完整目錄:錯誤。