permdock 0.6.0 → 0.7.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 (399) hide show
  1. package/README.md +1 -0
  2. package/dist/{2020-C9VN-x8c.js → 2020-CJbO8Bvz.js} +1 -3
  3. package/dist/a2a/index.d.ts +5 -5
  4. package/dist/a2a/index.js +4 -4
  5. package/dist/{adoption-scan-DCYe60Pi.js → adoption-scan-D-feE7BE.js} +1 -1
  6. package/dist/agent/index.d.ts +3 -3
  7. package/dist/agent/index.js +1 -1
  8. package/dist/ai-sdk/index.d.ts +21 -6
  9. package/dist/ai-sdk/index.js +19 -9
  10. package/dist/{approval-prompt-DvSSPQJX.d.ts → approval-prompt-K0faOLFp.d.ts} +2 -2
  11. package/dist/approvals/index.d.ts +4 -4
  12. package/dist/approvals/index.js +4 -4
  13. package/dist/{approvals-p1PGoVjb.js → approvals-CMRfsbBh.js} +74 -48
  14. package/dist/{arazzo-41WuzHou.js → arazzo-ClrF3PVb.js} +2 -2
  15. package/dist/{arazzo-CaUVBn7D.js → arazzo-rdeJ8n1J.js} +4 -4
  16. package/dist/{args-IIolxxKA.js → args-CC3OwyHW.js} +112 -2
  17. package/dist/astro/index.d.ts +6 -6
  18. package/dist/astro/index.js +2 -2
  19. package/dist/{audience-DabClFLQ.js → audience-BrMs8c4o.js} +1 -1
  20. package/dist/{auth-DRLEizbL.js → auth-CscUX8Sy.js} +3 -4
  21. package/dist/authzen/index.d.ts +162 -22
  22. package/dist/authzen/index.js +315 -146
  23. package/dist/{automation-BNM3TtDo.d.ts → automation-DOtAFWdy.d.ts} +3 -3
  24. package/dist/{automation-DVjOckjV.js → automation-ySsxp_Yp.js} +1 -1
  25. package/dist/base-url-BQbddZO5.js +9 -0
  26. package/dist/better-auth/index.d.ts +2 -2
  27. package/dist/better-auth/index.js +2 -2
  28. package/dist/better-supabase/index.d.ts +50 -8
  29. package/dist/better-supabase/index.js +94 -32
  30. package/dist/{bind-CAvgUuKs.js → bind-D1nO6fiC.js} +5 -5
  31. package/dist/{bind-DOyrv6zi.d.ts → bind-JDuvxjuW.d.ts} +7 -7
  32. package/dist/{build-instance-Bu_eDqYw.js → build-instance-DxE7czo9.js} +1 -1
  33. package/dist/{canonical-json-BC8_nMTw.js → canonical-json-DwilZ1iB.js} +6 -3
  34. package/dist/{capability-Drg-1YhL.d.ts → capability-D4UC9TTW.d.ts} +2 -2
  35. package/dist/{capability-N1K9IpqS.js → capability-L7NnfhiE.js} +1 -1
  36. package/dist/{casbin-Cr7RLtA3.js → casbin-BHijLuHY.js} +2 -2
  37. package/dist/{casbin-Dyu2gnUD.js → casbin-BW9pS47J.js} +1 -1
  38. package/dist/{casl-64gpT-8x.js → casl-CmJlgziS.js} +2 -2
  39. package/dist/{casl-DjEDhTCP.js → casl-UbI8v-Wq.js} +1 -1
  40. package/dist/catalog/index.d.ts +70 -10
  41. package/dist/catalog/index.js +112 -6
  42. package/dist/catalog-BCCjSAdH.js +1 -0
  43. package/dist/{catalog-DZVbrF-z.js → catalog-BaMQfQ6Y.js} +3 -3
  44. package/dist/{catalog-HD2gf5mY.js → catalog-D69WhFii.js} +43 -13
  45. package/dist/{catalog-doc-Cdn7-0pQ.js → catalog-doc-CV9s6B1S.js} +28 -5
  46. package/dist/{catalog-fingerprint-CCP0-yQS.js → catalog-fingerprint-C144oW4M.js} +1 -1
  47. package/dist/{create-CQWIpUG-.js → challenge-nGWma08S.js} +122 -25
  48. package/dist/chat/index.d.ts +1 -1
  49. package/dist/chat/index.js +2 -2
  50. package/dist/chat-sdk/index.d.ts +3 -3
  51. package/dist/checks-BC_0mPIq.js +37 -0
  52. package/dist/claude-agent/index.d.ts +3 -3
  53. package/dist/claude-agent/index.js +2 -2
  54. package/dist/clerk/index.d.ts +3 -3
  55. package/dist/clerk/index.js +1 -1
  56. package/dist/cli/bin.js +2 -2
  57. package/dist/cli/index.d.ts +3 -3
  58. package/dist/cli/index.js +3 -3
  59. package/dist/cloud/index.d.ts +127 -11
  60. package/dist/cloud/index.js +251 -3
  61. package/dist/{cloud-ytQ3uviA.js → cloud-CO8pYtwr.js} +5 -5
  62. package/dist/{codemod-ua2hHy7w.js → codemod-BP6n8N9L.js} +3 -3
  63. package/dist/{codemod-5mpSpnUY.js → codemod-i4N2BhDc.js} +3 -3
  64. package/dist/{collect-mLTY4nwb.js → collect-BaWeMjwV.js} +17 -8
  65. package/dist/{collect-Ci6akVwH.js → collect-DNm3oKOk.js} +6 -5
  66. package/dist/collect-k_Dulxr5.js +1 -0
  67. package/dist/compile/index.d.ts +1 -1
  68. package/dist/compile/index.js +1 -1
  69. package/dist/{compile-CMqMx_IN.js → compile-BAEO5bOk.js} +2 -2
  70. package/dist/{compile-1bsAezL0.d.ts → compile-COM2VKuN.d.ts} +3 -3
  71. package/dist/{completion-BTFrHZhX.js → completion-BNdVf7X2.js} +3 -3
  72. package/dist/{config-bs_kZeYq.js → config-CoQHDrDi.js} +7 -3
  73. package/dist/{config-Bj2Ixq3d.js → config-DGZzMV1B.js} +2 -2
  74. package/dist/{config-report-DSpsLpCE.js → config-report-Cm7UITNA.js} +3 -3
  75. package/dist/connector-schema-_VCSsdtD.js +433 -0
  76. package/dist/{context-B_u3_bDJ.js → context--uM9V3Ui.js} +2 -2
  77. package/dist/convex/index.d.ts +5 -5
  78. package/dist/convex/index.js +2 -2
  79. package/dist/core-B5-w6Kw9.js +335 -0
  80. package/dist/core-CQkE8Esr.d.ts +179 -0
  81. package/dist/{create-B30OMUOJ.d.ts → create-B2-CQqCI.d.ts} +5 -5
  82. package/dist/{create-CGm28gyI.js → create-Cb86Z8Im.js} +9 -8
  83. package/dist/{create-DCF2SvoT.js → create-DJrlhEp9.js} +5 -5
  84. package/dist/{create-D_NmPOdH.d.ts → create-QHuuPw3q.d.ts} +7 -7
  85. package/dist/{create-CBMmFie-.d.ts → create-UTnUVETj.d.ts} +30 -10
  86. package/dist/{credential-BcoiejpY.js → credential-CIM7wHwA.js} +4 -4
  87. package/dist/{credentials-DPyHmYz5.js → credentials-BpuTwd7a.js} +1 -1
  88. package/dist/{deciding-columns-Sq12mEA6.js → deciding-columns-DRo2QM6E.js} +13 -2
  89. package/dist/{delegation-j6-rc37o.js → delegation-C5rOdTp5.js} +5 -5
  90. package/dist/delivery/index.d.ts +5 -5
  91. package/dist/delivery/index.js +4 -3
  92. package/dist/{delivery-DCAgNyCn.js → delivery-rwOKLWRm.js} +51 -2
  93. package/dist/{denied-text-CiFQNuMV.d.ts → denied-text-D4qkEtFz.d.ts} +2 -2
  94. package/dist/{describe-BybMRY7A.js → describe-DZlnEtLy.js} +1 -0
  95. package/dist/{describe-B5qgDvEW.d.ts → describe-O0SwYIK8.d.ts} +2 -2
  96. package/dist/{diff-CmuSD3vc.js → diff-Btr1c1Ow.js} +2 -2
  97. package/dist/{diff-3jbx6Rm4.js → diff-DzT-xD13.js} +10 -10
  98. package/dist/{doctor-BdZw53-k.js → doctor-BU1szn63.js} +241 -49
  99. package/dist/{doctor-3ZqNHX_G.js → doctor-BoK_y0KO.js} +4 -4
  100. package/dist/drizzle/index.d.ts +3 -3
  101. package/dist/drizzle/index.js +1 -1
  102. package/dist/elysia/index.d.ts +5 -5
  103. package/dist/elysia/index.js +3 -3
  104. package/dist/{errors-DH5QEGXh.d.ts → errors-B_Qg9D3e.d.ts} +2 -2
  105. package/dist/{errors-CWTY6uih.js → errors-DpP_U6Bf.js} +1 -0
  106. package/dist/{evaluate-CNClBvcj.js → evaluate-B_oqFbWC.js} +55 -12
  107. package/dist/{evaluations-C_1vdSZc.d.ts → evaluations-BCddEOfO.d.ts} +4 -4
  108. package/dist/{evaluations-beAooV0n.js → evaluations-DReoiVBw.js} +4 -4
  109. package/dist/eve/index.d.ts +5 -5
  110. package/dist/eve/index.js +3 -3
  111. package/dist/express/index.d.ts +5 -5
  112. package/dist/express/index.js +3 -3
  113. package/dist/fastify/index.d.ts +5 -5
  114. package/dist/fastify/index.js +3 -3
  115. package/dist/{fetch-adapter-Dw0MTS3D.d.ts → fetch-adapter-CuhyYwC1.d.ts} +4 -4
  116. package/dist/{fetch-adapter-DD_gfmrC.js → fetch-adapter-De-u6vhJ.js} +3 -3
  117. package/dist/{fields-OYG_JR78.js → fields-ClcQLrVc.js} +48 -13
  118. package/dist/{files-C5ZI27eV.js → files-acUbNwWt.js} +15 -2
  119. package/dist/{fixtures-B7XrLeY7.js → fixtures-BTMW6zgU.js} +2 -2
  120. package/dist/{flags-C3DktBDY.js → flags-aaeRAdiD.js} +2 -2
  121. package/dist/{from-snapshot-CTCMsFZk.js → from-snapshot-Bz469dIh.js} +17 -10
  122. package/dist/generate-3FNO5P5Q.js +159 -0
  123. package/dist/{grantee-hxrUsbpM.js → grantee-BrRANrpT.js} +1 -1
  124. package/dist/{grantee-conditions-CcXf8hMM.js → grantee-conditions-C6P1Tvol.js} +1 -1
  125. package/dist/{grantee-match-DbENBHLB.js → grantee-match-0RtHWQDx.js} +3 -3
  126. package/dist/h3/index.d.ts +4 -4
  127. package/dist/h3/index.js +3 -3
  128. package/dist/{helpers-CBYVMjwh.js → helpers-DY3vXuGs.js} +3 -3
  129. package/dist/hono/index.d.ts +5 -5
  130. package/dist/hono/index.js +3 -3
  131. package/dist/{hosted-BPk51-kX.d.ts → hosted-BmjzxQhp.d.ts} +14 -2
  132. package/dist/{hosted-B46-5LYE.js → hosted-XBgU6RGT.js} +34 -12
  133. package/dist/{http-DXJ37RDA.js → http-Ca3Sr5x1.js} +1 -1
  134. package/dist/{import-wyqXUyjm.js → import-Bz0GefWg.js} +5 -5
  135. package/dist/{import-CNV7I2Dw.js → import-DB2EG_iy.js} +3 -3
  136. package/dist/{index-C-6Letxg.d.ts → index-BE8y3moU.d.ts} +50 -8
  137. package/dist/{index-B8YjkEZ4.d.ts → index-BSfRl0hZ.d.ts} +6 -6
  138. package/dist/{index-DuWxc5dt.d.ts → index-BTkwrK7v.d.ts} +143 -14
  139. package/dist/index-CMiPH2Tv.d.ts +206 -0
  140. package/dist/{index-Ch8LpNDp.d.ts → index-DH9DUDEM.d.ts} +18 -3
  141. package/dist/{index-CueFnxQc.d.ts → index-DLwcuCN_.d.ts} +86 -7
  142. package/dist/index.d.ts +18 -17
  143. package/dist/index.js +27 -25
  144. package/dist/{init-GG6Jh8HD.js → init-5TXNji88.js} +5 -5
  145. package/dist/{init-DeMYsnYJ.js → init-B-4t1NzC.js} +4 -4
  146. package/dist/{instance-options-BsVpyGIk.d.ts → instance-options-CU0YQCLH.d.ts} +1 -1
  147. package/dist/{instance-snapshot-BOEhcqIF.js → instance-snapshot-CjcwnEiV.js} +16 -11
  148. package/dist/jwt/index.d.ts +83 -5
  149. package/dist/jwt/index.js +143 -5
  150. package/dist/{kernel-D0NVb1cK.js → kernel-DVExUwkp.js} +138 -12
  151. package/dist/kysely/index.d.ts +3 -3
  152. package/dist/kysely/index.js +1 -1
  153. package/dist/load-DrMHXzgN.js +1 -0
  154. package/dist/{load-DFS_ry0W.js → load-DxroEzH1.js} +2 -2
  155. package/dist/{login-Dfe5RlYk.js → login-ClSJh5t5.js} +4 -4
  156. package/dist/{login-imhuwvn3.js → login-WC9HPJpU.js} +6 -10
  157. package/dist/{manifest-C8IzW3Do.js → manifest-B79YuF2T.js} +6 -2
  158. package/dist/{manifest-mx5ghSFM.d.ts → manifest-BVhExYhw.d.ts} +23 -2
  159. package/dist/{manifest-dvL2XEe1.js → manifest-Dv6q4i5s.js} +54 -35
  160. package/dist/{manifest-schema-C0kK_8V0.js → manifest-schema-Cv492Qev.js} +37 -7
  161. package/dist/{map-claims-D3oRmTk0.js → map-claims-etGIS_Dv.js} +14 -11
  162. package/dist/{map-error-Dr70oA_3.js → map-error-BQGyDm_M.js} +1 -1
  163. package/dist/mcp/index.d.ts +64 -10
  164. package/dist/mcp/index.js +151 -22
  165. package/dist/{migrate-CIq4-E9F.js → migrate-Dodel0IJ.js} +77 -24
  166. package/dist/nest/index.d.ts +5 -5
  167. package/dist/nest/index.js +3 -3
  168. package/dist/next/index.d.ts +9 -9
  169. package/dist/next/index.js +5 -5
  170. package/dist/next/plugin.d.ts +1 -1
  171. package/dist/next/plugin.js +2 -2
  172. package/dist/node/index.d.ts +5 -5
  173. package/dist/node/index.js +2 -2
  174. package/dist/openai/index.d.ts +5 -5
  175. package/dist/openai/index.js +3 -3
  176. package/dist/openapi/index.d.ts +14 -4
  177. package/dist/openapi/index.js +1 -1
  178. package/dist/{openapi-C4dDTVEd.js → openapi-DMocl1bh.js} +4 -4
  179. package/dist/{openapi-B7HZ0f-r.js → openapi-Ddb4628T.js} +2 -7
  180. package/dist/{openapi-DD1SV-5-.js → openapi-ztVaux6j.js} +8 -8
  181. package/dist/{operation-findings-DgATDpvh.js → operation-findings-kRcEp8lE.js} +1 -1
  182. package/dist/{operations-X0VM7F3m.d.ts → operations-BNLes61x.d.ts} +1 -1
  183. package/dist/orpc/index.d.ts +6 -6
  184. package/dist/orpc/index.js +4 -4
  185. package/dist/otel/index.d.ts +3 -3
  186. package/dist/otel/index.js +1 -1
  187. package/dist/{output-1im__-08.js → output-DgSDLrta.js} +7 -3
  188. package/dist/{output-files-DhRBBE4n.js → output-files-CCMdcFWb.js} +2 -2
  189. package/dist/output-files-DPAY7-GX.js +1 -0
  190. package/dist/{ownership-DqaJNJPn.js → ownership-Z2LpxsGa.js} +443 -40
  191. package/dist/parse-Ddhz2xf4.js +148 -0
  192. package/dist/{parse-local-manifest-CVSs6J9I.js → parse-local-manifest-T1Q8X3A2.js} +24 -1
  193. package/dist/{parse-local-manifest-5ODwqu8b.d.ts → parse-local-manifest-e4mdwAKd.d.ts} +1 -1
  194. package/dist/pdp/index.d.ts +3 -3
  195. package/dist/pdp/index.js +7 -3
  196. package/dist/peer-12Q5I_RH.js +976 -0
  197. package/dist/{peer-CRvC7Hg1.js → peer-DOzzMfZL.js} +1 -1
  198. package/dist/{permdock-BrAscV28.js → permdock-BfWEeMqk.js} +177 -49
  199. package/dist/{permdock-SV8ZINYT.d.ts → permdock-Dco6dyCK.d.ts} +34 -5
  200. package/dist/{permission-registry-Br4jSNVi.js → permission-registry-DpPtSKNZ.js} +1 -1
  201. package/dist/{vocabulary-CN8H_9UF.d.ts → permission-types-DhNEmoRG.d.ts} +4 -64
  202. package/dist/{permissions-u_KHg-sN.d.ts → permissions-CoqCOfJ3.d.ts} +2 -1
  203. package/dist/{permissions-B-wCP5ZC.js → permissions-CyH2MV_B.js} +1 -1
  204. package/dist/{permissions-BPxlwVhN.js → permissions-DArULThq.js} +4 -4
  205. package/dist/pkce-5h8NgMQJ.js +21 -0
  206. package/dist/{policies-DP2nqRoF.js → policies-B2lZIurj.js} +3 -3
  207. package/dist/{policy-77FSAq_V.d.ts → policy-B7dg_yzu.d.ts} +2 -2
  208. package/dist/{policy-DbM5YYtV.js → policy-DaJy4nK4.js} +17 -8
  209. package/dist/{policy-from-catalog-C2gMa2Dj.js → policy-from-catalog-DFWYkNdu.js} +9 -9
  210. package/dist/{policy-types-n0uRTsW8.d.ts → policy-types-B-F-qAZk.d.ts} +83 -4
  211. package/dist/postgres-store-BL9M_CA1.js +1001 -0
  212. package/dist/{postgrest-CEgRbefE.d.ts → postgrest-CYacM_u7.d.ts} +12 -1
  213. package/dist/{postgrest-DAjUkF9w.js → postgrest-r5faDIzD.js} +37 -4
  214. package/dist/{powersync-DISt6SUe.js → powersync-13odBwiM.js} +3 -3
  215. package/dist/powersync-53ryOZip.js +1059 -0
  216. package/dist/powersync-CExtTLyA.js +1 -0
  217. package/dist/preset/index.js +1 -1
  218. package/dist/{preset-DZdU0tne.js → preset-CHTbd-DQ.js} +2 -2
  219. package/dist/prisma/index.d.ts +2 -2
  220. package/dist/prisma/index.js +1 -1
  221. package/dist/{problem-details-B45V5d2b.d.ts → problem-details-Tcfmj3bV.d.ts} +1 -1
  222. package/dist/prompt-DwtVemER.js +935 -0
  223. package/dist/{protected-RFGpakN9.d.ts → protected-BwZYL1X_.d.ts} +6 -5
  224. package/dist/{protected-BlF0F6wL.js → protected-DXE3cQYg.js} +2 -2
  225. package/dist/{provider-B1al-AZz.js → provider-BQsTYYy0.js} +2 -2
  226. package/dist/{provider-DILL9oPK.d.ts → provider-Csk-THAn.d.ts} +1 -1
  227. package/dist/{provider-store-DM6DNcta.js → provider-store-IgqQipCO.js} +2 -2
  228. package/dist/{pull-jEwbf80J.js → pull-CN5OLxrF.js} +7 -7
  229. package/dist/pull-Cty6Ovyu.js +1 -0
  230. package/dist/{push-BD4f15co.js → push-tVdG8dUL.js} +54 -14
  231. package/dist/{pushed-policy-DCW91KQU.d.ts → pushed-policy-BbcY9r_4.d.ts} +1 -1
  232. package/dist/{pushed-policy-CqtxkV4G.js → pushed-policy-CmYeSQqq.js} +1 -1
  233. package/dist/react/index.d.ts +6 -6
  234. package/dist/react/index.js +3 -3
  235. package/dist/react/provider-client.d.ts +1 -1
  236. package/dist/react/provider-client.js +1 -1
  237. package/dist/react-native/index.d.ts +25 -11
  238. package/dist/react-native/index.js +209 -21
  239. package/dist/{refs-K6eWUJ0H.js → refs-CNJBiTFo.js} +15 -1
  240. package/dist/{replay-Bkm-Ne4l.d.ts → replay-DCDD_ZfL.d.ts} +1 -1
  241. package/dist/{request-context-C81Pip9n.js → request-context-BykKLmyE.js} +2 -2
  242. package/dist/resend/index.d.ts +4 -4
  243. package/dist/{retire-CALjIQ7e.js → retire-Wj6dkyDE.js} +2 -2
  244. package/dist/{rls-BAqP4q66.js → rls-Ck_PocSp.js} +32 -8
  245. package/dist/{rls-DTvYd-lI.js → rls-CxBZb4rZ.js} +478 -534
  246. package/dist/{role-builder-uWQZ4v84.js → role-builder-C1VJzvcr.js} +3 -3
  247. package/dist/rolldown-runtime-C3I60J0A.js +4 -0
  248. package/dist/{rpc-VSl5ylaQ.js → rpc-Bk1bE-ch.js} +1 -1
  249. package/dist/run-Bz7DXGEv.js +1 -0
  250. package/dist/{run-ticket-sxFI6LAk.js → run-ticket-B5d_63Ma.js} +22 -1
  251. package/dist/{run-ticket-DocwaTKn.d.ts → run-ticket-BpNi7BCi.d.ts} +8 -3
  252. package/dist/{run-CUKuz0A8.js → run-uLD2aWuO.js} +7 -7
  253. package/dist/{runtime-D3VTAiCA.js → runtime-Vif3_-zf.js} +1 -1
  254. package/dist/{saas-BCXRuXca.js → saas-Az_Kz-4D.js} +7 -7
  255. package/dist/{scan-qvOe0hS3.js → scan-DSljDE1Y.js} +1 -1
  256. package/dist/{schema-yLuBWWsh.js → schema-gIcCx7zM.js} +1 -1
  257. package/dist/scim/index.d.ts +3 -2
  258. package/dist/scim/index.js +3 -1
  259. package/dist/scim-C7JYhySg.js +44 -0
  260. package/dist/{scim-ClHtpyV3.js → scim-YNieBO1U.js} +3 -537
  261. package/dist/{scopes-CUPSjo9q.js → scopes-BHsc-wp-.js} +1 -1
  262. package/dist/server/index.d.ts +11 -11
  263. package/dist/server/index.js +7 -7
  264. package/dist/{server-C3WXAHZj.js → server-Bnga6I50.js} +3 -3
  265. package/dist/{server-types-CKQiCnON.d.ts → server-types-C4gZUEZy.d.ts} +9 -9
  266. package/dist/{shared-DvvFzYXe.js → shared-C8LjUdVU.js} +1 -1
  267. package/dist/{shared-BAK8t4Uo.js → shared-fMKOv8hP.js} +44 -38
  268. package/dist/{sink-4-eQIRWf.d.ts → sink-Cp_Eot9W.d.ts} +4 -4
  269. package/dist/{skills-B6lVdD_W.js → skills-Dcjw9_1C.js} +5 -5
  270. package/dist/{skills-CTYwkl44.js → skills-g_3BS9NC.js} +3 -3
  271. package/dist/{snapshot-B7DkYg6g.js → snapshot-CbGmYiJx.js} +4 -4
  272. package/dist/{snapshot-cache-DZHQ0KF6.js → snapshot-cache-DT2Kx1F8.js} +1 -1
  273. package/dist/{snapshot-cache-PXI-vGpY.d.ts → snapshot-cache-Pahd3IYJ.d.ts} +1 -1
  274. package/dist/solid/index.d.ts +6 -5
  275. package/dist/solid/index.js +4 -4
  276. package/dist/solid-start/index.d.ts +6 -6
  277. package/dist/solid-start/index.js +3 -3
  278. package/dist/{sources-vs5dGOsS.js → sources-B-zYB4GS.js} +111 -25
  279. package/dist/{sources-Xc4zkszI.d.ts → sources-DEEuGb8S.d.ts} +63 -14
  280. package/dist/sources-qoIJQrJv.js +11 -0
  281. package/dist/{sql-B5T1IL83.js → sql-B96movIM.js} +5 -1
  282. package/dist/sql-CDEPCA0g.d.ts +10 -0
  283. package/dist/{src-Cu2m75uo.js → src-DLFrtrrC.js} +140 -48
  284. package/dist/ssf/index.d.ts +4 -4
  285. package/dist/ssf/index.js +640 -211
  286. package/dist/{standard-webhooks-rbD7LS6h.d.ts → standard-webhooks-B_qRg0ws.d.ts} +2 -2
  287. package/dist/{store-BDutJkDL.js → store-DSP9oGvh.js} +15 -7
  288. package/dist/{store-options-u0ycEe8s.js → store-options-B_poyYvg.js} +6 -6
  289. package/dist/{stores-BUGNbJTt.d.ts → stores-BAZwSYZA.d.ts} +6 -5
  290. package/dist/{stores-CGhM8BN_.js → stores-BqEuHC05.js} +2 -2
  291. package/dist/{stream-CeCMJmBB.d.ts → stream-2Vwhl7Er.d.ts} +2 -2
  292. package/dist/{style-Uao76wO7.js → style-Cqo7-73x.js} +1 -0
  293. package/dist/{style-DiKWnAvn.js → style-Ds1gNm77.js} +1 -1
  294. package/dist/supabase/index.d.ts +8 -7
  295. package/dist/supabase/index.js +5 -5
  296. package/dist/supabase/middleware.d.ts +6 -6
  297. package/dist/supabase/middleware.js +6 -6
  298. package/dist/{supabase-x5JtR2tc.js → supabase--0aJdLEd.js} +8 -6
  299. package/dist/{supabase-By_frk3U.js → supabase-Bqp7iIY2.js} +3 -3
  300. package/dist/supabase-hook-DJxC4PoP.js +1 -0
  301. package/dist/{supabase-hook-BMoEhZir.js → supabase-hook-Dg0SrQWR.js} +1060 -119
  302. package/dist/supautils-MESd81bF.js +689 -0
  303. package/dist/{support-session-C6xJQYut.js → support-session-Cna49-3f.js} +2 -2
  304. package/dist/svelte/index.d.ts +5 -5
  305. package/dist/svelte/index.js +5 -5
  306. package/dist/svelte/runtime.d.ts +3 -3
  307. package/dist/svelte/runtime.js +3 -3
  308. package/dist/svelte/source.d.ts +5 -5
  309. package/dist/svelte/source.js +2 -2
  310. package/dist/sveltekit/index.d.ts +6 -6
  311. package/dist/sveltekit/index.js +2 -2
  312. package/dist/tanstack-start/index.d.ts +6 -6
  313. package/dist/tanstack-start/index.js +2 -2
  314. package/dist/{tenancy-DN2QAx9z.js → tenancy-CmeBQ6Nv.js} +12 -3
  315. package/dist/terminal/index.d.ts +6 -6
  316. package/dist/terminal/index.js +3 -3
  317. package/dist/testing/core.d.ts +2 -0
  318. package/dist/testing/core.js +1 -0
  319. package/dist/testing/fixtures/index.d.ts +12 -0
  320. package/dist/testing/fixtures/index.js +770 -0
  321. package/dist/testing/index.d.ts +41 -174
  322. package/dist/testing/index.js +241 -731
  323. package/dist/testing/saas/index.d.ts +5 -5
  324. package/dist/testing/saas/index.js +2 -2
  325. package/dist/testing/saas/permissions.d.ts +1 -1
  326. package/dist/testing/saas/permissions.js +1 -1
  327. package/dist/text-diff-DhITNCnD.js +444 -0
  328. package/dist/{token-BLZcjQgt.js → token-2qYDvXz2.js} +1 -1
  329. package/dist/{tool-refusal-CMLN5xhn.js → tool-refusal-reAhIzdl.js} +1 -1
  330. package/dist/trpc/index.d.ts +6 -6
  331. package/dist/trpc/index.js +3 -3
  332. package/dist/{types-Dfv0aTER.d.ts → types-BWZybqs9.d.ts} +110 -8
  333. package/dist/{types-Druu9wLM.d.ts → types-BXMYB7BY.d.ts} +2 -2
  334. package/dist/{types-BzzaHxt3.d.ts → types-BfVmNJO5.d.ts} +14 -3
  335. package/dist/{types-lUlG3TVd.d.ts → types-CIeZcG8v.d.ts} +2 -2
  336. package/dist/{types-B3_ooZgZ.d.ts → types-CKV71kKh.d.ts} +4 -4
  337. package/dist/{types-CX7AvRqP.d.ts → types-CQVEQawv.d.ts} +4 -4
  338. package/dist/{types-C863OQiR.d.ts → types-CeIscIKp.d.ts} +4 -4
  339. package/dist/{types-C_IiEs64.d.ts → types-CfiKC5aN.d.ts} +1 -1
  340. package/dist/{types-_WhUP7ud.d.ts → types-D3A19w4A.d.ts} +8 -4
  341. package/dist/{types-8RnwUAvE.d.ts → types-DBHftjgS.d.ts} +15 -8
  342. package/dist/{types-BdtSDbBO.d.ts → types-DC44-hqb.d.ts} +2 -2
  343. package/dist/{types-CVZSNocA.d.ts → types-DXmwu78Z.d.ts} +4 -4
  344. package/dist/{types-C791HFu9.d.ts → types-cTos-H7p.d.ts} +2 -2
  345. package/dist/{types-BtHtcKMr.d.ts → types-g7Oy6Zg4.d.ts} +3 -3
  346. package/dist/unplugin/index.d.ts +1 -1
  347. package/dist/unplugin/index.js +2 -2
  348. package/dist/{usage-SveTRTyr.js → usage-Bybmck7f.js} +7 -7
  349. package/dist/{usage-CRVl9V3t.js → usage-CDaLXCfL.js} +2 -2
  350. package/dist/usage-y-kBNdqs.js +1 -0
  351. package/dist/{validation-error-DHUrAkku.d.ts → validation-error-CQpoquwj.d.ts} +1 -1
  352. package/dist/{verify-BaAKx7t6.js → verify-pe8rL_Qw.js} +350 -83
  353. package/dist/{views-DTIIO-oQ.js → views-BB8E9dTP.js} +1 -1
  354. package/dist/vocabulary-Bg3eeT2H.d.ts +67 -0
  355. package/dist/vue/index.d.ts +6 -5
  356. package/dist/vue/index.js +4 -4
  357. package/dist/{waiter-B986w1oK.d.ts → waiter-DwEyouan.d.ts} +2 -2
  358. package/dist/watch-Bd9mx3rd.js +1 -0
  359. package/dist/{watch-vea54PRw.js → watch-C-4VmFlL.js} +3 -3
  360. package/dist/webmcp/index.d.ts +2 -2
  361. package/dist/webmcp/index.js +2 -2
  362. package/dist/{where-scope-C9vfEphl.js → where-scope-wq2coopv.js} +41 -21
  363. package/dist/{who-can-DWBAcaCQ.js → who-can-BwNsI_Js.js} +6 -6
  364. package/dist/workflow/index.d.ts +3 -3
  365. package/dist/workflow/index.js +5 -5
  366. package/dist/workflow-sdk/index.d.ts +5 -5
  367. package/dist/{wrap-C5jC6sKI.d.ts → wrap-B6R0cMRL.d.ts} +2 -2
  368. package/dist/{wrap-gX61EtpB.js → wrap-CiSc_KCq.js} +8 -2
  369. package/package.json +22 -11
  370. package/schemas/cli-result-v1.json +1 -0
  371. package/schemas/cloud-api-v1.json +152 -7
  372. package/schemas/connector-v1.json +8 -0
  373. package/schemas/supabase-claims-v1.json +5 -0
  374. package/schemas/supabase-manifest-v1.json +276 -75
  375. package/skills/permdock/SKILL.md +1 -0
  376. package/skills/permdock-agents/references/runtimes.md +3 -1
  377. package/skills/permdock-approvals/references/stores-and-resume.md +2 -1
  378. package/skills/permdock-data/references/queries-and-rls.md +4 -1
  379. package/skills/permdock-tenancy/SKILL.md +1 -1
  380. package/skills/permdock-tenancy/references/ownership-and-custom-roles.md +1 -0
  381. package/skills/permdock-wire/references/adapters.md +14 -0
  382. package/skills/permdock-wire/references/policy-options.md +1 -0
  383. package/dist/catalog-BoWglDPI.js +0 -1
  384. package/dist/collect-D7eQDffE.js +0 -1
  385. package/dist/index-Cp_ilKi1.d.ts +0 -91
  386. package/dist/load-BxEtwlBS.js +0 -1
  387. package/dist/output-files-B3IZJksC.js +0 -1
  388. package/dist/parse-Cxq0WxrA.js +0 -53
  389. package/dist/peer-C8swJ2GH.js +0 -50
  390. package/dist/powersync-BkWWCS1K.js +0 -538
  391. package/dist/powersync-MMJ2mI2q.js +0 -1
  392. package/dist/prompt-DfZLJLIn.js +0 -32
  393. package/dist/pull-CvpINcMK.js +0 -1
  394. package/dist/run-DDwUaJDU.js +0 -1
  395. package/dist/supabase-hook-CCw5m35C.js +0 -1
  396. package/dist/supautils-B6HYL9eL.js +0 -91
  397. package/dist/text-diff-BQcUapOb.js +0 -15
  398. package/dist/usage-CZpSQP6L.js +0 -1
  399. package/dist/watch--ovlPOc0.js +0 -1
