permdock 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (323) hide show
  1. package/dist/a2a/index.d.ts +7 -5
  2. package/dist/a2a/index.js +14 -21
  3. package/dist/ai-sdk/index.d.ts +18 -6
  4. package/dist/ai-sdk/index.js +8 -5
  5. package/dist/app-data-CV8pqqMi.js +59 -0
  6. package/dist/approvals/index.d.ts +2 -2
  7. package/dist/approvals/index.js +3 -3
  8. package/dist/{approvals-DbYAuNkG.js → approvals-DZL76lKW.js} +35 -19
  9. package/dist/{approvers-DT4AVyIg.js → approvers-CqPOLD-o.js} +1 -1
  10. package/dist/{arazzo-BAgw9ap6.js → arazzo-C4BcskbX.js} +6 -5
  11. package/dist/{arazzo-C9chp3ST.js → arazzo-CD8d0WLb.js} +2 -2
  12. package/dist/authzen/index.d.ts +3 -3
  13. package/dist/authzen/index.js +161 -23
  14. package/dist/better-auth/index.d.ts +2 -2
  15. package/dist/better-auth/index.js +6 -19
  16. package/dist/better-supabase/index.d.ts +112 -87
  17. package/dist/better-supabase/index.js +257 -156
  18. package/dist/{bind-C_ob3LKa.d.ts → bind-BzqG8SMb.d.ts} +16 -5
  19. package/dist/{bind-CuzGytRf.js → bind-kk-uyCwH.js} +14 -6
  20. package/dist/build-instance-DIIV9DbC.js +51 -0
  21. package/dist/{canonical-json-DemiK4Vp.js → canonical-json-CFKyFOke.js} +1 -3
  22. package/dist/{capability-2kYTMWDv.d.ts → capability-CksyCWDH.d.ts} +2 -2
  23. package/dist/{capability-4qPdO9gk.js → capability-txATtKV4.js} +3 -2
  24. package/dist/catalog/index.d.ts +1 -1
  25. package/dist/catalog/index.js +1 -1
  26. package/dist/catalog-6yWAX6Ps.js +2 -0
  27. package/dist/{catalog-CYj-mX2O.js → catalog-WHG4kBq7.js} +2 -2
  28. package/dist/{catalog-doc-DdGm82XT.js → catalog-doc-DWtg0gGR.js} +17 -10
  29. package/dist/{catalog-DWF5cYqB.js → catalog-tZUyOfOv.js} +4 -4
  30. package/dist/claude-agent/index.d.ts +4 -4
  31. package/dist/claude-agent/index.js +4 -5
  32. package/dist/clerk/index.d.ts +3 -3
  33. package/dist/clerk/index.js +8 -33
  34. package/dist/cli/bin.js +1 -1
  35. package/dist/cli/index.d.ts +3 -3
  36. package/dist/cli/index.js +3 -3
  37. package/dist/cloud/index.d.ts +4 -4
  38. package/dist/cloud/index.js +9 -15
  39. package/dist/{cloud-CfwZ39Rl.js → cloud-C7bSDgX4.js} +2 -2
  40. package/dist/{cloud-DWLXO2af.js → cloud-PeBN3IEn.js} +5 -5
  41. package/dist/{collect-DSTE3ml9.js → collect-2SrB7lfk.js} +6 -8
  42. package/dist/collect-Bw2Jdq1q.js +2 -0
  43. package/dist/{collect-CHB22Sx4.js → collect-CQx4hX4N.js} +3 -3
  44. package/dist/{commands-BeaGNj_o.js → commands-Cpip4ULx.js} +13 -13
  45. package/dist/{compact-y6vfu2_f.js → compact-CAf557ZB.js} +11 -1
  46. package/dist/compile/index.d.ts +1 -1
  47. package/dist/compile/index.js +1 -1
  48. package/dist/{compile-Ba4blROR.js → compile-CcEu2ZQ1.js} +6 -5
  49. package/dist/{compile-BkRyBDBw.d.ts → compile-CtTP7eur.d.ts} +2 -2
  50. package/dist/{config-BJiM-xAm.js → config-BIjREpyE.js} +4 -4
  51. package/dist/{config-ir40y8m0.js → config-DLt6BvV7.js} +2 -2
  52. package/dist/{config-report-CdCgbRnV.js → config-report--Xb9APJS.js} +5 -6
  53. package/dist/{connection-CU8MQxRi.js → connection-DUsczpB5.js} +3 -2
  54. package/dist/constants-DcJABbLW.js +20 -0
  55. package/dist/{context-Bcy85rGi.js → context-DieZVJXu.js} +16 -5
  56. package/dist/convex/index.d.ts +4 -4
  57. package/dist/convex/index.js +14 -16
  58. package/dist/create-BBO24vj6.js +324 -0
  59. package/dist/{credential-8P81ff4u.js → credential-kue0qpbm.js} +4 -4
  60. package/dist/deciding-columns-Sq12mEA6.js +123 -0
  61. package/dist/delegation-input-FTxMK3ji.js +19 -0
  62. package/dist/denied-text-DfRk15uw.d.ts +19 -0
  63. package/dist/{describe-DMYiCjJV.d.ts → describe-C3ldA9os.d.ts} +11 -2
  64. package/dist/{describe-B_gVZEVi.js → describe-Cgkp7eP0.js} +5 -4
  65. package/dist/{diff-C9RDjjhs.js → diff-Cnghh_Ip.js} +2 -2
  66. package/dist/{diff-2VaeChzX.js → diff-rpqoe7Ag.js} +9 -9
  67. package/dist/{doctor-Djh6JDEk.js → doctor-CuVJvhV_.js} +2 -2
  68. package/dist/doctor-owlv6ddh.js +2166 -0
  69. package/dist/drizzle/index.d.ts +2 -2
  70. package/dist/drizzle/index.js +6 -14
  71. package/dist/elysia/index.d.ts +4 -4
  72. package/dist/elysia/index.js +7 -7
  73. package/dist/{errors-DPdqI50v.d.ts → errors-OD_4c__H.d.ts} +2 -2
  74. package/dist/{errors-Cz5zn0mW.js → errors-drnfxYtm.js} +3 -3
  75. package/dist/{evaluations-tz6D9A2G.js → evaluations-C9YJsv3H.js} +45 -7
  76. package/dist/eve/index.d.ts +5 -5
  77. package/dist/eve/index.js +5 -5
  78. package/dist/express/index.d.ts +4 -4
  79. package/dist/express/index.js +8 -7
  80. package/dist/fastify/index.d.ts +15 -5
  81. package/dist/fastify/index.js +9 -29
  82. package/dist/fields-fwfXN01n.js +1016 -0
  83. package/dist/{files-DWUN9PzI.js → files-D_JIzUrP.js} +12 -3
  84. package/dist/{fixtures-ht28Cplr.js → fixtures-h2eG2OZV.js} +5 -6
  85. package/dist/{from-snapshot-CTLijvdm.js → from-snapshot-7REQZe0H.js} +32 -22
  86. package/dist/{generate-CH7ThJ14.js → generate-CNU1pIOo.js} +1 -3
  87. package/dist/grantee-CXY-0C87.js +254 -0
  88. package/dist/grantee-conditions-MQs5NIuX.js +270 -0
  89. package/dist/grantee-match-fxYp9Nmt.js +141 -0
  90. package/dist/{helpers-H_KARDVC.js → helpers-C9YmyBIa.js} +7 -6
  91. package/dist/hono/index.d.ts +4 -4
  92. package/dist/hono/index.js +8 -8
  93. package/dist/hosted-CBHgkzDl.js +655 -0
  94. package/dist/{http-SO7qGttf.js → http-BBoKefeS.js} +35 -2
  95. package/dist/{http-BIhmLfCc.js → http-BEbdMZ05.js} +2 -24
  96. package/dist/{rls-import-4lLW_pbb.js → import-BFIlhetD.js} +86 -80
  97. package/dist/{openapi-import-cmwetbzw.js → import-UHk1N7vH.js} +6 -7
  98. package/dist/{index-C8z4RjQV.d.ts → index-BwMe94Xq.d.ts} +1 -1
  99. package/dist/{index-0nNqHspG.d.ts → index-DgM7w96K.d.ts} +2 -2
  100. package/dist/{index-C7Tw7MaW.d.ts → index-Dz7didpC.d.ts} +3 -3
  101. package/dist/{index-BNc7Wer_.d.ts → index-UdVQgq48.d.ts} +105 -38
  102. package/dist/index.d.ts +15 -13
  103. package/dist/index.js +22 -22
  104. package/dist/{instance-options-jARdwfPN.d.ts → instance-options-plkrsAog.d.ts} +1 -1
  105. package/dist/{json-schema-BsAflUGW.js → json-schema-DBkdUuCO.js} +1 -3
  106. package/dist/jwt/index.d.ts +4 -4
  107. package/dist/jwt/index.js +13 -13
  108. package/dist/{kernel-_RRHmnMY.js → kernel-CB3LrIJM.js} +81 -32
  109. package/dist/kysely/index.d.ts +2 -2
  110. package/dist/kysely/index.js +6 -14
  111. package/dist/{live-options-H2eG8dfJ.js → live-options-BaAFCIiu.js} +16 -3
  112. package/dist/load-B_shHrmd.js +2 -0
  113. package/dist/{load-DhWEB9P1.js → load-C3_oTHyO.js} +11 -3
  114. package/dist/{manifest-iaoBXHKc.js → manifest-BngWKryL.js} +5 -18
  115. package/dist/{map-claims-Z4QZOiFF.js → map-claims-AVlBaJoa.js} +7 -16
  116. package/dist/{map-error-pgUWNNtG.js → map-error-BE27PYkE.js} +5 -5
  117. package/dist/{markers-B0hYLvMD.js → markers-CiwAOZtO.js} +6 -2
  118. package/dist/{may-use-DndQ5tQ-.js → may-use-BOn_dR_w.js} +4 -3
  119. package/dist/mcp/index.d.ts +26 -9
  120. package/dist/mcp/index.js +456 -446
  121. package/dist/{rls-migrate-CN4TlJTO.js → migrate-BwlB5-Wk.js} +75 -15
  122. package/dist/nest/index.d.ts +4 -4
  123. package/dist/nest/index.js +11 -12
  124. package/dist/next/index.d.ts +31 -8
  125. package/dist/next/index.js +29 -33
  126. package/dist/next/plugin.d.ts +1 -1
  127. package/dist/next/plugin.js +1 -1
  128. package/dist/node/index.d.ts +4 -4
  129. package/dist/node/index.js +4 -4
  130. package/dist/ocsf-uYFXGVw2.js +115 -0
  131. package/dist/openai/index.d.ts +5 -5
  132. package/dist/openai/index.js +4 -4
  133. package/dist/openapi/index.d.ts +3 -3
  134. package/dist/openapi/index.js +1 -1
  135. package/dist/{openapi-CqqEbHsL.js → openapi-BJdBmYDG.js} +19 -23
  136. package/dist/{openapi-BqRBvAng.js → openapi-DBzHQbov.js} +2 -5
  137. package/dist/{openapi-CQTsgKoB.js → openapi-DcQ1Kg40.js} +3 -3
  138. package/dist/orpc/index.d.ts +5 -5
  139. package/dist/orpc/index.js +9 -11
  140. package/dist/otel/index.d.ts +2 -2
  141. package/dist/otel/index.js +26 -23
  142. package/dist/output-files-CgIsvdSa.js +140 -0
  143. package/dist/{rls-ownership-DcYlZ09B.js → ownership-BgnbOJFT.js} +387 -379
  144. package/dist/{package-root-zGkxO3oN.js → package-root-DtNnpmUd.js} +3 -2
  145. package/dist/{parse-BuyjxhZV.js → parse-CVFQHk6c.js} +3 -3
  146. package/dist/{parse-local-manifest-BrMozJCo.js → parse-local-manifest-C-q02Pal.js} +2 -4
  147. package/dist/{parse-local-manifest-JLnaBH0S.d.ts → parse-local-manifest-DvYUR0QL.d.ts} +1 -1
  148. package/dist/pdp/index.d.ts +2 -2
  149. package/dist/pdp/index.js +8 -9
  150. package/dist/{permdock-CbgvLnuo.js → permdock-L26udHSk.js} +1387 -1220
  151. package/dist/{rls-fields-CZOMlwHl.js → permission-keys-B26jeyUv.js} +1268 -2053
  152. package/dist/permission-registry-BsblpB-z.js +385 -0
  153. package/dist/{permissions-DttnqqxC.d.ts → permission-types-Dg39z9Kv.d.ts} +59 -31
  154. package/dist/{permissions-RJrovw98.js → permissions-CCJ6lmxB.js} +3 -3
  155. package/dist/{permissions-CDgvKG5L.d.ts → permissions-COH-KRgT.d.ts} +2 -2
  156. package/dist/{policy-IB6yxpC3.d.ts → policy-types-sp9P1iIV.d.ts} +661 -582
  157. package/dist/{policy-D2W7uv19.d.ts → policy-ur0BBK_e.d.ts} +2 -2
  158. package/dist/{powersync-DE19b9dT.js → powersync-BFSbSElQ.js} +2 -2
  159. package/dist/powersync-DCDgZgfZ.js +2 -0
  160. package/dist/{powersync-Dv1_aWF7.js → powersync-cZe4kzhL.js} +26 -27
  161. package/dist/prisma/index.d.ts +1 -1
  162. package/dist/prisma/index.js +6 -12
  163. package/dist/{problem-B43ztKIm.js → problem-CnWraVhC.js} +8 -7
  164. package/dist/{problem-details-_AMOYz-w.js → problem-details-DECUnXAn.js} +2 -2
  165. package/dist/{problem-details-DX075l9V.d.ts → problem-details-rIIo8B2C.d.ts} +1 -1
  166. package/dist/{protected-BdnIEFkY.d.ts → protected-B3_P7xuw.d.ts} +9 -6
  167. package/dist/{protected-utU-SLuy.js → protected-BN7BUdY1.js} +20 -8
  168. package/dist/{provider-CSQd7m92.js → provider-Bi-0HNL3.js} +23 -10
  169. package/dist/{provider-DZY281jO.d.ts → provider-kwHC9CZD.d.ts} +1 -1
  170. package/dist/{provider-store-Cfbvo4DE.js → provider-store-Ctt-81Ni.js} +6 -3
  171. package/dist/react/index.d.ts +7 -7
  172. package/dist/react/index.js +5 -5
  173. package/dist/react/provider-client.d.ts +1 -1
  174. package/dist/react/provider-client.js +1 -1
  175. package/dist/react-native/index.d.ts +20 -11
  176. package/dist/react-native/index.js +33 -17
  177. package/dist/{refs-KYb4ntaz.js → refs-f3imMjNW.js} +1 -1
  178. package/dist/relation-walk-BXnlLtg2.js +186 -0
  179. package/dist/relations-CvmSpG67.js +163 -0
  180. package/dist/{replay-CYyiLiwO.d.ts → replay-COpmZUfx.d.ts} +1 -1
  181. package/dist/request-context-BVM7aWNs.js +71 -0
  182. package/dist/request-context-C2_1lchh.d.ts +5 -0
  183. package/dist/resource-relations-D1Un3pcz.js +243 -0
  184. package/dist/retire-BFcxL6un.js +240 -0
  185. package/dist/{rls-P-BeBx0n.js → rls-55DQTuf5.js} +14 -2
  186. package/dist/{rls-DiinlRML.js → rls-BEktUaqg.js} +884 -484
  187. package/dist/{policy-xyWg7QdR.js → role-builder-B4GbKM-i.js} +235 -541
  188. package/dist/{rpc-DL9Sqvb1.js → rpc-nYC3cR_u.js} +15 -5
  189. package/dist/{run-BWc9qqvE.js → run-DMcNW_8T.js} +3 -3
  190. package/dist/{runtime-CB8gQFvS.js → runtime-D_tMXnsq.js} +1 -1
  191. package/dist/{saas-DhpJjnBA.js → saas-DtaA4JuW.js} +9 -8
  192. package/dist/{openapi-schema--ARjyfQr.js → schema-Bol6gqXA.js} +3 -5
  193. package/dist/{schema-D3MnDMxa.js → schema-DjQBcAP9.js} +4 -2
  194. package/dist/scim/index.d.ts +1 -1
  195. package/dist/scim/index.js +1 -1
  196. package/dist/{scim-BFGBBAqR.js → scim-vFR1O5oG.js} +6 -12
  197. package/dist/{scopes-CVdPrWN-.js → scopes-tvpa34ep.js} +10 -2
  198. package/dist/server/index.d.ts +18 -8
  199. package/dist/server/index.js +8 -8
  200. package/dist/{server-DlZ28Oz5.js → server-ZYEHtNHS.js} +4 -4
  201. package/dist/{create-B1d519XK.d.ts → server-types-DdqBwZN_.d.ts} +69 -33
  202. package/dist/shared-1uhI-e-b.js +294 -0
  203. package/dist/{shared-DRu8VFVM.js → shared-JhMeOsDF.js} +1 -1
  204. package/dist/{sink-Bd9PSyfq.js → sink-BTtmfPnE.js} +1 -1
  205. package/dist/{sink-B1fZzIMf.d.ts → sink-CPN4zLfs.d.ts} +1 -1
  206. package/dist/{skills-Biq5uup3.js → skills-BZdRQkxs.js} +4 -3
  207. package/dist/{skills-C5CbhhpG.js → skills-Dle4ZoLF.js} +2 -2
  208. package/dist/{snapshot-Enxd0n-s.js → snapshot-Dc9CHPME.js} +116 -13
  209. package/dist/{snapshot-cache-D6jLelln.js → snapshot-cache-Cod40D-X.js} +1 -1
  210. package/dist/{snapshot-cache-B-LxUahj.d.ts → snapshot-cache-J5T7t_vD.d.ts} +1 -1
  211. package/dist/solid/index.d.ts +22 -22
  212. package/dist/solid/index.js +40 -10
  213. package/dist/{sources-4p0-4xQm.js → sources-B1-b-R0t.js} +25 -26
  214. package/dist/{sources-BdOYLw9j.d.ts → sources-Dljy06zL.d.ts} +6 -2
  215. package/dist/{sql-xdg5f4Fi.js → sql-t8teWOfo.js} +19 -3
  216. package/dist/{src-BDwEZWrZ.js → src-Bb7no-hS.js} +194 -123
  217. package/dist/ssf/index.d.ts +3 -3
  218. package/dist/ssf/index.js +58 -61
  219. package/dist/store-9KO4BlkB.d.ts +147 -0
  220. package/dist/{store-DMAWwAJV.js → store-ILkLUj_o.js} +4 -4
  221. package/dist/{store-options-ByhZq1jV.js → store-options-CbUHDoSe.js} +16 -7
  222. package/dist/{stores-BT2pSCub.js → stores-BHXdSbw9.js} +7 -3
  223. package/dist/stores-Cxz_80g-.d.ts +31 -0
  224. package/dist/{stream-DDpvynSB.js → stream-BMui98KK.js} +2 -1
  225. package/dist/{stream-D1RXubw2.d.ts → stream-Dwsg6WwE.d.ts} +2 -2
  226. package/dist/{subject-settings-BwBmCdTR.js → subject-settings-CkEwrw3W.js} +23 -5
  227. package/dist/supabase/index.d.ts +4 -4
  228. package/dist/supabase/index.js +3 -3
  229. package/dist/supabase/middleware.d.ts +11 -5
  230. package/dist/supabase/middleware.js +10 -7
  231. package/dist/{supabase-C0qESOa-.js → supabase-C1TWlnYt.js} +36 -52
  232. package/dist/{supabase-s4o9s1O1.js → supabase-Dbqr141s.js} +2 -2
  233. package/dist/supabase-hook-BULg7Ut_.js +2 -0
  234. package/dist/{supabase-hook-ntETYCQu.js → supabase-hook-Cva3ikXz.js} +730 -781
  235. package/dist/{supautils-u8jXTGrt.js → supautils-B6HYL9eL.js} +2 -2
  236. package/dist/svelte/Protected.svelte +9 -2
  237. package/dist/svelte/index.d.ts +6 -6
  238. package/dist/svelte/index.js +25 -11
  239. package/dist/svelte/runtime.d.ts +10 -3
  240. package/dist/svelte/runtime.js +4 -3
  241. package/dist/svelte/source.d.ts +6 -6
  242. package/dist/svelte/source.js +4 -4
  243. package/dist/{validity-uOp8UpYn.js → tenancy-DshjqY0n.js} +3 -39
  244. package/dist/terminal/index.d.ts +5 -5
  245. package/dist/terminal/index.js +18 -34
  246. package/dist/testing/index.d.ts +89 -77
  247. package/dist/testing/index.js +1276 -1262
  248. package/dist/testing/saas/index.d.ts +4 -4
  249. package/dist/testing/saas/index.js +2 -2
  250. package/dist/testing/saas/permissions.d.ts +1 -1
  251. package/dist/testing/saas/permissions.js +1 -1
  252. package/dist/thenable-QSmvt-vZ.js +65 -0
  253. package/dist/{token-CIWxJOCh.js → token-Bx9qy30p.js} +1 -1
  254. package/dist/tool-refusal-CQ7OEia9.js +41 -0
  255. package/dist/trpc/index.d.ts +5 -5
  256. package/dist/trpc/index.js +7 -8
  257. package/dist/{types-DbjRN55U.d.ts → types-7rJVLWe5.d.ts} +82 -4
  258. package/dist/{types-aqOHTHcp.d.ts → types-Br9IXUBG.d.ts} +1 -1
  259. package/dist/types-ByUo56un.d.ts +88 -0
  260. package/dist/{types-Cq_H8N_X.d.ts → types-C0dee0Qf.d.ts} +2 -2
  261. package/dist/{types-DxiCGmmd.d.ts → types-CIlTojhq.d.ts} +2 -2
  262. package/dist/{types-QCuYJh4q.d.ts → types-CUcFZfpU.d.ts} +1 -1
  263. package/dist/{types-C97IskBw.d.ts → types-CqrjB9o6.d.ts} +1 -1
  264. package/dist/{types-BnIEz1sU.d.ts → types-DHrmBJLH.d.ts} +24 -6
  265. package/dist/{types-Dnemwd77.d.ts → types-DpWfQdSh.d.ts} +5 -1
  266. package/dist/{types-Bvc4oWT6.d.ts → types-DvupZE0w.d.ts} +1 -1
  267. package/dist/unplugin/index.d.ts +1 -1
  268. package/dist/unplugin/index.js +1 -1
  269. package/dist/{usage-B0YuTQRO.js → usage-4G6t643F.js} +2 -2
  270. package/dist/{usage-ItS7nLcl.js → usage-CG9nOYZw.js} +7 -7
  271. package/dist/usage-iFLJ0-rr.js +2 -0
  272. package/dist/{validation-error-Ba264OaJ.d.ts → validation-error-BSSw0l_8.d.ts} +1 -1
  273. package/dist/{validation-error-CnjZ4t92.js → validation-error-ZfUHbKvf.js} +1 -1
  274. package/dist/validity-dVrUEPJt.js +62 -0
  275. package/dist/{verifier-BxBo6I44.js → verifier-DMr_Zmb_.js} +51 -15
  276. package/dist/{rls-verify-CMv9H3wV.js → verify-BztUB3GX.js} +668 -239
  277. package/dist/{version-BvacVTZR.js → version-BEogC-rB.js} +1 -1
  278. package/dist/{views-Soe7bGSC.js → views-CJsrBS1f.js} +23 -1
  279. package/dist/{vocabulary-DA8a8zif.d.ts → vocabulary-hhEFLFoL.d.ts} +15 -6
  280. package/dist/vue/index.d.ts +24 -19
  281. package/dist/vue/index.js +24 -8
  282. package/dist/watch-C8TpsAl4.js +2 -0
  283. package/dist/{watch-DVBJ9cBq.js → watch-zeucB7eF.js} +6 -6
  284. package/dist/webmcp/index.d.ts +2 -2
  285. package/dist/webmcp/index.js +13 -37
  286. package/dist/wrap-Cr93qqdP.d.ts +14 -0
  287. package/dist/wrap-DWsU_5Bd.js +26 -0
  288. package/package.json +8 -8
  289. package/schemas/catalog-v1.json +6 -0
  290. package/skills/permdock-credentials/SKILL.md +1 -1
  291. package/skills/permdock-credentials/references/tokens-keys-links.md +7 -2
  292. package/skills/permdock-data/references/queries-and-rls.md +4 -0
  293. package/skills/permdock-tenancy/references/ownership-and-custom-roles.md +1 -0
  294. package/skills/permdock-wire/SKILL.md +2 -2
  295. package/skills/permdock-wire/references/adapters.md +9 -1
  296. package/skills/permdock-wire/references/policy-options.md +10 -1
  297. package/dist/catalog-CRW5Mx_B.js +0 -2
  298. package/dist/collect-DSWOk19R.js +0 -2
  299. package/dist/create-CMwIl-w2.js +0 -253
  300. package/dist/delegation-D8pUEuce.js +0 -103
  301. package/dist/doctor-DvH0IQff.js +0 -1194
  302. package/dist/doctor-collect-CjdO0wQX.js +0 -665
  303. package/dist/evaluation-items-CfCCejQd.js +0 -40
  304. package/dist/fields-BMl1RxoX.js +0 -26
  305. package/dist/grantee-Cv5Sogfv.js +0 -602
  306. package/dist/hosted-DJQw46hn.js +0 -266
  307. package/dist/instance-options-Dhhcnm3y.js +0 -22
  308. package/dist/load-DwJUafYN.js +0 -2
  309. package/dist/map-AcxK9o_D.js +0 -147
  310. package/dist/permissions-DVoF6dMt.js +0 -662
  311. package/dist/powersync-CrBX8hY6.js +0 -2
  312. package/dist/protected-xAr24FEC.d.ts +0 -40
  313. package/dist/relations-cjOL01hZ.js +0 -505
  314. package/dist/rls-graph-CHwx0V8q.js +0 -435
  315. package/dist/rls-shared-Cewd_-zI.js +0 -495
  316. package/dist/store-jOBYpiXC.d.ts +0 -36
  317. package/dist/stores-C0IMQrGI.d.ts +0 -52
  318. package/dist/supabase-config-Bj3iDH2I.js +0 -472
  319. package/dist/supabase-hook-gd3s8bxB.js +0 -2
  320. package/dist/thenable-okc2xDsj.js +0 -10
  321. package/dist/types-CKNSyIf_.d.ts +0 -52
  322. package/dist/usage-DorAJiU_.js +0 -2
  323. package/dist/watch-BwxLTYMZ.js +0 -2
