Manifest 契約
teamsync.worker.json(manifestVersion: "2")是 manifest 為先的 API 契約:後端從它推導 OpenAPI 3.1,平台從它生成 runtime router。實作程式碼永遠不是獨立的契約權威 — 每個 operationId 都必須對應到宣告 entrypoint 可達的具名 export,而 router 擁有 method/path 比對、404/405、關聯 ID 與安全的 500 外層格式。
專案形狀
一次提交是一個 gzip tar 壓縮檔(department-worker.tar.gz),根目錄佈局固定如下:
teamsync.worker.json # the contract
package.json # private, type: module, catalog deps only
package-lock.json # lockfileVersion 3, exact catalog match
tsconfig.json # pinned compiler contract (required even for JS)
src/
index.ts # entrypoint: re-exports one handler per operationId
**/*.ts|.mts|.js|.mjs # sources
data/**/*.json # optional non-secret immutable JSON開發者專案裡還有 README.md、.teamsyncignore 與 tests/ — 本機必備,但永不出貨(pack 只收 4 個根檔案 + src/**)。已經具備這個佈局的範本可以從後端下載(範本與原始碼下載)。
module.name/module.version 是不可變的專案身分,必須與 package.json 和 package-lock.json 根層的 name/version 完全相等,讓 API 視圖、SBOM 根元件與建置溯源共用同一身分。
Handlers
handler 收到一個凍結的輸入,回傳 Response(或其 promise):
export async function getUser(
{ request, params, query, operationId, config, secrets, storage }: TeamSyncWorker.HandlerInput<"getUser">
): Promise<Response> { … }params— 已解碼、通過圍欄驗證的路徑參數(凍結)。query—URLSearchParams。storage— Durable Object 儲存介面。config— 執行期設定介面(configured、revision、await config.get(...))。secrets— Worker 機密介面:await secrets.get("NAME")以字串回傳機密,未設定時回傳undefined。以 builder 政策 v2.6 或更新版本建置的 bundle 才有。
執行期的輸入還帶有 teamsync(服務帳號輔助物件,需要 builder 政策 v2.9 或更新版本;服務帳號)。產生的 HandlerInput 型別宣告了 secrets,但沒有宣告 teamsync。
Tag、文件與 example
api.tags 是給 API 文件分組用的有界目錄:kebab slug 唯一 + 顯示 name 唯一;每個操作宣告恰好一個 tag;未知/重複/未使用的 tag 都會驗證失敗。生成的 OpenAPI 顯示 display name,並以 x-teamsync-slug 保留 slug。每個新操作都需要非空的 summary 與有用的 description。
參數與 JSON body 各可帶一個合成的 example。example 會成為不可變 manifest 與已發布 OpenAPI 契約的一部分,因此絕不接受憑證:自動篩查會拒絕像機密的鍵名與 19 種憑證格式(包括 TeamSync 自己的 tsw_ 金鑰與一般 JWT),但無法證明任意不透明字串是安全的 — 使用合成值仍是提交者的責任。builder 會用宣告的 schema 上限驗證 example,但刻意不執行呼叫者撰寫的 pattern regex(ReDoS 防禦)— 作者必須自己確保 example 與 pattern 相容。沒有 body 的回應(例如 204)schema 與 example 都要省略。
靜態資料
非機密的不可變 JSON 只能放在 src/data/**/*.json。每個巢狀整數都必須在 JavaScript 安全範圍內(更大的識別碼用字串,bundle 過程才不會默默四捨五入),且與 example 相同的憑證篩查會掃過每個巢狀鍵與值,檔案才進得了 bundle。匯入時要帶副檔名(import ref from "./data/user-reference.json")。
為什麼路徑與 query 有上限
簽章的調度斷言內嵌精確的請求路徑與 query,而整個 JWS 必須塞進 8 KiB。所以 manifest 驗證器事先做最壞情況的計算:路徑參數必須是帶 maxLength 的 inline string schema(三個上限同時成立:2048 字元/2048 UTF-8 位元組/4096 percent-encoded);query schema 必須有限有界(字串要 maxLength 或有界 enum、整數要上下界或有界 enum、裸 number 直接拒絕、陣列要 maxItems),讓最壞情況的 form-encoded query 塞進 1024 位元組。能通過驗證的 manifest 就永遠不會產生無法簽章的請求。精確規則與有界 JSON-Schema 子集:Manifest 規格。
正規形式與身分
儲存的 manifest 是正規化 JSON(排序鍵、排序/定序的集合、緊湊分隔符),請求的順序永遠不會滲入不可變修訂版。兩個摘要釘住它:manifest_digest(整份 manifest)與 contract_digest(只有 api 物件 — 排除了 server URL/安全描述,所以主機名稱改變不會使修訂版失效)。早期 V2 留下、缺少較嚴格文件欄位的舊列仍可讀取;builder 對每個新壓縮檔都套用嚴格規則。
生成的 OpenAPI 文件
manifest 決定操作、schema 與 example。server URL、安全機制與每個操作的存取,則是後端每次被呼叫 GET /worker/openapi.json 時,依上線的存取政策與操作設定加上去的;它們可以在不產生新修訂版的情況下改變,而有了逐操作存取,某個操作可以帶有自己的 security 與 servers(Manifest 規格)。TeamSync 網頁 App 部門 Worker 的「API 文件」分頁顯示的就是同一份文件;沒有獨立的文件服務。