public_read line
public_read 宣告一個免驗證、唯讀的 capability token,綁定一張表的一個受管理 view。與單例的 client_access 不同,同一張表可以有多行 public_read。這是唯一會把資料發佈到已驗證介面之外的 IaC kind,因此請把它當成安全變更審查,而不是設定變更。
JSONL Line
{"kind":"public_read","table":"appointments","ref":"board_token","spec":{"view":"public_board","name":"Waiting-room board","visible_columns":["code"],"read_filter":{"and":[{"column":"code","op":"is_not_null"}]},"allow_query":false,"rpm":120,"secretless":true}}欄位契約
頂層對應 IacPublicReadLine:
| 欄位 | 必填 | 精確契約 |
|---|---|---|
kind | 是 | 固定 "public_read" |
table | 是 | 父資料表 ref |
ref | 是 | Token ref,在該表的 qualified-ref 命名空間內唯一 |
state | 否 | "present" 或 "absent",預設 "present" |
spec | 條件式 | IacPublicReadSpec;present 必填、absent 禁止 |
本 kind 沒有 renamed_from,因此 present 行一律需要 spec。
IacPublicReadSpec 只接受:
| 欄位 | 必填 | 精確契約 |
|---|---|---|
view | 是 | 本表的受管理 view ref;v1 沒有 view 認領 |
name | 否 | string 或 null,最多 128 字元;僅供 console 與稽核標示 |
visible_columns | 否 | Token 自身資料表的 column ref 陣列或 null;省略代表 view 的完整投影 |
read_filter | 否 | Row policy——任一棵由 predicate 與 link_target leaf 組成的 and / or / not 樹——或 null;每個 predicate 的 column 是本表的 column ref,link_target 的 target 內欄位則是被連結那張表的 ref |
allow_query | 否 | boolean,預設 false;明確 null 會被拒絕 |
rpm | 否 | 1–600 的整數;省略即採 mint 預設值 60;明確 null 會被拒絕 |
valid_until | 否 | 時間戳或 null;帶時區的值會正規化為等值的 naive UTC 瞬間 |
secretless | 否 | boolean,預設 true;v1 只接受 true |
只有作者實際宣告的欄位會被比較與寫入,與 client_access 完全一致。更新時省略某欄位會保留 live 值,不會把它重設。
那兩個明確 null 的拒絕值得完整引用,因為此處「省略」與「填 null」是真的不同:
rpm may not be null — omit the field to keep the mint default (60)
allow_query may not be null — omit the field to keep the default (false)valid_until 的正規化對收斂很重要:+08:00 的到期時間會存成等值的 naive UTC 瞬間,讓 plan、apply 與 export 一致,而不是永遠規劃一個幻影 update。
v1 只支援 secretless
已發佈的 token 是一個 UUID capability URL,不儲存任何 secret hash。Bearer 密鑰 token 的密鑰只在 mint 當下顯示一次,無法在文件之間往返,因此 IaC 拒絕宣告這種 token:
public_read v1 supports secretless tokens only; declare secretless: true (a Bearer-secreted token cannot be minted through IaC — use the REST mint lane)Apply 會以稍微不同的措辭再擋一次:
public_read v1 supports secretless tokens only; declare secretless: true (mint a Bearer-secreted token via REST)對 REST 發行的密鑰 token,export 會忠實輸出 secretless: false,而那份 export 刻意無法重新匯入。要讓含密鑰 token 的 scope 完整往返,必須移除那些行,或改由 REST 重新發行。
綁定的 view 必須受管理
spec.view 必須指向本文件或先前 apply 所管理的 view。未受管理、只存在於 live 的 view 會被拒絕——v1 沒有 view 認領,這也是 REST mint 與 IaC 在此處分歧的原因:REST 願意發佈該表的任何 view,包含私有 view,IaC 則不會。
view ref 'public_board' is not a managed view in this document (declare a `view` line for it, or manage it via a prior apply — no view adoption in v1)
bound view ref 'public_board' is deleted in this document (state: absent) — revoke this token line (state: absent) first, or keep the view
bound view ref 'public_board' has no live view row (deleted out of band) — re-declare the view (state: present) or revoke this token (state: absent)Apply 回報第一則時會加上 public_read '<table>.<ref>': 前綴,並把「for it」寫成「BEFORE this line」;兩種寫法意義相同。
同文件內的 absent-view 防護不是吹毛求疵。view_id 外鍵是 ON DELETE CASCADE,刪除該 view 會硬刪除 token,破壞「永不硬刪除」的 tombstone 規則。請先撤銷 token 行。
文件順序是寬容的:view 宣告在後面的 public_read 行會由 executor 延到第二輪處理,而不是直接失敗,因為 export 本來就會產生這種順序。Plan 仍可能顯示 create 並附上延後驗證的 warning。
Ref 與身分規則
State 存的是 {table}.{ref},resource_id 為 token UUID。這正是 column、rule、trigger、view refs 與固定的 client_access identity 所在的 qualified 命名空間,因此 token ref 不能與它們相撞:
duplicate ref 'appointments.board_token' (already declared in this document)Planner 載入的 state map 只以 ref 為鍵,但 persistent uniqueness 仍包含 resource type。不同 kind 的舊 state 列若使用同一 ref,就是損壞狀態:載入時會警告並只保留其中一列,而 kind 檢查會避免把不相符的 survivor 當成 token 或 bound view 執行。請清理 collision,不要依賴哪一列被保留。
visible_columns 項目與 read_filter predicate 的 column 值都是 token 自身資料表的 column ref,apply 時翻成內部 key,export 時反向翻回。
unresolved visible_columns ref 'missing': unknown column ref 'appointments.missing'
unresolved read_filter ref 'missing': unknown column ref 'appointments.missing'
read_filter predicate must be an object with a 'column' key (got str)
read_filter predicate is missing a 'column' refread_filter 只取不含主體的子集
公開 token 沒有操作主體,因此 $me 與 $me.department 會在 mint 時被拒絕。這道檢查會走遍整棵 and / or / not 樹,包含 link_target 的 target 內部,所以身分 token 沒辦法藏在下一層偷渡進去。$today、$today+Nd、$today-Nd(N 為 1 到 730,以 Asia/Taipei 為日界)與 $now 則可用。這不是 grant read_filter 的對等物——把 grant filter 複製進 public_read spec 可能在 apply 時失敗:
read_filter may not reference $me / $me.department on a public token (no principal exists); $today and $now are allowed生命週期與規劃
| 宣告 | State 列 | Live token | Action |
|---|---|---|---|
present | 無 | — | create——全新的 token ID 與 URL |
present | 有 | Live | 就地 update,或 noop |
present | 有 | 已撤銷 | create——新的 token;被撤銷的列原封不動 |
absent | 有 | Live | delete |
absent | 無 | — | noop,附帶 ref not managed; nothing to revoke |
Absent 行的 plan action 字串是 delete,即使 apply 端的效果是撤銷。只有 apply 會設定 revoked = true。
有兩個性質特別關鍵:
- 更新是就地進行。 Token 列被更新,因此 token ID 與 capability URL 在多次 apply 之間保持穩定。重新套用文件永遠不會輪替已發佈的 URL。
- 撤銷是永久 tombstone。 Absent 行會設定
revoked = true並移除 state 列;該列永不硬刪除,燒掉的 URL 也永遠是死的。重新宣告該行不會讓它復活——它會發行一個 UUID 全新的 token,所有已嵌入的 URL 都必須更新。
v1 沒有以名稱認領的機制。沒有 state 列、也沒有 live claim 的行一律規劃 create,因此宣告一個已由 REST 發行的 token 只會靜默地把公開面積加倍,而不是認領它。請一開始就決定要走哪條路。
每一次 apply 都會透過 REST mint 使用的同一條程式路徑,重新執行完整的 mint policy 驗證——SCP channel 規則、view 是否存在與設定是否合法、跨表 computed 語法、強制隱藏欄位的允許清單、row policy,以及主體 token 的拒絕。IaC 永遠無法發佈 REST 會拒絕的東西。
未被宣告且已撤銷的 state 列不會解析為 live,會被 apply 結束時的孤兒回收清掉——這就是撤銷之後那些列消失的原因。
Export
Export 為每個 live、未撤銷、且其綁定 view 也一併匯出的 token 輸出一行。綁定到私有或未受管理 view 的 token 會被靜默略過。受管理的 token 沿用其 state 列的 child ref;REST 發行的 token 則以其 name 產生去重後的 slug(沒有名稱時用 public_read)。
因為沒有認領機制,匯出含有 REST 發行 token 的 scope 會產生使用自創 ref 的 public_read 行,匯入時會發行重複 token。再加上 secretless: false 的失敗與靜默略過,這個 kind 的 export → apply 往返並非無損。套用一份 export 之前,請逐行讀過所有 public_read 行。
驗證錯誤
state=absent lines carry no spec (table lines are the sole adopt-then-delete exception)
state=present lines require a spec
unknown table ref 'appointments'
duplicate ref 'appointments.board_token' (already declared in this document)
public_read v1 supports secretless tokens only; declare secretless: true (a Bearer-secreted token cannot be minted through IaC — use the REST mint lane)
bound view ref 'public_board' is deleted in this document (state: absent) — revoke this token line (state: absent) first, or keep the view工作台會檢查欄位契約、明確 null 的拒絕、secretless 規則、共用的 qualified-ref 命名空間、自身資料表的 column ref,以及同文件的 absent-view 防護。它看不到 IaC state,因此「這個 view 是否受管理」、mint policy 驗證,以及所有與 live token 有關的判斷都交由伺服器決定。
動手試試
在 IaC 工作台宣告一張表、一個欄位、一個 view 與上面的範例 token。把 secretless 設成 false 觀察本機 validate 錯誤,還原後把 view 行改成 state: "absent",觀察 CASCADE 防護。匿名讀取介面請見公開讀取,REST 的 mint、list 與 revoke 路由請見 public-read 參考。