@@ -1,27 +1,28 @@
1
- import { t as compact } from "./compact-y6vfu2_f.js";
2
- import { _ as listPermissions, y as renamedKeys } from "./permissions-DVoF6dMt.js";
3
- import { d as scopeList, o as resolveScope } from "./scopes-CVdPrWN-.js";
4
- import { E as listRoles, s as flattenGrantee } from "./grantee-Cv5Sogfv.js";
5
- import { h as scopeField } from "./validity-uOp8UpYn.js";
6
- import { i as describeError, o as usageResult } from "./errors-CWfoNg9N.js";
1
+ import { t as compact } from "./compact-CAf557ZB.js";
2
+ import { d as scopeList, o as resolveScope } from "./scopes-tvpa34ep.js";
3
+ import { c as listPermissions, u as renamedKeys } from "./permission-registry-BsblpB-z.js";
4
+ import { o as flattenGrantee, v as listRoles } from "./grantee-CXY-0C87.js";
5
+ import { p as scopeField } from "./tenancy-DshjqY0n.js";
6
+ import { a as exitCodeOf, i as describeError, n as cliErrorKind, o as usageResult } from "./errors-CWfoNg9N.js";
7
7
  import { t as policyRowConditionKeys } from "./row-conditions-h2HythXQ.js";
8
- import { a as loadModule, n as asPolicy, o as pickNamed } from "./load-DhWEB9P1.js";
8
+ import { c as requirePolicy } from "./load-C3_oTHyO.js";
9
9
  import "./budget-NbEtbJC_.js";
10
- import { n as escapeSqlIdent } from "./sql-xdg5f4Fi.js";
11
- import "./sources-4p0-4xQm.js";
12
- import { i as SEEDS_MARKER, r as INDEXES_MARKER, t as GRANTS_MARKER } from "./markers-B0hYLvMD.js";
10
+ import { l as sqlTextArray, n as escapeSqlIdent, r as qualifySqlTable } from "./sql-t8teWOfo.js";
11
+ import { l as roleColumn } from "./sources-B1-b-R0t.js";
12
+ import { i as INDEXES_MARKER, n as GRANTS_MARKER, o as SEEDS_MARKER, t as BACKFILL_MARKER } from "./markers-CiwAOZtO.js";
13
13
  import { n as requirePeer } from "./peer-BOr_kJE1.js";
14
- import { f as splitUndiffed, n as supabaseConfig } from "./supabase-config-Bj3iDH2I.js";
15
- import { $ as sessionLiveHelperSql, G as quoteIdent$1, K as quoteLiteral, Q as scopeTypeOf, R as memberIdsHelper, T as graphSqlName, U as permittedIdsHelper, V as parseMembershipsFlag, W as qualifiedTable$1, X as scopeSources, Z as scopeTable, f as SESSION_LIVE_HELPER, o as qualified$1, q as quoteTable, r as HELPERS, rt as subjectIdSql, tt as subjectClaimJsonSql, w as graphHelper, y as checkSuspension } from "./rls-shared-Cewd_-zI.js";
16
- import { A as helpersSql, B as breakGlassHolder, C as readTableIndexFacts, E as accessSql, F as seedSql, H as collectGrants, S as pruneIndexTargets, T as connectPgPool, U as roleNames, V as breakGlassKey, a as fieldViewsSql, b as indexTargets, c as assemblePolicies, d as readOnlyActorPolicies, f as branchClauses, g as permissionHelpersSql, h as tableFor, i as fieldViews, l as orSql, m as compileGrants, n as columnGrantSql, o as rowBranches, q as apiKeysPlan, r as columnRevokeSql, u as readOnlyActorKinds, v as permittedByPermissionHelper, x as indexesSql, y as COLUMNS_SQL } from "./rls-fields-CZOMlwHl.js";
17
- import { c as helperCallKeys, i as HELPER_TABLES, n as graphPlan, r as graphSql, s as callsHelper, t as closureDepths } from "./rls-graph-CHwx0V8q.js";
18
- import { a as ownershipRules, o as ownershipSql, r as assignmentSql } from "./rls-ownership-DcYlZ09B.js";
19
- import { _ as seedsMigration, d as resolveAuthorize, f as driftOf, g as pgDeltaPath, h as partPath, l as parseRbacAuthorize, m as parseSplit, o as supabaseHookSql, p as isSeedsDirectory, t as grantsLabel, u as rbacScaffold, v as writeSqlFiles } from "./supabase-hook-ntETYCQu.js";
20
- import { n as runRlsVerify } from "./rls-verify-CMv9H3wV.js";
21
- import { t as supautilsRejections } from "./supautils-u8jXTGrt.js";
22
- import { basename, delimiter, join, resolve } from "node:path";
14
+ import { c as rbacScaffold, l as resolveAuthorize, o as supabaseHookSql, p as supabaseConfig, s as parseRbacAuthorize, t as grantsLabel } from "./supabase-hook-Cva3ikXz.js";
15
+ import { C as parseMembershipsFlag, E as scopeTypeOf, F as quoteQualified, I as quoteTable, M as qualified, N as quoteIdent$1, O as tenantTypeOf, P as quoteLiteral, _ as subjectClaimJsonSql, a as customRolesOn, c as SESSION_LIVE_HELPER, g as sessionLiveHelperSql, r as HELPERS, t as CUSTOM_ROLES, y as subjectIdSql } from "./shared-1uhI-e-b.js";
16
+ import { a as parseSplit, c as seedsMigration, i as isSeedsDirectory, l as writeOutputFiles, n as driftOf, o as partPath, s as pgDeltaPath, t as dataMigrationFile } from "./output-files-CgIsvdSa.js";
17
+ import { a as splitUndiffed } from "./deciding-columns-Sq12mEA6.js";
18
+ import { B as memberIdsHelper, C as apiKeysPlan, D as roleNames, E as collectGrants, G as scopeSources, K as scopeTable, M as graphHelper, N as graphSqlName, T as breakGlassKey, U as permittedIdsHelper, X as connectPgPool, Y as checkSuspension, a as accessSql, o as helpersSql, r as permittedByPermissionHelper, s as seedSql, t as permissionHelpersSql, v as currentKeyExpr, w as breakGlassHolder } from "./permission-keys-B26jeyUv.js";
19
+ import { n as ownershipSql, r as assignmentSql, t as ownershipRules } from "./ownership-BgnbOJFT.js";
20
+ import { _ as indexTargets, a as fieldViewsSql, b as readTableIndexFacts, c as assemblePolicies, d as readOnlyActorPolicies, f as branchClauses, g as COLUMNS_SQL, h as tableFor, i as fieldViews, l as orSql, m as compileGrants, n as columnGrantSql, o as rowBranches, r as columnRevokeSql, u as readOnlyActorKinds, v as indexesSql, y as pruneIndexTargets } from "./fields-fwfXN01n.js";
21
+ import { a as graphSql, c as callsHelper, i as graphPlan, l as helperCallKeys, n as runRlsVerify, o as HELPER_TABLES, r as closureDepths } from "./verify-BztUB3GX.js";
22
+ import { t as supautilsRejections } from "./supautils-B6HYL9eL.js";
23
+ import { basename, delimiter, join } from "node:path";
23
24
  import { spawnSync } from "node:child_process";
24
- //#region src/cli/rls-advisors.ts
25
+ //#region src/cli/rls/advisors.ts
25
26
  /** The PermDock doctor check that reports the same problem in migrations. */
26
27
  const ADVISOR_CODES = {
27
28
  function_search_path_mutable: "PD048",
@@ -127,6 +128,255 @@ function runRlsAdvisors(input) {
127
128
  };
128
129
  }
129
130
  //#endregion
