Skip to Content
參考資料Manifest 規格

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

欄位規則
name1–64 字元;^[a-z][a-z0-9-]{0,62}[a-z0-9]$ 或單字元 ^[a-z]$(kebab-case、無結尾 -)
version5–64 字元,完整 semver(含 prerelease + build metadata)
languagejavascript | typescript
entrypoint≤256 字元,相對 POSIX 路徑;無 \ ? # % 或 NUL;無空段/./..;每段 ^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$
compatibilityDateISO 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

欄位上限
title1–128 字元
description選填,1–2048
tags1–32
operations1–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

欄位規則
operationId1–64、^[A-Za-z][A-Za-z0-9_]{0,63}$、唯一;必須對應 entrypoint 可達的具名 export
methodGET / POST / PUT / PATCH / DELETE
path見下方
tags恰好一個 slug
summary1–160 — builder 對每個新壓縮檔必填(Python 讀取側選填只為相容早期 V2 舊列)
description1–2048 — builder 對新壓縮檔必填
parameters0–32
requestBody選填
responses1–20,鍵 ^[1-5][0-9]{2}$

路徑規則

  1. ≤512 字元;恰好一個開頭 /;無 //、無結尾斜線。單獨的根路徑 / 同樣有效:它在其他檢查之前就被接受,且沒有任何段,所以規則 2–4 不適用於它。
  2. /_teamsync 與 /_teamsync/* 保留(manifest_route_reserved)— 執行期設定的控制通道在那裡。
  3. 禁用字元:\ ? # % 與 NUL;無空段/./.. 段。
  4. 段是參數 {name}(^\{([A-Za-z][A-Za-z0-9_]{0,63})\}$)或靜態 ^[A-Za-z0-9_~-][A-Za-z0-9._~-]{0,127}$。
  5. (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 / description1–128/1–1024
enum1–100 個成員,僅限純量
properties / required各 ≤ 100;屬性名 1–128、無控制字元
minItems/maxItems0–10000
pattern1–256,可攜子集(見下)
minLength/maxLength0–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。永遠不是呼叫者的 Host header。
  • 根層 security 跟隨 Worker 全域存取模式:teamsync_internal → TeamSyncBearer(http/bearer JWT);api_key/api_key_and_ip → TeamSyncWorkerApiKey(apiKey header X-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() 以哪個帳號的憑證登入(服務帳號)。
  • 最多兩種安全機制。 components.securitySchemes 至多包含 TeamSyncBearer 與 TeamSyncWorkerApiKey:Worker 全域機制與每個操作機制的聯集。X-TeamSync-Worker-Key header 攜帶該操作自己的金鑰;沿用全域政策、且沒有自己金鑰的操作則攜帶 Worker 全域金鑰。
  • 文件的 ETag 是渲染後文件的摘要,所以操作設定或服務帳號綁定改變時它也會改變,不需要重新部署。
Last updated on