External callback 與 token lifecycle
Callback 讓外部系統在沒有使用者 session 的情況下,對一張指定的自訂資料表執行 create 或 update。Token 綁定資料表、允許操作與 minting moderator;寫入仍會通過 ACL actor、rules、unique constraints、invariant、history 與 triggers,不是繞過業務契約的後門。
生命週期:mint → 保存一次性 secret → POST → revoke
1. Moderator mint token
curl -X POST \
"$BASE_URL/private/module/custom_tables/chatroom/11111111-1111-4111-8111-111111111111/tables/22222222-2222-4222-8222-222222222222/callback-tokens" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "ERP 訂單匯入",
"allowed_ops": "create,update",
"valid_until": "2026-12-31T23:59:59"
}'allowed_ops 只接受兩個 literal:
"create":只能新增,最小權限的預設選擇。"create,update":可新增及更新。沒有 update-only token。
回應為 201,包含 URL 使用的 token_id 與一次性 secret:
{
"token_id": "33333333-3333-4333-8333-333333333333",
"secret": "cbsec_example_only_0000000000000000",
"table_id": "22222222-2222-4222-8222-222222222222",
"allowed_ops": "create,update",
"valid_until": "2026-12-31T23:59:59"
}2. 立即保存 secret
Secret 只在 mint response 出現一次。把它寫入整合服務的 secret manager,不要記錄到 application logs、analytics、ticket 或瀏覽器 storage。之後 callbackTokens.list 只回 metadata,不會再次顯示 secret;遺失時應 revoke 並 mint 新 token。
3. External POST
Bearer 模式:
curl -X POST \
"$BASE_URL/public/module/custom_tables/callback/33333333-3333-4333-8333-333333333333" \
-H "Authorization: Bearer $CALLBACK_SECRET" \
-H "Content-Type: application/json" \
-d '{
"op": "create",
"data": { "訂單編號": "ORD-1042", "數量": 3 },
"idempotency_key": "erp-event-1042"
}'也可用 replay-protected signature:X-Callback-Timestamp 是 Unix seconds(允許正負 300 秒),X-Callback-Signature 是以 sha256(secret) 的 32 raw bytes 作 key,對 "{timestamp}.{raw body}" 計算的 hex HMAC-SHA256。簽章必須使用實際送出的 raw bytes,不能重新序列化 JSON 後再簽。
Create 的 data 可用顯示名稱或 internal keys;update 必須帶 record_id,且 data 只接受 internal keys:
{
"op": "update",
"record_id": "44444444-4444-4444-8444-444444444444",
"data": { "col_aaaaaaaa_aaaa_4aaa_8aaa_aaaaaaaaaaaa": 5 },
"idempotency_key": "erp-event-1043"
}idempotency_key 會在同一 token 下保留 24 小時;相同 key 回傳原始 accepted result 並設 replayed: true,不再次寫入。每個外部 event 都應提供穩定且唯一的 key。
4. Revoke
curl -X DELETE \
"$BASE_URL/private/module/custom_tables/chatroom/11111111-1111-4111-8111-111111111111/tables/22222222-2222-4222-8222-222222222222/callback-tokens/33333333-3333-4333-8333-333333333333" \
-H "Authorization: Bearer $TOKEN"輪替時先 mint 並部署新 credential,確認新 token 有成功 request,再 revoke 舊 token。未知、已撤銷、已過期與 auth 失敗一律對 public caller 回 404 Not found,避免 token enumeration;client 不應根據 404 猜測是哪一種原因。
Mint、list、revoke 的完整 contract 分別見 callbackTokens.mint、callbackTokens.list、callbackTokens.revoke。
Accepted response 與 approval
一般成功的 status 是 created 或 updated,並回傳 record_id。若 require_approval 命中,public callback 刻意不回 private API 的 409,而是回 2xx:
held_for_approval:已建立 staged change;process_id、staged_change_id、rule_label有值,record_id為null。這是 terminal success,不要重送。already_pending:相同目標或 natural key 已在等待;沿用原本 process,不建立第二筆。
{
"ok": true,
"op": "create",
"status": "held_for_approval",
"record_id": null,
"process_id": "55555555-5555-4555-8555-555555555555",
"staged_change_id": "66666666-6666-4666-8666-666666666666",
"rule_label": "高額訂單變更",
"replayed": false
}一定要用 status discriminator,而不是只看 HTTP 2xx。Approval 後的套用仍會重新通過當時有效的規則與 invariant。
Body、rate 與 lock 行為
| Status | 契約 | Client 行為 |
|---|---|---|
400 / 409 | Shape、rule、unique 或 transition 問題 | 修正 payload 或業務衝突;不要盲目重試 |
404 | Token/auth 的 uniform response | 停止並由 operator 檢查 token lifecycle |
413 | Raw request body 超過 65,536 bytes | 縮小 request;不可拆成同一 event 的無序重送 |
423 | Table 正在 migration 等 lock | 使用相同 idempotency key 指數退避後重試 |
429 | 每 token 每分鐘超過 60 requests | 等下一個 rate window,加入 jitter,維持同一 idempotency key |
429 structured detail | Approval pending bucket 已滿 | 暫停 producer,讓 reviewer 消化 queue;這不是短暫 rate window |
Body cap 按實際 bytes 計算,不是 JSON 字元數。Rate limit 是 token 共用額度;不要靠多 mint tokens 規避節流,應做 producer backpressure。已知的 4xx / 423 / 429 會允許修正後沿用 key 重試;ambiguous 5xx 可能發生在 commit 之後,client 必須保留同一 key,避免重複資料。
Warning Public callback 沒有 acting chatroom。Enforcing
channeltable 會拒絕沒有 room binding 的 callback 寫入;請改用具 acting-room contract 的 private integration。invariant則刻意套用於 callback。
Public endpoint 的精確 response 與 errors 見 callback.write。完整端到端流程見自動化指南,或用流程精靈練習 private 設定步驟。
讓公開表單自己長出欄位
透過 callback token 送資料的公開表單,可以直接問 API「我能寫哪些欄位」,不必把欄位清單寫死,然後每次有人改表就過期:
curl "$BASE_URL/public/module/custom_tables/callback/33333333-3333-4333-8333-333333333333/form-schema" \
-H "Authorization: Bearer $CALLBACK_SECRET"回應是資料表的顯示名稱,加上可寫入欄位的 {name, type}。rollup、lookup、formula 不會出現,因為它們本來就寫不進去;link、attachment、json、user、social_client 會列出來,因為表單確實可以送這些值。
驗證方式與寫入路由完全一致,只有一個細節不同:GET 沒有 body,所以 HMAC 那條路徑簽的是 "{timestamp}."(空 body),金鑰是 sha256(secret) 的 32 個原始位元組,時間窗同樣是 ±300 秒。不存在、已撤銷、已過期的權杖,以及任何驗證失敗,都回和寫入路由一樣的統一 404。
在拿它來開發前,還有兩個和寫入路由不同的地方值得知道:這個路由不看權杖的 allowed_ops,所以只能 create 的權杖同樣能取得表格描述;而它沒有流量限制,所以永遠不會回 429。
反方向、也就是把資料唯讀公開給匿名讀者,請見公開讀取。