@@ -1,30 +1,30 @@
1
1
  import { t as compact } from "./compact-CAf557ZB.js";
2
- import { c as listPermissions, u as renamedKeys } from "./permission-registry-Br4jSNVi.js";
2
+ import { c as listPermissions, u as renamedKeys } from "./permission-registry-DpPtSKNZ.js";
3
3
  import { i as dateAt } from "./time-DkWjdL98.js";
4
- import { d as scopeList, o as resolveScope } from "./scopes-CUPSjo9q.js";
5
- import { o as flattenGrantee, v as listRoles } from "./grantee-hxrUsbpM.js";
6
- import { p as scopeField } from "./tenancy-DN2QAx9z.js";
7
- import { a as describeError, i as configError, o as exitCodeOfError, u as usageResult } from "./errors-CWTY6uih.js";
4
+ import { p as scopeField } from "./tenancy-CmeBQ6Nv.js";
5
+ import { a as describeError, i as configError, o as exitCodeOfError, u as usageResult } from "./errors-DpP_U6Bf.js";
8
6
  import { t as policyRowConditionKeys } from "./row-conditions-h2HythXQ.js";
9
- import { c as requirePolicy } from "./load-DFS_ry0W.js";
10
- import "./budget-NbEtbJC_.js";
11
- import { i as qualifySqlTable, r as escapeSqlIdent, u as sqlTextArray } from "./sql-B5T1IL83.js";
12
- import { l as roleColumn } from "./sources-vs5dGOsS.js";
13
- import { t as policyFromCatalog } from "./policy-from-catalog-C2gMa2Dj.js";
7
+ import { o as sqlFiles } from "./files-D_JIzUrP.js";
8
+ import { c as requirePolicy } from "./load-DxroEzH1.js";
9
+ import { i as qualifySqlTable, r as escapeSqlIdent } from "./sql-B96movIM.js";
10
+ import "./sources-B-zYB4GS.js";
11
+ import { t as policyFromCatalog } from "./policy-from-catalog-DFWYkNdu.js";
14
12
  import { t as beforeFirst } from "./text-BMFRfBqc.js";
