Skip to Content
核心概念外部回呼External callbacks

External callback 與 token lifecycle

Callback 讓外部系統在沒有使用者 session 的情況下,對一張指定的自訂資料表執行 create 或 update。Token 綁定資料表、允許操作與 minting moderator。Token 本身就是授權依據:每次請求不會重新解析資料表的列與欄 ACL,寫入會記在 minting moderator 名下(link 目標仍會以該使用者的權限檢查)。寫入仍會通過 rules、unique constraints、invariant、history 與 triggers,不是繞過業務契約的後門。Sandbox run 的回寫 token(列為 sandbox-run:<run_id>)會以送出的使用者身分寫入,但把這些寫入標示為 AI:create 會存 created_by_ai: true,update 會在 history 記下 changed_by_ai: true;moderator 發行的 token 兩者都是 false。

生命週期: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": "example_only_000000000000000000000000000000", "table_id": "22222222-2222-4222-8222-222222222222", "allowed_ops": "create,update", "valid_until": "2026-12-31T23:59:59" }

2. 立即保存 secret

Secret 只在 mint response 出現一次:它是 43 個 URL-safe base64 字元、沒有前綴,伺服器只保存它的 SHA-256 雜湊。把它寫入整合服務的 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 後再簽。簽章沒有 nonce,但沒有 idempotency_key、且是靠簽章通過驗證的請求(兩者都有效時以 Bearer 為準)只會被送達一次:簽章仍有效時,用相同的 timestamp 與 body 再送一次,會回傳原始結果與 replayed: true,帶 + 前綴的 timestamp header 也視為同一次送達。重新簽章的重試帶著新的 timestamp,就是新的一次送達,所以送出端可能以重新簽章來重試時,請搭配 idempotency_key 使用。

Body 是封閉物件,未知的 key 會被拒絕:

欄位契約
op必填:create 或 update,而且必須在 token 的 allowed_ops 之內
record_idupdate 必填;create 會忽略
data必填的 JSON 物件
idempotency_key選填,最多 128 字元

這條路由自己解析 body,所以缺少 op 或 data、其他 op、不是物件的 data、未知的 key、無效的 JSON 或過長的 key,都會回 400 {"detail":"invalid body: <驗證訊息>"}——絕不是 422。檢查順序為:body 大小的 413、token 與驗證的統一 404、429 流量限制、400 invalid body、400 op '<op>' is not allowed by this token (allowed: <ops>),最後是 400 op 'update' requires record_id。帶 idempotency_key 的請求,或沒有 key、以簽章通過驗證的請求,接著會在寫入任何東西之前先經過下方的 receipt 查詢。

Create 的 data 可用顯示名稱或 internal keys,未知的 key 會被靜默略過。Update 必須帶 record_id;它的 data 同樣接受已知的顯示名稱或 internal col_<hex> keys,但未知的欄位 key 會回 400 Column '<key>' does not exist in table schema:

{ "op": "update", "record_id": "44444444-4444-4444-8444-444444444444", "data": { "col_aaaaaaaa_aaaa_4aaa_8aaa_aaaaaaaaaaaa": 5 }, "idempotency_key": "erp-event-1043" }

Durable receipt 與 receipt 視窗

idempotency_key(最多 128 字元;空字串視為沒有 key)會在同一 token 下,於寫入的同一個資料庫交易內記錄成一份 durable receipt,所以寫入成功的資料列與它的 receipt 會一起 commit。Receipt 視窗預設是 24 小時。部署可以用環境變數 CUSTOM_TABLE_CALLBACK_RECEIPT_TTL_SECONDS 縮短,值為 1 到 86400 秒的整數;沒有設定、空白、不是整數或超出範圍的值,都維持 24 小時。視窗在 receipt 建立時就已固定,而 key 是逐 byte 比對,所以大小寫、重音符號與結尾空白都會讓 key 不同。重複的 key 會怎樣:

相同的 key 送達時…結果
在視窗內,且 op、record_id、data 相同原始的 accepted result 加上 replayed: true,包含 held_for_approval 或 already_pending 的 ticket;不會第二次寫入
在視窗內,但 op、record_id 或 data 不同409 {"detail":{"error":"idempotency_key_reuse"}},而且不寫入:一個 key 代表一個事件,不是可以覆寫的欄位
使用它的第一個請求仍在執行不會第二次寫入。第二個請求在第一個 commit 後回 replayed: true,或回 409 a request with this idempotency_key is in progress(body 不同時是 idempotency_key_reuse)
視窗已過key 已釋出:請求以任何 body 都會當成新的一次執行,並記錄新的 receipt
被拒絕、且寫入已 rollback 的嘗試之後(規則否決、驗證失敗、unique 衝突或 429 上限)該次拒絕沒有留下 receipt,所以修正後的請求可以沿用這個 key

