架構與信任邊界
這個模組讓每個 TeamSync 部門擁有一個小型 HTTP Worker。它是部門擁有的整合介面,不是通用 serverless 平台:每部門一個 Worker(資料庫強制)、僅限 JavaScript/TypeScript、以標準化壓縮檔提交、由無憑證的平台 builder 建置、以不可變修訂版部署。
兩種 runtime
後端依 EDGE_WORKERS_PROVIDER 選擇 runtime:未設定或 cloudflare(TeamSync 雲端)時,部門 Worker 跑在 Cloudflare Workers for Platforms 上;onprem 由安裝了 Edge Workers 附加元件的地端套件營運者設定,同一份 bundle 改跑在自架的 workerd 上(每個 Worker 一個行程、位於 relay 之後,產物存在套件自己的儲存)。Worker 專案、builder、成員 API、生成的 OpenAPI、存取模式與兩條通道在兩者上都相同;限制、對外連線與公開 URL 則不同(地端)。除非特別註明,本頁其餘內容描述雲端 runtime。
拓撲
department member public caller
│ TeamSync JWT │ (per access mode:
│ /private/module/edge-workers/… │ X-TeamSync-Worker-Key / IP)
▼ ▼
TeamSync backend ──30 s EdDSA assertion──▶ dispatch Worker ◀── /public/{script}
(tenant, contract, artifact, (verify / sanitize / forward)
deployment authority) │ script lookup across bound locations
│ ▼
│ control plane department user Worker
▼ (generated router + bundle
Cloudflare API ── PUT script ──▶ + SQLite Durable Object)
R2 (archives + bundles) │ any fetch() → destination (unrestricted)
▼
destination (no Outbound Worker; fetch() is the only
outbound primitive — cloudflare:sockets is not importable;
bounded by default to 500 subrequests / 50 ms CPU per call)- Worker 位置:staging 與 production 的 dispatcher 都綁定同一組 Workers for Platforms dispatch namespace — 一個共用位置加兩個舊有位置 — 由所有部門 Worker 共用;沒有 per-company 或 per-department 的 namespace。兩條通道上的每個請求,dispatcher 都先找這個 script 的啟用 — 由伺服器掌控的啟用對照表中的條目,或平台維運者搬移 Worker 到另一個位置時由後端記錄的啟用。這樣釘選的 script 只有一個候選位置,若該位置沒有這個 script 就回 404
worker_not_found,絕不改找其他位置(除非 dispatcher 在當下無法查找啟用,此時它改用下文所述的固定順序;見調度協定)。在簽章通道上,後端呼叫它已記錄位置的 Worker 時(不一定每次呼叫都如此),還會在斷言的serving_targetclaim 中自行指名 Worker 的位置(調度協定),dispatcher 便只問那一個位置(例外見調度協定)。Worker 由哪個位置服務是平台的紀錄,不是呼叫者能選擇的:沒有任何回應欄位會回報它(診斷 headerx-teamsync-serving-namespace與x-teamsync-routing-epoch會在由 Worker 產生的回應上指出它),部署永遠不會改變它,而新 Worker 的首次部署會落在其環境設定用來建立 Worker 的位置(EDGE_WORKERS_CLOUDFLARE_DISPATCH_NAMESPACE)。既沒有啟用也沒有 claim 的 script 依固定順序嘗試(先兩個舊有位置,再共用位置);第一個持有它的位置負責回答,而它回傳的任何結果 — 應用程式的 404、存取拒絕、已退役 Worker 的墓碑 — 都是最終結果。只有所有候選位置都回報 script 不存在時,dispatcher 才回 404worker_not_found。因此任一環境的 dispatcher 都可能服務某個部門的 Worker,而每個 API 環境仍保有自己的 dispatcher origin(即其公開 URL 的主機)。這些 namespace 維持 Cloudflare 預設的 untrusted 模式。地端沒有 dispatch namespace:edge host 依 script 名稱路由,未知或已退役的 script 同樣得到 404。 - Script 名稱是推導的,不可選擇:
dw-+sha256("edge-worker-v2\0" + company_id + "\0" + department_id)前 48 個 hex — 不透明、改名不變,後端、dispatcher 與部署腳本用完全相同的推導。公司+部門隔離靠 script 名稱。 - 兩條呼叫通道,都經過 dispatcher:
- 簽章通道(
/dispatch/*):後端驗證租戶後,帶著綁定 method/path/query/body/script/release generation 的 30 秒 Ed25519 簽章斷言轉發(調度協定)。任何存取模式下都可用;存取政策與逐操作准入永遠不套用在這條通道上。 - 公開通道(
/public/{script_name}/*):直接來自網際網路的流量,由部門的存取模式准入 — 修訂版支援逐操作存取時依比對到的操作准入 — 在生成的 runtime 內強制執行,dispatcher 只轉發證據(驗證過格式的 API 金鑰 header、用戶端 IP:雲端是 Cloudflare 提供的,地端是套件 nginx 看到的呼叫者位址)。
- 簽章通道(
- 雲端的對外連線不受限制:沒有任何綁定的位置掛著 Outbound Worker,部門 Worker 的
fetch()直接送達目的地 — 任何主機,進站與出站一視同仁。Cloudflare 同樣不再攔截 TCPconnect(),但部門 bundle 無法匯入cloudflare:sockets(builder 會以source_import_forbidden拒絕所有帶 scheme 前綴的 import specifier),因此在兩種 runtime 上,handler 程式碼唯一可用的對外原語都是全域fetch()。沒有 egress 允許清單、政策、功能旗標或環境變數可設定,而部署腳本會拒絕任何試圖加上 outbound binding 的 dispatcher 設定。唯一的界限是 dispatcher 對每個使用者 Worker 套用的每次呼叫資源上限 — 預設 500 個子請求與 50 ms CPU。簽章通道上,上限隨後端簽發的斷言傳遞(limitsclaim,來自後端的EDGE_WORKERS_LIMIT_SUBREQUESTS/EDGE_WORKERS_LIMIT_CPU_MS);公開通道上,dispatcher 使用自己的EDGE_WORKER_SUBREQUESTS(500)/EDGE_WORKER_CPU_MS(50)。超過任一項回 429worker_limit_exceeded(限制)。地端的對外連線可到達網際網路與客戶的區域網路,但排除169.254.0.0/16、fe80::/10、127.0.0.0/8、::1/128與 edge 控制子網路;地端沒有每次呼叫的 CPU 上限,改用牆鐘時間預算(EDGE_HOST_INVOCATION_TIMEOUT_MS,預設 30 秒;超過回 429worker_limit_exceeded),子請求額度則以每個 Worker 行程為單位共用。 - 儲存:每個部門 Worker 得到一個平台擁有的 SQLite Durable Object(
TEAMSYNC_DEPARTMENT_STORAGE→ classTeamSyncDepartmentStorage、實例department)— 唯一的 binding(地端由 workerd 執行同一個 class,SQLite 存在本機磁碟)。沒有執行期 R2/D1/KV/service binding;R2 純屬控制面儲存。
信任邊界
| 元件 | 信任什麼 | 永遠看不到什麼 |
|---|---|---|
| 後端 | JWT principal(租戶來自資料庫使用者列,絕非請求欄位) | — |
| Builder 子行程 | 什麼都不信(無憑證、無網路、6 個變數的乾淨環境) | Cloudflare/R2/租戶憑證 |
| Dispatcher | Ed25519 公開 JWK + issuer/audience 變數 + 由伺服器掌控的啟用(啟用對照表,以及後端所記錄的啟用) | 私鑰、Cloudflare token |
| 生成的 runtime | dispatcher 提供的 context header(mode/purpose/release generation/request id) | 調度斷言、私鑰、Cloudflare token(它不把原始 env、其他 binding 或 cloudflare:workers 交給 handler 程式碼) |
| 提交的 handler 程式碼 | 凍結的 {request, params, query, operationId, storage, config, secrets, teamsync} | env、控制路徑、儲存內部、API 金鑰、服務帳號憑證與 token |
secrets 只提供 get(name),用來讀取部門的 Worker 機密;teamsync 提供 apiBase 與 authHeaders(),後者回傳該操作所綁定服務帳號的 bearer header — 絕不回傳其憑證。背後的平台資料表不經任何 facade 對外開放,但 handler 程式碼與核准的相依套件和 runtime 跑在同一個 isolate 裡:請把部署到 Worker 的每一行程式碼都視為能讀到 Worker 持有的一切。
機密留在後端由 Infisical 管理的通道:Cloudflare API token 與 Ed25519 私鑰永不離開後端/部署行程;dispatcher 只拿到公開 JWK;R2 使用專屬 bucket 範圍的 EDGE_WORKERS_R2_* 憑證對(絕不用應用程式的 S3 憑證;地端則由套件的 edge-artifacts 儲存使用 EDGE_WORKERS_ARTIFACTS_*);使用者 API 金鑰(Worker 全域與逐操作)只儲存 SHA-256 摘要 + 後四碼。Worker 機密的值只存在該部門 Worker 的 Durable Object 裡;後端只在簽章的控制請求中送出一次值,自己只保留名稱與加鹽摘要。服務帳號憑證由後端在交付到 Worker 的 Durable Object 時才產生,任何 API 都不會回傳;後端只保留 HMAC 驗證值與加鹽摘要。
一份契約,兩個生成介面
manifest(teamsync.worker.json)同時驅動受信任的 runtime router(method/path 比對、404/405、關聯 ID、安全的 500 外層格式 — 由平台生成、接到專案的具名 export),以及後端生成的 OpenAPI 3.1.1(後端擁有 server URL、安全機制、閘道錯誤格式)。實作程式碼永遠不是獨立的契約權威,呼叫者也永遠無法控制 servers、security 或錯誤格式。見 Manifest 契約。
Release generation:防混合狀態的圍欄
sha256("teamsync-edge-worker-release-generation-v2\0" + project_digest + "\0" + dependency_digest + "\0" + builder_policy_version) — 在 bundle 之前就能算出來,所以生成的 router 可以內嵌它而不需要 bundle 摘要的自我參照。後端在每個簽章請求上從已部署的版本列重新計算;dispatcher 以驗證過的 header 轉發;router 在選路之前與自己內嵌的值比較,不一致就回 503 release_generation_mismatch。Cloudflare 傳播期間(或地端切換 release 時),舊請求打到新 bundle(或反過來)會快速失敗,而不是在錯的程式碼上執行。調升 builderPolicy.version(目前 department-worker-project-v2.10)會改變每個新建置的 generation — 同一壓縮檔重建後會得到不同的 release。修訂版建置時的 policy 也決定它擁有哪些 runtime 功能(Worker 機密、逐操作存取、服務帳號):runtime 是生成進 bundle 的,所以 Worker 要在新修訂版建置並部署後才會得到這些功能(提交管線)。
公開通道刻意不帶 release generation — 公開流量永遠打到當下上線的修訂版。
各元件跑在哪
- API replicas(FastAPI):所有路由,加上 60 秒回收迴圈(部署重發、控制面稽核調節、產物清理調節),以及每 15 秒重試已刪除部門 Worker 退役作業的迴圈(退役)。
- 背景 worker(GCP 用 Pub/Sub consumer,其他環境用 SQS/Batch/legacy):部署任務。
- Cloudflare(TeamSync 雲端):dispatcher Worker(只能由
deploy-platform.mjs佈建 — 見平台佈建)、各部門 Worker、其 Durable Object,以及 R2。任何綁定的位置上都不存在 Outbound Worker。 - 地端套件(
EDGE_WORKERS_PROVIDER=onprem):附加元件的 edge host(每個部門 Worker 一個 workerd 行程)、其 relay,以及專屬的產物儲存(地端)。 - API 文件:TeamSync 網頁應用程式中部門 Worker 的「API 文件」分頁會渲染後端的
GET /worker/openapi.json;沒有獨立的文件服務,也沒有任何東西生成第二份 spec。