15
13
  import { i as INDEXES_MARKER, n as GRANTS_MARKER, o as SEEDS_MARKER, t as BACKFILL_MARKER } from "./markers-C6joDI3o.js";
16
- import { i as requirePeer, n as packageManagerOf, t as addLine } from "./peer-C8swJ2GH.js";
14
+ import { i as requirePeer, n as packageManagerOf, t as addLine } from "./peer-12Q5I_RH.js";
17
15
  import { t as POLICY_NOT_PUBLISHED } from "./problem-types-CJ2325SZ.js";
18
- import { t as fetchProjectPolicy } from "./pull-jEwbf80J.js";
19
- import { a as isSeedsDirectory, c as parseSplit, d as seedsMigration, f as writeOutputFiles, l as partPath, n as dataMigrationFile, r as driftOf, u as pgDeltaPath } from "./output-files-DhRBBE4n.js";
20
- import { c as approvalsSql, d as resolveAuthorize, h as schemasRefusal, l as parseRbacAuthorize, m as resolveSchemas, o as supabaseHookSql, p as isSupabaseSchemas, s as permissionHoldersSql, t as grantsLabel, u as rbacScaffold } from "./supabase-hook-BMoEhZir.js";
21
- 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-BAK8t4Uo.js";
22
- import { a as splitUndiffed } from "./deciding-columns-Sq12mEA6.js";
23
- import { a as readOnlyActorKinds, c as branchClauses, d as connectPgPool, i as orSql, n as actorMayWriteSql, o as readOnlyActorPolicies, r as assemblePolicies, t as ACTOR_MAY_WRITE } from "./policies-DP2nqRoF.js";
24
- import { A as collectGrants, C as byKey, D as apiKeysPlan, G as memberIdsHelper, I as graphHelper, J as permittedIdsHelper, L as graphSqlName, O as breakGlassHolder, S as byCodeUnit, X as scopeSources, Z as scopeTable, b as currentKeyExpr, et as checkSuspension, f as permittedByPermissionHelper, g as seedSql, h as helpersSql, i as assignmentSql, j as roleNames, k as breakGlassKey, m as accessSql, n as ownershipSql, t as ownershipRules, u as permissionHelpersSql, x as rolesTableScope } from "./ownership-DqaJNJPn.js";
25
- import { _ as indexesSql, a as graphSql, c as callsHelper, g as indexTargets, h as COLUMNS_SQL, i as graphPlan, l as helperCallKeys, m as tableFor, n as runRlsVerify, o as HELPER_TABLES, p as compileGrants, r as closureDepths, v as pruneIndexTargets, y as readTableIndexFacts } from "./verify-BaAKx7t6.js";
26
- import { a as fieldViewsSql, i as fieldViews, n as columnGrantSql, o as rowBranches, r as columnRevokeSql } from "./fields-OYG_JR78.js";
27
- import { t as supautilsRejections } from "./supautils-B6HYL9eL.js";
16
+ import { t as fetchProjectPolicy } from "./pull-CN5OLxrF.js";
17
+ import { n as MIGRATION_DIRS } from "./constants-DcJABbLW.js";
18
+ import { a as isSeedsDirectory, c as parseSplit, d as seedsMigration, f as writeOutputFiles, l as partPath, n as dataMigrationFile, r as driftOf, u as pgDeltaPath } from "./output-files-CCMdcFWb.js";
19
+ import { b as rbacScaffold, c as approvalsSql, d as resolveSchemas, f as schemasRefusal, o as supabaseHookSql, p as declarativeSql, s as permissionHoldersSql, t as grantsLabel, u as isSupabaseSchemas, y as parseRbacAuthorize } from "./supabase-hook-Dg0SrQWR.js";
20
+ import { A as quoteQualified, D as qualified$1, F as scopeTypeOf, O as quoteIdent$1, _ as subjectClaimJsonSql, c as SESSION_LIVE_HELPER, g as sessionLiveHelperSql, j as quoteTable, k as quoteLiteral, r as HELPERS, t as CUSTOM_ROLES, y as subjectIdSql } from "./shared-fMKOv8hP.js";
21
+ import { D as byKey, F as collectGrants, H as graphHelper, N as breakGlassHolder, P as breakGlassKey, Z as memberIdsHelper, _ as seedSql, a as assignmentSql, d as permissionHelpersSql, et as permittedIdsHelper, g as helpersSql, h as accessSql, n as ownershipSql, nt as scopeSources, p as permittedByPermissionHelper, rt as scopeTable } from "./ownership-Z2LpxsGa.js";
22
+ import { a as splitUndiffed } from "./deciding-columns-DRo2QM6E.js";
23
+ import { a as readOnlyActorKinds, d as connectPgPool, n as actorMayWriteSql, o as readOnlyActorPolicies, r as assemblePolicies, t as ACTOR_MAY_WRITE } from "./policies-B2lZIurj.js";
24
+ import { C as INDEX_COLUMNS_SQL, D as readTableIndexFacts, E as pruneIndexTargets, S as COLUMNS_SQL, T as indexesSql, a as graphSql, b as compileGrants, c as verifyResult, d as tableNames, g as helperCallKeys, h as callsHelper, n as runRlsVerify, o as rlsFinding, p as HELPER_TABLES, s as verifyIgnores, w as indexTargets, x as tableFor } from "./verify-pe8rL_Qw.js";
25
+ import { a as fieldViewsSql, i as fieldViews, n as columnGrantSql, o as rowBranches, r as columnRevokeSql } from "./fields-ClcQLrVc.js";
26
+ import { a as generateOptions, c as buildContext, i as rowHelpersSql, l as DROPPED_LIST, n as inheritTargets, o as isRefusal, r as levelKeys, s as splitsPart, t as supautilsRejections, u as rebuildPlan } from "./supautils-MESd81bF.js";
27
+ import { readFileSync } from "node:fs";
28
28
  import { basename, delimiter, join } from "node:path";
29
29
  import { spawn } from "node:child_process";
30
30
  //#region src/cli/rls/advisors.ts
@@ -159,142 +159,160 @@ async function runRlsAdvisors(input) {
159
159
  };
160
160
  }
161
161
  //#endregion