Receipt 存在資料庫裡,所以 Redis 中斷或被淘汰,既不會讓重複寫入通過,也不會遺失 receipt。滾動升級期間,較舊的 pod 只記錄在 Redis 的 receipt,仍會在它自己的 24 小時結束前繼續回應。沒有 key 的 Bearer 請求不會有 receipt,所以不會被去重(下方審批閘門以自然鍵判斷的 already_pending 後備機制仍然適用)。每個外部 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 重放寫入(核准的重放只跳過 immutable_when),所以已核准的變更仍可能以 apply_failed 結束。沒有 idempotency_key 時,create 的 already_pending 只能透過資料表第一條單欄 unique 規則偵測。

Body、rate 與 lock 行為

Status契約Client 行為
400 / 409Shape、rule、unique(unique 或 unique_tuple)、same_dimension_reference、date_interval_exclusion 或 transition 問題;token 不允許的 op(op 'update' is not allowed by this token (allowed: create));未知的頂層 body key 或過長的 idempotency_key(400 invalid body: …)修正 payload 或業務衝突;不要盲目重試(409 date_interval_exclusion_retry 不會寫入任何東西,所以重送請求是安全的)
403Enforcing channel table 上的 {"error": "scp_out_of_scope"},或 invariant 的 {"error": "scp_invariant_violation"},或資料表的 write_policy 為 commands_only 時的 {"error": "governed_context_required", …}(在 token 檢查之後、durable receipt 或 idempotency marker 之前提出,所以沒有東西被保留)。Minting 使用者無法讀取的 link 目標是下方的 404 linked record not found,不是 403修正資料或目標;不要盲目重試
404Not found 是 token/auth 的 uniform response;Record not found 表示 update 目標不存在、在垃圾桶或屬於其他資料表;linked record not found 表示 link 值指向的目標紀錄不存在、在垃圾桶或無法讀取;CustomTable Not found 表示綁定的資料表在垃圾桶中(資料表丟進垃圾桶時 token 仍在,永久清除資料表時 token 會被刪除)只有 Not found 時才檢查 token lifecycle;遇到 CustomTable Not found 請還原資料表;否則修正 record id 或 link id
409 a request with this idempotency_key is in progress使用該 key 的第一個請求尚未完成,或它的 receipt 尚未定案稍候後用同一個 key 重試
409 {"detail":{"error":"idempotency_key_reuse"}}在 receipt 視窗內,相同的 key 搭配不同的 op、record_id 或 data修正送出端:一個事件一個 key。不要重試改過的 body
413Raw request body 超過 65,536 bytes縮小 request;不可拆成同一 event 的無序重送
423Table 正在 migration 等 lock使用相同 idempotency key 指數退避後重試
429每 token 每分鐘超過 60 requests等下一個 rate window,加入 jitter,維持同一 idempotency key
429 structured detailApproval pending bucket 已滿暫停 producer,讓 reviewer 消化 queue;這不是短暫 rate window

Body cap 按實際 bytes 計算,不是 JSON 字元數。Rate limit 是 token 共用額度;不要靠多 mint tokens 規避節流,應做 producer backpressure。已知的 4xx / 423 / 429 拒絕會 rollback 而且不留下 receipt,所以修正後可以沿用 key 重試——409 idempotency_key_reuse 除外(該 key 已屬於一個已記錄的事件,請改用新的 key);ambiguous 5xx 可能發生在 commit 之後,client 必須保留同一 key,避免重複資料:若寫入已 commit,重試會 replay 它的 receipt;若已 rollback,重試就會執行。

Warning Public callback 沒有 acting chatroom。Enforcing channel table 會拒絕沒有 room binding 的 callback 寫入;請改用具 acting-room contract 的 private integration。invariant 則刻意套用於 callback。若同一張表還有符合的 require_approval 規則,create 會先被暫存(held_for_approval),再在套用時被 channel floor 拒絕,因此該 staged change 會以 apply_failed 結束。

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 不會出現,因為它們本來就寫不進去;對 minting 使用者被 column ACL 隱藏的欄位也不會出現(moderator 沒有被隱藏的欄位);link、attachment、json、user、social_client 會列出來,因為表單確實可以送這些值。decimal 欄位會以 type: "decimal" 列出,但不含精度與小數位數:請以純十進位字串("12.50")送出,JSON 數字會被拒絕。

驗證方式與寫入路由完全一致,只有一個細節不同:GET 沒有 body,所以 HMAC 那條路徑簽的是 "{timestamp}."(空 body),金鑰是 sha256(secret) 的 32 個原始位元組,時間窗同樣是 ±300 秒。不存在、已撤銷、已過期的權杖,以及任何驗證失敗,都回和寫入路由一樣的統一 404;綁定的資料表在垃圾桶中時則回 404 CustomTable Not found。

在拿它來開發前,還有兩個和寫入路由不同的地方值得知道:這個路由不看權杖的 allowed_ops,所以只能 create 的權杖同樣能取得表格描述;而它沒有流量限制,所以永遠不會回 429。

反方向、也就是把資料唯讀公開給匿名讀者,請見公開讀取。

Last updated on