131
+ //#region src/cli/rls/generate-context.ts
132
+ /** `rls.customRoleWrites.requires` as permission keys, or why it names none. */
133
+ function writeRequirement(policy, requires) {
134
+ if (requires === void 0) return {};
135
+ const leaves = listPermissions(policy.permissions);
136
+ const keys = requires === "manageRoles" ? leaves.filter((leaf) => leaf.meta.manageRoles === true).map((leaf) => leaf.key) : [...requires];
137
+ if (keys.length === 0) return { error: requires === "manageRoles" ? "rls.customRoleWrites.requires is 'manageRoles', but no permission declares meta.manageRoles" : "rls.customRoleWrites.requires names no permission" };
138
+ const known = new Set(leaves.map((leaf) => leaf.key));
139
+ const unknown = keys.filter((key) => !known.has(key));
140
+ if (unknown.length > 0) return { error: `rls.customRoleWrites.requires names undeclared permissions: ${unknown.join(", ")}` };
141
+ return { keys: [...new Set(keys)].toSorted() };
142
+ }
143
+ /** Declared role names, and those a tenant admin may compose into custom roles. */
144
+ function customRoleNames(policy, requires) {
145
+ const declared = [...roleNames(policy)].toSorted();
146
+ const assignable = declared.filter((name) => policy.rolesByName.get(name)?.assignable ?? listRoles(policy.vocabulary.roles).some((leaf) => leaf.key === name && leaf.assignable));
147
+ const renamed = renamedKeys(policy.vocabulary.permissions);
148
+ const leaves = listPermissions(policy.permissions);
149
+ const manage = leaves.filter((leaf) => leaf.meta.manageRoles === true).map((leaf) => leaf.key);
150
+ return {
151
+ declared,
152
+ assignable,
153
+ ...renamed.size === 0 ? {} : { renamed: Object.fromEntries(renamed) },
154
+ permissions: leaves.map((leaf) => leaf.key).toSorted(),
155
+ ...manage.length === 0 ? {} : { manage: manage.toSorted() },
156
+ ...requires === void 0 ? {} : { requires },
157
+ ...policy.levels === void 0 ? {} : { levels: true }
158
+ };
159
+ }
160
+ function skippedOwnershipTriggers(triggers, scopes) {
161
+ if (triggers === false) return "all";
162
+ return Object.keys(triggers).map((key) => {
163
+ const name = resolveScope(scopes, key);
164
+ if (name === void 0) throw new Error(`PermDock CLI: rls.ownershipTriggers.${key} names a scope the policy does not declare`);
165
+ return name;
166
+ });
167
+ }
168
+ /** The SQL context every emitter reads, from the flags, `rls` config and the policy. */
169
+ function buildContext(input, policy, options) {
170
+ const rls = input.config.rls;
171
+ const memberships = parseMembershipsFlag(input.memberships) ?? rls?.memberships;
172
+ if (input.rbac && input.dialect !== "supabase") return { refusal: `rls generate --rbac supabase needs --dialect supabase (got ${input.dialect})` };
173
+ const schema = input.rbacSchema ?? rls?.schema ?? rls?.rbac?.schema ?? "permdock";
174
+ const authorize = resolveAuthorize(input.config, {
175
+ authorize: input.authorize,
176
+ rbac: input.rbac,
177
+ memberships
178
+ });
179
+ const memberSources = rls?.membershipSources ?? input.config.supabase?.hook?.memberships;
180
+ const sources = authorize === "database" ? memberSources : void 0;
181
+ const scopes = scopeList(policy.scopes);
182
+ const suspension = checkSuspension(rls?.suspension, scopes);
183
+ const hookRoles = input.config.supabase?.hook?.roles;
184
+ const roles = rls?.roles ?? (hookRoles === false ? void 0 : hookRoles);
185
+ if (roles !== void 0 && input.rbac) return { refusal: "rls generate --rbac supabase creates its own user_roles (user_id, role app_role); drop rls.roles (or supabase.hook.roles) or the --rbac flag" };
186
+ const requires = writeRequirement(policy, rls?.customRoleWrites?.requires);
187
+ if (requires.error !== void 0) return { refusal: requires.error };
188
+ const readers = rls?.trustedReaders;
189
+ if (readers !== void 0 && (!Array.isArray(readers) || !readers.every((name) => typeof name === "string" && name !== ""))) return { refusal: "rls.trustedReaders must list Postgres role names, such as ['service_role']" };
190
+ const ownership = ownershipRules(policy, scopes);
191
+ const graph = graphPlan(policy);
192
+ const apiKeys = apiKeysPlan(rls?.apiKeys, new Set(roleNames(policy)), Object.fromEntries(renamedKeys(policy.vocabulary.permissions)));
193
+ return {
194
+ ctx: {
195
+ dialect: input.dialect,
196
+ scopes,
197
+ tenantClaim: rls?.tenantClaim ?? "tenant_id",
198
+ ...rls?.tenants === "all" ? { tenants: "all" } : {},
199
+ gucPrefix: input.gucPrefix ?? rls?.gucPrefix ?? "app",
200
+ ...rls?.actions === void 0 ? {} : { actions: rls.actions },
201
+ inlineFunctions: input.inlineFunctions || rls?.inlineFunctions === true,
202
+ schema,
203
+ authorize,
204
+ roleClaim: rls?.roleClaim ?? "user_role",
205
+ tenantType: input.tenantType ?? rls?.tenantType ?? "uuid",
206
+ ...rls?.teamType === void 0 ? {} : { teamType: rls.teamType },
207
+ ...rls?.scopeTypes === void 0 ? {} : { scopeTypes: rls.scopeTypes },
208
+ ...memberships === void 0 ? {} : { memberships },
209
+ ...sources === void 0 || sources.length === 0 ? {} : { sources },
210
+ ...memberSources === void 0 || memberSources.length === 0 ? {} : { memberSources },
211
+ ...suspension === void 0 ? {} : { suspension },
212
+ ...roles === void 0 ? {} : { roles },
213
+ ...input.customRoles === true || customRolesOn(rls) ? { customRoles: {
214
+ ...customRoleNames(policy, requires.keys),
215
+ ...rls?.customRoleWrites?.roles === void 0 ? {} : { table: rls.customRoleWrites.roles }
216
+ } } : {},
217
+ ...input.capabilities === true || rls?.capabilities === true ? { capabilities: true } : {},
218
+ ...apiKeys === void 0 ? {} : { apiKeys },
219
+ ...rls?.anonymousSignIns === "deny" ? { anonymousSignIns: "deny" } : {},
220
+ ...ownership === void 0 ? {} : { ownership },
221
+ ...rls?.ownershipTriggers === void 0 ? {} : { skipOwnershipTriggers: skippedOwnershipTriggers(rls.ownershipTriggers, scopes) },
222
+ ...rls?.assignments === void 0 ? {} : { assignments: rls.assignments === true ? { tables: [] } : {
223
+ tables: rls.assignments.tables ?? [],
224
+ ...rls.assignments.ownRole === void 0 ? {} : { ownRole: rls.assignments.ownRole }
225
+ } },
226
+ ...options.fieldsMode === void 0 ? {} : { fields: options.fieldsMode },
227
+ ...graph.size === 0 ? {} : { graph: {
228
+ closures: closureDepths(graph),
229
+ resources: policy.resources,
230
+ ...rls?.tables === void 0 ? {} : { tables: rls.tables }
231
+ } }
232
+ },
233
+ graph,
234
+ ownership,
235
+ schema,
236
+ authorize
237
+ };
238
+ }
239
+ //#endregion
240
+ //#region src/cli/rls/generate-options.ts
241
+ function isRefusal(value) {
242
+ return "refusal" in value;
243
+ }
244
+ /** The checks that need no policy: `--fields` and `--revoke-columns`. */
245
+ function generateOptions(input) {
246
+ const rls = input.config.rls;
247
+ const fieldsMode = input.fields ?? rls?.fields;
248
+ if (fieldsMode !== void 0 && fieldsMode !== "views") return { refusal: `rls generate --fields must be views (got ${fieldsMode})` };
249
+ const revokeColumns = input.revokeColumns === true || rls?.revokeColumns === true;
250
+ if (revokeColumns && fieldsMode === void 0) return { refusal: "rls generate --revoke-columns needs --fields views" };
251
+ return {
252
+ fieldsMode,
253
+ revokeColumns
254
+ };
255
+ }
256
+ /** Whether `--split` names `part`, which then leaves the helpers or the warnings. */
257
+ function splitsPart(raw, part) {
258
+ const split = parseSplit(raw);
259
+ return typeof split !== "string" && split?.includes(part) === true;
260
+ }
261
+ //#endregion
262
+ //#region src/cli/rls/generate-output.ts
263
+ function moveUndiffed(sql, label, part) {
264
+ const { kept, moved } = splitUndiffed(sql);
265
+ const [first = "", ...rest] = kept.split("\n");
266
+ return {
267
+ sql: [
268
+ first,
269
+ `-- what supabase db diff drops from this part is in ${label}`,
270
+ ...rest
271
+ ].join("\n"),
272
+ grants: `-- the privileges and view options supabase db diff drops from the ${part} part\n${moved.join("\n")}`
273
+ };
274
+ }
275
+ /** The files `generate` writes: one, or one per `--split` part, plus the hook's grants. */
276
+ function outputFiles(plan) {
277
+ const { input } = plan;
278
+ const split = parseSplit(input.split);
279
+ if (typeof split === "string") return split;
280
+ if (split?.includes("policies") === true && plan.helpersOnly) return "rls generate --helpers-only writes no policies part; drop it from --split";
281
+ if (split === void 0) {
282
+ if (input.grantsOut !== void 0) return "rls generate --grants-out needs --split with the helpers or hook part";
283
+ if (input.seedsOut !== void 0) return "rls generate --seeds-out needs --split with the seeds part";
284
+ return [{
285
+ part: "rls",
286
+ rel: plan.outRel,
287
+ text: plan.text
288
+ }];
289
+ }
290
+ if (input.target !== "sql") return "rls generate --split needs --target sql";
291
+ const pgDelta = input.out === void 0 && input.config.rls?.out === void 0 ? supabaseConfig(input.cwd).pgDelta : void 0;
292
+ if (pgDelta === void 0 && !plan.outRel.includes("{part}")) return `rls generate --split needs {part} in --out, for example supabase/schemas/identity/056_permdock_{part}.sql (got ${plan.outRel})`;
293
+ if (pgDelta !== void 0 && split.includes("helpers") && !split.includes("seeds")) return "rls generate --split helpers under pg-delta needs the seeds part with --seeds-out: pg-delta rejects the role_permissions rows in a declarative file";
294
+ if (pgDelta !== void 0 && split.includes("seeds") && input.seedsOut === void 0) return "rls generate --split seeds under pg-delta needs --seeds-out: pg-delta does not diff the role_permissions rows, so they go in a migration";
295
+ const at = (part, schema = plan.schema) => pgDelta === void 0 ? partPath(plan.outRel, part) : pgDeltaPath(pgDelta.schemaDir, part, schema);
296
+ if (input.grantsOut !== void 0 && !split.includes("hook") && !split.includes("helpers")) return "rls generate --grants-out needs the helpers or hook part in --split";
297
+ if (input.seedsOut !== void 0 && !split.includes("seeds")) return "rls generate --seeds-out needs the seeds part in --split";
298
+ if (split.includes("hook") && input.config.supabase?.hook === void 0) return "rls generate --split hook needs supabase.hook in permdock.config.ts";
299
+ const sql = plan.sql();
300
+ const relocated = pgDelta === void 0 ? void 0 : sql.servicePolicies;
301
+ if (relocated !== void 0 && split.includes("policies") && !split.includes("seeds")) return "rls generate --split policies under pg-delta with rls.realtime or rls.storage needs the seeds part with --seeds-out: the shadow database has no realtime.messages or storage.objects, so their policies go in that migration";
302
+ const seeds = relocated === void 0 || !split.includes("policies") ? sql.seeds : `${sql.seeds}\n${relocated}`;
303
+ const files = [];
304
+ const label = grantsLabel(input.grantsOut);
305
+ let grantsMarker = `${GRANTS_MARKER} schema=${plan.schema}`;
306
+ const grants = [];
307
+ for (const part of split) switch (part) {
308
+ case "helpers": {
309
+ if (label === void 0) {
310
+ files.push({
311
+ part,
312
+ rel: at(part),
313
+ text: sql.helpers
314
+ });
315
+ break;
316
+ }
317
+ const moved = moveUndiffed(sql.helpers, label, part);
318
+ files.push({
319
+ part,
320
+ rel: at(part),
321
+ text: moved.sql
322
+ });
323
+ grants.push(moved.grants);
324
+ break;
325
+ }
326
+ case "seeds": {
327
+ const out = input.seedsOut;
328
+ if (out !== void 0 && isSeedsDirectory(input.cwd, out)) {
329
+ const planned = seedsMigration(input.cwd, out, SEEDS_MARKER, seeds, input.io.now?.() ?? /* @__PURE__ */ new Date());
330
+ if (!planned.current || input.check) files.push({
331
+ part,
332
+ rel: planned.rel,
333
+ text: seeds
334
+ });
335
+ break;
336
+ }
337
+ files.push({
338
+ part,
339
+ rel: out ?? at(part),
340
+ text: seeds
341
+ });
342
+ break;
343
+ }
344
+ case "indexes":
345
+ files.push({
346
+ part,
347
+ rel: at(part),
348
+ text: sql.indexes
349
+ });
350
+ break;
351
+ case "policies":
352
+ files.push({
353
+ part,
354
+ rel: at(part),
355
+ text: pgDelta === void 0 ? sql.policies : sql.declarativePolicies
356
+ });
357
+ break;
358
+ case "hook": {
359
+ const hook = supabaseHookSql(plan.scopes, input.config, {}, label);
360
+ files.push({
361
+ part,
362
+ rel: at(part, hook.manifest.hook.schema),
363
+ text: hook.sql
364
+ });
365
+ const [marker = grantsMarker, ...rest] = hook.grants.split("\n");
366
+ grantsMarker = marker;
367
+ grants.push(rest.join("\n").trimEnd());
368
+ break;
369
+ }
370
+ default: return part;
371
+ }
372
+ if (input.grantsOut !== void 0) files.push({
373
+ part: "grants",
374
+ rel: input.grantsOut,
375
+ text: `${[grantsMarker, ...grants].join("\n")}\n`
376
+ });
377
+ return files;
378
+ }
379
+ //#endregion
130
380
  //#region src/cli/approval-schema.ts
131
381
  /** An RFC 3339 instant as `Temporal.Instant` prints it; pg_jsonschema asserts no `format`. */
132
382
  const INSTANT = String.raw`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$`;
@@ -249,7 +499,7 @@ const APPROVAL_REQUEST_SCHEMA = {
249
499
  }
250
500
  };
251
501
  //#endregion
252
- //#region src/cli/rls-approvals.ts
502
+ //#region src/cli/rls/approvals.ts
253
503
  /** The objects `rls.approvals` adds. Names are part of the SQL contract `supabaseApprovalStore` calls. */
