Manifest 規格
驗證存在於兩個刻意鏡射的權威:提交/讀取邊界的控制面 Pydantic 模型(全面 extra="forbid" — 未知鍵一律拒絕),以及作為壓縮檔唯一權威、帶型別化拒絕代碼的 edge/department-workers/scripts/build.mjs。控制面接受的 payload 絕不能因為無憑證 builder 套用不同契約而事後失敗。敘事頁講模型;本頁是完整規則集。下文列出的拒絕代碼是 teamsync-worker validate 會印出的內容;透過 HTTP 提交被拒絕時,回應的是 422 project_invalid(提交管線)。
頂層
恰好三個鍵;任何其他頂層鍵都會被拒絕(manifest_unknown_field):
| 鍵 | 規則 |
|---|---|
manifestVersion | 字面值 "2"(拒絕代碼 manifest_version_unsupported) |
module | 物件,恰好 5 個鍵,全部必填 |
api | 物件,其鍵必須是 title、description、tags、operations、schemas 的子集(未知鍵為 manifest_api_unknown_field);title、tags、operations 必填,description 與 schemas 選填 |
module
| 欄位 | 規則 |
|---|---|
name | 1–64 字元;^[a-z][a-z0-9-]{0,62}[a-z0-9]$ 或單字元 ^[a-z]$(kebab-case、無結尾 -) |
version | 5–64 字元,完整 semver(含 prerelease + build metadata) |
language | javascript | typescript |
entrypoint | ≤256 字元,相對 POSIX 路徑;無 \ ? # % 或 NUL;無空段/./..;每段 ^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$ |
compatibilityDate | ISO YYYY-MM-DD 字串(不是日期物件) |
entrypoint 副檔名必須配合 language:javascript → .js/.mjs、typescript → .ts/.mts(拒絕 manifest_entrypoint_language_mismatch)。module.name/module.version 必須與 package.json 和 package-lock.json 根層的 name/version 完全相等 — API 視圖、SBOM 根與建置溯源共用一個身分。
api
| 欄位 | 上限 |
|---|---|
title | 1–128 字元 |
description | 選填,1–2048 |
tags | 1–32 |
operations | 1–64 |
schemas | 選填(預設為無),0–128 個具名 schema;每個名稱須符合 ^[A-Za-z][A-Za-z0-9._-]{0,63}$ |
Tags
slug:1–64、^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$、唯一。name:1–80、無控制字元、大小寫不敏感地唯一。description:選填,1–512。- 每個 tag 至少被一個操作引用(
manifest_tag_unreferenced);每個操作恰好宣告一個 tag。
Operations
| 欄位 | 規則 |
|---|---|
operationId | 1–64、^[A-Za-z][A-Za-z0-9_]{0,63}$、唯一;必須對應 entrypoint 可達的具名 export |
method | GET / POST / PUT / PATCH / DELETE |
path | 見下方 |
tags | 恰好一個 slug |
summary | 1–160 — builder 對每個新壓縮檔必填(Python 讀取側選填只為相容早期 V2 舊列) |
description | 1–2048 — builder 對新壓縮檔必填 |
parameters | 0–32 |
requestBody | 選填 |
responses | 1–20,鍵 ^[1-5][0-9]{2}$ |
路徑規則
- ≤512 字元;恰好一個開頭
/;無//、無結尾斜線。單獨的根路徑/同樣有效:它在其他檢查之前就被接受,且沒有任何段,所以規則 2–4 不適用於它。 /_teamsync與/_teamsync/*保留(manifest_route_reserved)— 執行期設定的控制通道在那裡。- 禁用字元:
\ ? # %與 NUL;無空段/./..段。 - 段是參數
{name}(^\{([A-Za-z][A-Za-z0-9_]{0,63})\}$)或靜態^[A-Za-z0-9_~-][A-Za-z0-9._~-]{0,127}$。 (method, path)與路徑形狀(參數正規化後)都必須唯一 —GET /users/{id}與GET /users/{slug}相撞(manifest_route_ambiguous)。
生成 router 的優先序是確定性的:字面段數遞減、再路徑 ASCII、再方法順序(GET、POST、PUT、PATCH、DELETE)、再 operationId。
Parameters
name:1–64、^[A-Za-z][A-Za-z0-9_-]{0,63}$;(location, name)唯一,header 名稱大小寫不敏感比較。in:path|query|header;路徑參數必須設required: true;header 參數不得撞上閘道擁有的 header(manifest_parameter_header_reserved)。description:選填,1–512。required預設為false。- 路徑佔位符必須恰好等於宣告的
in: path參數集合。
路徑參數 schema 必須是帶 maxLength ≥ 1 的 inline string schema(不可 $ref)。minLength 缺席/0 會自動正規化為 1。pattern 會被自動設成(或必須恰好等於)閘道擁有的圍欄。必須逐字元相同的字串如下(控制字元寫成六個字元的跳脫序列 \u0000、\u001F、\u007F、\u009F,不是 \x 跳脫):
^(?!\.{1,2}$)[^/\\%?#\u0000-\u001F\u007F-\u009F]+$在 JSON manifest 內每個反斜線都要加倍,所以該值寫成 "^(?!\\.{1,2}$)[^/\\\\%?#\\u0000-\\u001F\\u007F-\\u009F]+$"。任何其他 pattern 都會使 manifest 被拒絕(builder 代碼 manifest_path_parameter_invalid;控制面的措辭是 path parameter pattern must use the gateway-owned single-segment restriction)— 包含用 \x00-\x1f\x7f-\x9f 寫法表達同樣字元類別的版本。最簡單且正確的做法是省略 pattern,由後端設定。這個圍欄擋掉路徑分隔符、%(二次解碼)、?/#(重新解析)、C0+C1 控制字元與純點段。
最壞情況路徑計算 — 佔位符成本 len(name)+2,三個上限必須同時成立:解碼後字元 ≤ 2048、UTF-8 位元組(maxLength × 4)≤ 2048、percent-encoded(maxLength × 12)≤ 4096。
Query 線上上限 — form-encoded 的總 query 必須塞進 1024 位元組,計算式為 sum(len(name) + 1 + value_cost) + (pairs − 1):
| Query schema | 規則/成本 |
|---|---|
| string | 必須有 maxLength(成本 maxLength × 12),或有界 enum(成本 = 最長的 percent-encoded 成員) |
| integer | 上下界皆必須,±9007199254740991 內(成本 = 較長的編碼界),或由安全整數組成的有界 enum(成本 = 最長的編碼成員) |
| number | 必須是有界 enum — 裸 type: number 直接拒絕(“use a bounded string for arbitrary decimal text”);每個值固定計 64 位元組 |
| boolean / null | 成本 5/4 |
| array | 必須有 maxItems,項目僅限純量;配對數 = max(maxItems, 1) |
| object | 完全不支援 |
path 與 query 參數 schema 禁用 $ref。這項限制不適用於 header 參數 schema。
JSON Schema 子集
允許的關鍵字,僅此而已:$ref、type、title、description、enum、properties、required、additionalProperties、items、minItems、maxItems、uniqueItems、format、pattern、minLength、maxLength、minimum、maximum、exclusiveMinimum、exclusiveMaximum、multipleOf。沒有組合子(anyOf/oneOf/allOf/not)、沒有 default/examples、沒有遠端 dialect。
| 上限 | 值 |
|---|---|
| 深度 | ≤ 12 |
| 節點 | ≤ 512 — 全部 component schema 加每個操作 schema 的總量,單一共用計數器,在遞迴解析前先檢查 |
$ref | ^#/schemas/([A-Za-z][A-Za-z0-9._-]{0,63})$,僅限內部,不可與其他關鍵字並用 |
title / description | 1–128/1–1024 |
enum | 1–100 個成員,僅限純量 |
properties / required | 各 ≤ 100;屬性名 1–128、無控制字元 |
minItems/maxItems | 0–10000 |
pattern | 1–256,可攜子集(見下) |
minLength/maxLength | 0–1000000 |
format | 僅 date、date-time、email、uuid |
型別/關鍵字耦合強制檢查(物件關鍵字要 type: object、陣列要 items、字串關鍵字要 type: string、數字界限要 integer/number)。保留的 schema 名稱:EdgeWorkerError、TeamSyncGatewayError。
可攜 regex 圍欄(pattern):每個 (? 必須是 (?: (?= (?! (?<= (?<! 之一;拒絕 \A、\Z、\N{ 與佔有量詞;pattern 必須可編譯。注意准入管線永遠不會拿呼叫者的 pattern 去跑值(ReDoS 防禦)— example 與 pattern 的相容性由作者自行負責。唯一的例外是上面由閘道擁有的路徑參數圍欄,它會拿來驗證路徑參數的 example。
Examples
參數與 JSON request/response body 各可宣告一個合成 example:
| 上限 | 值 |
|---|---|
| 編碼大小 | ≤ 16384 位元組(16 KiB) |
| 深度/節點 | ≤ 12/≤ 512 |
| 每集合成員 | ≤ 128 |
| 整數 | Number.isSafeInteger 範圍(±9007199254740991) |
外加:只允許有限數字、純 JSON 型別、字串鍵、無 NUL。example 會對宣告的 schema 做結構驗證(數字相等用精確十進位比較、multipleOf 用精確十進位取餘)— 唯獨呼叫者撰寫的 pattern 刻意不執行。
限制:
- 名稱看起來像憑證的參數完全不能宣告 example。
- Response example 需要宣告的 response schema;
1xx、204、205、304回應不能宣告 example(204類回應 schema 與 example 都要省略)。
憑證篩查
掃過每個 example 值以及 src/data/**/*.json 的每個巢狀值。兩層:
像機密的鍵名 — 鍵小寫化並剝除非英數字後做子字串比對:password、passwd、secret、token、privatekey、apikey、credential、credentials、authorization、cookie。所以 API_KEY、api-key、Api.Key 全都命中。
像機密的值格式 — 19 個 pattern:Bearer/Basic 認證值、PEM 私鑰、AWS AKIA…、GitHub gh[pousr]_…/github_pat_…、GitLab glpat-…、Slack xox[baprs]-…、Stripe [rs]k_(live|test)_…、OpenAI 風格 sk-…、Google AIza…/ya29.…、TeamSync Worker 金鑰 tsw_…、HuggingFace hf_…、npm npm_…、PyPI pypi-…、SendGrid SG.….…、webhook whsec_…,以及一般的三段式 JWS(各段 16+ 字元 base64url 的 xxx.yyy.zzz)。
依命中內容分成三種不同的拒絕(先列控制面的文字,再列 builder 代碼):
| 命中內容 | 拒絕文字 | Builder 代碼 |
|---|---|---|
| 像機密的值 | “examples cannot contain credentials or secret-looking values; use a non-secret synthetic value or omit the example” | manifest_example_secret_forbidden |
| example 內像機密的鍵名 | “examples cannot include secret-bearing fields; omit the example or use a schema without credential values” | manifest_example_secret_field_forbidden |
| 宣告了 example、且名稱像憑證的參數 | “credential-like parameters cannot declare examples; omit the example” | manifest_example_secret_field_forbidden |
靜態資料檔使用相同的篩查,代碼為 static_data_secret_forbidden / static_data_secret_field_forbidden。篩查無法證明不透明字串是安全的 — 提交者仍需自行使用合成值。
正規化與摘要
儲存的 manifest 是正規 JSON(sort_keys、,/: 分隔符、ensure_ascii=False):required 排序、enum 依正規 JSON 排序、properties 依鍵排序、type: object 預設補 additionalProperties: false、tags 依 slug、operations 依 (path, method order, operationId)、parameters 依 (location order — path, query, header —, name)、responses 依狀態碼、component schemas 依名稱。header 名稱只在建立排序鍵時小寫化;儲存的參數保留作者撰寫的名稱。請求的鍵/清單順序永遠滲不進不可變修訂版。
兩個摘要,皆為 SHA-256 hex:
manifest_digest— 完整正規 manifest。contract_digest— 只有正規的api子物件,刻意排除後端擁有的 server URL/安全描述,主機名稱改變不會使不可變修訂版失效。
verify_manifest_integrity 在讀取時重新驗證兩者,失敗丟出固定的 “persisted manifest integrity check failed”,不洩漏帶輸入的診斷。
生成的 OpenAPI
GET /worker/openapi.json 從正規操作清單渲染 OpenAPI 3.1.1(jsonSchemaDialect draft 2020-12):
servers[0].url由後端擁有:公開 dispatcher URL,或 Worker 全域存取模式為teamsync_internal時的閘道 invoke base。永遠不是呼叫者的Hostheader。- 根層
security跟隨 Worker 全域存取模式:teamsync_internal→TeamSyncBearer(http/bearer JWT);api_key/api_key_and_ip→TeamSyncWorkerApiKey(apiKey headerX-TeamSync-Worker-Key,描述為操作 API 金鑰);public/ip_allowlist→ 無。 - 閘道錯誤狀態 400、401、403、404、405、413、414、422、429、500、502、503 渲染為呼叫者 schema(若有宣告)、
EdgeWorkerError與TeamSyncGatewayError的anyOf;每個操作都有responses.default → #/components/responses/StandardError。呼叫者永遠控制不了閘道錯誤外層。 - 擴充欄位:根層
x-teamsync-access-policy(mode、ipAllowlistConfigured、apiKeyConfigured— 永不含實際 CIDR)、x-teamsync-max-query-bytes: 1024、每個 tag 的x-teamsync-slug,以及下方的操作設定擴充欄位。
文件中的逐操作存取
當上線 release 以 v2.8 或更新政策建置時(能力表),route 也會讀取上線的操作設定(逐操作存取),文件因此描述每個操作各自的存取。產生的用戶端必須遵守逐操作的 security 與 servers:某個操作可能需要金鑰,而根層卻寫著 security: [];也可能位於不同的 base URL。
- 根層
x-teamsync-operation-settings-state—{state, revision}。state為legacy、reconciled或pending;沒有操作設定可用時(低於 v2.8 的 release)渲染的文件回報legacy,revision為0。 - 只有在 state 為
reconciled時:- 每個有設定列、或曾經擁有過操作 API 金鑰的操作,都會有自己的
x-teamsync-access-policy:{mode, ipAllowlistConfigured, apiKeyConfigured, source}。操作有自己的模式、自己的金鑰或曾經有過時,source為operation;沿用 Worker 全域政策時為worker。根層擴充欄位仍然描述 Worker 全域政策,且沒有source。 - 該操作的有效模式與 Worker 全域模式不同,或其
source為operation時,也會有自己的security:teamsync_internal為[{"TeamSyncBearer": []}],api_key/api_key_and_ip為[{"TeamSyncWorkerApiKey": []}],public/ip_allowlist為[]。 - 其 URL 與文件的 URL 不同時,會有自己的
servers: [{url}]:teamsync_internal用閘道 invoke base,其他模式用公開 URL。 - 每個操作都有
x-teamsync-service-account:{id, username, source}—source為operation(該操作自己的綁定)或default(部門預設帳號)— 兩者都未設定時為null;帳號無法解析時username為null。它指出context.teamsync.authHeaders()以哪個帳號的憑證登入(服務帳號)。
- 每個有設定列、或曾經擁有過操作 API 金鑰的操作,都會有自己的
- 最多兩種安全機制。
components.securitySchemes至多包含TeamSyncBearer與TeamSyncWorkerApiKey:Worker 全域機制與每個操作機制的聯集。X-TeamSync-Worker-Keyheader 攜帶該操作自己的金鑰;沿用全域政策、且沒有自己金鑰的操作則攜帶 Worker 全域金鑰。 - 文件的
ETag是渲染後文件的摘要,所以操作設定或服務帳號綁定改變時它也會改變,不需要重新部署。