162
- //#region src/cli/rls/generate-context.ts
163
- /** The scopes an `rls.migrate` `role` helper names, so `rls generate` writes their role-filtered member helper. */
164
- function roleMemberScopesOf(migrate) {
165
- const scopes = [...new Set(Object.values(migrate?.helpers ?? {}).flatMap((helper) => helper.form === "role" ? [helper.scope] : []))].toSorted();
166
- return scopes.length === 0 ? {} : { roleMemberScopes: scopes };
167
- }
168
- /** `rls.customRoleWrites.requires` as permission keys, or why it names none. */
169
- function writeRequirement(policy, requires) {
170
- if (requires === void 0) return {};
171
- const leaves = listPermissions(policy.permissions);
172
- const keys = requires === "manageRoles" ? leaves.filter((leaf) => leaf.meta.manageRoles === true).map((leaf) => leaf.key) : [...requires];
173
- 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" };
174
- const known = new Set(leaves.map((leaf) => leaf.key));
175
- const unknown = keys.filter((key) => !known.has(key));
176
- if (unknown.length > 0) return { error: `rls.customRoleWrites.requires names undeclared permissions: ${unknown.join(", ")}` };
177
- return { keys: [...new Set(keys)].toSorted() };
178
- }
179
- /** Declared role names, and those a tenant admin may compose into custom roles. */
180
- function customRoleNames(policy, requires) {
181
- const declared = [...roleNames(policy)].toSorted();
182
- const assignable = declared.filter((name) => policy.rolesByName.get(name)?.assignable ?? listRoles(policy.vocabulary.roles).some((leaf) => leaf.key === name && leaf.assignable));
183
- const renamed = renamedKeys(policy.vocabulary.permissions);
184
- const leaves = listPermissions(policy.permissions);
185
- const manage = leaves.filter((leaf) => leaf.meta.manageRoles === true).map((leaf) => leaf.key);
186
- return {
187
- declared,
188
- assignable,
189
- ...renamed.size === 0 ? {} : { renamed: Object.fromEntries(renamed) },
190
- permissions: leaves.map((leaf) => leaf.key).toSorted(),
191
- ...manage.length === 0 ? {} : { manage: manage.toSorted() },
192
- ...requires === void 0 ? {} : { requires },
193
- ...policy.levels === void 0 ? {} : { levels: true }
194
- };
162
+ //#region src/cli/rls/bindings.ts
163
+ /**
164
+ * The table each resource with row policies binds to. With `helpersOnly`,
165
+ * a resource `rls.tables` does not name has no table and no binding.
166
+ */
167
+ function tableBindings(resources, tables, helpersOnly) {
168
+ const out = [];
169
+ for (const resource of [...new Set(resources)].toSorted()) {
170
+ const named = tables !== void 0 && Object.hasOwn(tables, resource);
171
+ if (helpersOnly && !named) continue;
172
+ out.push({
173
+ resource,
174
+ table: qualifySqlTable(tableFor(resource, tables)),
175
+ inferred: !named
176
+ });
177
+ }
178
+ return out;
195
179
  }
196
- function skippedOwnershipTriggers(triggers, scopes) {
197
- if (triggers === false) return "all";
198
- return Object.keys(triggers).map((key) => {
199
- const name = resolveScope(scopes, key);
200
- if (name === void 0) throw configError(`PermDock CLI: rls.ownershipTriggers.${key} names a scope the policy does not declare`);
201
- return name;
202
- });
180
+ /** The generate notes for `bindings`: the inferred ones, and where to see them all. */
181
+ function bindingNotes(bindings, withDatabase) {
182
+ const inferred = bindings.filter((binding) => binding.inferred);
183
+ if (inferred.length === 0) return [];
184
+ return [`table bindings (no rls.tables entry, so the table is named after the resource): ${inferred.map((binding) => `${binding.resource} -> ${binding.table}`).join(", ")}${withDatabase ? "" : "; run permdock rls explain, or generate with --db-url-stdin, to check each table exists"}`];
203
185
  }
204
- /** The SQL context every emitter reads, from the flags, `rls` config and the policy. */
205
- function buildContext(input, policy, options) {
206
- const rls = input.config.rls;
207
- const memberships = parseMembershipsFlag(input.memberships) ?? rls?.memberships;
208
- if (input.rbac && input.dialect !== "supabase") return { refusal: `rls generate --rbac supabase needs --dialect supabase (got ${input.dialect})` };
209
- const schema = input.rbacSchema ?? rls?.schema ?? rls?.rbac?.schema ?? "permdock";
210
- const authorize = resolveAuthorize(input.config, {
211
- authorize: input.authorize,
212
- rbac: input.rbac,
213
- memberships
214
- });
215
- const memberSources = rls?.membershipSources ?? input.config.supabase?.hook?.memberships;
216
- const sources = authorize === "database" ? memberSources : void 0;
217
- const scopes = scopeList(policy.scopes);
218
- const suspension = checkSuspension(rls?.suspension, scopes);
219
- const hookRoles = input.config.supabase?.hook?.roles;
220
- const roles = rls?.roles ?? (hookRoles === false ? void 0 : hookRoles);
221
- 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" };
222
- const requires = writeRequirement(policy, rls?.customRoleWrites?.requires);
223
- if (requires.error !== void 0) return { refusal: requires.error };
224
- const readers = rls?.trustedReaders;
225
- 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']" };
226
- const ownership = ownershipRules(policy, scopes);
227
- const graph = graphPlan(policy);
228
- const apiKeys = apiKeysPlan(rls?.apiKeys, new Set(roleNames(policy)), Object.fromEntries(renamedKeys(policy.vocabulary.permissions)));
229
- return {
230
- ctx: {
231
- dialect: input.dialect,
232
- scopes,
233
- tenantClaim: rls?.tenantClaim ?? "tenant_id",
234
- ...roleMemberScopesOf(rls?.migrate),
235
- ...rls?.roleValues === void 0 ? {} : { roleValues: rls.roleValues },
236
- ...rls?.tenants === "all" ? { tenants: "all" } : {},
237
- gucPrefix: input.gucPrefix ?? rls?.gucPrefix ?? "app",
238
- ...rls?.actions === void 0 ? {} : { actions: rls.actions },
239
- inlineFunctions: input.inlineFunctions || rls?.inlineFunctions === true,
240
- schema,
241
- authorize,
242
- roleClaim: rls?.roleClaim ?? "user_role",
243
- tenantType: input.tenantType ?? rls?.tenantType ?? "uuid",
244
- ...rls?.teamType === void 0 ? {} : { teamType: rls.teamType },
245
- ...rls?.scopeTypes === void 0 ? {} : { scopeTypes: rls.scopeTypes },
246
- ...memberships === void 0 ? {} : { memberships },
247
- ...sources === void 0 || sources.length === 0 ? {} : { sources },
248
- ...memberSources === void 0 || memberSources.length === 0 ? {} : { memberSources },
249
- ...suspension === void 0 ? {} : { suspension },
250
- ...roles === void 0 ? {} : { roles },
251
- ...input.customRoles === true || customRolesOn(rls) ? { customRoles: {
252
- ...customRoleNames(policy, requires.keys),
253
- ...rls?.customRoleWrites?.roles === void 0 ? {} : { table: rls.customRoleWrites.roles }
254
- } } : {},
255
- ...input.capabilities === true || rls?.capabilities === true ? { capabilities: true } : {},
256
- ...apiKeys === void 0 ? {} : { apiKeys },
257
- ...rls?.anonymousSignIns === "deny" ? { anonymousSignIns: "deny" } : {},
258
- ...ownership === void 0 ? {} : { ownership },
259
- ...rls?.ownershipTriggers === void 0 ? {} : { skipOwnershipTriggers: skippedOwnershipTriggers(rls.ownershipTriggers, scopes) },
260
- ...rls?.assignments === void 0 ? {} : { assignments: rls.assignments === true ? { tables: [] } : {
261
- tables: rls.assignments.tables ?? [],
262
- ...rls.assignments.ownRole === void 0 ? {} : { ownRole: rls.assignments.ownRole }
263
- } },
264
- ...options.fieldsMode === void 0 ? {} : { fields: options.fieldsMode },
265
- ...graph.size === 0 ? {} : { graph: {
266
- closures: closureDepths(graph),
267
- resources: policy.resources,
268
- ...rls?.tables === void 0 ? {} : { tables: rls.tables }
269
- } }
270
- },
271
- graph,
272
- ownership,
273
- schema,
274
- authorize
275
- };
186
+ /**
187
+ * The generate notes for the bindings whose table no project SQL file
188
+ * creates. A project with no `create table` in those files gets none: its
189
+ * schema lives elsewhere (an ORM, another repository).
190
+ */
191
+ function unknownTableNotes(bindings, created) {
192
+ if (created.size === 0) return [];
193
+ return bindings.filter((binding) => !created.has(binding.table) && !created.has(binding.table.toLowerCase())).map((binding) => `resource ${binding.resource} is bound to ${binding.table}, which no SQL file in the migration or schema folders creates; name its table in rls.tables, or set rls.tables.${binding.resource}: false when it has none`);
194
+ }
195
+ /** The inferred bindings whose table `facts` shows missing. */
196
+ function missingBindings(bindings, columns) {
197
+ return bindings.filter((binding) => binding.inferred && columns[binding.table] === void 0);
276
198
  }
277
199
  //#endregion
278
- //#region src/cli/rls/generate-options.ts
279
- function isRefusal(value) {
280
- return "refusal" in value;
200
+ //#region src/cli/rls/trigger-notes.ts
201
+ const CREATE = /^create trigger\s+("[^"]+"|[\w$]+)[^;]*?\bon\s+("[^"]+"(?:\."[^"]+")?|[\w$.]+)/gmu;
202
+ function unquote(name) {
203
+ return name.replaceAll("\"", "");
204
+ }
205
+ function reasonOf(name) {
206
+ if (name === "permdock_custom_role_name_guard") return "customRoleWrites.roles: a tenant role cannot take a declared or shared role's name";
207
+ if (name === "permdock_custom_role_cascade") return "customRoleWrites.roles: a rename or delete of a role carries its grants";
208
+ if (name.startsWith("permdock_sync_custom_role")) return "customRoles.from.sync: each write to the legacy table rebuilds the role's grants";
209
+ if (name.startsWith("permdock_holders") || name.startsWith("permdock_transfer_only")) return "ownershipTriggers; set rls.ownershipTriggers.<scope>: false to skip";
210
+ if (name === "permdock_assignment") return "assignments: a role assignment is checked against the caller's own roles";
211
+ if (name.startsWith("permdock_closure")) return "the relationship closure table follows its source table";
212
+ return "generated by rls generate";
213
+ }
214
+ /** The triggers `sql` creates, in order, each with the option behind it. */
215
+ function generatedTriggers(sql) {
216
+ const found = [];
217
+ for (const match of sql.matchAll(CREATE)) {
218
+ const name = unquote(match.slice(1, 2).join(""));
219
+ const table = unquote(match.slice(2, 3).join(""));
220
+ found.push({
221
+ name,
222
+ table,
223
+ reason: reasonOf(name)
224
+ });
225
+ }
226
+ return found;
281
227
  }
282
- /** The checks that need no policy: `--fields` and `--revoke-columns`. */
283
- function generateOptions(input) {
284
- const rls = input.config.rls;
285
- const fieldsMode = input.fields ?? rls?.fields;
286
- if (fieldsMode !== void 0 && fieldsMode !== "views") return { refusal: `rls generate --fields must be views (got ${fieldsMode})` };
287
- const revokeColumns = input.revokeColumns === true || rls?.revokeColumns === true;
288
- if (revokeColumns && fieldsMode === void 0) return { refusal: "rls generate --revoke-columns needs --fields views" };
228
+ /** One generate note per trigger. */
229
+ function triggerNotes(sql) {
230
+ return generatedTriggers(sql).map((trigger) => `generated trigger ${trigger.name} on ${trigger.table} (${trigger.reason})`);
231
+ }
232
+ //#endregion
233
+ //#region src/cli/rls/explain.ts
234
+ function holds(table) {
235
+ if (table.tenant === void 0) return ["global"];
236
+ return [
237
+ "tenant",
238
+ ...table.shared === void 0 ? [] : ["shared"],
239
+ "global"
240
+ ];
241
+ }
242
+ function explainRls(input) {
243
+ const { config, ctx, generated } = input;
244
+ const rls = config.rls;
245
+ const bindings = tableBindings(generated.compiled.rowColumns.map((item) => item.resource), tableNames(rls?.tables), generated.helpersOnly);
246
+ const sources = ctx.memberSources ?? ctx.sources ?? [];
247
+ const roles = rls?.customRoleWrites?.roles;
248
+ const wanted = rls?.rowHelpers;
249
+ const resources = wanted === void 0 ? [] : wanted === true ? [...new Set(generated.compiled.branches.flatMap((branch) => branch.resource === void 0 ? [] : [branch.resource]))].toSorted() : [...new Set(wanted)];
289
250
  return {
290
- fieldsMode,
291
- revokeColumns
251
+ schema: input.schema,
252
+ authorize: input.authorize,
253
+ scopes: ctx.scopes.map((scope) => scope.within === void 0 ? {
254
+ name: scope.name,
255
+ key: scope.key ?? scope.name
256
+ } : {
257
+ name: scope.name,
258
+ key: scope.key ?? scope.name,
259
+ within: scope.within
260
+ }),
261
+ bindings: bindings.map((binding) => {
262
+ const via = ctx.vias?.[binding.resource];
263
+ return via === void 0 ? binding : Object.assign({}, binding, { via: `${via.helper} over ${via.table}.${via.column} (${binding.table}.${via.key})` });
264
+ }),
265
+ membershipSources: sources.map((source) => ({
266
+ table: source.sql.table,
267
+ select: source.sql.select("$1")
268
+ })),
269
+ ...roles === void 0 ? {} : { customRoles: {
270
+ table: roles.table,
271
+ key: roles.key,
272
+ ...roles.tenant === void 0 ? {} : { tenant: roles.tenant },
273
+ ...roles.shared === void 0 ? {} : { shared: roles.shared },
274
+ ...roles.skip === void 0 ? {} : { skip: roles.skip },
275
+ holds: holds(roles)
276
+ } },
277
+ rowHelpers: resources.map((resource) => ({
278
+ resource,
279
+ permissions: [...new Set(generated.compiled.branches.flatMap((branch) => branch.resource === resource && branch.coverage !== true ? [branch.permissionKey] : []))].toSorted()
280
+ })),
281
+ triggers: generatedTriggers(input.text)
292
282
  };
293
283
  }
294
- /** Whether `--split` names `part`, which then leaves the helpers or the warnings. */
295
- function splitsPart(raw, part) {
296
- const split = parseSplit(raw);
297
- return typeof split !== "string" && split?.includes(part) === true;
284
+ /** The text report of `explain`. */
285
+ function describeExplain(explain) {
286
+ const lines = [
287
+ `schema ${explain.schema}, authorize ${explain.authorize}`,
288
+ `scopes: ${explain.scopes.map((scope) => `${scope.name} (${scope.key}${scope.within === void 0 ? "" : `, within ${scope.within}`})`).join(", ")}`,
289
+ "",
290
+ "table bindings:",
291
+ ...explain.bindings.map((binding) => ` ${binding.resource} -> ${binding.table}${binding.inferred ? " (named after the resource; set rls.tables to change)" : ""}${binding.via === void 0 ? "" : ` via ${binding.via}`}${binding.exists === false ? " MISSING" : ""}`)
292
+ ];
293
+ if (explain.membershipSources.length > 0) {
294
+ lines.push("", "membership sources:");
295
+ for (const source of explain.membershipSources) lines.push(` ${source.table}`, ` ${source.select.replaceAll("\n", "\n ")}`);
296
+ }
297
+ if (explain.customRoles !== void 0) {
298
+ const roles = explain.customRoles;
299
+ const columns = [
300
+ `key ${roles.key}`,
301
+ ...roles.tenant === void 0 ? [] : [`tenant ${roles.tenant}`],
302
+ ...roles.shared === void 0 ? [] : [`shared ${roles.shared}`],
303
+ ...roles.skip === void 0 ? [] : [`skip ${roles.skip}`]
304
+ ];
305
+ lines.push("", `custom roles: ${roles.table} (${columns.join(", ")}); holds ${roles.holds.join(", ")} roles`);
306
+ }
307
+ if (explain.rowHelpers.length > 0) {
308
+ lines.push("", "row helpers:");
309
+ for (const helper of explain.rowHelpers) lines.push(` ${helper.resource}: ${helper.permissions.join(", ")}`);
310
+ }
311
+ if (explain.triggers.length > 0) {
312
+ lines.push("", "generated triggers:");
313
+ for (const trigger of explain.triggers) lines.push(` ${trigger.name} on ${trigger.table} (${trigger.reason})`);
314
+ }
315
+ return lines.join("\n");
298
316
  }
299
317
  //#endregion
300
318
  //#region src/cli/rls/generate-output.ts
@@ -312,8 +330,8 @@ function moveUndiffed(sql, label, part) {
312
330
  }
313
331
  /** Why `--split` cannot be written for `mode`, or `undefined`. */
314
332
  function splitRefusal(mode, plan) {
315
- const seedsWithoutOut = plan.split.includes("seeds") && plan.seedsOut === void 0;
316
- const helpersWithoutSeeds = plan.split.includes("helpers") && !plan.split.includes("seeds");
333
+ const seedsWithoutOut = plan.split.includes("seeds") && plan.seedsOut === void 0 && !plan.noSeeds && !plan.seedView;
334
+ const helpersWithoutSeeds = plan.split.includes("helpers") && !plan.noSeeds && !plan.split.includes("seeds") && !plan.seedFunction && !plan.seedView;
317
335
  switch (mode) {
318
336
  case "pg-delta":
319
337
  if (plan.explicitOut && !plan.placeholder) return `rls generate --split needs {part} in --out, or no --out for pg-delta's per-schema layout (got ${plan.outRel})`;
@@ -342,6 +360,7 @@ function outputFiles(plan) {
342
360
  if (split === void 0) {
343
361
  if (input.grantsOut !== void 0) return "rls generate --grants-out needs --split with the helpers or hook part";
344
362
  if (input.seedsOut !== void 0) return "rls generate --seeds-out needs --split with the seeds part";
363
+ if (input.noSeeds === true) return "rls generate --no-seeds needs --split: without it the seeds are part of the one file";
345
364
  return [{
346
365
  part: "rls",
347
366
  rel: plan.outRel,
@@ -358,11 +377,15 @@ function outputFiles(plan) {
358
377
  explicitOut: input.out !== void 0,
359
378
  outRel: plan.outRel,
360
379
  grantsOut: input.grantsOut,
361
- seedsOut: input.seedsOut
380
+ seedsOut: input.seedsOut,
381
+ noSeeds: input.noSeeds === true,
382
+ seedFunction: input.config.rls?.seeds === "function",
383
+ seedView: input.config.rls?.seeds === "view"
362
384
  });
363
385
  if (refused !== void 0) return refused;
364
386
  const at = (part, schema = plan.schema) => pgDelta === void 0 || placeholder ? partPath(plan.outRel, part) : pgDeltaPath(pgDelta.schemaDir, part, schema);
365
387
  if (input.grantsOut !== void 0 && !split.includes("hook") && !split.includes("helpers")) return "rls generate --grants-out needs the helpers or hook part in --split";
388
+ if (input.noSeeds === true && input.seedsOut !== void 0) return "rls generate --no-seeds and --seeds-out exclude each other: drop one";
366
389
  if (input.seedsOut !== void 0 && !split.includes("seeds")) return "rls generate --seeds-out needs the seeds part in --split";
367
390
  if (split.includes("hook") && input.config.supabase?.hook === void 0) return "rls generate --split hook needs supabase.hook in permdock.config.ts";
368
391
  const sql = plan.sql();
@@ -393,6 +416,10 @@ function outputFiles(plan) {
393
416
  break;
394
417
  }
395
418
  case "seeds": {
419
+ if (input.noSeeds === true) {
420
+ if (relocated !== void 0 && split.includes("policies")) return "rls generate --no-seeds cannot drop the seeds part under pg-delta with rls.realtime or rls.storage: their policies live in it";
421
+ break;
422
+ }
396
423
  const out = input.seedsOut;
397
424
  if (out !== void 0 && isSeedsDirectory(input.cwd, out)) {
398
425
  const planned = seedsMigration(input.cwd, out, SEEDS_MARKER, seeds, input.io.now?.() ?? dateAt());
@@ -438,6 +465,12 @@ function outputFiles(plan) {
438
465
  }
439
466
  default: return part;
440
467
  }
468
+ if (pgDelta !== void 0 && !placeholder) {
469
+ for (const [index, file] of files.entries()) if (file.part !== "seeds" || input.seedsOut === void 0) files[index] = {
470
+ ...file,
471
+ text: declarativeSql(file.text)
472
+ };
473
+ }
441
474
  if (input.grantsOut !== void 0) files.push({
442
475
  part: "grants",
443
476
  rel: input.grantsOut,
@@ -447,48 +480,6 @@ function outputFiles(plan) {
447
480
  }
448
481
  //#endregion
449
482
  //#region src/cli/rls/backfill.ts
450
- const PREFIX$1 = "PermDock CLI";
451
- /** A column of the roles-table row `r`. */
452
- function col(name) {
453
- return `r.${quoteIdent$1(name)}`;
454
- }
455
- function schemaOf(table) {
456
- return table.includes(".") ? beforeFirst(table, ".") : "public";
457
- }
458
- /** The roles-table column a reference joins on, after checking it points at `roles`. */
459
- function roleRef(role, table, roles, label) {
460
- const pairs = Object.entries(role.on);
461
- const [pair] = pairs;
462
- if (pair === void 0 || pairs.length !== 1) throw configError(`${PREFIX$1}: ${label}.on must map exactly one column of ${table} to ${role.through}, for example { role_id: 'id' }`);
463
- const through = role.through.includes(".") ? role.through : `${schemaOf(table)}.${role.through}`;
464
- if (qualifySqlTable(through) !== qualifySqlTable(roles.table)) throw configError(`${PREFIX$1}: ${label}.through must be rls.customRoleWrites.roles.table (${roles.table}), got ${role.through}`);
465
- if (role.column !== roles.key) throw configError(`${PREFIX$1}: ${label}.column must be rls.customRoleWrites.roles.key (${roles.key}), got ${role.column}`);
466
- const [ref, id] = pair;
467
- return {
468
- ref,
469
- id
470
- };
471
- }
472
- /**
473
- * A legacy key as SQL the way `mapKey` maps it: `rls.migrate.keys`, then
474
- * `renamed`, then the longest `rls.migrate.prefixes` match, and a former key
475
- * read as its current key.
476
- */
477
- function legacyKeySql(expr, migrate, renamed) {
478
- const exact = new Map(Object.entries(renamed));
479
- for (const [from, to] of Object.entries(migrate?.keys ?? {})) exact.set(from, to);
480
- const prefixes = Object.entries(migrate?.prefixes ?? {}).toSorted(([a], [b]) => b.length - a.length || byCodeUnit(a, b));
481
- 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)`;
482
- const mapped = exact.size === 0 ? prefixed : `(case ${expr} ${[...exact.entries()].toSorted(byKey).map(([from, to]) => `when ${quoteLiteral(from)} then ${quoteLiteral(to)}`).join(" ")} else ${prefixed} end)`;
483
- const renames = Object.entries(renamed).toSorted(byKey);
484
- return currentKeyExpr(renames, mapped);
485
- }
486
- function sourceSql(source, table, label) {
487
- return roleColumn(source, table, "x", {
488
- label,
489
- prefix: PREFIX$1
490
- }).lookup;
491
- }
492
483
  /**
493
484
  * The `--backfill-out` migration: every custom role in the application's
494
485
  * roles table that has no rows in `custom_role_permissions` or
@@ -498,73 +489,10 @@ function sourceSql(source, table, label) {
498
489
  * nothing, so a role edited after the copy keeps its edits.
499
490
  */
500
491
  function backfillSql(ctx, from, migrate) {
501
- const custom = ctx.customRoles;
502
- const root = ctx.scopes[0]?.name;
503
- if (custom === void 0 || root === void 0) throw configError(`${PREFIX$1}: rls.customRoles.from needs custom roles on`);
504
- if (ctx.authorize !== "database") throw configError(`${PREFIX$1}: rls.customRoles.from needs database mode; in jwt mode custom roles ride the token, so build the grants claim with customRoleClaim`);
505
- const roles = custom.table;
506
- if (roles === void 0) throw configError(`${PREFIX$1}: rls.customRoles.from needs rls.customRoleWrites.roles, the roles table each row belongs to`);
507
- const leveled = custom.levels === true;
508
- if (from.permissions.level !== void 0 && !leveled) throw configError(`${PREFIX$1}: rls.customRoles.from.permissions.level is set, but no resource declares levels`);
509
- const tenantType = tenantTypeOf(ctx);
510
- const rolesTable = quoteQualified(roles.table);
511
- const tenant = roles.tenant === void 0 ? `null::${tenantType}` : `${col(roles.tenant)}::${tenantType}`;
512
- const scope = rolesTableScope(roles, tenant, root, col);
513
- const scopeId = roles.id === void 0 ? "null::text" : `${col(roles.id)}::text`;
514
- const skipped = roles.skip === void 0 ? "false" : `coalesce(${col(roles.skip)}, false)`;
515
- const renamed = custom.renamed ?? {};
516
- const renames = Object.entries(renamed).toSorted(byKey);
517
- const current = (expr) => currentKeyExpr(renames, expr);
518
- const known = sqlTextArray([...custom.permissions ?? [], ...renames.map(([former]) => former)]);
519
- const declared = sqlTextArray(custom.declared);
520
- const scopes = sqlTextArray(ctx.scopes.map((item) => item.name));
521
- const ceiling = qualified(ctx, CUSTOM_ROLES.ceiling);
522
- const rp = qualified(ctx, "role_permissions");
523
- const perms = qualified(ctx, CUSTOM_ROLES.permissions);
524
- const includes = qualified(ctx, CUSTOM_ROLES.includes);
525
- const replace = qualified(ctx, CUSTOM_ROLES.trustedReplace);
526
- const p = from.permissions;
527
- const pTable = qualifySqlTable(p.table);
528
- const pRef = roleRef(p.role, p.table, roles, "rls.customRoles.from.permissions.role");
529
- const legacy = sourceSql(p.permission, pTable, "rls.customRoles.from.permissions.permission");
530
- const effect = p.effect === void 0 ? "'allow'" : `coalesce(lower(x.${quoteIdent$1(p.effect)}::text), 'allow')`;
531
- const level = p.level === void 0 ? "null::text" : `nullif(x.${quoteIdent$1(p.level)}::text, '')`;
532
- const inCeiling = (key) => `exists (select 1 from ${ceiling} c where c.scope = v.scope and c.effect = 'allow' and ${current("c.permission")} = ${key})`;
533
- const levelCheck = leveled ? `
534
- when m.effect = 'allow' and m.level is not null and not exists (
535
- select 1 from ${ceiling} c
536
- where c.scope = v.scope and c.effect = 'allow' and ${current("c.permission")} = m.key
537
- and split_part(c.grant_key, '@', 2) = m.level
538
- ) then 'level ' || m.level || ' is not declared'` : "";
539
- const allowEntry = leveled ? "j.key || coalesce('@' || j.level, '')" : "j.key";
540
- const i = from.includes;
541
- const iTable = i === void 0 ? void 0 : qualifySqlTable(i.table);
542
- const iRef = i === void 0 || iTable === void 0 ? void 0 : roleRef(i.role, i.table, roles, "rls.customRoles.from.includes.role");
543
- const included = i === void 0 || iTable === void 0 ? void 0 : sourceSql(i.include, iTable, "rls.customRoles.from.includes.include");
544
- const refs = [`${col(pRef.id)} as permissions_ref`, ...iRef === void 0 ? [] : [`${col(iRef.id)} as includes_ref`]].join(", ");
545
- const includeBlock = i === void 0 || iTable === void 0 || iRef === void 0 || included === void 0 ? " v_include := null;" : ` with raw as (
546
- select ${included} as role
547
- from ${quoteTable(iTable)} x
548
- where x.${quoteIdent$1(iRef.ref)} = v.includes_ref
549
- ),
550
- judged as (
551
- select n.role, case
552
- when n.role is null or not (n.role = any(${declared})) then 'not a declared role'
553
- when exists (
554
- select 1 from ${rp} rp
555
- where rp.role = n.role and rp.effect = 'allow'
556
- and not ${inCeiling(current("rp.permission"))}
557
- ) then 'allows permissions outside the ceiling of ' || v.scope
558
- end as dropped
559
- from raw n
560
- )
561
- select array_agg(distinct j.role) filter (where j.dropped is null),
562
- array_agg(v.role || ': includes ' || coalesce(j.role, 'null') || ', ' || j.dropped) filter (where j.dropped is not null)
563
- into v_include, v_lost
564
- from judged j;
565
- v_dropped := v_dropped || coalesce(v_lost, '{}'::text[]);`;
492
+ const plan = rebuildPlan(ctx, from, migrate);
493
+ const { rolesTable, perms, includes, replace, scopes } = plan;
566
494
  return `${BACKFILL_MARKER} schema=${ctx.schema ?? "permdock"}
567
- -- custom roles copied from ${qualifySqlTable(roles.table)} (rls.customRoles.from) through ${replace}
495
+ -- custom roles copied from ${qualifySqlTable(plan.roles.table)} (rls.customRoles.from) through ${replace}
568
496
  -- a role that already has rows is left as it is; entries PermDock cannot hold are dropped and listed in one warning
569
497
  do $permdock$
570
498
  declare
@@ -578,9 +506,9 @@ declare
578
506
  v_present integer := 0;
579
507
  begin
580
508
  for v in
581
- select ${tenant} as tenant_id, ${scope} as scope, ${scopeId} as scope_id, ${col(roles.key)}::text as role, ${refs}
509
+ ${plan.identity}
582
510
  from ${rolesTable} r
583
- where not ${skipped} and not (${col(roles.key)}::text = any(${declared}))
511
+ where ${plan.eligible}
584
512
  order by 4, 2, 1, 3
585
513
  loop
586
514
  if v.role is null or not (v.scope = 'global' or v.scope = any(${scopes})) then
@@ -597,30 +525,7 @@ begin
597
525
  v_present := v_present + 1;
598
526
  continue;
599
527
  end if;
600
- with raw as (
601
- select ${legacy} as legacy, ${effect} as effect, ${level} as level
602
- from ${quoteTable(pTable)} x
603
- where x.${quoteIdent$1(pRef.ref)} = v.permissions_ref
604
- ),
605
- mapped as (
606
- select n.legacy, ${legacyKeySql("n.legacy", migrate, renamed)} as key, n.effect, n.level
607
- from raw n
608
- ),
609
- judged as (
610
- select m.*, case
611
- when m.effect not in ('allow', 'deny') then 'effect ' || m.effect || ' is not allow or deny'
612
- when m.key is null or not (m.key = any(${known})) then 'not a declared permission'
613
- when m.effect = 'allow' and not ${inCeiling("m.key")} then 'outside the ceiling of ' || v.scope${levelCheck}
614
- end as dropped
615
- from mapped m
616
- )
617
- select array_agg(distinct ${allowEntry}) filter (where j.dropped is null and j.effect = 'allow'),
618
- array_agg(distinct j.key) filter (where j.dropped is null and j.effect = 'deny'),
619
- array_agg(v.role || ': ' || coalesce(j.legacy, 'null') || ', ' || j.dropped) filter (where j.dropped is not null)
620
- into v_allow, v_deny, v_lost
621
- from judged j;
622
- v_dropped := v_dropped || coalesce(v_lost, '{}'::text[]);
623
- ${includeBlock}
528
+ ${plan.collect}
624
529
  if v_allow is null and v_deny is null and v_include is null then
625
530
  continue;
626
531
  end if;
@@ -629,7 +534,7 @@ ${includeBlock}
629
534
  end loop;
630
535
  raise notice 'permdock backfill: copied % custom role(s), % already present', v_copied, v_present;
631
536
  if cardinality(v_dropped) > 0 then
632
- raise warning 'permdock backfill: dropped % entr(ies) PermDock cannot hold: %', cardinality(v_dropped), array_to_string(v_dropped[1:50], '; ');
537
+ raise warning 'permdock backfill: dropped % entr(ies) PermDock cannot hold: %', cardinality(v_dropped), ${DROPPED_LIST};
633
538
  end if;
634
539
  end
635
540
  $permdock$;
@@ -639,7 +544,7 @@ $permdock$;
639
544
  //#region src/cli/rls/break-glass.ts
640
545
  /** The audit table every generated break-glass read writes to. Part of the SQL contract. */
641
546
  const BREAK_GLASS_AUDIT = "permdock_break_glass_audit";
642
- const IDENT$1 = /^[a-z][a-z0-9_]*$/u;
547
+ const IDENT$2 = /^[a-z][a-z0-9_]*$/u;
643
548
  /**
644
549
  * The resources a `breakGlass` grant targets, with the table each reads. A
645
550
  * break-glass grant is non-portable, so `compileGrants` never turns it into a
@@ -688,7 +593,7 @@ function rowsFilter(ctx, entry) {
688
593
  const asked = `p_permission = ${quoteLiteral(grant.permission)}`;
689
594
  if (grant.scope === "anyone") {
690
595
  if (grant.root === void 0) return [`(${asked})`];
691
- return grant.column === void 0 ? [] : [`(${asked} and ${quoteIdent$1(grant.column)} in (select ${qualified(ctx, memberIdsHelper(grant.root))}()))`];
596
+ return grant.column === void 0 ? [] : [`(${asked} and ${quoteIdent$1(grant.column)} in (select ${qualified$1(ctx, memberIdsHelper(grant.root))}()))`];
692
597
  }
693
598
  if (grant.scope !== "global" && grant.column === void 0) return [];
694
599
  return [`(${asked} and ${accessSql(ctx, grant.scope, breakGlassKey(grant.permission), grant.column)})`];
@@ -696,11 +601,11 @@ function rowsFilter(ctx, entry) {
696
601
  return arms.length === 0 ? "false" : [...new Set(arms)].join("\n or ");
697
602
  }
698
603
  function functionName$1(resource) {
699
- if (!IDENT$1.test(resource)) throw configError(`PermDock CLI: unsafe break-glass resource '${resource}'`);
604
+ if (!IDENT$2.test(resource)) throw configError(`PermDock CLI: unsafe break-glass resource '${resource}'`);
700
605
  return `permdock_break_glass_${resource}`;
701
606
  }
702
607
  function auditSql$1(ctx) {
703
- const audit = qualified(ctx, quoteIdent$1(BREAK_GLASS_AUDIT));
608
+ const audit = qualified$1(ctx, quoteIdent$1(BREAK_GLASS_AUDIT));
704
609
  return `-- break-glass audit: one row per session that read restricted data
705
610
  -- RLS never compiles a break-glass override, so restricted rows stay denied to a direct read
706
611
  create table if not exists ${audit} (
@@ -715,8 +620,8 @@ alter table ${audit} enable row level security;
715
620
  revoke all on table ${audit} from anon, authenticated, public;`;
716
621
  }
717
622
  function readFunctionSql(ctx, entry) {
718
- const fn = qualified(ctx, functionName$1(entry.resource));
719
- const audit = qualified(ctx, quoteIdent$1(BREAK_GLASS_AUDIT));
623
+ const fn = qualified$1(ctx, functionName$1(entry.resource));
624
+ const audit = qualified$1(ctx, quoteIdent$1(BREAK_GLASS_AUDIT));
720
625
  const table = quoteQualified(entry.table);
721
626
  const session = subjectClaimJsonSql(ctx, "break_glass");
722
627
  const subject = subjectIdSql(ctx);
@@ -770,7 +675,7 @@ function breakGlassSql(ctx, entries) {
770
675
  //#endregion
771
676
  //#region src/cli/rls/emit.ts
772
677
  const HEADER = "-- generated by permdock rls generate\n-- fail-closed: never target a bypass role\n";
773
- const TRUSTED_READER_GRANT = /^grant execute on function [^\n]+\.permdock_trusted_role_permissions\([^)\n]*\) to [^\n;]+;$/gmu;
678
+ const TRUSTED_READER_GRANT = /^grant usage on schema "[^"\n]+" to [^\n;]+;\ngrant execute on function [^\n]+\.permdock_trusted_role_permissions(?:_by_id)?\([^)\n]*\) to [^\n;]+;$/gmu;
774
679
  function assertNoServiceRole(text) {
775
680
  if (/\bservice_role\b/iu.test(text.replace(TRUSTED_READER_GRANT, ""))) throw new Error("PermDock CLI: generated RLS must never emit service_role");
776
681
  }
@@ -982,7 +887,7 @@ function declaredScope(facts, scope, where) {
982
887
  * relationship or a `where` on the same permission never widens it.
983
888
  */
984
889
  function permitted(ctx, value, scope, key) {
985
- return `${value} in (select x::text from ${qualified(ctx, permittedByPermissionHelper(scope))}(${quoteLiteral(key)}) x)`;
890
+ return `${value} in (select x::text from ${qualified$1(ctx, permittedByPermissionHelper(scope))}(${quoteLiteral(key)}) x)`;
986
891
  }
987
892
  function escapeRegex(text) {
988
893
  return text.replaceAll(/[.*+?^${}()|[\]\\]/gu, String.raw`\$&`);
@@ -1099,165 +1004,6 @@ function realtimeStoragePolicies(ctx, facts, config, warnings = []) {
1099
1004
  return policies;
1100
1005
  }
1101
1006
  //#endregion
1102
- //#region src/cli/rls/rows.ts
1103
- /** `permitted_<resource>_rows(p_permission)`: the resource's row ids the subject may act on with one permission. */
1104
- function rowsHelper(resource) {
1105
- const name = graphSqlName(resource);
1106
- const helper = `permitted_${name ?? resource}_rows`;
1107
- if (name === void 0 || new TextEncoder().encode(`${helper}_for`).length > 63) throw configError(`PermDock CLI: rls.rowHelpers cannot name a helper for resource '${resource}'; use a SQL name of at most 45 bytes`);
1108
- return helper;
1109
- }
1110
- /** The branches a row check of a permission reads: its allows and denies on rows that exist, for signed-in callers. */
1111
- function rowBranchesOf(branches, resource) {
1112
- const out = /* @__PURE__ */ new Map();
1113
- for (const branch of branches) {
1114
- if (branch.resource !== resource || branch.command === "insert" || branch.coverage === true || !branch.roles.includes("authenticated")) continue;
1115
- out.set(branch.permissionKey, [...out.get(branch.permissionKey) ?? [], branch]);
1116
- }
1117
- return out;
1118
- }
1119
- function usingOf(branches) {
1120
- const parts = branches.map((branch) => branchClauses(branch).using ?? "true");
1121
- return parts.length === 0 ? void 0 : orSql(parts);
1122
- }
1123
- function permissionCase(key, branches) {
1124
- const allow = usingOf(branches.filter((branch) => branch.effect === "allow"));
1125
- if (allow === void 0) return;
1126
- const deny = usingOf(branches.filter((branch) => branch.effect === "deny"));
1127
- const granted = `coalesce((${allow}), false)`;
1128
- return ` when ${quoteLiteral(key)} then ${deny === void 0 ? granted : `${granted} and (${deny}) is not true`}`;
1129
- }
1130
- function forUserSql(ctx, fn, helper) {
1131
- const user = ctx.dialect === "supabase" ? "uuid" : "text";
1132
- const signature = `${user}, text, jsonb`;
1133
- const head = `create or replace function ${fn}(p_user ${user}, p_permission text, p_claims jsonb default '{}')
1134
- returns setof text
1135
- language plpgsql
1136
- volatile
1137
- security definer
1138
- set search_path = ''
1139
- as $$`;
1140
- const tail = `revoke execute on function ${fn}(${signature}) from public, anon, authenticated;`;
1141
- if (ctx.dialect === "supabase") return `${head}
1142
- declare
1143
- v_claims text := pg_catalog.current_setting('request.jwt.claims', true);
1144
- begin
1145
- perform pg_catalog.set_config(
1146
- 'request.jwt.claims',
1147
- (coalesce(p_claims, '{}'::jsonb) || pg_catalog.jsonb_build_object('sub', p_user::text, 'role', 'authenticated'))::text,
1148
- true
1149
- );
1150
- return query select r.id from ${helper}(p_permission) r(id);
1151
- perform pg_catalog.set_config('request.jwt.claims', coalesce(v_claims, ''), true);
1152
- end
1153
- $$;
1154
- ${tail}`;
1155
- const prefix = ctx.gucPrefix;
1156
- return `${head}
1157
- declare
1158
- v_saved jsonb := '{}'::jsonb;
1159
- v_key text;
1160
- v_value text;
1161
- begin
1162
- for v_key, v_value in
1163
- select 'user_id', p_user
1164
- union all
1165
- select c.key, c.value from pg_catalog.jsonb_each_text(coalesce(p_claims, '{}'::jsonb)) c where c.key <> 'user_id'
1166
- loop
1167
- v_saved := v_saved || pg_catalog.jsonb_build_object(v_key, coalesce(pg_catalog.current_setting(${quoteLiteral(`${prefix}.`)} || v_key, true), ''));
1168
- perform pg_catalog.set_config(${quoteLiteral(`${prefix}.`)} || v_key, v_value, true);
1169
- end loop;
1170
- return query select r.id from ${helper}(p_permission) r(id);
1171
- for v_key, v_value in select s.key, s.value from pg_catalog.jsonb_each_text(v_saved) s loop
1172
- perform pg_catalog.set_config(${quoteLiteral(`${prefix}.`)} || v_key, v_value, true);
1173
- end loop;
1174
- end
1175
- $$;
1176
- ${tail}`;
1177
- }
1178
- function inheritTargets(policy) {
1179
- const out = /* @__PURE__ */ new Set();
1180
- for (const grant of policy.grants) for (const item of flattenGrantee(grant.to)) if (item.kind === "inherit") out.add(item.resource);
1181
- return [...out].toSorted();
1182
- }
1183
- function stubSql(ctx, resource) {
1184
- return `create or replace function ${qualified(ctx, rowsHelper(resource))}(p_permission text)
1185
- returns setof text
1186
- language sql
1187
- stable
1188
- security definer
1189
- set search_path = ''
1190
- as $$
1191
- select null::text where false
1192
- $$;`;
1193
- }
1194
- function rowSql(ctx, rows, table, where) {
1195
- const fn = qualified(ctx, rows.replace(/_rows$/u, "_row"));
1196
- return `create or replace function ${fn}(p_row ${table}, p_permission text)
1197
- returns boolean
1198
- language sql
1199
- stable
1200
- security definer
1201
- set search_path = ''
1202
- as $$
1203
- select exists (select 1 from (select (p_row).*) r where ${where})
1204
- $$;
1205
- revoke execute on function ${fn}(${table}, text) from public, anon;
1206
- grant execute on function ${fn}(${table}, text) to authenticated;`;
1207
- }
1208
- function resourceSql(ctx, node, branches, tables) {
1209
- const name = rowsHelper(node.name);
1210
- const fn = qualified(ctx, name);
1211
- const table = quoteQualified(tableFor(node.name, tables));
1212
- const cases = [...branches.keys()].toSorted().flatMap((key) => {
1213
- const sql = permissionCase(key, branches.get(key) ?? []);
1214
- return sql === void 0 ? [] : [sql];
1215
- });
1216
- const where = cases.length === 0 ? "false" : `case p_permission\n${cases.join("\n")}\n else false\n end`;
1217
- const rows = `-- ${node.name}: ids of the rows the caller may act on with p_permission, as the table's policies decide it
1218
- create or replace function ${fn}(p_permission text)
1219
- returns setof text
1220
- language sql
1221
- stable
1222
- security definer
1223
- set search_path = ''
1224
- as $$
1225
- select ${quoteIdent$1(node.id)}::text from ${table}
1226
- where ${where}
1227
- $$;
1228
- revoke execute on function ${fn}(text) from public, anon;
1229
- grant execute on function ${fn}(text) to authenticated;
1230
- ${rowSql(ctx, name, table, where)}`;
1231
- if (ctx.dialect === "neon") return rows;
1232
- return `${rows}
1233
- -- the same rows for a user the caller names, with claims it supplies; no client role may execute it
1234
- ${forUserSql(ctx, qualified(ctx, `${name}_for`), fn)}`;
1235
- }
1236
- /**
1237
- * `permitted_<resource>_rows(p_permission)` for each resource `select`
1238
- * names (or every resource a grant reaches with `true`): the ids of its rows
1239
- * the caller may read, update or delete with that permission, from the same
1240
- * compiled allows and denies the table policies use, so closure walks, link
1241
- * hops, the restricted stop, `includes`, groups and `requires` all apply. A
1242
- * `security definer` helper, so hand-written policies and functions call it
1243
- * without restating the graph. Outside the `neon` dialect a
1244
- * `permitted_<resource>_rows_for(p_user, p_permission, p_claims)` form runs it
1245
- * as the named user, with the claims the caller supplies, for trusted SQL.
1246
- */
1247
- function rowHelpersSql(ctx, policy, branches, select, tables, warnings) {
1248
- const names = select === true ? [...new Set(branches.flatMap((branch) => branch.resource === void 0 ? [] : [branch.resource]))].toSorted() : [...new Set(select)];
1249
- const called = inheritTargets(policy);
1250
- for (const name of called) if (!names.includes(name)) names.push(name);
1251
- const chunks = called.map((name) => stubSql(ctx, name));
1252
- for (const name of names) {
1253
- const node = policy.resources.get(name);
1254
- if (node === void 0) throw configError(`PermDock CLI: rls.rowHelpers names '${name}', which the policy does not declare`);
1255
- chunks.push(resourceSql(ctx, node, rowBranchesOf(branches, name), tables));
1256
- }
1257
- if (chunks.length > 0 && ctx.dialect === "neon") warnings.push("rls.rowHelpers: the neon dialect reads the subject from auth.session(), which SQL cannot set, so no permitted_<resource>_rows_for is written");
1258
- return chunks.length === 0 ? "" : `${chunks.join("\n\n")}\n`;
1259
- }
1260
- //#endregion
1261
1007
  //#region src/cli/rls/generate-policies.ts
1262
1008
  const SERVICE_TABLES_HEADER = "-- Supabase Realtime and Storage: rls.realtime and rls.storage. RLS on both tables is Supabase's.";
1263
1009
  /**
@@ -1293,12 +1039,13 @@ function generatePolicies(input, policy, options, built, warnings) {
1293
1039
  const { ctx } = built;
1294
1040
  const { fieldsMode, revokeColumns } = options;
1295
1041
  databaseModeWarnings(policy, built, warnings);
1296
- const compiled = compileGrants(policy, ctx, rls?.tables, warnings, input.skipClosures, rls?.rowHelpers !== void 0 || inheritTargets(policy).length > 0);
1042
+ const compiled = compileGrants(policy, ctx, tableNames(rls?.tables), warnings, input.skipClosures, rls?.rowHelpers !== void 0 || inheritTargets(policy).length > 0);
1297
1043
  const helpersOnly = input.helpersOnly === true || rls?.helpersOnly === true;
1298
1044
  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" };
1299
1045
  const views = fieldsMode === void 0 ? [] : fieldViews(policy, ctx, compiled.branches, {
1300
1046
  revokeColumns,
1301
- helpersOnly
1047
+ helpersOnly,
1048
+ mask: fieldsMode === "mask"
1302
1049
  }, warnings);
1303
1050
  const force = input.force === true || rls?.force === true;
1304
1051
  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");
@@ -1308,7 +1055,7 @@ function generatePolicies(input, policy, options, built, warnings) {
1308
1055
  ...policyName === void 0 ? {} : { name: policyName }
1309
1056
  });
1310
1057
  const actorKinds = readOnlyActorKinds(rls?.readOnlyActors);
1311
- if (actorKinds !== void 0 && helpersOnly) warnings.push(`rls.readOnlyActors with --helpers-only writes no policies: add \`and (select ${qualified(ctx, ACTOR_MAY_WRITE)}())\` to each hand-written insert, update and delete policy, or a restrictive policy that calls it`);
1058
+ if (actorKinds !== void 0 && helpersOnly) warnings.push(`rls.readOnlyActors with --helpers-only writes no policies: add \`and (select ${qualified$1(ctx, ACTOR_MAY_WRITE)}())\` to each hand-written insert, update and delete policy, or a restrictive policy that calls it`);
1312
1059
  const withActors = (items) => actorKinds === void 0 ? items : [...items, ...readOnlyActorPolicies(items, subjectClaimJsonSql(ctx, "act"), actorKinds)];
1313
1060
  let tablePolicies;
1314
1061
  try {
@@ -1341,7 +1088,7 @@ function generatePolicies(input, policy, options, built, warnings) {
1341
1088
  }
1342
1089
  //#endregion
1343
1090
  //#region src/cli/rls/shims.ts
1344
- const NAME = /^[a-z_][a-z0-9_]*$/u;
1091
+ const NAME$1 = /^[a-z_][a-z0-9_]*$/u;
1345
1092
  /**
1346
1093
  * The key a legacy caller passes, mapped the way `rls migrate` maps it:
1347
1094
  * `keys` and `renamed` exactly, then the longest `prefixes` entry.
@@ -1424,7 +1171,7 @@ function grantKeys(keys, scope, effect) {
1424
1171
  }
1425
1172
  /** The instances of `scope` the caller holds an unconditional allow of the key in, minus those it holds a deny in. */
1426
1173
  function idsSql(ctx, keys, scope) {
1427
- const helper = qualified(ctx, permittedIdsHelper(scope));
1174
+ const helper = qualified$1(ctx, permittedIdsHelper(scope));
1428
1175
  return ` select a.id
1429
1176
  from ${grantKeys(keys, scope, "allow")}
1430
1177
  cross join lateral ${helper}(g.grant_key) a(id)
@@ -1434,7 +1181,7 @@ function idsSql(ctx, keys, scope) {
1434
1181
  cross join lateral ${helper}(g.grant_key) d(id)`;
1435
1182
  }
1436
1183
  function hasSql(ctx, keys) {
1437
- const has = qualified(ctx, HELPERS.has);
1184
+ const has = qualified$1(ctx, HELPERS.has);
1438
1185
  return `exists (select 1 from ${grantKeys(keys, "global", "allow")} where ${has}(g.grant_key))
1439
1186
  and not exists (select 1 from ${grantKeys(keys, "global", "deny")} where ${has}(g.grant_key))`;
1440
1187
  }
@@ -1452,14 +1199,14 @@ function shim(ctx, config, keys, fn, helper) {
1452
1199
  case "membership": {
1453
1200
  const scope = scopeOf(ctx, helper.scope);
1454
1201
  const type = scopeTypeOf(ctx, scope);
1455
- return shimFunction(fn, `p_id ${type}`, type, "boolean", ` select coalesce(p_id in (select ${qualified(ctx, memberIdsHelper(scope))}()), false)`);
1202
+ return shimFunction(fn, `p_id ${type}`, type, "boolean", ` select coalesce(p_id in (select ${qualified$1(ctx, memberIdsHelper(scope))}()), false)`);
1456
1203
  }
1457
1204
  case "role": {
1458
1205
  const scope = scopeOf(ctx, helper.scope);
1459
1206
  const type = scopeTypeOf(ctx, scope);
1460
1207
  const values = Object.entries(ctx.roleValues ?? {});
1461
1208
  const role = values.length === 0 ? "p_role" : `case p_role ${values.map(([from, to]) => `when ${quoteLiteral(from)} then ${quoteLiteral(to)}`).join(" ")} else p_role end`;
1462
- return shimFunction(fn, `p_id ${type}, p_role text`, `${type}, text`, "boolean", ` select coalesce(p_id in (select ${qualified(ctx, memberIdsHelper(scope))}(array[${role}])), false)`);
1209
+ return shimFunction(fn, `p_id ${type}, p_role text`, `${type}, text`, "boolean", ` select coalesce(p_id in (select ${qualified$1(ctx, memberIdsHelper(scope))}(array[${role}])), false)`);
1463
1210
  }
1464
1211
  case "global": return shimFunction(fn, "p_key text", "text", "boolean", ` select ${hasSql(ctx, keys)}`);
1465
1212
  case "scoped": {
@@ -1489,7 +1236,7 @@ function shimsSql(ctx, config, shims, renamed, grants) {
1489
1236
  };
1490
1237
  const chunks = [`-- permdock shims: legacy helper names over the permdock helpers; drop each once doctor reports no caller (PD056)`];
1491
1238
  for (const [name, helper] of Object.entries(config.helpers).toSorted(byKey)) {
1492
- if (!NAME.test(name)) throw configError(`PermDock CLI: unsafe rls.migrate helper name '${name}'`);
1239
+ if (!NAME$1.test(name)) throw configError(`PermDock CLI: unsafe rls.migrate helper name '${name}'`);
1493
1240
  chunks.push(shim(ctx, config, keys, `${quoteIdent$1(schema)}.${name}`, helper));
1494
1241
  }
1495
1242
  return `${chunks.join("\n\n")}\n`;
@@ -1508,7 +1255,7 @@ const AUDITED = [
1508
1255
  CUSTOM_ROLES.includes,
1509
1256
  "user_roles"
1510
1257
  ];
1511
- const IDENT = /^[a-z_][a-z0-9_$]*$/u;
1258
+ const IDENT$1 = /^[a-z_][a-z0-9_$]*$/u;
1512
1259
  /** The tables `sql` creates in `schema`, by name without the schema. */
1513
1260
  function generatedTables(sql, schema) {
1514
1261
  const prefix = `create table if not exists ${quoteIdent$1(schema)}.`;
@@ -1570,7 +1317,7 @@ function auditSql(audit, schema, tables) {
1570
1317
  if (listed !== void 0) for (const table of listed) knownTable(table, tables, "rls.audit.tables");
1571
1318
  const targets = listed ?? AUDITED.filter((table) => tables.includes(table));
1572
1319
  const args = Object.entries(audit.args ?? {}).map(([name, value]) => {
1573
- if (!IDENT.test(name)) throw configError(`${PREFIX}: rls.audit.args names ${JSON.stringify(name)}, which is not a lower-case parameter name`);
1320
+ if (!IDENT$1.test(name)) throw configError(`${PREFIX}: rls.audit.args names ${JSON.stringify(name)}, which is not a lower-case parameter name`);
1574
1321
  return `, ${quoteIdent$1(name)} => ${quoteLiteral(value)}`;
1575
1322
  });
1576
1323
  if (targets.length === 0) return "";
@@ -1633,14 +1380,15 @@ function sectionsSql(input, policy, options, built, generated, jsonSchema, warni
1633
1380
  } catch (cause) {
1634
1381
  return { refusal: describeError(cause) };
1635
1382
  }
1636
- const graphed = graphSql(ctx, graph, rls?.tables);
1383
+ if (typeof rls?.approvals === "object" && rls.approvals.delegations === true && ctx.authorize !== "database") warnings.push("rls.approvals.delegations with --authorize jwt: permdock_is_approver cannot read a delegator's permissions from the caller's token, so in SQL a cover never satisfies a holder() approver; approvalsHandler still does");
1384
+ const graphed = graphSql(ctx, graph, tableNames(rls?.tables));
1637
1385
  let rowHelpers = "";
1638
1386
  if (rls?.rowHelpers !== void 0 || inheritTargets(policy).length > 0) try {
1639
- rowHelpers = rowHelpersSql(ctx, policy, [...options.fieldsMode === void 0 ? compiled.branches : rowBranches(compiled.branches), ...compiled.actionBranches], rls?.rowHelpers ?? [], rls?.tables, warnings);
1387
+ rowHelpers = rowHelpersSql(ctx, policy, [...options.fieldsMode === void 0 ? compiled.branches : rowBranches(compiled.branches), ...compiled.actionBranches], rls?.rowHelpers ?? [], tableNames(rls?.tables), warnings);
1640
1388
  } catch (cause) {
1641
1389
  return { refusal: describeError(cause) };
1642
1390
  }
1643
- const breakGlass = breakGlassSql(ctx, breakGlassEntries(policy, rls?.tables));
1391
+ const breakGlass = breakGlassSql(ctx, breakGlassEntries(policy, tableNames(rls?.tables)));
1644
1392
  return [
1645
1393
  approvals,
1646
1394
  owned === "" ? void 0 : owned,
@@ -1715,6 +1463,14 @@ function preambleSql(input, policy, options, built, generated, jsonSchema, warni
1715
1463
  audit
1716
1464
  };
1717
1465
  }
1466
+ function seedsBody(ctx, schema, rows) {
1467
+ switch (ctx.seeds) {
1468
+ case "function": return `select ${quoteQualified(`${schema}.seed`)}();`;
1469
+ case "view": return "-- rls.seeds: 'view': the role_permissions rows are in the helpers part";
1470
+ case void 0: return seedSql(ctx, rows);
1471
+ default: return ctx.seeds;
1472
+ }
1473
+ }
1718
1474
  function forceWarnings({ graph }, warnings) {
1719
1475
  for (const entry of graph.values()) {
1720
1476
  const own = [...entry.relations].filter((name) => !("edge" in (entry.node.relations[name] ?? {})));
@@ -1795,18 +1551,144 @@ function generateSql(input, policy, options, built, warnings) {
1795
1551
  ...generated,
1796
1552
  ...emitted,
1797
1553
  backfill: checked.backfill,
1798
- indexes: indexTargets(ctx, tableColumns(generated.compiled.rowColumns, rls?.tables, helpersOnly, warnings)),
1554
+ indexes: indexTargets(ctx, tableColumns(generated.compiled.rowColumns, tableNames(rls?.tables), helpersOnly, warnings)),
1799
1555
  parts: (indexes) => ({
1800
1556
  policies: dialectRoles(withHelperTables(emitSql(policies, "", force, views), helperTables), ctx.dialect),
1801
1557
  declarativePolicies: dialectRoles(emitSql(policies, "", force, views), ctx.dialect),
1802
1558
  ...guardedTables === void 0 ? {} : { servicePolicies: dialectRoles(guardedTables, ctx.dialect) },
1803
1559
  helpers: dialectRoles(emitSql([], preamble, false, helpersOnly ? views : []), ctx.dialect),
1804
- seeds: `${SEEDS_MARKER} schema=${built.schema}\n${seedSql(ctx, generated.compiled.rolePermissions)}\n${seedsSplit ? audit : ""}`,
1560
+ seeds: `${SEEDS_MARKER} schema=${built.schema}\n${seedsBody(ctx, built.schema, generated.compiled.rolePermissions)}\n${seedsSplit ? audit : ""}`,
1805
1561
  indexes: `${INDEXES_MARKER}\n${indexesSql(indexes)}\n`
1806
1562
  })
1807
1563
  };
1808
1564
  }
1809
1565
  //#endregion
1566
+ //#region src/cli/rls/index-scan.ts
1567
+ /** An index PermDock generated (`permdock_<table>_<columns>_idx`), in the indexes part or in a migration derived from it: it never counts as serving its own target. */
1568
+ const GENERATED_INDEX = /\bcreate\s+index\s+(?:if\s+not\s+exists\s+)?"?permdock_\w*_idx"?[^;]*;/giu;
1569
+ const IDENT = String.raw`(?:"(?:[^"]|"")+"|[A-Za-z_][\w$]*)`;
1570
+ const NAME = String.raw`${IDENT}(?:\s*\.\s*${IDENT})?`;
1571
+ const CREATE_INDEX = new RegExp(String.raw`\bcreate\s+(?:unique\s+)?index\s+(?:concurrently\s+)?(?:if\s+not\s+exists\s+)?(?:${IDENT}\s+)?on\s+(?:only\s+)?(${NAME})\s*(?:using\s+\w+\s*)?\(`, "giu");
1572
+ const ADD_KEY = new RegExp(String.raw`\balter\s+table\s+(?:if\s+exists\s+)?(?:only\s+)?(${NAME})\s+add\s+(?:constraint\s+${IDENT}\s+)?(?:unique|primary\s+key)\s*(?:nulls\s+not\s+distinct\s*)?\(`, "giu");
1573
+ const CREATE_TABLE = new RegExp(String.raw`\bcreate\s+(?:unlogged\s+|temp(?:orary)?\s+)?table\s+(?:if\s+not\s+exists\s+)?(${NAME})\s*\(`, "giu");
1574
+ const TABLE_NAME = new RegExp(String.raw`\bcreate\s+(?:unlogged\s+)?table\s+(?:if\s+not\s+exists\s+)?(${NAME})`, "giu");
1575
+ const TABLE_KEY = new RegExp(String.raw`^(?:constraint\s+${IDENT}\s+)?(?:unique|primary\s+key)\s*(?:nulls\s+not\s+distinct\s*)?\(`, "iu");
1576
+ const COLUMN_KEY = new RegExp(String.raw`^(${IDENT})\s+[^,]*?\b(?:primary\s+key|unique)\b`, "iu");
1577
+ const NOT_A_COLUMN = /^(?:constraint|check|foreign|exclude|like)\b/iu;
1578
+ function bare(part) {
1579
+ const trimmed = part.trim();
1580
+ return trimmed.startsWith("\"") ? trimmed.slice(1, -1).replaceAll("\"\"", "\"") : trimmed.toLowerCase();
1581
+ }
1582
+ /** `schema.table`, with `public` for an unqualified name. */
1583
+ function qualified(raw) {
1584
+ const parts = raw.split(".").map(bare);
1585
+ return parts.length === 1 ? `public.${parts.join("")}` : parts.join(".");
1586
+ }
1587
+ function stripComments(sql) {
1588
+ return sql.replaceAll(/\/\*[\s\S]*?\*\//gu, " ").replaceAll(/--[^\n]*/gu, " ");
1589
+ }
1590
+ /** The text between the `(` that ends `match` and its closing `)`, and the index after it. */
1591
+ function group(text, open) {
1592
+ let depth = 1;
1593
+ for (let index = open; index < text.length; index += 1) {
1594
+ const char = text[index];
1595
+ if (char === "'" || char === "\"") {
1596
+ const close = text.indexOf(char, index + 1);
1597
+ if (close === -1) return;
1598
+ index = close;
1599
+ } else if (char === "(") depth += 1;
1600
+ else if (char === ")") {
1601
+ depth -= 1;
1602
+ if (depth === 0) return {
1603
+ body: text.slice(open, index),
1604
+ end: index + 1
1605
+ };
1606
+ }
1607
+ }
1608
+ }
1609
+ function splitTop(body) {
1610
+ const items = [];
1611
+ let depth = 0;
1612
+ let from = 0;
1613
+ for (let index = 0; index < body.length; index += 1) {
1614
+ const char = body[index];
1615
+ if (char === "'" || char === "\"") index = Math.max(body.indexOf(char, index + 1), index);
1616
+ else if (char === "(") depth += 1;
1617
+ else if (char === ")") depth -= 1;
1618
+ else if (char === "," && depth === 0) {
1619
+ items.push(body.slice(from, index));
1620
+ from = index + 1;
1621
+ }
1622
+ }
1623
+ items.push(body.slice(from));
1624
+ return items.map((item) => item.trim());
1625
+ }
1626
+ /** The first capture group of a match; every pattern here captures it. */
1627
+ function captured(match) {
1628
+ return match.slice(1, 2).join("");
1629
+ }
1630
+ /** The index just past a match. */
1631
+ function endOf(match) {
1632
+ return match.index + match[0].length;
1633
+ }
1634
+ /** The plain key columns of an index column list, cut at the first expression. */
1635
+ function keyColumns(body) {
1636
+ const columns = [];
1637
+ for (const item of splitTop(body)) {
1638
+ const match = new RegExp(`^(${IDENT})(?:\\s|$)`, "u").exec(item);
1639
+ if (match === null || item.startsWith("(")) break;
1640
+ columns.push(bare(captured(match)));
1641
+ }
1642
+ return columns;
1643
+ }
1644
+ /** Key columns of each index, unique constraint and primary key the SQL creates, per `schema.table`. */
1645
+ function indexKeysFromSql(sql) {
1646
+ const text = stripComments(sql);
1647
+ const keys = {};
1648
+ const add = (table, columns) => {
1649
+ if (columns.length > 0) (keys[qualified(table)] ??= []).push(columns);
1650
+ };
1651
+ for (const pattern of [CREATE_INDEX, ADD_KEY]) for (const match of text.matchAll(pattern)) {
1652
+ const found = group(text, endOf(match));
1653
+ if (found !== void 0) add(captured(match), keyColumns(found.body));
1654
+ }
1655
+ for (const match of text.matchAll(CREATE_TABLE)) {
1656
+ const found = group(text, endOf(match));
1657
+ if (found === void 0) continue;
1658
+ for (const item of splitTop(found.body)) if (TABLE_KEY.test(item)) {
1659
+ const inner = group(item, item.indexOf("(") + 1);
1660
+ if (inner !== void 0) add(captured(match), keyColumns(inner.body));
1661
+ } else if (!NOT_A_COLUMN.test(item)) {
1662
+ const column = COLUMN_KEY.exec(item)?.[1];
1663
+ if (column !== void 0) add(captured(match), [bare(column)]);
1664
+ }
1665
+ }
1666
+ return keys;
1667
+ }
1668
+ /** The index keys of the SQL files under the migration and schema folders of `cwd`. */
1669
+ function indexKeysFromProject(cwd) {
1670
+ const all = {};
1671
+ for (const file of sqlFiles(cwd, [...MIGRATION_DIRS])) {
1672
+ const keys = indexKeysFromSql(readFileSync(file, "utf8").replaceAll(GENERATED_INDEX, ""));
1673
+ for (const [table, list] of Object.entries(keys)) (all[table] ??= []).push(...list);
1674
+ }
1675
+ return all;
1676
+ }
1677
+ /** Every `schema.table` the SQL creates, temporary tables left out. */
1678
+ function tablesFromSql(sql) {
1679
+ return new Set([...stripComments(sql).matchAll(TABLE_NAME)].map((match) => qualified(captured(match))));
1680
+ }
1681
+ /** Every `schema.table` the SQL files under the migration and schema folders of `cwd` create. */
1682
+ function tablesFromProject(cwd) {
1683
+ const all = /* @__PURE__ */ new Set();
1684
+ for (const file of sqlFiles(cwd, [...MIGRATION_DIRS])) for (const table of tablesFromSql(readFileSync(file, "utf8"))) all.add(table);
1685
+ return all;
1686
+ }
1687
+ /** The targets no index in `keys` leads with: its key columns start with the target's, as `pruneIndexTargets` reads a database. */
1688
+ function withoutServedTargets(targets, keys) {
1689
+ return targets.filter((target) => !(keys[target.table] ?? []).some((key) => target.columns.every((column, at) => key[at] === column)));
1690
+ }
1691
+ //#endregion
1810
1692
  //#region src/cli/rls/generate.ts
1811
1693
  function loadPolicy(cwd, config, from) {
1812
1694
  const policyPath = from === "drizzle" ? config.policy : from ?? config.policy;
@@ -1884,8 +1766,19 @@ async function runRlsGenerate(input) {
1884
1766
  const { ctx } = built;
1885
1767
  const { text, migration, migrationRel, helpersOnly } = generated;
1886
1768
  let indexes = generated.indexes;
1769
+ const bindings = tableBindings(generated.compiled.rowColumns.map((item) => item.resource), tableNames(input.config.rls?.tables), helpersOnly);
1770
+ if (input.config.rls?.tables === void 0) warnings.push(...bindingNotes(bindings, input.db !== void 0));
1771
+ warnings.push(...triggerNotes(text));
1772
+ if (ctx.seeds === "function") warnings.push(`run select ${built.schema}.seed(); after each deploy`);
1773
+ if (input.db === void 0) {
1774
+ indexes = withoutServedTargets(indexes, indexKeysFromProject(input.cwd));
1775
+ warnings.push(...unknownTableNotes(bindings, tablesFromProject(input.cwd)));
1776
+ }
1887
1777
  if (input.db !== void 0) try {
1888
- const pruned = pruneIndexTargets(indexes, await readTableIndexFacts(input.db, [...new Set(indexes.map((target) => target.table))], input.connect));
1778
+ const facts = await readTableIndexFacts(input.db, [.../* @__PURE__ */ new Set([...indexes.map((target) => target.table), ...bindings.map((binding) => binding.table)])], input.connect);
1779
+ const missing = missingBindings(bindings, facts.columns);
1780
+ if (missing.length > 0) return refused(`rls generate: ${missing.map((binding) => `resource ${binding.resource} is bound to ${binding.table}, which the database does not have`).join("; ")}; name the table in rls.tables, or set rls.tables.<resource>: false for a resource with no table`);
1781
+ const pruned = pruneIndexTargets(indexes, facts);
1889
1782
  indexes = pruned.targets;
1890
1783
  warnings.push(...pruned.warnings);
1891
1784
  } catch (cause) {
@@ -1907,10 +1800,19 @@ async function runRlsGenerate(input) {
1907
1800
  keys: {
1908
1801
  permissions: listPermissions(policy.vocabulary.permissions).map((leaf) => leaf.key),
1909
1802
  rowConditions: [...policyRowConditionKeys(policy)],
1803
+ levels: levelKeys(policy),
1910
1804
  renamed: Object.fromEntries(renamedKeys(policy.vocabulary.permissions))
1911
1805
  },
1912
1806
  schema: built.schema,
1913
1807
  helpersOnly,
1808
+ explain: explainRls({
1809
+ config: input.config,
1810
+ ctx,
1811
+ schema: built.schema,
1812
+ authorize: built.authorize,
1813
+ generated,
1814
+ text
1815
+ }),
1914
1816
  ...extras
1915
1817
  };
1916
1818
  const planned = outputFiles({
@@ -2034,7 +1936,7 @@ function missingIndexes(expected, actual) {
2034
1936
  return expected.indexes.flatMap((target) => {
2035
1937
  const column = target.columns[0];
2036
1938
  const columns = actual.columns?.[target.table];
2037
- return column === void 0 || columns !== void 0 && !columns.includes(column) || actual.leadingColumns[target.table]?.includes(column) === true ? [] : [`warning: ${target.table}: no index starts with ${column}, which the policies or helpers filter on; add it, or write it with rls generate --split ...,indexes`];
1939
+ return column === void 0 || columns !== void 0 && !columns.includes(column) || actual.leadingColumns[target.table]?.includes(column) === true ? [] : [rlsFinding("index-missing", target.table, column, `${target.table}: no index starts with ${column}, which the policies or helpers filter on; add it, or write it with rls generate --split ...,indexes`, "warning")];
2038
1940
  });
2039
1941
  }
2040
1942
  function sameList(left, right) {
@@ -2042,42 +1944,42 @@ function sameList(left, right) {
2042
1944
  const b = right.toSorted();
2043
1945
  return a.length === b.length && a.every((item, index) => item === b[index]);
2044
1946
  }
2045
- /** One line per difference; policy expressions are not compared, since Postgres rewrites their text. */
1947
+ /** One finding per difference; policy expressions are not compared, since Postgres rewrites their text. */
2046
1948
  function diffRls(expected, actual, options = {}) {
2047
1949
  const out = [];
2048
1950
  const found = new Map(actual.policies.map((item) => [`${item.table}\u0000${item.name}`, item]));
2049
1951
  for (const want of expected.policies) {
2050
1952
  const have = found.get(`${want.table}\u0000${want.name}`);
2051
1953
  if (have === void 0) {
2052
- out.push(`${want.table}: policy ${want.name} is missing`);
1954
+ out.push(rlsFinding("policy-missing", want.table, want.name, `${want.table}: policy ${want.name} is missing`));
2053
1955
  continue;
2054
1956
  }
2055
- if (have.command !== want.command) out.push(`${want.table}: policy ${want.name} is for ${have.command}, expected ${want.command}`);
2056
- if (have.permissive !== want.permissive) out.push(`${want.table}: policy ${want.name} is ${have.permissive ? "permissive" : "restrictive"}, expected ${want.permissive ? "permissive" : "restrictive"}`);
2057
- if (!sameList(have.roles, want.roles)) out.push(`${want.table}: policy ${want.name} applies to ${have.roles.join(", ")}, expected ${want.roles.join(", ")}`);
1957
+ if (have.command !== want.command) out.push(rlsFinding("policy-command", want.table, want.name, `${want.table}: policy ${want.name} is for ${have.command}, expected ${want.command}`));
1958
+ if (have.permissive !== want.permissive) out.push(rlsFinding("policy-mode", want.table, want.name, `${want.table}: policy ${want.name} is ${have.permissive ? "permissive" : "restrictive"}, expected ${want.permissive ? "permissive" : "restrictive"}`));
1959
+ if (!sameList(have.roles, want.roles)) out.push(rlsFinding("policy-roles", want.table, want.name, `${want.table}: policy ${want.name} applies to ${have.roles.join(", ")}, expected ${want.roles.join(", ")}`));
2058
1960
  }
2059
1961
  const wanted = new Set(expected.policies.map((item) => `${item.table}\u0000${item.name}`));
2060
- for (const have of actual.policies) if (expected.tables.includes(have.table) && !wanted.has(`${have.table}\u0000${have.name}`)) out.push(`${have.table}: policy ${have.name} is not generated (${have.permissive ? "permissive" : "restrictive"} ${have.command}); a permissive one widens access`);
1962
+ for (const have of actual.policies) if (expected.tables.includes(have.table) && !wanted.has(`${have.table}\u0000${have.name}`)) out.push(rlsFinding("policy-extra", have.table, have.name, `${have.table}: policy ${have.name} is not generated (${have.permissive ? "permissive" : "restrictive"} ${have.command}); a permissive one widens access`));
2061
1963
  for (const table of expected.tables) {
2062
1964
  if (actual.rlsEnabled[table] !== true) {
2063
- out.push(actual.rlsEnabled[table] === void 0 ? `${table}: table is missing` : `${table}: row level security is disabled`);
1965
+ out.push(actual.rlsEnabled[table] === void 0 ? rlsFinding("table-missing", table, "table", `${table}: table is missing`) : rlsFinding("rls-disabled", table, "table", `${table}: row level security is disabled`));
2064
1966
  continue;
2065
1967
  }
2066
1968
  for (const role of ROLES) {
2067
1969
  const want = expected.grants[table]?.[role] ?? [];
2068
1970
  const have = actual.grants[table]?.[role] ?? [];
2069
- for (const privilege of want) if (!have.includes(privilege) && !(privilege === "select" && options.columnGrants === true)) out.push(`${table}: ${role} lacks ${privilege}, so the policy answers 42501 instead of filtering`);
2070
- for (const privilege of have) if (!want.includes(privilege)) out.push(`${table}: ${role} holds ${privilege}, which no generated policy allows`);
1971
+ for (const privilege of want) if (!have.includes(privilege) && !(privilege === "select" && options.columnGrants === true)) out.push(rlsFinding("grant-missing", table, `${role}.${privilege}`, `${table}: ${role} lacks ${privilege}, so the policy answers 42501 instead of filtering`));
1972
+ for (const privilege of have) if (!want.includes(privilege)) out.push(rlsFinding("grant-extra", table, `${role}.${privilege}`, `${table}: ${role} holds ${privilege}, which no generated policy allows`));
2071
1973
  }
2072
1974
  }
2073
1975
  for (const helper of expected.helpers) {
2074
1976
  const have = actual.helpers[helper];
2075
1977
  if (have === void 0) {
2076
- out.push(`${helper}: helper is missing`);
1978
+ out.push(rlsFinding("helper-missing", helper, "helper", `${helper}: helper is missing`));
2077
1979
  continue;
2078
1980
  }
2079
- if (!have.securityDefiner) out.push(`${helper}: helper is not security definer`);
2080
- if (!have.emptySearchPath) out.push(`${helper}: helper does not set search_path = ''`);
1981
+ if (!have.securityDefiner) out.push(rlsFinding("helper-definer", helper, "helper", `${helper}: helper is not security definer`));
1982
+ if (!have.emptySearchPath) out.push(rlsFinding("helper-search-path", helper, "helper", `${helper}: helper does not set search_path = ''`));
2081
1983
  }
2082
1984
  return out;
2083
1985
  }
@@ -2098,12 +2000,6 @@ const HELPERS_SQL = `select n.nspname || '.' || p.proname as target, p.prosecdef
2098
2000
  from pg_proc p
2099
2001
  join pg_namespace n on n.oid = p.pronamespace
2100
2002
  where n.nspname || '.' || p.proname = any($1::text[])`;
2101
- const INDEXES_SQL = `select n.nspname || '.' || c.relname as target, a.attname as leading
2102
- from pg_index i
2103
- join pg_class c on c.oid = i.indrelid
2104
- join pg_namespace n on n.oid = c.relnamespace
2105
- join pg_attribute a on a.attrelid = i.indrelid and a.attnum = i.indkey[0]
2106
- where i.indpred is null and n.nspname || '.' || c.relname = any($1::text[])`;
2107
2003
  function commandOf(cmd) {
2108
2004
  const text = String(cmd ?? "").toLowerCase();
2109
2005
  return text === "*" ? "all" : text;
@@ -2122,13 +2018,16 @@ async function introspectRls(db, expected, connect = connectIntrospect) {
2122
2018
  client.query(TABLES_SQL, [tables]),
2123
2019
  client.query(GRANTS_SQL, [tables]),
2124
2020
  client.query(HELPERS_SQL, [[...expected.helpers]]),
2125
- client.query(INDEXES_SQL, [indexed]),
2021
+ client.query(INDEX_COLUMNS_SQL, [indexed]),
2126
2022
  client.query(COLUMNS_SQL, [indexed])
2127
2023
  ]);
2128
2024
  const columnsOf = {};
2129
2025
  for (const row of columns.rows) (columnsOf[String(row["target"])] ??= []).push(String(row["name"]));
2130
2026
  const leadingColumns = {};
2131
- for (const row of indexes.rows) (leadingColumns[String(row["target"])] ??= []).push(String(row["leading"]));
2027
+ for (const row of indexes.rows) {
2028
+ const lead = (Array.isArray(row["columns"]) ? row["columns"] : []).at(0);
2029
+ if (typeof lead === "string") (leadingColumns[String(row["target"])] ??= []).push(lead);
2030
+ }
2132
2031
  const byTable = {};
2133
2032
  for (const row of grants.rows) {
2134
2033
  const table = String(row["target"]);
@@ -2176,7 +2075,7 @@ function unguardedWrites(policies, fn) {
2176
2075
  const permissive = applying.filter((policy) => policy.restrictive !== true && (policy.roles ?? []).some((role) => role === "authenticated" || role === "public"));
2177
2076
  if (permissive.length === 0) continue;
2178
2077
  const calls = (expression) => expression.includes(fn);
2179
- if (!(applying.some((policy) => policy.restrictive === true && calls(policy.expression)) || permissive.every((policy) => calls(policy.expression)))) out.push(`${table}: ${command} policies for authenticated do not call ${fn}(), so a read-only support or impersonation session can write (rls.readOnlyActors)`);
2078
+ if (!(applying.some((policy) => policy.restrictive === true && calls(policy.expression)) || permissive.every((policy) => calls(policy.expression)))) out.push(rlsFinding("write-unguarded", table, command, `${table}: ${command} policies for authenticated do not call ${fn}(), so a read-only support or impersonation session can write (rls.readOnlyActors)`));
2180
2079
  }
2181
2080
  return out;
2182
2081
  }
@@ -2205,26 +2104,37 @@ function inScope(table) {
2205
2104
  function seedLine(row) {
2206
2105
  return `${row.role} ${row.effect} ${row.grantKey} on ${row.scope}`;
2207
2106
  }
2107
+ function seedKey(line) {
2108
+ return line.replace(" on ", " ").replaceAll(" ", "/");
2109
+ }
2208
2110
  /** Drift is an error; `info` names RLS tables whose policies call no PermDock helper. */
2209
2111
  function diffMixed(expected, actual) {
2210
2112
  const drift = [];
2113
+ const seeds = `${expected.schema}.role_permissions`;
2211
2114
  const want = new Set(expected.seeds.map(seedLine));
2212
2115
  const have = new Set(actual.seeds.map(seedLine));
2213
- for (const line of want) if (!have.has(line)) drift.push(`${expected.schema}.role_permissions: missing ${line}`);
2214
- for (const line of have) if (!want.has(line)) drift.push(`${expected.schema}.role_permissions: unexpected ${line}`);
2116
+ for (const line of want) if (!have.has(line)) drift.push(rlsFinding("seed-missing", seeds, seedKey(line), `${seeds}: missing ${line}`));
2117
+ for (const line of have) if (!want.has(line)) drift.push(rlsFinding("seed-extra", seeds, seedKey(line), `${seeds}: unexpected ${line}`));
2215
2118
  const permissions = new Set(expected.permissions);
2216
2119
  const conditioned = new Set(expected.rowConditions);
2120
+ const levels = new Map((expected.levels ?? []).map((level) => [level.key, level]));
2121
+ const notes = [];
2217
2122
  const covered = /* @__PURE__ */ new Set();
2218
2123
  for (const policy of actual.policies) {
2219
2124
  if (callsHelper(policy.expression)) covered.add(policy.table);
2220
2125
  const blind = new Set(helperCallKeys(policy.expression, true));
2221
- for (const key of helperCallKeys(policy.expression)) if (!permissions.has(key)) drift.push(`${policy.table}: policy ${policy.name} passes ${key}, which the policy does not declare, so it always denies`);
2222
- else if (conditioned.has(key) && blind.has(key)) drift.push(`${policy.table}: policy ${policy.name} passes ${key}, whose grants carry row conditions the helpers do not check: the policy grants more than the application does`);
2126
+ for (const key of helperCallKeys(policy.expression)) {
2127
+ const level = levels.get(key);
2128
+ if (level !== void 0) {
2129
+ if (level.conditioned) notes.push(rlsFinding("level-unconditioned", policy.table, `${policy.name}/${key}`, `${policy.table}: policy ${policy.name} passes ${key}, which skips the level's row condition, so holders of it are denied there; use ${level.rowHelper}(${policy.table.slice(policy.table.lastIndexOf(".") + 1)}, '${level.permission}')`, "info"));
2130
+ } else if (!permissions.has(key)) drift.push(rlsFinding("undeclared-key", policy.table, `${policy.name}/${key}`, `${policy.table}: policy ${policy.name} passes ${key}, which the policy does not declare, so it always denies`));
2131
+ else if (conditioned.has(key) && blind.has(key)) drift.push(rlsFinding("condition-blind", policy.table, `${policy.name}/${key}`, `${policy.table}: policy ${policy.name} passes ${key}, whose grants carry row conditions the helpers do not check: the policy grants more than the application does`));
2132
+ }
2223
2133
  }
2224
2134
  if (expected.readOnlyActors === true) drift.push(...unguardedWrites(actual.policies, ACTOR_MAY_WRITE));
2225
2135
  return {
2226
2136
  drift,
2227
- info: actual.rlsTables.filter((table) => !covered.has(table)).map((table) => `${table}: no policy calls a PermDock helper`)
2137
+ info: [...notes, ...actual.rlsTables.filter((table) => !covered.has(table)).map((table) => rlsFinding("helper-uncalled", table, "table", `${table}: no policy calls a PermDock helper`, "info"))]
2228
2138
  };
2229
2139
  }
2230
2140
  const ALL_POLICIES_SQL = `select schemaname || '.' || tablename as target, policyname,
@@ -2270,12 +2180,15 @@ async function introspectMixed(db, schema, connect = connectIntrospect) {
2270
2180
  }
2271
2181
  //#endregion
2272
2182
  //#region src/cli/rls/rls.ts
2273
- const RLS_HELP = `permdock rls generate | import | verify | migrate
2183
+ /** `rls migrate` and `rls import` read the pgsql-parser 18 AST. */
2184
+ const PGSQL_PARSER_MAJOR = 18;
2185
+ const RLS_HELP = `permdock rls generate | import | verify | migrate | explain
2274
2186
 
2275
2187
  generate Compile the policy into RLS policies and helpers
2276
2188
  import Turn existing policies (--sql or --db-url-stdin) into a permissions module
2277
2189
  verify Run the fixtures, or compare a live database with --introspect
2278
2190
  migrate Rewrite the rls.migrate helpers' calls onto the generated helpers
2191
+ explain Show the tables, membership sources, custom roles and triggers the SQL binds to
2279
2192
 
2280
2193
  Run permdock rls <command> --help for its flags.
2281
2194
  Never emits service_role, except in the execute grant rls.trustedReaders asks for. memberOf compiles through the dialect memberships mapping.
@@ -2318,11 +2231,48 @@ function generateInput(input, target, dialect) {
2318
2231
  ...isSupabaseSchemas(input.schemas) ? { schemas: input.schemas } : {},
2319
2232
  ...input.grantsOut === void 0 ? {} : { grantsOut: input.grantsOut },
2320
2233
  ...input.seedsOut === void 0 ? {} : { seedsOut: input.seedsOut },
2234
+ ...input.noSeeds === true ? { noSeeds: true } : {},
2321
2235
  ...input.backfillOut === void 0 ? {} : { backfillOut: input.backfillOut },
2322
2236
  helpersOnly: input.helpersOnly,
2323
2237
  shims: input.shims === true
2324
2238
  };
2325
2239
  }
2240
+ async function explain(input) {
2241
+ const dialect = asDialect(input.dialect ?? input.config.rls?.dialect);
2242
+ if (dialect === void 0) return {
2243
+ code: 2,
2244
+ output: "rls explain --dialect (or rls.dialect) must be supabase, neon or guc"
2245
+ };
2246
+ const generated = await runRlsGenerate({
2247
+ ...generateInput(input, "sql", dialect),
2248
+ check: false,
2249
+ write: false
2250
+ });
2251
+ if (generated.explain === void 0) return {
2252
+ code: generated.code,
2253
+ output: generated.output
2254
+ };
2255
+ let report = generated.explain;
2256
+ if (input.db !== void 0) try {
2257
+ const facts = await readTableIndexFacts(input.db, report.bindings.map((binding) => binding.table), input.connect);
2258
+ report = {
2259
+ ...report,
2260
+ bindings: report.bindings.map((binding) => ({
2261
+ ...binding,
2262
+ exists: facts.columns[binding.table] !== void 0
2263
+ }))
2264
+ };
2265
+ } catch (cause) {
2266
+ return {
2267
+ code: exitCodeOfError(cause),
2268
+ output: describeError(cause)
2269
+ };
2270
+ }
2271
+ return {
2272
+ code: 0,
2273
+ output: input.json ? JSON.stringify(report, null, 2) : describeExplain(report)
2274
+ };
2275
+ }
2326
2276
  async function migrate(input) {
2327
2277
  const config = input.config.rls?.migrate;
2328
2278
  if (config === void 0) return {
@@ -2347,8 +2297,8 @@ async function migrate(input) {
2347
2297
  code: 2,
2348
2298
  output: generated.output
2349
2299
  };
2350
- await requirePeer(() => import("pgsql-parser"), "pgsql-parser", "permdock rls migrate", input.cwd);
2351
- const { migrateTarget, runRlsMigrate } = await import("./migrate-CIq4-E9F.js");
2300
+ await requirePeer(() => import("pgsql-parser"), "pgsql-parser", "permdock rls migrate", input.cwd, PGSQL_PARSER_MAJOR);
2301
+ const { migrateTarget, runRlsMigrate } = await import("./migrate-Dodel0IJ.js");
2352
2302
  return runRlsMigrate({
2353
2303
  cwd: input.cwd,
2354
2304
  sql: input.sql,
@@ -2365,7 +2315,7 @@ async function migrate(input) {
2365
2315
  });
2366
2316
  }
2367
2317
  /** `verify --introspect` with `--helpers-only`: helpers and seeds exactly, hand-written policies by their keys. */
2368
- async function introspectHelpersOnly(db, expected, generated, connect, readOnlyActors) {
2318
+ async function introspectHelpersOnly(db, expected, generated, connect, readOnlyActors, ignore) {
2369
2319
  const schema = generated.schema ?? "permdock";
2370
2320
  try {
2371
2321
  const actual = await introspectRls(db, expected, connect);
@@ -2376,31 +2326,25 @@ async function introspectHelpersOnly(db, expected, generated, connect, readOnlyA
2376
2326
  seeds: generated.seeds ?? [],
2377
2327
  permissions: generated.keys?.permissions ?? [],
2378
2328
  rowConditions: generated.keys?.rowConditions ?? [],
2329
+ levels: generated.keys?.levels ?? [],
2379
2330
  readOnlyActors
2380
2331
  }, await introspectMixed(db, schema, connect));
2381
- const drift = [...helpers, ...mixed.drift];
2382
- const info = mixed.info.map((line) => `info: ${line}`);
2383
- return drift.length > 0 ? {
2384
- code: 1,
2385
- output: [
2386
- ...drift,
2332
+ return verifyResult({
2333
+ findings: [
2334
+ ...helpers,
2335
+ ...mixed.drift,
2387
2336
  ...warnings,
2388
- ...info
2389
- ].join("\n")
2390
- } : {
2391
- code: 0,
2392
- output: [
2393
- ...warnings,
2394
- ...info,
2395
- `introspected ${String(expected.helpers.length)} helper(s) and ${String(generated.seeds?.length ?? 0)} seeded row(s), helpers only: no drift`
2396
- ].join("\n")
2397
- };
2337
+ ...mixed.info
2338
+ ],
2339
+ ignore,
2340
+ summary: `introspected ${String(expected.helpers.length)} helper(s) and ${String(generated.seeds?.length ?? 0)} seeded row(s), helpers only: no drift`
2341
+ });
2398
2342
  } catch (cause) {
2399
2343
  return usageResult(cause);
2400
2344
  }
2401
2345
  }
2402
2346
  /** `verify --introspect`: the catalogs against what `generate` would write with the same flags. */
2403
- async function introspect(input) {
2347
+ async function introspect(input, ignore) {
2404
2348
  if (input.db === void 0) return {
2405
2349
  code: 2,
2406
2350
  output: "PermDock CLI: rls verify --introspect needs a database: --db-url-stdin, DATABASE_URL or SUPABASE_DB_URL"
@@ -2420,19 +2364,15 @@ async function introspect(input) {
2420
2364
  output: generated.output
2421
2365
  };
2422
2366
  const expected = expectedRls(generated.policies, generated.text, generated.indexes);
2423
- if (generated.helpersOnly === true) return introspectHelpersOnly(input.db, expected, generated, input.connect, readOnlyActorKinds(input.config.rls?.readOnlyActors) !== void 0);
2367
+ if (generated.helpersOnly === true) return introspectHelpersOnly(input.db, expected, generated, input.connect, readOnlyActorKinds(input.config.rls?.readOnlyActors) !== void 0, ignore);
2424
2368
  try {
2425
2369
  const actual = await introspectRls(input.db, expected, input.connect);
2426
- const drift = diffRls(expected, actual, { columnGrants: (input.fields ?? input.config.rls?.fields) === "views" });
2427
- const warnings = missingIndexes(expected, actual);
2428
- if (drift.length > 0) return {
2429
- code: 1,
2430
- output: [...drift, ...warnings].join("\n")
2431
- };
2432
- return {
2433
- code: 0,
2434
- output: `${warnings.map((line) => `${line}\n`).join("")}introspected ${String(expected.policies.length)} policies on ${String(expected.tables.length)} table(s) and ${String(expected.helpers.length)} helper(s): no drift`
2435
- };
2370
+ const drift = diffRls(expected, actual, { columnGrants: (input.fields ?? input.config.rls?.fields) !== void 0 });
2371
+ return verifyResult({
2372
+ findings: [...drift, ...missingIndexes(expected, actual)],
2373
+ ignore,
2374
+ summary: `introspected ${String(expected.policies.length)} policies on ${String(expected.tables.length)} table(s) and ${String(expected.helpers.length)} helper(s): no drift`
2375
+ });
2436
2376
  } catch (cause) {
2437
2377
  return usageResult(cause);
2438
2378
  }
@@ -2467,8 +2407,8 @@ async function runRls(input) {
2467
2407
  });
2468
2408
  }
2469
2409
  case "import": {
2470
- await requirePeer(() => import("pgsql-parser"), "pgsql-parser", "permdock rls import", input.cwd);
2471
- const { runRlsImport } = await import("./import-wyqXUyjm.js");
2410
+ await requirePeer(() => import("pgsql-parser"), "pgsql-parser", "permdock rls import", input.cwd, PGSQL_PARSER_MAJOR);
2411
+ const { runRlsImport } = await import("./import-Bz0GefWg.js");
2472
2412
  return runRlsImport({
2473
2413
  cwd: input.cwd,
2474
2414
  config: input.config,
@@ -2490,7 +2430,8 @@ async function runRls(input) {
2490
2430
  ...input.db === void 0 ? {} : { db: input.db },
2491
2431
  ...input.exec === void 0 ? {} : { exec: input.exec }
2492
2432
  });
2493
- if (input.introspect) return introspect(input);
2433
+ const ignore = verifyIgnores(input.config, input.ignore);
2434
+ if (input.introspect) return introspect(input, ignore);
2494
2435
  const format = input.format ?? "node";
2495
2436
  if (format !== "node" && format !== "pgtap") return {
2496
2437
  code: 2,
@@ -2502,13 +2443,16 @@ async function runRls(input) {
2502
2443
  format,
2503
2444
  tree: input.tree,
2504
2445
  io: input.io,
2446
+ ...input.out === void 0 ? {} : { out: input.out },
2505
2447
  ...input.fixtures === void 0 ? {} : { fixtures: input.fixtures },
2506
2448
  ...input.db === void 0 ? {} : { db: input.db },
2507
2449
  ...input.from === void 0 ? {} : { from: input.from },
2508
- ...input.connect === void 0 ? {} : { connect: input.connect }
2450
+ ...input.connect === void 0 ? {} : { connect: input.connect },
2451
+ ignore
2509
2452
  });
2510
2453
  }
2511
2454
  case "migrate": return migrate(input);
2455
+ case "explain": return explain(input);
2512
2456
  default: return {
2513
2457
  code: 2,
2514
2458
  output: RLS_HELP