254
504
  const APPROVAL_STORE = {
255
505
  table: "approval_requests",
@@ -315,8 +565,8 @@ $permdock$;\n`;
315
565
  * resolved by the server, which grants its own role.
316
566
  */
317
567
  function approvalStoreSql(ctx, jsonSchema = false) {
318
- const table = qualified$1(ctx, APPROVAL_STORE.table);
319
- const fn = (name) => qualified$1(ctx, name);
568
+ const table = qualified(ctx, APPROVAL_STORE.table);
569
+ const fn = (name) => qualified(ctx, name);
320
570
  const index = (name) => `"${APPROVAL_STORE.table}_${name}"`;
321
571
  return `-- approval store: ApprovalRequest bodies, opened, resolved and consumed through the functions below; supabaseApprovalStore calls them
322
572
  create table if not exists ${table} (
@@ -514,7 +764,7 @@ function mirrorSet(table, mirror, body) {
514
764
  * grants and policies stay the app's.
515
765
  */
516
766
  function adoptedApprovalStoreSql(ctx, adopt, jsonSchema = false) {
517
- const table = quoteTable(qualifiedTable$1(adopt.table));
767
+ const table = quoteQualified(adopt.table);
518
768
  const base = adopt.table.split(".").at(-1) ?? adopt.table;
519
769
  const token = quoteIdent$1(adopt.token ?? "token");
520
770
  const body = quoteIdent$1(adopt.body ?? "body");
@@ -524,7 +774,7 @@ function adoptedApprovalStoreSql(ctx, adopt, jsonSchema = false) {
524
774
  if (open !== "insert" && open !== "attach") throw new Error(`PermDock CLI: rls.approvals.open must be 'insert' or 'attach', got '${String(open)}'`);
525
775
  const schema = adopt.schema;
526
776
  if (schema !== void 0 && (typeof schema !== "string" || schema === "")) throw new Error("PermDock CLI: rls.approvals.schema must be a schema name");
527
- const fn = (name) => schema === void 0 ? qualified$1(ctx, name) : `${quoteIdent$1(schema)}.${name}`;
777
+ const fn = (name) => schema === void 0 ? qualified(ctx, name) : `${quoteIdent$1(schema)}.${name}`;
528
778
  const index = (name) => quoteIdent$1(`${base}_permdock_${name}`);
529
779
  const status = `a.${body} ->> 'status'`;
530
780
  const expires = `(a.${body} ->> 'expiresAt')::timestamptz`;
@@ -695,10 +945,200 @@ ${serverOnly(fn(APPROVAL_STORE.cancel), "jsonb, text, text, text")}
695
945
  `;
696
946
  }
697
947
  //#endregion
698
- //#region src/cli/rls-break-glass.ts
948
+ //#region src/cli/rls/backfill.ts
949
+ const PREFIX$1 = "PermDock CLI";
950
+ /** A column of the roles-table row `r`. */
951
+ function col(name) {
952
+ return `r.${quoteIdent$1(name)}`;
953
+ }
954
+ function schemaOf(table) {
955
+ return table.includes(".") ? table.split(".")[0] ?? "public" : "public";
956
+ }
957
+ /** The roles-table column a reference joins on, after checking it points at `roles`. */
958
+ function roleRef(role, table, roles, label) {
959
+ const pairs = Object.entries(role.on);
960
+ const [pair] = pairs;
961
+ if (pair === void 0 || pairs.length !== 1) throw new Error(`${PREFIX$1}: ${label}.on must map exactly one column of ${table} to ${role.through}, for example { role_id: 'id' }`);
962
+ const through = role.through.includes(".") ? role.through : `${schemaOf(table)}.${role.through}`;
963
+ if (qualifySqlTable(through) !== qualifySqlTable(roles.table)) throw new Error(`${PREFIX$1}: ${label}.through must be rls.customRoleWrites.roles.table (${roles.table}), got ${role.through}`);
964
+ if (role.column !== roles.key) throw new Error(`${PREFIX$1}: ${label}.column must be rls.customRoleWrites.roles.key (${roles.key}), got ${role.column}`);
965
+ const [ref, id] = pair;
966
+ return {
967
+ ref,
968
+ id
969
+ };
970
+ }
971
+ /**
972
+ * A legacy key as SQL the way `mapKey` maps it: `rls.migrate.keys`, then
973
+ * `renamed`, then the longest `rls.migrate.prefixes` match, and a former key
974
+ * read as its current key.
975
+ */
976
+ function legacyKeySql(expr, migrate, renamed) {
977
+ const exact = new Map(Object.entries(renamed));
978
+ for (const [from, to] of Object.entries(migrate?.keys ?? {})) exact.set(from, to);
979
+ const prefixes = Object.entries(migrate?.prefixes ?? {}).toSorted(([a], [b]) => b.length - a.length || (a < b ? -1 : a > b ? 1 : 0));
980
+ const prefixed = prefixes.length === 0 ? expr : `(case ${prefixes.map(([from, to]) => `when left(${expr}, ${String(from.length)}) = ${quoteLiteral(from)} then ${quoteLiteral(to)} || substr(${expr}, ${String(from.length + 1)})`).join(" ")} else ${expr} end)`;
981
+ const mapped = exact.size === 0 ? prefixed : `(case ${expr} ${[...exact.entries()].toSorted(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([from, to]) => `when ${quoteLiteral(from)} then ${quoteLiteral(to)}`).join(" ")} else ${prefixed} end)`;
982
+ const renames = Object.entries(renamed).toSorted(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
983
+ return currentKeyExpr(renames, mapped);
984
+ }
985
+ function sourceSql(source, table, label) {
986
+ return roleColumn(source, table, "x", {
987
+ label,
988
+ prefix: PREFIX$1
989
+ }).lookup;
990
+ }
991
+ /**
992
+ * The `--backfill-out` migration: every custom role in the application's
993
+ * roles table that has no rows in `custom_role_permissions` or
994
+ * `custom_role_includes` yet is saved through
995
+ * `permdock_trusted_replace_custom_role_grants`, with the entries PermDock
996
+ * cannot hold left out and listed in one warning. Running it again copies
997
+ * nothing, so a role edited after the copy keeps its edits.
998
+ */
999
+ function backfillSql(ctx, from, migrate) {
1000
+ const custom = ctx.customRoles;
1001
+ const root = ctx.scopes[0]?.name;
1002
+ if (custom === void 0 || root === void 0) throw new Error(`${PREFIX$1}: rls.customRoles.from needs custom roles on`);
1003
+ if (ctx.authorize !== "database") throw new Error(`${PREFIX$1}: rls.customRoles.from needs database mode; in jwt mode custom roles ride the token, so build the grants claim with customRoleClaim`);
1004
+ const roles = custom.table;
1005
+ if (roles === void 0) throw new Error(`${PREFIX$1}: rls.customRoles.from needs rls.customRoleWrites.roles, the roles table each row belongs to`);
1006
+ const leveled = custom.levels === true;
1007
+ if (from.permissions.level !== void 0 && !leveled) throw new Error(`${PREFIX$1}: rls.customRoles.from.permissions.level is set, but no resource declares levels`);
1008
+ const tenantType = tenantTypeOf(ctx);
1009
+ const rolesTable = quoteQualified(roles.table);
1010
+ const tenant = roles.tenant === void 0 ? `null::${tenantType}` : `${col(roles.tenant)}::${tenantType}`;
1011
+ const scope = roles.scope === void 0 ? `case when ${tenant} is null then 'global' else ${quoteLiteral(root)} end` : `case when ${tenant} is null then 'global' else ${col(roles.scope)}::text end`;
1012
+ const scopeId = roles.id === void 0 ? "null::text" : `${col(roles.id)}::text`;
1013
+ const skipped = roles.skip === void 0 ? "false" : `coalesce(${col(roles.skip)}, false)`;
1014
+ const renamed = custom.renamed ?? {};
1015
+ const renames = Object.entries(renamed).toSorted(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
1016
+ const current = (expr) => currentKeyExpr(renames, expr);
1017
+ const known = sqlTextArray([...custom.permissions ?? [], ...renames.map(([former]) => former)]);
1018
+ const declared = sqlTextArray(custom.declared);
1019
+ const scopes = sqlTextArray(ctx.scopes.map((item) => item.name));
1020
+ const ceiling = qualified(ctx, CUSTOM_ROLES.ceiling);
1021
+ const rp = qualified(ctx, "role_permissions");
1022
+ const perms = qualified(ctx, CUSTOM_ROLES.permissions);
1023
+ const includes = qualified(ctx, CUSTOM_ROLES.includes);
1024
+ const replace = qualified(ctx, CUSTOM_ROLES.trustedReplace);
1025
+ const p = from.permissions;
1026
+ const pTable = qualifySqlTable(p.table);
1027
+ const pRef = roleRef(p.role, p.table, roles, "rls.customRoles.from.permissions.role");
1028
+ const legacy = sourceSql(p.permission, pTable, "rls.customRoles.from.permissions.permission");
1029
+ const effect = p.effect === void 0 ? "'allow'" : `coalesce(lower(x.${quoteIdent$1(p.effect)}::text), 'allow')`;
1030
+ const level = p.level === void 0 ? "null::text" : `nullif(x.${quoteIdent$1(p.level)}::text, '')`;
1031
+ const inCeiling = (key) => `exists (select 1 from ${ceiling} c where c.scope = v.scope and c.effect = 'allow' and ${current("c.permission")} = ${key})`;
1032
+ const levelCheck = leveled ? `
1033
+ when m.effect = 'allow' and m.level is not null and not exists (
1034
+ select 1 from ${ceiling} c
1035
+ where c.scope = v.scope and c.effect = 'allow' and ${current("c.permission")} = m.key
1036
+ and split_part(c.grant_key, '@', 2) = m.level
1037
+ ) then 'level ' || m.level || ' is not declared'` : "";
1038
+ const allowEntry = leveled ? "j.key || coalesce('@' || j.level, '')" : "j.key";
1039
+ const i = from.includes;
1040
+ const iTable = i === void 0 ? void 0 : qualifySqlTable(i.table);
1041
+ const iRef = i === void 0 || iTable === void 0 ? void 0 : roleRef(i.role, i.table, roles, "rls.customRoles.from.includes.role");
1042
+ const included = i === void 0 || iTable === void 0 ? void 0 : sourceSql(i.include, iTable, "rls.customRoles.from.includes.include");
1043
+ const refs = [`${col(pRef.id)} as permissions_ref`, ...iRef === void 0 ? [] : [`${col(iRef.id)} as includes_ref`]].join(", ");
1044
+ const includeBlock = i === void 0 || iTable === void 0 || iRef === void 0 || included === void 0 ? " v_include := null;" : ` with raw as (
1045
+ select ${included} as role
1046
+ from ${quoteTable(iTable)} x
1047
+ where x.${quoteIdent$1(iRef.ref)} = v.includes_ref
1048
+ ),
1049
+ judged as (
1050
+ select n.role, case
1051
+ when n.role is null or not (n.role = any(${declared})) then 'not a declared role'
1052
+ when exists (
1053
+ select 1 from ${rp} rp
1054
+ where rp.role = n.role and rp.effect = 'allow'
1055
+ and not ${inCeiling(current("rp.permission"))}
1056
+ ) then 'allows permissions outside the ceiling of ' || v.scope
1057
+ end as dropped
1058
+ from raw n
1059
+ )
1060
+ select array_agg(distinct j.role) filter (where j.dropped is null),
1061
+ array_agg(v.role || ': includes ' || coalesce(j.role, 'null') || ', ' || j.dropped) filter (where j.dropped is not null)
1062
+ into v_include, v_lost
1063
+ from judged j;
1064
+ v_dropped := v_dropped || coalesce(v_lost, '{}'::text[]);`;
1065
+ return `${BACKFILL_MARKER} schema=${ctx.schema ?? "permdock"}
1066
+ -- custom roles copied from ${qualifySqlTable(roles.table)} (rls.customRoles.from) through ${replace}
1067
+ -- a role that already has rows is left as it is; entries PermDock cannot hold are dropped and listed in one warning
1068
+ do $permdock$
1069
+ declare
1070
+ v record;
1071
+ v_allow text[];
1072
+ v_deny text[];
1073
+ v_include text[];
1074
+ v_lost text[];
1075
+ v_dropped text[] := '{}'::text[];
1076
+ v_copied integer := 0;
1077
+ v_present integer := 0;
1078
+ begin
1079
+ for v in
1080
+ select ${tenant} as tenant_id, ${scope} as scope, ${scopeId} as scope_id, ${col(roles.key)}::text as role, ${refs}
1081
+ from ${rolesTable} r
1082
+ where not ${skipped} and not (${col(roles.key)}::text = any(${declared}))
1083
+ order by 4, 2, 1, 3
1084
+ loop
1085
+ if v.role is null or not (v.scope = 'global' or v.scope = any(${scopes})) then
1086
+ v_dropped := v_dropped || (coalesce(v.role, 'null') || ': scope ' || coalesce(v.scope, 'null') || ' is not declared');
1087
+ continue;
1088
+ end if;
1089
+ if exists (
1090
+ select 1 from ${perms} c
1091
+ where c.tenant_id is not distinct from v.tenant_id and c.scope = v.scope and c.scope_id is not distinct from v.scope_id and c.role = v.role
1092
+ ) or exists (
1093
+ select 1 from ${includes} c
1094
+ where c.tenant_id is not distinct from v.tenant_id and c.scope = v.scope and c.scope_id is not distinct from v.scope_id and c.role = v.role
1095
+ ) then
1096
+ v_present := v_present + 1;
1097
+ continue;
1098
+ end if;
1099
+ with raw as (
1100
+ select ${legacy} as legacy, ${effect} as effect, ${level} as level
1101
+ from ${quoteTable(pTable)} x
1102
+ where x.${quoteIdent$1(pRef.ref)} = v.permissions_ref
1103
+ ),
1104
+ mapped as (
1105
+ select n.legacy, ${legacyKeySql("n.legacy", migrate, renamed)} as key, n.effect, n.level
1106
+ from raw n
1107
+ ),
1108
+ judged as (
1109
+ select m.*, case
1110
+ when m.effect not in ('allow', 'deny') then 'effect ' || m.effect || ' is not allow or deny'
1111
+ when m.key is null or not (m.key = any(${known})) then 'not a declared permission'
1112
+ when m.effect = 'allow' and not ${inCeiling("m.key")} then 'outside the ceiling of ' || v.scope${levelCheck}
1113
+ end as dropped
1114
+ from mapped m
1115
+ )
1116
+ select array_agg(distinct ${allowEntry}) filter (where j.dropped is null and j.effect = 'allow'),
1117
+ array_agg(distinct j.key) filter (where j.dropped is null and j.effect = 'deny'),
1118
+ array_agg(v.role || ': ' || coalesce(j.legacy, 'null') || ', ' || j.dropped) filter (where j.dropped is not null)
1119
+ into v_allow, v_deny, v_lost
1120
+ from judged j;
1121
+ v_dropped := v_dropped || coalesce(v_lost, '{}'::text[]);
1122
+ ${includeBlock}
1123
+ if v_allow is null and v_deny is null and v_include is null then
1124
+ continue;
1125
+ end if;
1126
+ perform ${replace}(v.tenant_id, v.scope, v.scope_id, v.role, v_allow, v_deny, v_include);
1127
+ v_copied := v_copied + 1;
1128
+ end loop;
1129
+ raise notice 'permdock backfill: copied % custom role(s), % already present', v_copied, v_present;
1130
+ if cardinality(v_dropped) > 0 then
1131
+ raise warning 'permdock backfill: dropped % entr(ies) PermDock cannot hold: %', cardinality(v_dropped), array_to_string(v_dropped[1:50], '; ');
1132
+ end if;
1133
+ end
1134
+ $permdock$;
1135
+ `;
1136
+ }
1137
+ //#endregion
1138
+ //#region src/cli/rls/break-glass.ts
699
1139
  /** The audit table every generated break-glass read writes to. Part of the SQL contract. */
700
1140
  const BREAK_GLASS_AUDIT = "permdock_break_glass_audit";
701
- const IDENT = /^[a-z][a-z0-9_]*$/u;
1141
+ const IDENT$1 = /^[a-z][a-z0-9_]*$/u;
702
1142
  /**
703
1143
  * The resources a `breakGlass` grant targets, with the table each reads. A
704
1144
  * break-glass grant is non-portable, so `compileGrants` never turns it into a
@@ -747,22 +1187,19 @@ function rowsFilter(ctx, entry) {
747
1187
  const asked = `p_permission = ${quoteLiteral(grant.permission)}`;
748
1188
  if (grant.scope === "anyone") {
749
1189
  if (grant.root === void 0) return [`(${asked})`];
750
- return grant.column === void 0 ? [] : [`(${asked} and ${quoteIdent$1(grant.column)} in (select ${qualified$1(ctx, memberIdsHelper(grant.root))}()))`];
1190
+ return grant.column === void 0 ? [] : [`(${asked} and ${quoteIdent$1(grant.column)} in (select ${qualified(ctx, memberIdsHelper(grant.root))}()))`];
751
1191
  }
752
1192
  if (grant.scope !== "global" && grant.column === void 0) return [];
753
1193
  return [`(${asked} and ${accessSql(ctx, grant.scope, breakGlassKey(grant.permission), grant.column)})`];
754
1194
  });
755
1195
  return arms.length === 0 ? "false" : [...new Set(arms)].join("\n or ");
756
1196
  }
757
- function functionName(resource) {
758
- if (!IDENT.test(resource)) throw new Error(`PermDock CLI: unsafe break-glass resource '${resource}'`);
1197
+ function functionName$1(resource) {
1198
+ if (!IDENT$1.test(resource)) throw new Error(`PermDock CLI: unsafe break-glass resource '${resource}'`);
759
1199
  return `permdock_break_glass_${resource}`;
760
1200
  }
761
- function qualifiedTable(name) {
762
- return quoteTable(name.includes(".") ? name : `public.${name}`);
763
- }
764
- function auditSql(ctx) {
765
- const audit = qualified$1(ctx, quoteIdent$1(BREAK_GLASS_AUDIT));
1201
+ function auditSql$1(ctx) {
1202
+ const audit = qualified(ctx, quoteIdent$1(BREAK_GLASS_AUDIT));
766
1203
  return `-- break-glass audit: one row per session that read restricted data
767
1204
  -- RLS never compiles a break-glass override, so restricted rows stay denied to a direct read
768
1205
  create table if not exists ${audit} (
@@ -777,9 +1214,9 @@ alter table ${audit} enable row level security;
777
1214
  revoke all on table ${audit} from anon, authenticated, public;`;
778
1215
  }
779
1216
  function readFunctionSql(ctx, entry) {
780
- const fn = qualified$1(ctx, functionName(entry.resource));
781
- const audit = qualified$1(ctx, quoteIdent$1(BREAK_GLASS_AUDIT));
782
- const table = qualifiedTable(entry.table);
1217
+ const fn = qualified(ctx, functionName$1(entry.resource));
1218
+ const audit = qualified(ctx, quoteIdent$1(BREAK_GLASS_AUDIT));
1219
+ const table = quoteQualified(entry.table);
783
1220
  const session = subjectClaimJsonSql(ctx, "break_glass");
784
1221
  const subject = subjectIdSql(ctx);
785
1222
  return `-- ${entry.resource}: reads restricted rows through a checked, audited break-glass session
@@ -827,10 +1264,10 @@ grant execute on function ${fn}(text) to authenticated;`;
827
1264
  */
828
1265
  function breakGlassSql(ctx, entries) {
829
1266
  if (entries.length === 0) return "";
830
- return [auditSql(ctx), ...entries.map((entry) => readFunctionSql(ctx, entry))].join("\n\n");
1267
+ return [auditSql$1(ctx), ...entries.map((entry) => readFunctionSql(ctx, entry))].join("\n\n");
831
1268
  }
832
1269
  //#endregion
833
- //#region src/cli/rls-emit.ts
1270
+ //#region src/cli/rls/emit.ts
834
1271
  const HEADER = "-- generated by permdock rls generate\n-- fail-closed: never target a bypass role\n";
835
1272
  const TRUSTED_READER_GRANT = /^grant execute on function [^\n]+\.permdock_trusted_role_permissions\([^)\n]*\) to [^\n;]+;$/gmu;
836
1273
  function assertNoServiceRole(text) {
@@ -855,7 +1292,7 @@ function tablesOf(policies) {
855
1292
  /** Enable (and force) row level security, then the table grants for the commands a policy allows. */
856
1293
  function tableSql(table, policies, force, views) {
857
1294
  const closed = views.find((view) => view.table === table && view.companion !== void 0);
858
- const target = quoteTable(qualifiedTable$1(table));
1295
+ const target = quoteQualified(table);
859
1296
  const lines = [`alter table ${target} enable row level security;`];
860
1297
  if (force) lines.push(`alter table ${target} force row level security;`);
861
1298
  lines.push(`revoke all on table ${target} from anon, authenticated;`);
@@ -875,7 +1312,7 @@ function tableSql(table, policies, force, views) {
875
1312
  }
876
1313
  /** `drop policy if exists` and `create policy` for one policy, without the table's grants. */
877
1314
  function policySql(item) {
878
- const target = quoteTable(qualifiedTable$1(item.table));
1315
+ const target = quoteQualified(item.table);
879
1316
  const lines = [
880
1317
  `drop policy if exists ${quoteIdent$1(item.name)} on ${target};`,
881
1318
  `create policy ${quoteIdent$1(item.name)}`,
@@ -1018,7 +1455,7 @@ function defaultOut(target) {
1018
1455
  }
1019
1456
  }
1020
1457
  //#endregion
1021
- //#region src/cli/rls-realtime-storage.ts
1458
+ //#region src/cli/rls/realtime-storage.ts
1022
1459
  const SCOPE_SEGMENT = /^\{([a-z][a-z0-9_]*)\}$/u;
1023
1460
  const LITERAL_SEGMENT = /^[^{}:\s]+$/u;
1024
1461
  const TOPIC = "(select realtime.topic())";
@@ -1044,7 +1481,7 @@ function declaredScope(facts, scope, where) {
1044
1481
  * relationship or a `where` on the same permission never widens it.
1045
1482
  */
1046
1483
  function permitted(ctx, value, scope, key) {
1047
- return `${value} in (select x::text from ${qualified$1(ctx, permittedByPermissionHelper(scope))}(${quoteLiteral(key)}) x)`;
1484
+ return `${value} in (select x::text from ${qualified(ctx, permittedByPermissionHelper(scope))}(${quoteLiteral(key)}) x)`;
1048
1485
  }
1049
1486
  function escapeRegex(text) {
1050
1487
  return text.replaceAll(/[.*+?^${}()|[\]\\]/gu, String.raw`\$&`);
@@ -1161,7 +1598,7 @@ function realtimeStoragePolicies(ctx, facts, config, warnings = []) {
1161
1598
  return policies;
1162
1599
  }
1163
1600
  //#endregion
1164
- //#region src/cli/rls-rows.ts
1601
+ //#region src/cli/rls/rows.ts
1165
1602
  /** `permitted_<resource>_rows(p_permission)`: the resource's row ids the subject may act on with one permission. */
1166
1603
  function rowsHelper(resource) {
1167
1604
  const name = graphSqlName(resource);
@@ -1243,7 +1680,7 @@ function inheritTargets(policy) {
1243
1680
  return [...out].toSorted();
1244
1681
  }
1245
1682
  function stubSql(ctx, resource) {
1246
- return `create or replace function ${qualified$1(ctx, rowsHelper(resource))}(p_permission text)
1683
+ return `create or replace function ${qualified(ctx, rowsHelper(resource))}(p_permission text)
1247
1684
  returns setof text
1248
1685
  language sql
1249
1686
  stable
@@ -1254,7 +1691,7 @@ as $$
1254
1691
  $$;`;
1255
1692
  }
1256
1693
  function rowSql(ctx, rows, table, where) {
1257
- const fn = qualified$1(ctx, rows.replace(/_rows$/u, "_row"));
1694
+ const fn = qualified(ctx, rows.replace(/_rows$/u, "_row"));
1258
1695
  return `create or replace function ${fn}(p_row ${table}, p_permission text)
1259
1696
  returns boolean
1260
1697
  language sql
@@ -1269,8 +1706,8 @@ grant execute on function ${fn}(${table}, text) to authenticated;`;
1269
1706
  }
1270
1707
  function resourceSql(ctx, node, branches, tables) {
1271
1708
  const name = rowsHelper(node.name);
1272
- const fn = qualified$1(ctx, name);
1273
- const table = quoteTable(qualifiedTable$1(tableFor(node.name, tables)));
1709
+ const fn = qualified(ctx, name);
1710
+ const table = quoteQualified(tableFor(node.name, tables));
1274
1711
  const cases = [...branches.keys()].toSorted().flatMap((key) => {
1275
1712
  const sql = permissionCase(key, branches.get(key) ?? []);
1276
1713
  return sql === void 0 ? [] : [sql];
@@ -1293,7 +1730,7 @@ ${rowSql(ctx, name, table, where)}`;
1293
1730
  if (ctx.dialect === "neon") return rows;
1294
1731
  return `${rows}
1295
1732
  -- the same rows for a user the caller names, with claims it supplies; no client role may execute it
1296
- ${forUserSql(ctx, qualified$1(ctx, `${name}_for`), fn)}`;
1733
+ ${forUserSql(ctx, qualified(ctx, `${name}_for`), fn)}`;
1297
1734
  }
1298
1735
  /**
1299
1736
  * `permitted_<resource>_rows(p_permission)` for each resource `select`
@@ -1320,43 +1757,125 @@ function rowHelpersSql(ctx, policy, branches, select, tables, warnings) {
1320
1757
  return chunks.length === 0 ? "" : `${chunks.join("\n\n")}\n`;
1321
1758
  }
1322
1759
  //#endregion
1323
- //#region src/cli/rls-shims.ts
1324
- const NAME = /^[a-z_][a-z0-9_]*$/u;
1760
+ //#region src/cli/rls/generate-policies.ts
1761
+ const SERVICE_TABLES_HEADER = "-- Supabase Realtime and Storage: rls.realtime and rls.storage. RLS on both tables is Supabase's.";
1325
1762
  /**
1326
- * The key a legacy caller passes, mapped the way `rls migrate` maps it:
1327
- * `keys` and `renamed` exactly, then the longest `prefixes` entry.
1763
+ * A `realtime.messages` or `storage.objects` policy that applies only where
1764
+ * the table exists. The Realtime and Storage services create those tables, so
1765
+ * a local stack with the service off, and pg-delta's shadow database, have
1766
+ * none.
1328
1767
  */
1329
- function mappedKey(config, renamed) {
1330
- const exact = {
1331
- ...renamed,
1332
- ...config.keys
1333
- };
1334
- const prefixes = Object.entries(config.prefixes ?? {}).toSorted(([a], [b]) => b.length - a.length);
1335
- const fallback = prefixes.length === 0 ? "p_key" : `case ${prefixes.map(([from, to]) => `when pg_catalog.starts_with(p_key, ${quoteLiteral(from)}) then ${quoteLiteral(to)} || pg_catalog.substr(p_key, ${String(from.length + 1)})`).join(" ")} else p_key end`;
1336
- if (Object.keys(exact).length === 0) return fallback;
1337
- return `coalesce(${quoteLiteral(JSON.stringify(exact))}::jsonb ->> p_key, ${fallback})`;
1768
+ function guardedPolicySql(item) {
1769
+ const body = policySql(item).trimEnd().split("\n").map((line) => ` ${line}`).join("\n");
1770
+ return `do $permdock$
1771
+ begin
1772
+ if to_regclass(${quoteLiteral(qualifySqlTable(item.table))}) is not null then
1773
+ ${body}
1774
+ end if;
1775
+ end
1776
+ $permdock$;
1777
+ `;
1338
1778
  }
1339
- function scopeOf(ctx, scope) {
1340
- if (!ctx.scopes.some((item) => item.name === scope)) throw new Error(`PermDock CLI: rls.migrate helper scope '${scope}' is not a declared scope`);
1341
- return scope;
1779
+ /** Appends the `realtime.messages` and `storage.objects` policies, which never get `tableSql`. */
1780
+ function withHelperTables(sql, helperTables) {
1781
+ if (helperTables === void 0) return sql;
1782
+ const tables = `${helperTables.trimEnd()}\n`;
1783
+ return sql.trim() === "" ? tables : `${sql.trimEnd()}\n\n${tables}`;
1342
1784
  }
1343
- function addKey(list, key) {
1344
- if (!list.includes(key)) list.push(key);
1785
+ function databaseModeWarnings(policy, { ctx }, warnings) {
1786
+ if (ctx.authorize !== "database") return;
1787
+ for (const { name } of ctx.scopes) if (policy.grants.some((grant) => grant.scope === name) && scopeTable(ctx, name) === void 0 && scopeSources(ctx, name).length === 0) warnings.push(`${name}-scoped grants read the ${name} memberships table in database mode: set rls.memberships.scopes.${name}, rls.membershipSources or supabase.hook.memberships (or --memberships for the first scope), otherwise they deny`);
1345
1788
  }
1346
- /**
1347
- * The grant keys behind each permission, per scope: the keys of its
1348
- * unconditional allows, and every deny key. A shim cannot apply a row
1349
- * condition or a validity window, so a conditional allow answers nothing and
1350
- * a conditional deny always subtracts; the conditioned keys are listed apart.
1351
- * A break-glass key answers only the break-glass read, so it is left out too.
1352
- * Rows seeded under a former key are left out; the shim maps a former key to
1353
- * the current one first.
1354
- */
1355
- function shimGrants(rows, conditioned, renamed) {
1356
- const byScope = /* @__PURE__ */ new Map();
1357
- for (const row of rows.toSorted((a, b) => a.grantKey < b.grantKey ? -1 : a.grantKey > b.grantKey ? 1 : 0)) {
1358
- if (Object.hasOwn(renamed, row.permission) || row.effect === "allow" && row.grantKey === breakGlassKey(row.permission)) continue;
1359
- const scope = byScope.get(row.scope) ?? {};
1789
+ /** The grants compiled into table policies, and the Realtime and Storage policies beside them. */
1790
+ function generatePolicies(input, policy, options, built, warnings) {
1791
+ const rls = input.config.rls;
1792
+ const { ctx } = built;
1793
+ const { fieldsMode, revokeColumns } = options;
1794
+ databaseModeWarnings(policy, built, warnings);
1795
+ const compiled = compileGrants(policy, ctx, rls?.tables, warnings, input.skipClosures, rls?.rowHelpers !== void 0 || inheritTargets(policy).length > 0);
1796
+ const helpersOnly = input.helpersOnly === true || rls?.helpersOnly === true;
1797
+ if (helpersOnly && (input.target !== "sql" || revokeColumns)) return { refusal: "rls generate --helpers-only needs --target sql and no --revoke-columns: the table grants are hand-written, so revoke the restricted columns there" };
1798
+ const views = fieldsMode === void 0 ? [] : fieldViews(policy, ctx, compiled.branches, {
1799
+ revokeColumns,
1800
+ helpersOnly
1801
+ }, warnings);
1802
+ const force = input.force === true || rls?.force === true;
1803
+ if (force && views.some((view) => view.companion !== void 0)) warnings.push("--force with --revoke-columns: each <table>_visible_fields companion reads as its owner, so its owner needs BYPASSRLS or the restricted columns read as null");
1804
+ const policyName = input.policyName ?? rls?.policyName;
1805
+ const assembled = helpersOnly ? [] : assemblePolicies(fieldsMode === void 0 ? compiled.branches : rowBranches(compiled.branches), {
1806
+ perRole: input.policyPerRole === true || rls?.policyPerRole === true,
1807
+ ...policyName === void 0 ? {} : { name: policyName }
1808
+ });
1809
+ const actorKinds = readOnlyActorKinds(rls?.readOnlyActors);
1810
+ if (actorKinds !== void 0 && helpersOnly) warnings.push("rls.readOnlyActors adds policies, and --helpers-only writes none: add the restrictive read-only policies to the hand-written ones");
1811
+ const withActors = (items) => actorKinds === void 0 ? items : [...items, ...readOnlyActorPolicies(items, subjectClaimJsonSql(ctx, "act"), actorKinds)];
1812
+ let tablePolicies;
1813
+ try {
1814
+ tablePolicies = realtimeStoragePolicies(ctx, {
1815
+ scopes: new Set(ctx.scopes.map((scope) => scope.name)),
1816
+ keys: new Set(listPermissions(policy.vocabulary.permissions).map((leaf) => leaf.key)),
1817
+ rowConditions: new Set(policyRowConditionKeys(policy))
1818
+ }, compact({
1819
+ realtime: rls?.realtime,
1820
+ storage: rls?.storage
1821
+ }), warnings);
1822
+ } catch (cause) {
1823
+ return { refusal: describeError(cause) };
1824
+ }
1825
+ if (tablePolicies.length > 0 && helpersOnly) warnings.push("rls.realtime and rls.storage add policies, and --helpers-only writes none: no realtime.messages or storage.objects policy is written");
1826
+ const servicePolicies = tablePolicies.length === 0 || helpersOnly ? [] : withActors(tablePolicies);
1827
+ const generated = {
1828
+ compiled,
1829
+ helpersOnly,
1830
+ force,
1831
+ views,
1832
+ policies: withActors(assembled)
1833
+ };
1834
+ if (servicePolicies.length === 0) return generated;
1835
+ return {
1836
+ ...generated,
1837
+ helperTables: [SERVICE_TABLES_HEADER, ...servicePolicies.map(policySql)].join("\n"),
1838
+ guardedTables: [`${SERVICE_TABLES_HEADER} A stack without the service has no table, so each policy is skipped there.`, ...servicePolicies.map(guardedPolicySql)].join("\n")
1839
+ };
1840
+ }
1841
+ //#endregion
1842
+ //#region src/cli/rls/shims.ts
1843
+ const NAME = /^[a-z_][a-z0-9_]*$/u;
1844
+ /**
1845
+ * The key a legacy caller passes, mapped the way `rls migrate` maps it:
1846
+ * `keys` and `renamed` exactly, then the longest `prefixes` entry.
1847
+ */
1848
+ function mappedKey(config, renamed) {
1849
+ const exact = {
1850
+ ...renamed,
1851
+ ...config.keys
1852
+ };
1853
+ const prefixes = Object.entries(config.prefixes ?? {}).toSorted(([a], [b]) => b.length - a.length);
1854
+ const fallback = prefixes.length === 0 ? "p_key" : `case ${prefixes.map(([from, to]) => `when pg_catalog.starts_with(p_key, ${quoteLiteral(from)}) then ${quoteLiteral(to)} || pg_catalog.substr(p_key, ${String(from.length + 1)})`).join(" ")} else p_key end`;
1855
+ if (Object.keys(exact).length === 0) return fallback;
1856
+ return `coalesce(${quoteLiteral(JSON.stringify(exact))}::jsonb ->> p_key, ${fallback})`;
1857
+ }
1858
+ function scopeOf(ctx, scope) {
1859
+ if (!ctx.scopes.some((item) => item.name === scope)) throw new Error(`PermDock CLI: rls.migrate helper scope '${scope}' is not a declared scope`);
1860
+ return scope;
1861
+ }
1862
+ function addKey(list, key) {
1863
+ if (!list.includes(key)) list.push(key);
1864
+ }
1865
+ /**
1866
+ * The grant keys behind each permission, per scope: the keys of its
1867
+ * unconditional allows, and every deny key. A shim cannot apply a row
1868
+ * condition or a validity window, so a conditional allow answers nothing and
1869
+ * a conditional deny always subtracts; the conditioned keys are listed apart.
1870
+ * A break-glass key answers only the break-glass read, so it is left out too.
1871
+ * Rows seeded under a former key are left out; the shim maps a former key to
1872
+ * the current one first.
1873
+ */
1874
+ function shimGrants(rows, conditioned, renamed) {
1875
+ const byScope = /* @__PURE__ */ new Map();
1876
+ for (const row of rows.toSorted((a, b) => a.grantKey < b.grantKey ? -1 : a.grantKey > b.grantKey ? 1 : 0)) {
1877
+ if (Object.hasOwn(renamed, row.permission) || row.effect === "allow" && row.grantKey === breakGlassKey(row.permission)) continue;
1878
+ const scope = byScope.get(row.scope) ?? {};
1360
1879
  byScope.set(row.scope, scope);
1361
1880
  const keys = (Object.hasOwn(scope, row.permission) ? scope[row.permission] : void 0) ?? {
1362
1881
  allow: [],
@@ -1404,7 +1923,7 @@ function grantKeys(keys, scope, effect) {
1404
1923
  }
1405
1924
  /** The instances of `scope` the caller holds an unconditional allow of the key in, minus those it holds a deny in. */
1406
1925
  function idsSql(ctx, keys, scope) {
1407
- const helper = qualified$1(ctx, permittedIdsHelper(scope));
1926
+ const helper = qualified(ctx, permittedIdsHelper(scope));
1408
1927
  return ` select a.id
1409
1928
  from ${grantKeys(keys, scope, "allow")}
1410
1929
  cross join lateral ${helper}(g.grant_key) a(id)
@@ -1414,7 +1933,7 @@ function idsSql(ctx, keys, scope) {
1414
1933
  cross join lateral ${helper}(g.grant_key) d(id)`;
1415
1934
  }
1416
1935
  function hasSql(ctx, keys) {
1417
- const has = qualified$1(ctx, HELPERS.has);
1936
+ const has = qualified(ctx, HELPERS.has);
1418
1937
  return `exists (select 1 from ${grantKeys(keys, "global", "allow")} where ${has}(g.grant_key))
1419
1938
  and not exists (select 1 from ${grantKeys(keys, "global", "deny")} where ${has}(g.grant_key))`;
1420
1939
  }
@@ -1432,7 +1951,7 @@ function shim(ctx, config, keys, fn, helper) {
1432
1951
  case "membership": {
1433
1952
  const scope = scopeOf(ctx, helper.scope);
1434
1953
  const type = scopeTypeOf(ctx, scope);
1435
- return shimFunction(fn, `p_id ${type}`, type, "boolean", ` select coalesce(p_id in (select ${qualified$1(ctx, memberIdsHelper(scope))}()), false)`);
1954
+ return shimFunction(fn, `p_id ${type}`, type, "boolean", ` select coalesce(p_id in (select ${qualified(ctx, memberIdsHelper(scope))}()), false)`);
1436
1955
  }
1437
1956
  case "global": return shimFunction(fn, "p_key text", "text", "boolean", ` select ${hasSql(ctx, keys)}`);
1438
1957
  case "scoped": {
@@ -1468,222 +1987,111 @@ function shimsSql(ctx, config, shims, renamed, grants) {
1468
1987
  return `${chunks.join("\n\n")}\n`;
1469
1988
  }
1470
1989
  //#endregion
1471
- //#region src/cli/rls-generate.ts
1472
- async function loadPolicy(cwd, config, from) {
1473
- const policyPath = from === "drizzle" ? config.policy : from ?? config.policy;
1474
- if (policyPath === void 0) throw new Error("PermDock CLI: rls generate needs policy in permdock.config.ts or --from");
1475
- return asPolicy(pickNamed(await loadModule(resolve(cwd, policyPath)), ["policy"]));
1476
- }
1477
- /** `rls.customRoleWrites.requires` as permission keys, or why it names none. */
1478
- function writeRequirement(policy, requires) {
1479
- if (requires === void 0) return {};
1480
- const leaves = listPermissions(policy.permissions);
1481
- const keys = requires === "manageRoles" ? leaves.filter((leaf) => leaf.meta.manageRoles === true).map((leaf) => leaf.key) : [...requires];
1482
- if (keys.length === 0) return { error: requires === "manageRoles" ? "rls.customRoleWrites.requires is 'manageRoles', but no permission declares meta.manageRoles" : "rls.customRoleWrites.requires names no permission" };
1483
- const known = new Set(leaves.map((leaf) => leaf.key));
1484
- const unknown = keys.filter((key) => !known.has(key));
1485
- if (unknown.length > 0) return { error: `rls.customRoleWrites.requires names undeclared permissions: ${unknown.join(", ")}` };
1486
- return { keys: [...new Set(keys)].toSorted() };
1487
- }
1488
- /** Declared role names, and those a tenant admin may compose into custom roles. */
1489
- function customRoleNames(policy, requires) {
1490
- const declared = [...roleNames(policy)].toSorted();
1491
- const assignable = declared.filter((name) => policy.rolesByName.get(name)?.assignable ?? listRoles(policy.vocabulary.roles).some((leaf) => leaf.key === name && leaf.assignable));
1492
- const renamed = renamedKeys(policy.vocabulary.permissions);
1493
- const leaves = listPermissions(policy.permissions);
1494
- const manage = leaves.filter((leaf) => leaf.meta.manageRoles === true).map((leaf) => leaf.key);
1495
- return {
1496
- declared,
1497
- assignable,
1498
- ...renamed.size === 0 ? {} : { renamed: Object.fromEntries(renamed) },
1499
- permissions: leaves.map((leaf) => leaf.key).toSorted(),
1500
- ...manage.length === 0 ? {} : { manage: manage.toSorted() },
1501
- ...requires === void 0 ? {} : { requires },
1502
- ...policy.levels === void 0 ? {} : { levels: true }
1503
- };
1504
- }
1505
- function moveUndiffed(sql, label, part) {
1506
- const { kept, moved } = splitUndiffed(sql);
1507
- const [first = "", ...rest] = kept.split("\n");
1508
- return {
1509
- sql: [
1510
- first,
1511
- `-- what supabase db diff drops from this part is in ${label}`,
1512
- ...rest
1513
- ].join("\n"),
1514
- grants: `-- the privileges and view options supabase db diff drops from the ${part} part\n${moved.join("\n")}`
1515
- };
1990
+ //#region src/cli/rls/triggers.ts
1991
+ const PREFIX = "PermDock CLI";
1992
+ const EVENTS = /* @__PURE__ */ new Set([
1993
+ "insert",
1994
+ "update",
1995
+ "delete",
1996
+ "truncate"
1997
+ ]);
1998
+ const AUDITED = [
1999
+ CUSTOM_ROLES.permissions,
2000
+ CUSTOM_ROLES.includes,
2001
+ "user_roles"
2002
+ ];
2003
+ const IDENT = /^[a-z_][a-z0-9_$]*$/u;
2004
+ /** The tables `sql` creates in `schema`, by name without the schema. */
2005
+ function generatedTables(sql, schema) {
2006
+ const prefix = `create table if not exists ${quoteIdent$1(schema)}.`;
2007
+ const found = /* @__PURE__ */ new Set();
2008
+ for (const line of sql.split("\n")) {
2009
+ if (!line.startsWith(prefix)) continue;
2010
+ const name = /^"?([A-Za-z_][\w$]*)"?/u.exec(line.slice(prefix.length))?.[1];
2011
+ if (name !== void 0) found.add(name);
2012
+ }
2013
+ return [...found].toSorted();
2014
+ }
2015
+ function functionName(name, label) {
2016
+ if (typeof name !== "string" || name.split(".").length !== 2) throw new Error(`${PREFIX}: ${label} must be a schema-qualified function such as audit.log_change, got ${JSON.stringify(name)}`);
2017
+ return quoteTable(name);
2018
+ }
2019
+ function knownTable(table, tables, label) {
2020
+ if (!tables.includes(table)) throw new Error(`${PREFIX}: ${label} names ${table}, which rls generate does not create with this config; it creates ${tables.join(", ")}`);
2021
+ }
2022
+ function triggerSql(table, trigger, label) {
2023
+ if (typeof trigger.name !== "string" || trigger.name === "" || trigger.name.toLowerCase().startsWith("permdock_")) throw new Error(`${PREFIX}: ${label}.name must be a name outside permdock_, which generated triggers use, got ${JSON.stringify(trigger.name)}`);
2024
+ if (trigger.when !== "before" && trigger.when !== "after") throw new Error(`${PREFIX}: ${label}.when must be before or after`);
2025
+ const events = trigger.events;
2026
+ if (events.length === 0 || !events.every((event) => EVENTS.has(event))) throw new Error(`${PREFIX}: ${label}.events must list insert, update, delete or truncate`);
2027
+ const level = trigger.level ?? "row";
2028
+ if (level !== "row" && level !== "statement") throw new Error(`${PREFIX}: ${label}.level must be row or statement`);
2029
+ if (level === "row" && events.includes("truncate")) throw new Error(`${PREFIX}: ${label} fires on truncate, which Postgres allows only with level: 'statement'`);
2030
+ const fn = functionName(trigger.function, `${label}.function`);
2031
+ const args = (trigger.args ?? []).map((arg) => quoteLiteral(arg));
2032
+ const name = quoteIdent$1(trigger.name);
2033
+ return `drop trigger if exists ${name} on ${table};
2034
+ create trigger ${name}
2035
+ ${trigger.when} ${[...new Set(events)].join(" or ")} on ${table}
2036
+ for each ${level} execute function ${fn}(${args.join(", ")});`;
1516
2037
  }
1517
2038
  /**
1518
- * The row columns that belong to a table. With `rls.tables` set, a resource
1519
- * it does not name is a table only when generated policies are written for
1520
- * it; with `--helpers-only` it has none, so it gets no index.
2039
+ * `rls.triggers`: the application's triggers on generated tables, after the
2040
+ * tables. A table this run does not create is an error, so a trigger never
2041
+ * silently goes missing.
1521
2042
  */
1522
- function tableColumns(rowColumns, tables, helpersOnly, warnings) {
1523
- if (tables === void 0) return rowColumns;
1524
- const unmapped = [...new Set(rowColumns.map((item) => item.resource).filter((resource) => !Object.hasOwn(tables, resource)))].toSorted();
1525
- if (unmapped.length === 0) return rowColumns;
1526
- if (helpersOnly) {
1527
- warnings.push(`no index is suggested for resources without an rls.tables entry: ${unmapped.join(", ")}; map each one that is a table`);
1528
- return rowColumns.filter((item) => Object.hasOwn(tables, item.resource));
2043
+ function triggersSql(triggers, schema, tables) {
2044
+ if (triggers === void 0) return "";
2045
+ const chunks = [];
2046
+ for (const [table, list] of Object.entries(triggers).toSorted(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
2047
+ knownTable(table, tables, `rls.triggers.${table}`);
2048
+ const target = `${quoteIdent$1(schema)}.${quoteIdent$1(table)}`;
2049
+ for (const [index, trigger] of list.entries()) chunks.push(triggerSql(target, trigger, `rls.triggers.${table}[${String(index)}]`));
1529
2050
  }
1530
- warnings.push(`policies for resources without an rls.tables entry target a table named after the resource: ${unmapped.map((resource) => qualifiedTable$1(resource)).join(", ")}; map each one to its table`);
1531
- return rowColumns;
2051
+ return chunks.length === 0 ? "" : `-- rls.triggers: triggers the application owns on generated tables\n${chunks.join("\n")}\n`;
1532
2052
  }
1533
- async function runRlsGenerate(input) {
1534
- const rls = input.config.rls;
1535
- const fieldsMode = input.fields ?? rls?.fields;
1536
- if (fieldsMode !== void 0 && fieldsMode !== "views") return {
1537
- code: 2,
1538
- output: `rls generate --fields must be views (got ${fieldsMode})`,
1539
- text: ""
1540
- };
1541
- const revokeColumns = input.revokeColumns === true || rls?.revokeColumns === true;
1542
- if (revokeColumns && fieldsMode === void 0) return {
1543
- code: 2,
1544
- output: "rls generate --revoke-columns needs --fields views",
1545
- text: ""
1546
- };
1547
- const policy = await loadPolicy(input.cwd, input.config, input.from);
1548
- const memberships = parseMembershipsFlag(input.memberships) ?? rls?.memberships;
1549
- if (input.rbac && input.dialect !== "supabase") return {
1550
- code: 2,
1551
- output: `rls generate --rbac supabase needs --dialect supabase (got ${input.dialect})`,
1552
- text: ""
1553
- };
1554
- const schema = input.rbacSchema ?? rls?.schema ?? rls?.rbac?.schema ?? "permdock";
1555
- const authorize = resolveAuthorize(input.config, {
1556
- authorize: input.authorize,
1557
- rbac: input.rbac,
1558
- memberships
1559
- });
1560
- const memberSources = rls?.membershipSources ?? input.config.supabase?.hook?.memberships;
1561
- const sources = authorize === "database" ? memberSources : void 0;
1562
- const scopes = scopeList(policy.scopes);
1563
- const suspension = checkSuspension(rls?.suspension, scopes);
1564
- const hookRoles = input.config.supabase?.hook?.roles;
1565
- const roles = rls?.roles ?? (hookRoles === false ? void 0 : hookRoles);
1566
- if (roles !== void 0 && input.rbac) return {
1567
- code: 2,
1568
- output: "rls generate --rbac supabase creates its own user_roles (user_id, role app_role); drop rls.roles (or supabase.hook.roles) or the --rbac flag",
1569
- text: ""
1570
- };
1571
- const requires = writeRequirement(policy, rls?.customRoleWrites?.requires);
1572
- if (requires.error !== void 0) return {
1573
- code: 2,
1574
- output: requires.error,
1575
- text: ""
1576
- };
1577
- const readers = rls?.trustedReaders;
1578
- if (readers !== void 0 && (!Array.isArray(readers) || !readers.every((name) => typeof name === "string" && name !== ""))) return {
1579
- code: 2,
1580
- output: "rls.trustedReaders must list Postgres role names, such as ['service_role']",
1581
- text: ""
1582
- };
1583
- const ownership = ownershipRules(policy, scopes);
1584
- const graph = graphPlan(policy);
1585
- const apiKeys = apiKeysPlan(rls?.apiKeys, new Set(roleNames(policy)), Object.fromEntries(renamedKeys(policy.vocabulary.permissions)));
1586
- const ctx = {
1587
- dialect: input.dialect,
1588
- scopes,
1589
- tenantClaim: rls?.tenantClaim ?? "tenant_id",
1590
- ...rls?.tenants === "all" ? { tenants: "all" } : {},
1591
- gucPrefix: input.gucPrefix ?? rls?.gucPrefix ?? "app",
1592
- ...rls?.actions === void 0 ? {} : { actions: rls.actions },
1593
- inlineFunctions: input.inlineFunctions || rls?.inlineFunctions === true,
1594
- schema,
1595
- authorize,
1596
- roleClaim: rls?.roleClaim ?? "user_role",
1597
- tenantType: input.tenantType ?? rls?.tenantType ?? "uuid",
1598
- ...rls?.teamType === void 0 ? {} : { teamType: rls.teamType },
1599
- ...rls?.scopeTypes === void 0 ? {} : { scopeTypes: rls.scopeTypes },
1600
- ...memberships === void 0 ? {} : { memberships },
1601
- ...sources === void 0 || sources.length === 0 ? {} : { sources },
1602
- ...memberSources === void 0 || memberSources.length === 0 ? {} : { memberSources },
1603
- ...suspension === void 0 ? {} : { suspension },
1604
- ...roles === void 0 ? {} : { roles },
1605
- ...input.customRoles === true || rls?.customRoles === true ? { customRoles: {
1606
- ...customRoleNames(policy, requires.keys),
1607
- ...rls?.customRoleWrites?.roles === void 0 ? {} : { table: rls.customRoleWrites.roles }
1608
- } } : {},
1609
- ...input.capabilities === true || rls?.capabilities === true ? { capabilities: true } : {},
1610
- ...apiKeys === void 0 ? {} : { apiKeys },
1611
- ...rls?.anonymousSignIns === "deny" ? { anonymousSignIns: "deny" } : {},
1612
- ...ownership === void 0 ? {} : { ownership },
1613
- ...rls?.ownershipTriggers === void 0 ? {} : { skipOwnershipTriggers: rls.ownershipTriggers === false ? "all" : Object.keys(rls.ownershipTriggers).map((key) => {
1614
- const name = resolveScope(scopes, key);
1615
- if (name === void 0) throw new Error(`PermDock CLI: rls.ownershipTriggers.${key} names a scope the policy does not declare`);
1616
- return name;
1617
- }) },
1618
- ...rls?.assignments === void 0 ? {} : { assignments: rls.assignments === true ? { tables: [] } : {
1619
- tables: rls.assignments.tables ?? [],
1620
- ...rls.assignments.ownRole === void 0 ? {} : { ownRole: rls.assignments.ownRole }
1621
- } },
1622
- ...fieldsMode === void 0 ? {} : { fields: fieldsMode },
1623
- ...graph.size === 0 ? {} : { graph: {
1624
- closures: closureDepths(graph),
1625
- resources: policy.resources,
1626
- ...rls?.tables === void 0 ? {} : { tables: rls.tables }
1627
- } }
1628
- };
1629
- if (ctx.anonymousSignIns === "deny" && ctx.dialect !== "supabase") return {
1630
- code: 2,
1631
- output: `rls.anonymousSignIns needs --dialect supabase (got ${ctx.dialect})`,
1632
- text: ""
1633
- };
1634
- const jsonSchema = rls?.jsonSchema;
1635
- if (jsonSchema !== void 0 && jsonSchema !== "auto" && typeof jsonSchema !== "boolean") return {
1636
- code: 2,
1637
- output: `rls.jsonSchema must be 'auto', true or false (got ${JSON.stringify(jsonSchema)})`,
1638
- text: ""
1639
- };
1640
- const warnings = [];
1641
- if (jsonSchema !== void 0 && jsonSchema !== false && (rls?.approvals === void 0 || rls.approvals === false)) warnings.push("rls.jsonSchema constrains the approval store's body, which needs rls.approvals: true; nothing else is jsonb");
1642
- if (authorize === "database") {
1643
- for (const { name } of ctx.scopes) if (policy.grants.some((grant) => grant.scope === name) && scopeTable(ctx, name) === void 0 && scopeSources(ctx, name).length === 0) warnings.push(`${name}-scoped grants read the ${name} memberships table in database mode: set rls.memberships.scopes.${name}, rls.membershipSources or supabase.hook.memberships (or --memberships for the first scope), otherwise they deny`);
1644
- }
1645
- const compiled = compileGrants(policy, ctx, rls?.tables, warnings, input.skipClosures, rls?.rowHelpers !== void 0 || inheritTargets(policy).length > 0);
1646
- const helpersOnly = input.helpersOnly === true || rls?.helpersOnly === true;
1647
- if (helpersOnly && (input.target !== "sql" || revokeColumns)) return {
1648
- code: 2,
1649
- output: "rls generate --helpers-only needs --target sql and no --revoke-columns: the table grants are hand-written, so revoke the restricted columns there",
1650
- text: ""
1651
- };
1652
- const views = fieldsMode === void 0 ? [] : fieldViews(policy, ctx, compiled.branches, {
1653
- revokeColumns,
1654
- helpersOnly
1655
- }, warnings);
1656
- const force = input.force === true || rls?.force === true;
1657
- if (force && views.some((view) => view.companion !== void 0)) warnings.push("--force with --revoke-columns: each <table>_visible_fields companion reads as its owner, so its owner needs BYPASSRLS or the restricted columns read as null");
1658
- const policyName = input.policyName ?? rls?.policyName;
1659
- const assembled = helpersOnly ? [] : assemblePolicies(fieldsMode === void 0 ? compiled.branches : rowBranches(compiled.branches), {
1660
- perRole: input.policyPerRole === true || rls?.policyPerRole === true,
1661
- ...policyName === void 0 ? {} : { name: policyName }
2053
+ /**
2054
+ * `rls.audit`: one call of the audit function per generated table, which
2055
+ * registers the table with the application's audit module. Statements that
2056
+ * run code, so they travel with the seeds.
2057
+ */
2058
+ function auditSql(audit, schema, tables) {
2059
+ if (audit === void 0) return "";
2060
+ const fn = functionName(audit.function, "rls.audit.function");
2061
+ const listed = audit.tables;
2062
+ if (listed !== void 0) for (const table of listed) knownTable(table, tables, "rls.audit.tables");
2063
+ const targets = listed ?? AUDITED.filter((table) => tables.includes(table));
2064
+ const args = Object.entries(audit.args ?? {}).map(([name, value]) => {
2065
+ if (!IDENT.test(name)) throw new Error(`${PREFIX}: rls.audit.args names ${JSON.stringify(name)}, which is not a lower-case parameter name`);
2066
+ return `, ${quoteIdent$1(name)} => ${quoteLiteral(value)}`;
1662
2067
  });
1663
- const actorKinds = readOnlyActorKinds(rls?.readOnlyActors);
1664
- if (actorKinds !== void 0 && helpersOnly) warnings.push("rls.readOnlyActors adds policies, and --helpers-only writes none: add the restrictive read-only policies to the hand-written ones");
1665
- const policies = actorKinds === void 0 ? assembled : [...assembled, ...readOnlyActorPolicies(assembled, subjectClaimJsonSql(ctx, "act"), actorKinds)];
1666
- let tablePolicies;
1667
- try {
1668
- tablePolicies = realtimeStoragePolicies(ctx, {
1669
- scopes: new Set(ctx.scopes.map((scope) => scope.name)),
1670
- keys: new Set(listPermissions(policy.vocabulary.permissions).map((leaf) => leaf.key)),
1671
- rowConditions: new Set(policyRowConditionKeys(policy))
1672
- }, compact({
1673
- realtime: rls?.realtime,
1674
- storage: rls?.storage
1675
- }), warnings);
2068
+ if (targets.length === 0) return "";
2069
+ return `-- rls.audit: generated tables registered with ${audit.function}
2070
+ ${targets.map((table) => `select ${fn}(${quoteLiteral(`${quoteIdent$1(schema)}.${quoteIdent$1(table)}`)}::regclass${args.join("")});`).join("\n")}
2071
+ `;
2072
+ }
2073
+ //#endregion
2074
+ //#region src/cli/rls/generate-sql.ts
2075
+ /** The checks that need the SQL context: `--backfill-out`, `rls.anonymousSignIns`, `rls.jsonSchema`. */
2076
+ function preflight(input, { ctx }) {
2077
+ const rls = input.config.rls;
2078
+ const from = typeof rls?.customRoles === "object" ? rls.customRoles.from : void 0;
2079
+ if (input.backfillOut !== void 0 && from === void 0) return { refusal: "rls generate --backfill-out needs rls.customRoles.from in permdock.config.ts" };
2080
+ let backfill;
2081
+ if (from !== void 0) try {
2082
+ backfill = backfillSql(ctx, from, rls?.migrate);
1676
2083
  } catch (cause) {
1677
- return {
1678
- code: 2,
1679
- output: describeError(cause),
1680
- text: ""
1681
- };
2084
+ return { refusal: describeError(cause) };
1682
2085
  }
1683
- if (tablePolicies.length > 0 && helpersOnly) warnings.push("rls.realtime and rls.storage add policies, and --helpers-only writes none: no realtime.messages or storage.objects policy is written");
1684
- const servicePolicies = tablePolicies.length === 0 || helpersOnly ? [] : [...tablePolicies, ...actorKinds === void 0 ? [] : readOnlyActorPolicies(tablePolicies, subjectClaimJsonSql(ctx, "act"), actorKinds)];
1685
- const helperTables = servicePolicies.length === 0 ? void 0 : [SERVICE_TABLES_HEADER, ...servicePolicies.map(policySql)].join("\n");
1686
- const guardedTables = servicePolicies.length === 0 ? void 0 : [`${SERVICE_TABLES_HEADER} A stack without the service has no table, so each policy is skipped there.`, ...servicePolicies.map(guardedPolicySql)].join("\n");
2086
+ if (ctx.anonymousSignIns === "deny" && ctx.dialect !== "supabase") return { refusal: `rls.anonymousSignIns needs --dialect supabase (got ${ctx.dialect})` };
2087
+ const jsonSchema = rls?.jsonSchema;
2088
+ if (jsonSchema !== void 0 && jsonSchema !== "auto" && typeof jsonSchema !== "boolean") return { refusal: `rls.jsonSchema must be 'auto', true or false (got ${JSON.stringify(jsonSchema)})` };
2089
+ return {
2090
+ backfill,
2091
+ jsonSchema
2092
+ };
2093
+ }
2094
+ function rbacPart(input, policy, { ctx, schema, authorize }, warnings) {
1687
2095
  const rootMapped = ctx.scopes[0] === void 0 ? void 0 : scopeTable(ctx, ctx.scopes[0].name);
1688
2096
  const rootTable = rootMapped === void 0 ? void 0 : {
1689
2097
  ...rootMapped.table,
@@ -1702,51 +2110,66 @@ async function runRlsGenerate(input) {
1702
2110
  }) : void 0;
1703
2111
  if (rbac !== void 0) warnings.push(...rbac.warnings);
1704
2112
  if (input.rbac) warnings.push("the token hook that writes user_role and memberships comes from permdock supabase hook generate");
2113
+ return rbac;
2114
+ }
2115
+ /** The ownership, approval store, graph, row-helper and break-glass sections. */
2116
+ function sectionsSql(input, policy, options, built, generated, jsonSchema, warnings) {
2117
+ const rls = input.config.rls;
2118
+ const { ctx, graph, ownership } = built;
2119
+ const { compiled } = generated;
1705
2120
  const owned = [ownershipSql(ctx), assignmentSql(ctx)].filter((part) => part !== "").join("\n");
1706
2121
  if (ctx.assignments !== void 0 && (ownership === void 0 || ownership.assigns.length === 0)) warnings.push("rls.assignments needs a role that declares assigns: no assignment trigger is written");
1707
2122
  let approvals;
1708
2123
  try {
1709
2124
  approvals = rls?.approvals === true ? approvalStoreSql(ctx, jsonSchema ?? false) : typeof rls?.approvals === "object" ? adoptedApprovalStoreSql(ctx, rls.approvals, jsonSchema ?? false) : void 0;
1710
2125
  } catch (cause) {
1711
- return {
1712
- code: 2,
1713
- output: describeError(cause),
1714
- text: ""
1715
- };
2126
+ return { refusal: describeError(cause) };
1716
2127
  }
1717
2128
  const graphed = graphSql(ctx, graph, rls?.tables);
1718
2129
  let rowHelpers = "";
1719
2130
  if (rls?.rowHelpers !== void 0 || inheritTargets(policy).length > 0) try {
1720
- rowHelpers = rowHelpersSql(ctx, policy, [...fieldsMode === void 0 ? compiled.branches : rowBranches(compiled.branches), ...compiled.actionBranches], rls?.rowHelpers ?? [], rls?.tables, warnings);
2131
+ rowHelpers = rowHelpersSql(ctx, policy, [...options.fieldsMode === void 0 ? compiled.branches : rowBranches(compiled.branches), ...compiled.actionBranches], rls?.rowHelpers ?? [], rls?.tables, warnings);
1721
2132
  } catch (cause) {
1722
- return {
1723
- code: 2,
1724
- output: describeError(cause),
1725
- text: ""
1726
- };
2133
+ return { refusal: describeError(cause) };
1727
2134
  }
1728
2135
  const breakGlass = breakGlassSql(ctx, breakGlassEntries(policy, rls?.tables));
1729
- const configured = rls?.shims;
1730
- const shimsConfig = typeof configured === "object" ? configured : configured === true || input.shims === true ? {} : void 0;
1731
- if (shimsConfig !== void 0 && rls?.migrate === void 0) return {
1732
- code: 2,
1733
- output: "PermDock CLI: rls generate --shims needs rls.migrate.helpers in permdock.config.ts",
1734
- text: ""
1735
- };
1736
- const renamed = Object.fromEntries(renamedKeys(policy.vocabulary.permissions));
1737
- const grants = shimGrants(compiled.rolePermissions, compiled.conditionedKeys, renamed);
1738
- const anonExecute = rls?.anonExecute === true || views.some((view) => view.roles.includes("anon")) || compiled.branches.some((branch) => branch.roles.includes("anon") && [
2136
+ return [
2137
+ approvals,
2138
+ owned === "" ? void 0 : owned,
2139
+ graphed === "" ? void 0 : graphed,
2140
+ breakGlass === "" ? void 0 : breakGlass,
2141
+ rowHelpers === "" ? void 0 : rowHelpers
2142
+ ];
2143
+ }
2144
+ /** Whether `anon` calls a helper: `rls.anonExecute`, an anon field view or an anon branch reading the user id. */
2145
+ function anonExecutes(input, { compiled, views }) {
2146
+ return input.config.rls?.anonExecute === true || views.some((view) => view.roles.includes("anon")) || compiled.branches.some((branch) => branch.roles.includes("anon") && [
1739
2147
  branch.access,
1740
2148
  branch.using,
1741
2149
  branch.check
1742
2150
  ].some((sql) => sql?.includes("permdock_user_id") === true));
2151
+ }
2152
+ /** Everything before the table policies: helpers, sections, shims, scaffold and triggers. */
2153
+ function preambleSql(input, policy, options, built, generated, jsonSchema, warnings) {
2154
+ const rls = input.config.rls;
2155
+ const { ctx } = built;
2156
+ const { compiled } = generated;
2157
+ const rbac = rbacPart(input, policy, built, warnings);
2158
+ const sections = sectionsSql(input, policy, options, built, generated, jsonSchema, warnings);
2159
+ if (isRefusal(sections)) return sections;
2160
+ const configured = rls?.shims;
2161
+ const shimsConfig = typeof configured === "object" ? configured : configured === true || input.shims === true ? {} : void 0;
2162
+ if (shimsConfig !== void 0 && rls?.migrate === void 0) return { refusal: "PermDock CLI: rls generate --shims needs rls.migrate.helpers in permdock.config.ts" };
2163
+ const renamed = Object.fromEntries(renamedKeys(policy.vocabulary.permissions));
2164
+ const grants = shimGrants(compiled.rolePermissions, compiled.conditionedKeys, renamed);
2165
+ const anonExecute = anonExecutes(input, generated);
1743
2166
  const usesLiveSession = compiled.branches.some((branch) => [
1744
2167
  branch.access,
1745
2168
  branch.using,
1746
2169
  branch.check
1747
2170
  ].some((sql) => sql?.includes(SESSION_LIVE_HELPER) === true));
1748
2171
  const shims = shimsConfig === void 0 || rls?.migrate === void 0 ? void 0 : shimsSql(ctx, rls.migrate, shimsConfig, renamed, grants);
1749
- const preamble = [
2172
+ const core = [
1750
2173
  rbac?.head,
1751
2174
  helpersSql(ctx, compiled.rolePermissions, {
1752
2175
  levelReach: compiled.levelReach,
@@ -1758,18 +2181,56 @@ async function runRlsGenerate(input) {
1758
2181
  }),
1759
2182
  permissionHelpersSql(ctx, grants, renamed, anonExecute),
1760
2183
  usesLiveSession ? sessionLiveHelperSql(ctx) : void 0,
1761
- approvals,
1762
- owned === "" ? void 0 : owned,
1763
- graphed === "" ? void 0 : graphed,
1764
- breakGlass === "" ? void 0 : breakGlass,
1765
- rowHelpers === "" ? void 0 : rowHelpers,
2184
+ ...sections,
1766
2185
  shims,
1767
2186
  rbac?.tail
1768
2187
  ].filter((part) => part !== void 0).join("\n");
1769
- if (force) for (const entry of graph.values()) {
2188
+ const created = generatedTables(core, built.schema);
2189
+ let ownTriggers;
2190
+ let audit;
2191
+ try {
2192
+ ownTriggers = triggersSql(rls?.triggers, built.schema, created);
2193
+ audit = auditSql(rls?.audit, built.schema, created);
2194
+ } catch (cause) {
2195
+ return { refusal: describeError(cause) };
2196
+ }
2197
+ const seedsSplit = splitsPart(input.split, "seeds");
2198
+ return {
2199
+ preamble: [
2200
+ core,
2201
+ ownTriggers,
2202
+ seedsSplit ? "" : audit
2203
+ ].filter((part) => part !== "").join("\n"),
2204
+ audit
2205
+ };
2206
+ }
2207
+ function forceWarnings({ graph }, warnings) {
2208
+ for (const entry of graph.values()) {
1770
2209
  const own = [...entry.relations].filter((name) => !("edge" in (entry.node.relations[name] ?? {})));
1771
2210
  if (own.length > 0) warnings.push(`--force: ${entry.node.name} relations ${own.join(", ")} are read from the ${entry.node.name} table itself, so ${graphHelper(entry.node.name)} would recurse through its own policy; keep ${entry.node.name} unforced or move them to edge tables`);
1772
2211
  }
2212
+ }
2213
+ /**
2214
+ * The row columns that belong to a table. With `rls.tables` set, a resource
2215
+ * it does not name is a table only when generated policies are written for
2216
+ * it; with `--helpers-only` it has none, so it gets no index.
2217
+ */
2218
+ function tableColumns(rowColumns, tables, helpersOnly, warnings) {
2219
+ if (tables === void 0) return rowColumns;
2220
+ const unmapped = [...new Set(rowColumns.map((item) => item.resource).filter((resource) => !Object.hasOwn(tables, resource)))].toSorted();
2221
+ if (unmapped.length === 0) return rowColumns;
2222
+ if (helpersOnly) {
2223
+ warnings.push(`no index is suggested for resources without an rls.tables entry: ${unmapped.join(", ")}; map each one that is a table`);
2224
+ return rowColumns.filter((item) => Object.hasOwn(tables, item.resource));
2225
+ }
2226
+ warnings.push(`policies for resources without an rls.tables entry target a table named after the resource: ${unmapped.map((resource) => qualifySqlTable(resource)).join(", ")}; map each one to its table`);
2227
+ return rowColumns;
2228
+ }
2229
+ /** The `--target` file, and for Drizzle and Prisma the SQL migration beside it. */
2230
+ function emitTarget(input, built, generated, preamble) {
2231
+ const rls = input.config.rls;
2232
+ const { ctx } = built;
2233
+ const { policies, force, views, helperTables } = generated;
1773
2234
  const outRel = input.out ?? rls?.out ?? defaultOut(input.target);
1774
2235
  const migration = input.target === "sql" ? "" : dialectRoles(migrationSql(policies, withHelperTables(preamble, helperTables), force, views), ctx.dialect);
1775
2236
  const migrationRel = migration === "" ? void 0 : migrationOut(outRel);
@@ -1795,14 +2256,77 @@ async function runRlsGenerate(input) {
1795
2256
  break;
1796
2257
  default: return input.target;
1797
2258
  }
1798
- let indexes = indexTargets(ctx, tableColumns(compiled.rowColumns, rls?.tables, helpersOnly, warnings));
2259
+ return {
2260
+ text,
2261
+ migration,
2262
+ migrationRel,
2263
+ outRel
2264
+ };
2265
+ }
2266
+ /** The generated SQL for one run, before `--db` prunes the index targets. */
2267
+ function generateSql(input, policy, options, built, warnings) {
2268
+ const rls = input.config.rls;
2269
+ const { ctx } = built;
2270
+ const checked = preflight(input, built);
2271
+ if (isRefusal(checked)) return checked;
2272
+ const { jsonSchema } = checked;
2273
+ if (jsonSchema !== void 0 && jsonSchema !== false && (rls?.approvals === void 0 || rls.approvals === false)) warnings.push("rls.jsonSchema constrains the approval store's body, which needs rls.approvals: true; nothing else is jsonb");
2274
+ const generated = generatePolicies(input, policy, options, built, warnings);
2275
+ if (isRefusal(generated)) return generated;
2276
+ const assembled = preambleSql(input, policy, options, built, generated, jsonSchema, warnings);
2277
+ if (isRefusal(assembled)) return assembled;
2278
+ const { preamble, audit } = assembled;
2279
+ if (generated.force) forceWarnings(built, warnings);
2280
+ const emitted = emitTarget(input, built, generated, preamble);
2281
+ const { policies, force, views, helperTables, guardedTables, helpersOnly } = generated;
2282
+ const seedsSplit = splitsPart(input.split, "seeds");
2283
+ return {
2284
+ ...generated,
2285
+ ...emitted,
2286
+ backfill: checked.backfill,
2287
+ indexes: indexTargets(ctx, tableColumns(generated.compiled.rowColumns, rls?.tables, helpersOnly, warnings)),
2288
+ parts: (indexes) => ({
2289
+ policies: dialectRoles(withHelperTables(emitSql(policies, "", force, views), helperTables), ctx.dialect),
2290
+ declarativePolicies: dialectRoles(emitSql(policies, "", force, views), ctx.dialect),
2291
+ ...guardedTables === void 0 ? {} : { servicePolicies: dialectRoles(guardedTables, ctx.dialect) },
2292
+ helpers: dialectRoles(emitSql([], preamble, false, helpersOnly ? views : []), ctx.dialect),
2293
+ seeds: `${SEEDS_MARKER} schema=${built.schema}\n${seedSql(ctx, generated.compiled.rolePermissions)}\n${seedsSplit ? audit : ""}`,
2294
+ indexes: `${INDEXES_MARKER}\n${indexesSql(indexes)}\n`
2295
+ })
2296
+ };
2297
+ }
2298
+ //#endregion
2299
+ //#region src/cli/rls/generate.ts
2300
+ function loadPolicy(cwd, config, from) {
2301
+ const policyPath = from === "drizzle" ? config.policy : from ?? config.policy;
2302
+ return requirePolicy(cwd, policyPath, "rls generate needs policy in permdock.config.ts or --from");
2303
+ }
2304
+ function refused(output) {
2305
+ return {
2306
+ code: 2,
2307
+ output,
2308
+ text: ""
2309
+ };
2310
+ }
2311
+ async function runRlsGenerate(input) {
2312
+ const options = generateOptions(input);
2313
+ if (isRefusal(options)) return refused(options.refusal);
2314
+ const policy = await loadPolicy(input.cwd, input.config, input.from);
2315
+ const built = buildContext(input, policy, options);
2316
+ if (isRefusal(built)) return refused(built.refusal);
2317
+ const warnings = [];
2318
+ const generated = generateSql(input, policy, options, built, warnings);
2319
+ if (isRefusal(generated)) return refused(generated.refusal);
2320
+ const { ctx } = built;
2321
+ const { text, migration, migrationRel, helpersOnly } = generated;
2322
+ let indexes = generated.indexes;
1799
2323
  if (input.db !== void 0) try {
1800
2324
  const pruned = pruneIndexTargets(indexes, await readTableIndexFacts(input.db, [...new Set(indexes.map((target) => target.table))], input.connect));
1801
2325
  indexes = pruned.targets;
1802
2326
  warnings.push(...pruned.warnings);
1803
2327
  } catch (cause) {
1804
2328
  return {
1805
- code: 2,
2329
+ code: exitCodeOf(cliErrorKind(cause)),
1806
2330
  output: describeError(cause),
1807
2331
  text: ""
1808
2332
  };
@@ -1813,33 +2337,26 @@ async function runRlsGenerate(input) {
1813
2337
  code: 0,
1814
2338
  output: warnings.join("\n"),
1815
2339
  text,
1816
- policies,
1817
- seeds: compiled.rolePermissions,
2340
+ policies: generated.policies,
2341
+ seeds: generated.compiled.rolePermissions,
1818
2342
  indexes,
1819
2343
  keys: {
1820
2344
  permissions: listPermissions(policy.vocabulary.permissions).map((leaf) => leaf.key),
1821
2345
  rowConditions: [...policyRowConditionKeys(policy)],
1822
2346
  renamed: Object.fromEntries(renamedKeys(policy.vocabulary.permissions))
1823
2347
  },
1824
- schema,
2348
+ schema: built.schema,
1825
2349
  helpersOnly,
1826
2350
  ...extras
1827
2351
  };
1828
2352
  const planned = outputFiles({
1829
2353
  input,
1830
- outRel,
2354
+ outRel: generated.outRel,
1831
2355
  text,
1832
2356
  helpersOnly,
1833
- sql: () => ({
1834
- policies: dialectRoles(withHelperTables(emitSql(policies, "", force, views), helperTables), ctx.dialect),
1835
- declarativePolicies: dialectRoles(emitSql(policies, "", force, views), ctx.dialect),
1836
- ...guardedTables === void 0 ? {} : { servicePolicies: dialectRoles(guardedTables, ctx.dialect) },
1837
- helpers: dialectRoles(emitSql([], preamble, false, helpersOnly ? views : []), ctx.dialect),
1838
- seeds: `${SEEDS_MARKER} schema=${schema}\n${seedSql(ctx, compiled.rolePermissions)}\n`,
1839
- indexes: `${INDEXES_MARKER}\n${indexesSql(indexes)}\n`
1840
- }),
1841
- scopes,
1842
- schema
2357
+ sql: () => generated.parts(indexes),
2358
+ scopes: ctx.scopes,
2359
+ schema: built.schema
1843
2360
  });
1844
2361
  if (typeof planned === "string") return {
1845
2362
  code: 2,
@@ -1853,6 +2370,18 @@ async function runRlsGenerate(input) {
1853
2370
  rel: migrationRel,
1854
2371
  text: migration
1855
2372
  });
2373
+ if (input.backfillOut !== void 0 && generated.backfill !== void 0) {
2374
+ const file = dataMigrationFile({
2375
+ cwd: input.cwd,
2376
+ out: input.backfillOut,
2377
+ part: "backfill",
2378
+ marker: BACKFILL_MARKER,
2379
+ text: generated.backfill,
2380
+ now: input.io.now?.() ?? /* @__PURE__ */ new Date(),
2381
+ check: input.check
2382
+ });
2383
+ if (file !== void 0) files.push(file);
2384
+ }
1856
2385
  if (input.check) {
1857
2386
  const drift = driftOf(input.cwd, files);
1858
2387
  return drift.length === 0 ? {
@@ -1867,7 +2396,7 @@ async function runRlsGenerate(input) {
1867
2396
  ...extras
1868
2397
  };
1869
2398
  }
1870
- const written = writeSqlFiles(input.cwd, files);
2399
+ const written = writeOutputFiles(input.cwd, files);
1871
2400
  const extra = warnings.length === 0 ? "" : `\n${warnings.join("\n")}`;
1872
2401
  return {
1873
2402
  code: 0,
@@ -1876,141 +2405,8 @@ async function runRlsGenerate(input) {
1876
2405
  ...extras
1877
2406
  };
1878
2407
  }
1879
- /** Whether `--split` names `part`, which then leaves the helpers or the warnings. */
1880
- const SERVICE_TABLES_HEADER = "-- Supabase Realtime and Storage: rls.realtime and rls.storage. RLS on both tables is Supabase's.";
1881
- /**
1882
- * A `realtime.messages` or `storage.objects` policy that applies only where
1883
- * the table exists. The Realtime and Storage services create those tables, so
1884
- * a local stack with the service off, and pg-delta's shadow database, have
1885
- * none.
1886
- */
1887
- function guardedPolicySql(item) {
1888
- const body = policySql(item).trimEnd().split("\n").map((line) => ` ${line}`).join("\n");
1889
- return `do $permdock$
1890
- begin
1891
- if to_regclass(${quoteLiteral(qualifiedTable$1(item.table))}) is not null then
1892
- ${body}
1893
- end if;
1894
- end
1895
- $permdock$;
1896
- `;
1897
- }
1898
- /** Appends the `realtime.messages` and `storage.objects` policies, which never get `tableSql`. */
1899
- function withHelperTables(sql, helperTables) {
1900
- if (helperTables === void 0) return sql;
1901
- const tables = `${helperTables.trimEnd()}\n`;
1902
- return sql.trim() === "" ? tables : `${sql.trimEnd()}\n\n${tables}`;
1903
- }
1904
- function splitsPart(raw, part) {
1905
- const split = parseSplit(raw);
1906
- return typeof split !== "string" && split?.includes(part) === true;
1907
- }
1908
- /** The files `generate` writes: one, or one per `--split` part, plus the hook's grants. */
1909
- function outputFiles(plan) {
1910
- const { input } = plan;
1911
- const split = parseSplit(input.split);
1912
- if (typeof split === "string") return split;
1913
- if (split?.includes("policies") === true && plan.helpersOnly) return "rls generate --helpers-only writes no policies part; drop it from --split";
1914
- if (split === void 0) {
1915
- if (input.grantsOut !== void 0) return "rls generate --grants-out needs --split with the helpers or hook part";
1916
- if (input.seedsOut !== void 0) return "rls generate --seeds-out needs --split with the seeds part";
1917
- return [{
1918
- part: "rls",
1919
- rel: plan.outRel,
1920
- text: plan.text
1921
- }];
1922
- }
1923
- if (input.target !== "sql") return "rls generate --split needs --target sql";
1924
- const pgDelta = input.out === void 0 && input.config.rls?.out === void 0 ? supabaseConfig(input.cwd).pgDelta : void 0;
1925
- if (pgDelta === void 0 && !plan.outRel.includes("{part}")) return `rls generate --split needs {part} in --out, for example supabase/schemas/identity/056_permdock_{part}.sql (got ${plan.outRel})`;
1926
- if (pgDelta !== void 0 && split.includes("helpers") && !split.includes("seeds")) return "rls generate --split helpers under pg-delta needs the seeds part with --seeds-out: pg-delta rejects the role_permissions rows in a declarative file";
1927
- if (pgDelta !== void 0 && split.includes("seeds") && input.seedsOut === void 0) return "rls generate --split seeds under pg-delta needs --seeds-out: pg-delta does not diff the role_permissions rows, so they go in a migration";
1928
- const at = (part, schema = plan.schema) => pgDelta === void 0 ? partPath(plan.outRel, part) : pgDeltaPath(pgDelta.schemaDir, part, schema);
1929
- if (input.grantsOut !== void 0 && !split.includes("hook") && !split.includes("helpers")) return "rls generate --grants-out needs the helpers or hook part in --split";
1930
- if (input.seedsOut !== void 0 && !split.includes("seeds")) return "rls generate --seeds-out needs the seeds part in --split";
1931
- if (split.includes("hook") && input.config.supabase?.hook === void 0) return "rls generate --split hook needs supabase.hook in permdock.config.ts";
1932
- const sql = plan.sql();
1933
- const relocated = pgDelta === void 0 ? void 0 : sql.servicePolicies;
1934
- if (relocated !== void 0 && split.includes("policies") && !split.includes("seeds")) return "rls generate --split policies under pg-delta with rls.realtime or rls.storage needs the seeds part with --seeds-out: the shadow database has no realtime.messages or storage.objects, so their policies go in that migration";
1935
- const seeds = relocated === void 0 || !split.includes("policies") ? sql.seeds : `${sql.seeds}\n${relocated}`;
1936
- const files = [];
1937
- const label = grantsLabel(input.grantsOut);
1938
- let grantsMarker = `${GRANTS_MARKER} schema=${plan.schema}`;
1939
- const grants = [];
1940
- for (const part of split) switch (part) {
1941
- case "helpers": {
1942
- if (label === void 0) {
1943
- files.push({
1944
- part,
1945
- rel: at(part),
1946
- text: sql.helpers
1947
- });
1948
- break;
1949
- }
1950
- const moved = moveUndiffed(sql.helpers, label, part);
1951
- files.push({
1952
- part,
1953
- rel: at(part),
1954
- text: moved.sql
1955
- });
1956
- grants.push(moved.grants);
1957
- break;
1958
- }
1959
- case "seeds": {
1960
- const out = input.seedsOut;
1961
- if (out !== void 0 && isSeedsDirectory(input.cwd, out)) {
1962
- const planned = seedsMigration(input.cwd, out, SEEDS_MARKER, seeds, input.io.now?.() ?? /* @__PURE__ */ new Date());
1963
- if (!planned.current || input.check) files.push({
1964
- part,
1965
- rel: planned.rel,
1966
- text: seeds
1967
- });
1968
- break;
1969
- }
1970
- files.push({
1971
- part,
1972
- rel: out ?? at(part),
1973
- text: seeds
1974
- });
1975
- break;
1976
- }
1977
- case "indexes":
1978
- files.push({
1979
- part,
1980
- rel: at(part),
1981
- text: sql.indexes
1982
- });
1983
- break;
1984
- case "policies":
1985
- files.push({
1986
- part,
1987
- rel: at(part),
1988
- text: pgDelta === void 0 ? sql.policies : sql.declarativePolicies
1989
- });
1990
- break;
1991
- case "hook": {
1992
- const hook = supabaseHookSql(plan.scopes, input.config, {}, label);
1993
- files.push({
1994
- part,
1995
- rel: at(part, hook.manifest.hook.schema),
1996
- text: hook.sql
1997
- });
1998
- const [marker = grantsMarker, ...rest] = hook.grants.split("\n");
1999
- grantsMarker = marker;
2000
- grants.push(rest.join("\n").trimEnd());
2001
- break;
2002
- }
2003
- default: return part;
2004
- }
2005
- if (input.grantsOut !== void 0) files.push({
2006
- part: "grants",
2007
- rel: input.grantsOut,
2008
- text: `${[grantsMarker, ...grants].join("\n")}\n`
2009
- });
2010
- return files;
2011
- }
2012
2408
  //#endregion
2013
- //#region src/cli/rls-introspect.ts
2409
+ //#region src/cli/rls/introspect.ts
2014
2410
  const ROLES = ["anon", "authenticated"];
2015
2411
  /** Neon's `anonymous` is the role `rls generate` writes as `anon` for the other dialects. */
2016
2412
  function liveRole(role) {
@@ -2023,7 +2419,7 @@ const PRIVILEGES = [
2023
2419
  "delete"
2024
2420
  ];
2025
2421
  /** `posts` and `"app"."posts"` as `public.posts` and `app.posts`. */
2026
- function qualified(table) {
2422
+ function normalizeTableName(table) {
2027
2423
  const parts = table.split(".").map((part) => part.replaceAll("\"", ""));
2028
2424
  return parts.length === 1 ? `public.${parts[0] ?? ""}` : parts.join(".");
2029
2425
  }
@@ -2037,19 +2433,19 @@ function helpersOf(sql) {
2037
2433
  return [...found];
2038
2434
  }
2039
2435
  function expectedRls(policies, sql, indexes = []) {
2040
- const tables = [...new Set(policies.map((item) => qualified(item.table)))];
2436
+ const tables = [...new Set(policies.map((item) => normalizeTableName(item.table)))];
2041
2437
  const grants = {};
2042
2438
  for (const table of tables) {
2043
2439
  const byRole = {};
2044
2440
  for (const role of ROLES) {
2045
- const commands = new Set(policies.filter((item) => qualified(item.table) === table && item.effect === "allow" && item.roles.includes(role)).map((item) => item.command));
2441
+ const commands = new Set(policies.filter((item) => normalizeTableName(item.table) === table && item.effect === "allow" && item.roles.includes(role)).map((item) => item.command));
2046
2442
  byRole[role] = PRIVILEGES.filter((privilege) => commands.has(privilege));
2047
2443
  }
2048
2444
  grants[table] = byRole;
2049
2445
  }
2050
2446
  return {
2051
2447
  policies: policies.map((item) => ({
2052
- table: qualified(item.table),
2448
+ table: normalizeTableName(item.table),
2053
2449
  name: item.name,
2054
2450
  command: item.command,
2055
2451
  permissive: item.effect === "allow",
@@ -2060,7 +2456,7 @@ function expectedRls(policies, sql, indexes = []) {
2060
2456
  helpers: helpersOf(sql),
2061
2457
  indexes: indexes.map((target) => ({
2062
2458
  ...target,
2063
- table: qualified(target.table)
2459
+ table: normalizeTableName(target.table)
2064
2460
  }))
2065
2461
  };
2066
2462
  }
@@ -2278,7 +2674,7 @@ async function introspectMixed(db, schema, connect = connectIntrospect) {
2278
2674
  }
2279
2675
  }
2280
2676
  //#endregion
2281
- //#region src/cli/rls.ts
2677
+ //#region src/cli/rls/rls.ts
2282
2678
  const RLS_HELP = `permdock rls generate | import | verify | migrate
2283
2679
 
2284
2680
  generate --target drizzle|sql|prisma --dialect supabase|neon|guc
@@ -2288,15 +2684,16 @@ const RLS_HELP = `permdock rls generate | import | verify | migrate
2288
2684
  [--capabilities] [--fields views [--revoke-columns]]
2289
2685
  [--out <path>] [--check] [--skip-closures] [--inline-functions] [--force] [--guc-prefix app]
2290
2686
  [--split helpers,seeds,policies,hook --out <dir>/056_permdock_{part}.sql]
2291
- [--grants-out <file>|-] [--seeds-out <file>|-]
2687
+ [--grants-out <file>|-] [--seeds-out <file>|-] [--backfill-out <file>|<dir>/|-]
2292
2688
  [--helpers-only] [--shims] [--db $DATABASE_URL]
2293
2689
  import --sql schema.sql | --db $DATABASE_URL --out src/permissions.generated.ts
2294
2690
  [--schema zod|valibot|arktype] [--memberships <table>:tenant,user,role]
2295
2691
  verify [--db $DATABASE_URL] [--fixtures rls.fixtures.ts] [--format pgtap|node] [--tree]
2296
2692
  [--introspect --db $DATABASE_URL, with the generate flags]
2297
2693
  [--advisors [--db $DATABASE_URL] [--json]]: supabase db advisors, security lints
2298
- migrate --sql <dir> [--write] [--json], with the generate flags
2299
- rewrites the rls.migrate helpers' calls in policies onto the generated helpers
2694
+ migrate --sql <dir> [--write] [--json] [--retire-out <file>|<dir>/|-], with the generate flags
2695
+ rewrites the rls.migrate helpers' calls in policies onto the generated helpers;
2696
+ --retire-out drops each rls.migrate.tables table and its refresh triggers once unread
2300
2697
 
2301
2698
  Never emits service_role, except in the execute grant rls.trustedReaders asks for. memberOf compiles through the dialect memberships mapping.
2302
2699
  `;
@@ -2334,6 +2731,7 @@ function generateInput(input, target, dialect) {
2334
2731
  ...input.split === void 0 ? {} : { split: input.split },
2335
2732
  ...input.grantsOut === void 0 ? {} : { grantsOut: input.grantsOut },
2336
2733
  ...input.seedsOut === void 0 ? {} : { seedsOut: input.seedsOut },
2734
+ ...input.backfillOut === void 0 ? {} : { backfillOut: input.backfillOut },
2337
2735
  helpersOnly: input.helpersOnly,
2338
2736
  shims: input.shims === true
2339
2737
  };
@@ -2363,14 +2761,16 @@ async function migrate(input) {
2363
2761
  output: generated.output
2364
2762
  };
2365
2763
  await requirePeer(() => import("pgsql-parser"), "pgsql-parser", "permdock rls migrate", input.cwd);
2366
- const { migrateTarget, runRlsMigrate } = await import("./rls-migrate-CN4TlJTO.js");
2764
+ const { migrateTarget, runRlsMigrate } = await import("./migrate-BwlB5-Wk.js");
2367
2765
  return runRlsMigrate({
2368
2766
  cwd: input.cwd,
2369
2767
  sql: input.sql,
2370
2768
  config,
2371
2769
  target: migrateTarget(generated),
2372
2770
  write: input.write,
2373
- json: input.json
2771
+ json: input.json,
2772
+ retireOut: input.retireOut,
2773
+ ...input.io.now === void 0 ? {} : { now: input.io.now() }
2374
2774
  });
2375
2775
  }
2376
2776
  /** `verify --introspect` with `--helpers-only`: helpers and seeds exactly, hand-written policies by their keys. */
@@ -2471,7 +2871,7 @@ async function runRls(input) {
2471
2871
  }
2472
2872
  case "import": {
2473
2873
  await requirePeer(() => import("pgsql-parser"), "pgsql-parser", "permdock rls import", input.cwd);
2474
- const { runRlsImport } = await import("./rls-import-4lLW_pbb.js");
2874
+ const { runRlsImport } = await import("./import-BFIlhetD.js");
2475
2875
  return runRlsImport({
2476
2876
  cwd: input.cwd,
2477
2877
  config: input.config,