permdock 0.6.0 → 0.8.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 (507) hide show
  1. package/README.md +2 -3
  2. package/dist/{2020-C9VN-x8c.js → 2020-CJbO8Bvz.js} +1 -3
  3. package/dist/a2a/index.d.ts +7 -7
  4. package/dist/a2a/index.js +9 -9
  5. package/dist/action-result-CL-QCITA.d.ts +19 -0
  6. package/dist/{adapter-context-CRhqyzCk.js → adapter-context-CIQpQxbE.js} +8 -2
  7. package/dist/{adoption-scan-DCYe60Pi.js → adoption-scan-BLs67P1l.js} +5 -5
  8. package/dist/agent/index.d.ts +3 -3
  9. package/dist/agent/index.js +1 -1
  10. package/dist/ai-sdk/index.d.ts +31 -10
  11. package/dist/ai-sdk/index.js +22 -12
  12. package/dist/{app-data-MuyzVqX8.js → app-data-UNNm07jV.js} +10 -7
  13. package/dist/{approval-header-CJACtHkv.js → approval-header-8QV6MU4H.js} +6 -3
  14. package/dist/{approval-prompt-Dg5LeAcT.js → approval-prompt-B8hQY69x.js} +5 -6
  15. package/dist/{approval-prompt-DvSSPQJX.d.ts → approval-prompt-PYagyQUj.d.ts} +13 -11
  16. package/dist/approvals/index.d.ts +5 -5
  17. package/dist/approvals/index.js +7 -7
  18. package/dist/{approvals-p1PGoVjb.js → approvals-DnIBTuXE.js} +128 -78
  19. package/dist/{approvers-CqPOLD-o.js → approvers-B4r0N_Uz.js} +8 -2
  20. package/dist/{arazzo-41WuzHou.js → arazzo-3RhGfkAK.js} +2 -2
  21. package/dist/{arazzo-CaUVBn7D.js → arazzo-B5-ulWif.js} +6 -6
  22. package/dist/{args-IIolxxKA.js → args-0FrVs5EH.js} +125 -8
  23. package/dist/astro/index.d.ts +30 -12
  24. package/dist/astro/index.js +2 -2
  25. package/dist/{audience-DabClFLQ.js → audience-BAkBDMqn.js} +8 -8
  26. package/dist/{auth-DRLEizbL.js → auth--yPyZNgr.js} +21 -10
  27. package/dist/authzen/index.d.ts +171 -25
  28. package/dist/authzen/index.js +321 -149
  29. package/dist/{automation-BNM3TtDo.d.ts → automation-DqEoWHBc.d.ts} +42 -32
  30. package/dist/{automation-DVjOckjV.js → automation-x3KEUusw.js} +22 -21
  31. package/dist/base-url-BQbddZO5.js +9 -0
  32. package/dist/better-auth/index.d.ts +2 -2
  33. package/dist/better-auth/index.js +13 -7
  34. package/dist/better-supabase/index.d.ts +392 -220
  35. package/dist/better-supabase/index.js +293 -152
  36. package/dist/{bind-DOyrv6zi.d.ts → bind-Cp2hBHzP.d.ts} +27 -12
  37. package/dist/{bind-CAvgUuKs.js → bind-D_sZSVA3.js} +22 -10
  38. package/dist/{body-CH5of8Ne.js → body-CJQwYVa8.js} +3 -3
  39. package/dist/{body-text-QcXok_SI.js → body-text-DQhcmqdY.js} +8 -4
  40. package/dist/{boundary-BM8a_P6E.js → boundary-BV3O1eud.js} +8 -2
  41. package/dist/{boundary-BmHxVH1J.d.ts → boundary-C9ck-3uK.d.ts} +13 -4
  42. package/dist/{budget-NbEtbJC_.js → budget-BELja5A9.js} +6 -3
  43. package/dist/{budget-JqcRqyhK.d.ts → budget-hYsPz_yE.d.ts} +6 -3
  44. package/dist/{build-instance-Bu_eDqYw.js → build-instance-CNWaYJyr.js} +3 -3
  45. package/dist/{canonical-json-BC8_nMTw.js → canonical-json-DwilZ1iB.js} +6 -3
  46. package/dist/{capability-N1K9IpqS.js → capability-BqX94r07.js} +31 -25
  47. package/dist/{capability-Drg-1YhL.d.ts → capability-ChuiONQU.d.ts} +41 -29
  48. package/dist/{casbin-Dyu2gnUD.js → casbin-H0rKxBx2.js} +16 -8
  49. package/dist/{casbin-Cr7RLtA3.js → casbin-cbSbhT_k.js} +2 -2
  50. package/dist/{casl-64gpT-8x.js → casl-C4OINZHI.js} +2 -2
  51. package/dist/{casl-DjEDhTCP.js → casl-CHHoCldP.js} +19 -10
  52. package/dist/catalog/index.d.ts +104 -23
  53. package/dist/catalog/index.js +117 -8
  54. package/dist/{catalog-HD2gf5mY.js → catalog-BFpz2EmT.js} +57 -26
  55. package/dist/{catalog-DZVbrF-z.js → catalog-DCspHDc8.js} +3 -3
  56. package/dist/{catalog-doc-Cdn7-0pQ.js → catalog-doc-DjMKzJp9.js} +53 -16
  57. package/dist/{catalog-fingerprint-CCP0-yQS.js → catalog-fingerprint-e6aCWosn.js} +19 -15
  58. package/dist/catalog-mRQQQhNK.js +1 -0
  59. package/dist/catalog-operations-Dadrl7Z-.js +60 -0
  60. package/dist/{create-CQWIpUG-.js → challenge-DImNcR7B.js} +157 -39
  61. package/dist/chat/index.d.ts +1 -1
  62. package/dist/chat/index.js +13 -10
  63. package/dist/chat-sdk/index.d.ts +44 -26
  64. package/dist/chat-sdk/index.js +17 -17
  65. package/dist/checks-CGkP3Z7A.js +40 -0
  66. package/dist/{claims-map-7oQ3ue4X.js → claims-map-CKkoAAYF.js} +5 -2
  67. package/dist/{claims-map-Ca-QX8C6.d.ts → claims-map-D1Pb5oRq.d.ts} +8 -5
  68. package/dist/claude-agent/index.d.ts +6 -6
  69. package/dist/claude-agent/index.js +6 -3
  70. package/dist/clerk/index.d.ts +11 -7
  71. package/dist/clerk/index.js +6 -6
  72. package/dist/cli/bin.js +4 -4
  73. package/dist/cli/index.d.ts +82 -7
  74. package/dist/cli/index.js +6 -6
  75. package/dist/{clients-C7zLFuWT.d.ts → clients-C_NOET3z.d.ts} +5 -5
  76. package/dist/{clients-DjU5ABMy.js → clients-DYr3U86j.js} +3 -3
  77. package/dist/cloud/index.d.ts +186 -38
  78. package/dist/cloud/index.js +293 -31
  79. package/dist/{cloud-ytQ3uviA.js → cloud-BFlGMaak.js} +5 -6
  80. package/dist/{codemod-5mpSpnUY.js → codemod-DLQ9Zpo4.js} +3 -4
  81. package/dist/{codemod-ua2hHy7w.js → codemod-DWSN2erG.js} +523 -201
  82. package/dist/collect-B5BL3klN.js +1 -0
  83. package/dist/{collect-mLTY4nwb.js → collect-B7ZFAJU3.js} +36 -64
  84. package/dist/{collect-Ci6akVwH.js → collect-Bzv3le0A.js} +6 -6
  85. package/dist/{compare-pfyVnBjE.js → compare-DTfyUunF.js} +2 -2
  86. package/dist/compile/index.d.ts +2 -2
  87. package/dist/compile/index.js +1 -1
  88. package/dist/{compile-1bsAezL0.d.ts → compile-BFzysW3o.d.ts} +36 -12
  89. package/dist/{compile-CMqMx_IN.js → compile-EzEMB-7v.js} +77 -33
  90. package/dist/{completion-BTFrHZhX.js → completion-saoYiRFO.js} +17 -8
  91. package/dist/config-BwPBfN0t.js +108 -0
  92. package/dist/{config-Bj2Ixq3d.js → config-DBeXXmBF.js} +2 -2
  93. package/dist/{config-report-DSpsLpCE.js → config-report-BipSWA6V.js} +9 -6
  94. package/dist/config-shape-ykV0Vm2n.js +134 -0
  95. package/dist/{connection-DjhIJSs2.js → connection-B72dg5Q2.js} +23 -22
  96. package/dist/connector-schema-CYLQNMYq.js +434 -0
  97. package/dist/{context-B_u3_bDJ.js → context-BnIzxTNE.js} +4 -3
  98. package/dist/convex/index.d.ts +13 -7
  99. package/dist/convex/index.js +9 -6
  100. package/dist/core-H-mbhpZc.js +341 -0
  101. package/dist/core-hWIARPDz.d.ts +206 -0
  102. package/dist/{create-CBMmFie-.d.ts → create-BSNrVuxh.d.ts} +87 -34
  103. package/dist/{create-B30OMUOJ.d.ts → create-BVq1lNQE.d.ts} +49 -28
  104. package/dist/{create-D_NmPOdH.d.ts → create-CM1FciYG.d.ts} +37 -29
  105. package/dist/{create-CGm28gyI.js → create-Cq4Uox0f.js} +13 -9
  106. package/dist/{create-DCF2SvoT.js → create-_noSrY1T.js} +15 -21
  107. package/dist/{credential-BcoiejpY.js → credential-CyyyvCsL.js} +42 -39
  108. package/dist/{credentials-DPyHmYz5.js → credentials-CdDxp7jN.js} +6 -6
  109. package/dist/{deciding-columns-Sq12mEA6.js → deciding-columns-NB4wcyBf.js} +13 -2
  110. package/dist/{delegation-j6-rc37o.js → delegation-CDB6_keF.js} +31 -22
  111. package/dist/delivery/index.d.ts +5 -5
  112. package/dist/delivery/index.js +6 -5
  113. package/dist/{delivery-DCAgNyCn.js → delivery-BupfvJIV.js} +81 -20
  114. package/dist/{delivery-event-xX4obB8F.js → delivery-event-JioK5YbS.js} +6 -6
  115. package/dist/denied-text-CHNlVInN.d.ts +22 -0
  116. package/dist/{describe-BybMRY7A.js → describe-Bb4hGK5d.js} +6 -5
  117. package/dist/{describe-B5qgDvEW.d.ts → describe-ByS9LgHK.d.ts} +5 -5
  118. package/dist/{device-BARQ8eYG.js → device-BwbJNmb2.js} +11 -8
  119. package/dist/{diff-CmuSD3vc.js → diff-CN8IYhC_.js} +2 -2
  120. package/dist/{diff-3jbx6Rm4.js → diff-iZWV65nC.js} +23 -14
  121. package/dist/{digest-B3kc_PgQ.d.ts → digest-8Yx2CchM.d.ts} +3 -3
  122. package/dist/{doctor-BdZw53-k.js → doctor-CHD1M9R5.js} +485 -141
  123. package/dist/{doctor-3ZqNHX_G.js → doctor-D3wo-yuz.js} +4 -4
  124. package/dist/drizzle/index.d.ts +33 -19
  125. package/dist/drizzle/index.js +17 -9
  126. package/dist/elysia/index.d.ts +17 -11
  127. package/dist/elysia/index.js +5 -5
  128. package/dist/{errors-BKy0EfhN.js → errors-B6R_lg2a.js} +13 -4
  129. package/dist/{errors-BWXBMaKK.d.ts → errors-CHwoRRpB.d.ts} +4 -1
  130. package/dist/{errors-CWTY6uih.js → errors-Vpu6rKHl.js} +14 -10
  131. package/dist/{errors-DH5QEGXh.d.ts → errors-zi_4T-rx.d.ts} +22 -6
  132. package/dist/{evaluate-CNClBvcj.js → evaluate-Dd8Ta0Ie.js} +185 -80
  133. package/dist/{evaluations-beAooV0n.js → evaluations-IBceAeKT.js} +41 -20
  134. package/dist/{evaluations-C_1vdSZc.d.ts → evaluations-z8dQFDaN.d.ts} +12 -6
  135. package/dist/eve/index.d.ts +9 -6
  136. package/dist/eve/index.js +4 -4
  137. package/dist/{event-time-CsegxXxB.js → event-time-BGy48400.js} +8 -4
  138. package/dist/{expiry-D4_YKRKD.js → expiry-6XByK0dm.js} +8 -5
  139. package/dist/express/index.d.ts +9 -6
  140. package/dist/express/index.js +9 -6
  141. package/dist/fastify/index.d.ts +16 -10
  142. package/dist/fastify/index.js +8 -5
  143. package/dist/{fetch-adapter-Dw0MTS3D.d.ts → fetch-adapter-C1DCvpqV.d.ts} +9 -6
  144. package/dist/{fetch-adapter-DD_gfmrC.js → fetch-adapter-JL43jD3E.js} +14 -11
  145. package/dist/fields-D0vUI6dq.js +498 -0
  146. package/dist/{files-C5ZI27eV.js → files-B3AA05f1.js} +21 -5
  147. package/dist/{files-D_JIzUrP.js → files-BSjrtIHw.js} +11 -4
  148. package/dist/{fixtures-B7XrLeY7.js → fixtures-A1_sp_tR.js} +15 -6
  149. package/dist/{flags-C3DktBDY.js → flags-DXjoTVbS.js} +2 -2
  150. package/dist/{freeze-CrU8nQeX.js → freeze-jjan0lCv.js} +4 -1
  151. package/dist/{from-snapshot-CTCMsFZk.js → from-snapshot-DCakWRsr.js} +26 -16
  152. package/dist/gen-B5kvbAoZ.js +58 -0
  153. package/dist/gen-DWCYcMQM.js +827 -0
  154. package/dist/generate-CRVV5V7l.js +165 -0
  155. package/dist/generate-CezbCaTI.js +275 -0
  156. package/dist/{grantee-hxrUsbpM.js → grantee-Dke-9rd1.js} +4 -4
  157. package/dist/{grantee-conditions-CcXf8hMM.js → grantee-conditions-BrYPe-w0.js} +12 -12
  158. package/dist/{grantee-match-DbENBHLB.js → grantee-match-7RZZvoT_.js} +12 -6
  159. package/dist/{graph-sql-B-dsAT6j.d.ts → graph-sql-BwFC4Qjs.d.ts} +3 -3
  160. package/dist/h3/index.d.ts +10 -7
  161. package/dist/h3/index.js +3 -3
  162. package/dist/headers-B9jQxjtW.d.ts +10 -0
  163. package/dist/{helpers-CBYVMjwh.js → helpers-DJicaG6f.js} +40 -28
  164. package/dist/hono/index.d.ts +16 -10
  165. package/dist/hono/index.js +5 -5
  166. package/dist/{hosted-BPk51-kX.d.ts → hosted-Cmtxogd0.d.ts} +44 -20
  167. package/dist/{hosted-B46-5LYE.js → hosted-D0IzESu_.js} +68 -25
  168. package/dist/{http-ujKm0JXE.js → http-DH94eh6f.js} +4 -4
  169. package/dist/{http-BBoKefeS.js → http-DKLXrFGk.js} +13 -7
  170. package/dist/{http-DXJ37RDA.js → http-DOL33cO0.js} +11 -8
  171. package/dist/{import-wyqXUyjm.js → import-CST57XLp.js} +64 -51
  172. package/dist/{import-CNV7I2Dw.js → import-_YqhhwvX.js} +6 -6
  173. package/dist/{index-B8YjkEZ4.d.ts → index-B8s204GL.d.ts} +64 -52
  174. package/dist/{index-Ch8LpNDp.d.ts → index-BT5-1VsX.d.ts} +64 -25
  175. package/dist/{index-DuWxc5dt.d.ts → index-BW_0MZOS.d.ts} +388 -149
  176. package/dist/index-BhxEUY9J.d.ts +233 -0
  177. package/dist/index-Bv5iXPqU.d.ts +210 -0
  178. package/dist/{index-C-6Letxg.d.ts → index-_9UiQIkt.d.ts} +164 -77
  179. package/dist/index.d.ts +22 -21
  180. package/dist/index.js +35 -33
  181. package/dist/{init-GG6Jh8HD.js → init-Dz_PsHdv.js} +5 -5
  182. package/dist/{init-DeMYsnYJ.js → init-cE3A8aXq.js} +10 -10
  183. package/dist/{instance-options-BsVpyGIk.d.ts → instance-options-B0KXqTrJ.d.ts} +4 -4
  184. package/dist/{instance-snapshot-BOEhcqIF.js → instance-snapshot--JWiTrvO.js} +81 -56
  185. package/dist/{instant-BluqDvyS.js → instant-DK3Iz83e.js} +2 -2
  186. package/dist/{internals-Dk8A8fX3.js → internals-B3IgHrvo.js} +6 -3
  187. package/dist/{json-schema-B-vAwlde.js → json-schema-BPGa-RjQ.js} +9 -3
  188. package/dist/{json-schema-CN_iLFH2.d.ts → json-schema-BfXgnRNT.d.ts} +4 -4
  189. package/dist/jwt/index.d.ts +125 -17
  190. package/dist/jwt/index.js +157 -16
  191. package/dist/{kernel-D0NVb1cK.js → kernel-C-fYydV2.js} +151 -16
  192. package/dist/kysely/index.d.ts +49 -33
  193. package/dist/kysely/index.js +9 -7
  194. package/dist/{live-options-BaAFCIiu.js → live-options-BBIT68le.js} +2 -2
  195. package/dist/{load-DFS_ry0W.js → load-DjSU5KXO.js} +6 -6
  196. package/dist/load-Dm0pSYH3.js +1 -0
  197. package/dist/{login-Dfe5RlYk.js → login-D-8tMqzo.js} +4 -4
  198. package/dist/{login-imhuwvn3.js → login-DplQ0s84.js} +18 -15
  199. package/dist/{manifest-dvL2XEe1.js → manifest-Cgiq5oqc.js} +67 -37
  200. package/dist/{manifest-mx5ghSFM.d.ts → manifest-CzzWtnAb.d.ts} +144 -28
  201. package/dist/{manifest-schema-C0kK_8V0.js → manifest-schema-2r37XRhD.js} +80 -9
  202. package/dist/{manifest-C8IzW3Do.js → manifest-wg8WkkpD.js} +11 -7
  203. package/dist/{map-claims-D3oRmTk0.js → map-claims-BNWCJtmP.js} +27 -18
  204. package/dist/{map-error-Dr70oA_3.js → map-error-pasIGqMT.js} +7 -7
  205. package/dist/{markers-C6joDI3o.js → markers-BeHZ4x9Q.js} +20 -5
  206. package/dist/mcp/index.d.ts +154 -47
  207. package/dist/mcp/index.js +200 -41
  208. package/dist/{migrate-CIq4-E9F.js → migrate-B7oP_sQU.js} +102 -31
  209. package/dist/module-values-BJTBdo6y.js +107 -0
  210. package/dist/nest/index.d.ts +23 -13
  211. package/dist/nest/index.js +18 -11
  212. package/dist/next/client.d.ts +5 -2
  213. package/dist/next/client.js +1 -1
  214. package/dist/next/index.d.ts +72 -26
  215. package/dist/next/index.js +110 -6
  216. package/dist/next/plugin.d.ts +1 -1
  217. package/dist/next/plugin.js +4 -3
  218. package/dist/node/index.d.ts +5 -5
  219. package/dist/node/index.js +3 -3
  220. package/dist/{ocsf-BFp-PZZA.js → ocsf-TcjRBgIs.js} +15 -5
  221. package/dist/openai/index.d.ts +13 -7
  222. package/dist/openai/index.js +3 -3
  223. package/dist/openapi/index.d.ts +61 -30
  224. package/dist/openapi/index.js +2 -2
  225. package/dist/{openapi-B7HZ0f-r.js → openapi-BOsx0xn9.js} +34 -24
  226. package/dist/{openapi-C4dDTVEd.js → openapi-CBZUhA3l.js} +4 -5
  227. package/dist/{openapi-DD1SV-5-.js → openapi-VeHHU2l9.js} +23 -14
  228. package/dist/{operation-findings-DgATDpvh.js → operation-findings-CXtFzlKJ.js} +3 -3
  229. package/dist/{operations-X0VM7F3m.d.ts → operations-Ci_rT6Uo.d.ts} +10 -7
  230. package/dist/orpc/index.d.ts +68 -14
  231. package/dist/orpc/index.js +62 -13
  232. package/dist/otel/index.d.ts +7 -4
  233. package/dist/otel/index.js +8 -5
  234. package/dist/output-files-BHOIlrfD.js +1 -0
  235. package/dist/{output-files-DhRBBE4n.js → output-files-BIQ_9pIs.js} +29 -17
  236. package/dist/{output-1im__-08.js → output-tkju6U7R.js} +15 -7
  237. package/dist/ownership-B4L59T9u.js +1054 -0
  238. package/dist/parse-1_ZheTW5.js +151 -0
  239. package/dist/parse-local-manifest-BQhTlLQS.d.ts +10 -0
  240. package/dist/parse-local-manifest-DEiCF4ah.js +69 -0
  241. package/dist/{parse-request-D_tUXG6M.js → parse-request-CwCZpyjV.js} +7 -8
  242. package/dist/{paths-DXSHAMIa.js → paths-CUvURzHp.js} +4 -1
  243. package/dist/pdp/index.d.ts +9 -6
  244. package/dist/pdp/index.js +23 -12
  245. package/dist/{peer-CRvC7Hg1.js → peer-C9LvBCcb.js} +1 -1
  246. package/dist/peer-Q0fuZ6hx.js +982 -0
  247. package/dist/{permdock-SV8ZINYT.d.ts → permdock-B6Sf4T_S.d.ts} +270 -117
  248. package/dist/{permdock-BrAscV28.js → permdock-L-_I_3zP.js} +226 -74
  249. package/dist/{permission-registry-Br4jSNVi.js → permission-registry-B8tYxPIt.js} +24 -11
  250. package/dist/{vocabulary-CN8H_9UF.d.ts → permission-types-CAY-GU7e.d.ts} +130 -104
  251. package/dist/{permissions-u_KHg-sN.d.ts → permissions-Bx6UI8n1.d.ts} +6 -2
  252. package/dist/{permissions-B-wCP5ZC.js → permissions-CVXmRILq.js} +3 -3
  253. package/dist/{permissions-BPxlwVhN.js → permissions-mDcTWoBs.js} +8 -5
  254. package/dist/pg-DQTqRrou.js +60 -0
  255. package/dist/{pins-8ILTftC9.js → pins-nqarl3oE.js} +4 -1
  256. package/dist/pkce-CnmmYqUZ.js +21 -0
  257. package/dist/{policy-77FSAq_V.d.ts → policy-Ba4bDhaj.d.ts} +5 -5
  258. package/dist/{policy-DbM5YYtV.js → policy-CLQ8__GF.js} +49 -20
  259. package/dist/{policy-from-catalog-C2gMa2Dj.js → policy-from-catalog-CJgOm6aq.js} +28 -25
  260. package/dist/{policy-module-CTlwMvLI.js → policy-module-xym9fnSq.js} +12 -6
  261. package/dist/{policy-types-n0uRTsW8.d.ts → policy-types-CCiDRZBH.d.ts} +802 -334
  262. package/dist/postgres-store-DcyrPS-r.js +1028 -0
  263. package/dist/{postgrest-DAjUkF9w.js → postgrest-D4nuPXIQ.js} +88 -36
  264. package/dist/{postgrest-CEgRbefE.d.ts → postgrest-DicQyqm-.d.ts} +60 -30
  265. package/dist/powersync-DHNQX06W.js +1117 -0
  266. package/dist/{powersync-DISt6SUe.js → powersync-DXqGuq9A.js} +3 -3
  267. package/dist/powersync-Djf-zCpB.js +1 -0
  268. package/dist/preset/index.d.ts +38 -28
  269. package/dist/preset/index.js +1 -1
  270. package/dist/{preset-DZdU0tne.js → preset-BjLzeFpp.js} +58 -34
  271. package/dist/{principal-source-pHneukrx.js → principal-source-C6N60GtM.js} +10 -10
  272. package/dist/prisma/index.d.ts +69 -41
  273. package/dist/prisma/index.js +21 -17
  274. package/dist/{problem-CTviyH5L.js → problem-BVLHFK33.js} +41 -21
  275. package/dist/{problem-details-cn5X30gZ.js → problem-details-CLDgX1KV.js} +13 -4
  276. package/dist/{problem-types-CJ2325SZ.js → problem-types-DSsRfONQ.js} +4 -1
  277. package/dist/prompt-BYc6drYW.js +938 -0
  278. package/dist/protected-action-BE7N_Luo.d.ts +58 -0
  279. package/dist/protected-action-DUYqb-Ma.js +80 -0
  280. package/dist/{protected-BlF0F6wL.js → protected-action-QEA8arRB.js} +48 -14
  281. package/dist/{provider-B1al-AZz.js → provider-C55OGXJ7.js} +5 -4
  282. package/dist/{provider-DILL9oPK.d.ts → provider-DXfq5c2d.d.ts} +1 -1
  283. package/dist/{provider-store-DM6DNcta.js → provider-store-CaTv00tr.js} +13 -9
  284. package/dist/pull-B65ndFNZ.js +1 -0
  285. package/dist/{pull-jEwbf80J.js → pull-CwpGaQSi.js} +12 -11
  286. package/dist/{push-BD4f15co.js → push-DgssgzkK.js} +62 -16
  287. package/dist/{pushed-policy-DCW91KQU.d.ts → pushed-policy-D1Eoi6NM.d.ts} +12 -6
  288. package/dist/{pushed-policy-CqtxkV4G.js → pushed-policy-ITwYKvvH.js} +10 -7
  289. package/dist/react/index.d.ts +9 -8
  290. package/dist/react/index.js +7 -6
  291. package/dist/react/provider-client.d.ts +1 -1
  292. package/dist/react/provider-client.js +1 -1
  293. package/dist/react-native/index.d.ts +104 -51
  294. package/dist/react-native/index.js +394 -67
  295. package/dist/{refs-K6eWUJ0H.js → refs-C8BKxc_e.js} +17 -3
  296. package/dist/{relation-walk-3OyDBF0S.js → relation-walk-DK_gkgL7.js} +12 -9
  297. package/dist/{relations-BnCWnJGI.js → relations-BxvRvDHK.js} +3 -3
  298. package/dist/{replay-Du1tJjG7.js → replay-BU09ad4L.js} +1 -1
  299. package/dist/{replay-Bkm-Ne4l.d.ts → replay-DNftIb_R.d.ts} +1 -1
  300. package/dist/{request-context-C81Pip9n.js → request-context-C25ilqiJ.js} +8 -8
  301. package/dist/{request-schema-CMreVuFM.js → request-schema-DhX7UbfQ.js} +4 -1
  302. package/dist/resend/index.d.ts +34 -28
  303. package/dist/resend/index.js +29 -29
  304. package/dist/resolve-names-CF7xXLSG.js +201 -0
  305. package/dist/{resource-relations-D_jw0BFx.js → resource-relations-8-pCScBO.js} +18 -9
  306. package/dist/{retire-CALjIQ7e.js → retire-B2xffQgY.js} +15 -6
  307. package/dist/{revocations-R07lFBnX.d.ts → revocations-BaUXHM1N.d.ts} +6 -3
  308. package/dist/{rls-DTvYd-lI.js → rls-CNEH2_BG.js} +743 -624
  309. package/dist/{rls-BAqP4q66.js → rls-Ca8Jz9b5.js} +41 -10
  310. package/dist/{role-builder-uWQZ4v84.js → role-builder-BK2-T6oD.js} +41 -20
  311. package/dist/rolldown-runtime-C3I60J0A.js +4 -0
  312. package/dist/{row-conditions-h2HythXQ.js → row-conditions-BAGHOmVq.js} +4 -3
  313. package/dist/{rpc-VSl5ylaQ.js → rpc-fNqAIqQk.js} +8 -5
  314. package/dist/{run-CUKuz0A8.js → run-Bz26OTfO.js} +26 -23
  315. package/dist/run-D-JE8QWb.js +1 -0
  316. package/dist/{run-ticket-DocwaTKn.d.ts → run-ticket-DbWI9f99.d.ts} +33 -13
  317. package/dist/{run-ticket-sxFI6LAk.js → run-ticket-Dv7aF7Dk.js} +29 -8
  318. package/dist/{runtime-D3VTAiCA.js → runtime-BGnrOkVH.js} +5 -2
  319. package/dist/{saas-BCXRuXca.js → saas-iWnUdwCt.js} +42 -29
  320. package/dist/{scan-qvOe0hS3.js → scan-DCj1KQfn.js} +1 -1
  321. package/dist/{schema-B4v0Qrac.js → schema-BOJjqlWL.js} +3 -2
  322. package/dist/{schema-yLuBWWsh.js → schema-Dst9I9Gs.js} +9 -5
  323. package/dist/scim/index.d.ts +3 -2
  324. package/dist/scim/index.js +3 -1
  325. package/dist/scim-D8YupzX1.js +44 -0
  326. package/dist/{scim-ClHtpyV3.js → scim-jJBJ7g1Z.js} +35 -551
  327. package/dist/{scopes-CUPSjo9q.js → scopes-0Up_UA2t.js} +42 -24
  328. package/dist/server/index.d.ts +12 -12
  329. package/dist/server/index.js +13 -13
  330. package/dist/{server-C3WXAHZj.js → server-3cVlTLde.js} +31 -31
  331. package/dist/{server-types-CKQiCnON.d.ts → server-types-CIlpSA2G.d.ts} +81 -46
  332. package/dist/{sha256-D9ihqmvd.js → sha256-C-4WXKIu.js} +4 -3
  333. package/dist/{shared-BAK8t4Uo.js → shared-Afctg7B0.js} +100 -61
  334. package/dist/{shared-DvvFzYXe.js → shared-l0oGGetg.js} +9 -6
  335. package/dist/{signals-D3NcTKUS.js → signals-B1vHR3fN.js} +2 -2
  336. package/dist/{sink-BEZSlMS_.js → sink-D9_vh372.js} +32 -21
  337. package/dist/{sink-4-eQIRWf.d.ts → sink-Dvi7YZG_.d.ts} +44 -20
  338. package/dist/{skills-B6lVdD_W.js → skills-Bjj7BNEQ.js} +16 -12
  339. package/dist/{skills-CTYwkl44.js → skills-OuoMR49L.js} +3 -3
  340. package/dist/{snapshot-B7DkYg6g.js → snapshot-CXlzi69u.js} +21 -18
  341. package/dist/{snapshot-cache-PXI-vGpY.d.ts → snapshot-cache-BAXZ0KOp.d.ts} +21 -12
  342. package/dist/{snapshot-cache-DZHQ0KF6.js → snapshot-cache-Bx_4xC_6.js} +14 -11
  343. package/dist/solid/index.d.ts +55 -15
  344. package/dist/solid/index.js +46 -12
  345. package/dist/solid-start/index.d.ts +30 -12
  346. package/dist/solid-start/index.js +3 -3
  347. package/dist/sources-B1Os_YUu.d.ts +312 -0
  348. package/dist/{sources-vs5dGOsS.js → sources-CMAM-2A3.js} +156 -51
  349. package/dist/sources-qoIJQrJv.js +11 -0
  350. package/dist/{sql-B5T1IL83.js → sql-0WjxIp9g.js} +32 -7
  351. package/dist/sql-B80DFdpG.d.ts +10 -0
  352. package/dist/{src-Cu2m75uo.js → src-DZ0l9UAg.js} +305 -123
  353. package/dist/ssf/index.d.ts +4 -4
  354. package/dist/ssf/index.js +665 -224
  355. package/dist/{standard-webhooks-rbD7LS6h.d.ts → standard-webhooks-C-qX4mJW.d.ts} +7 -7
  356. package/dist/{standard-webhooks-DgFFD-Ga.js → standard-webhooks-D67a3nzk.js} +5 -5
  357. package/dist/{store-BDutJkDL.js → store-RNFvaJJv.js} +43 -29
  358. package/dist/{store-options-u0ycEe8s.js → store-options-Dil3lraY.js} +105 -15
  359. package/dist/stores-C0Kta5Ft.d.ts +59 -0
  360. package/dist/{stores-CGhM8BN_.js → stores-DxZWDXCs.js} +42 -5
  361. package/dist/{stream-CvdUS9V1.js → stream-CDvF0sJm.js} +5 -5
  362. package/dist/{stream-CeCMJmBB.d.ts → stream-ltuLdtTG.d.ts} +4 -3
  363. package/dist/{style-DiKWnAvn.js → style-DblOtSga.js} +1 -1
  364. package/dist/{style-Uao76wO7.js → style-Ds11Hmk4.js} +3 -1
  365. package/dist/{subject-settings-CkEwrw3W.js → subject-settings-8sH5HlII.js} +8 -4
  366. package/dist/{row-check-Dt35czLy.d.ts → subject-settings-vNW8qmUH.d.ts} +30 -18
  367. package/dist/supabase/index.d.ts +48 -37
  368. package/dist/supabase/index.js +6 -6
  369. package/dist/supabase/middleware.d.ts +19 -13
  370. package/dist/supabase/middleware.js +7 -7
  371. package/dist/{supabase-x5JtR2tc.js → supabase-BL1qJzzY.js} +82 -36
  372. package/dist/{supabase-By_frk3U.js → supabase-D39cuGPt.js} +3 -3
  373. package/dist/{supabase-hook-BMoEhZir.js → supabase-hook-B-yXnzUl.js} +1206 -286
  374. package/dist/supabase-hook-H-MtE9Wd.js +1 -0
  375. package/dist/supautils-BU-pAEpf.js +808 -0
  376. package/dist/{support-session-C6xJQYut.js → support-session-BsmPCbz8.js} +22 -22
  377. package/dist/svelte/PermissionBoundary.svelte +2 -6
  378. package/dist/svelte/Protected.svelte +2 -8
  379. package/dist/svelte/index.d.ts +9 -8
  380. package/dist/svelte/index.js +10 -9
  381. package/dist/svelte/runtime.d.ts +4 -4
  382. package/dist/svelte/runtime.js +4 -4
  383. package/dist/svelte/source.d.ts +9 -8
  384. package/dist/svelte/source.js +6 -5
  385. package/dist/sveltekit/index.d.ts +30 -12
  386. package/dist/sveltekit/index.js +2 -2
  387. package/dist/{ownership-DqaJNJPn.js → tables-DeMNEy6V.js} +1025 -1085
  388. package/dist/tanstack-start/index.d.ts +22 -10
  389. package/dist/tanstack-start/index.js +2 -2
  390. package/dist/{target-Bw37TFNA.js → target-qdEAnnhU.js} +6 -6
  391. package/dist/{tenancy-DN2QAx9z.js → tenancy-5d1zVBvK.js} +36 -21
  392. package/dist/terminal/index.d.ts +23 -14
  393. package/dist/terminal/index.js +17 -20
  394. package/dist/testing/core.d.ts +2 -0
  395. package/dist/testing/core.js +1 -0
  396. package/dist/testing/fixtures/index.d.ts +15 -0
  397. package/dist/testing/fixtures/index.js +831 -0
  398. package/dist/testing/index.d.ts +235 -285
  399. package/dist/testing/index.js +502 -927
  400. package/dist/testing/saas/index.d.ts +43 -27
  401. package/dist/testing/saas/index.js +2 -2
  402. package/dist/testing/saas/permissions.d.ts +1 -1
  403. package/dist/testing/saas/permissions.js +1 -1
  404. package/dist/text-diff-BtfSmdUo.js +444 -0
  405. package/dist/{thenable-yD7SabqG.js → thenable-BNSM1U2R.js} +12 -3
  406. package/dist/{time-DkWjdL98.js → time-nR1JesoH.js} +32 -8
  407. package/dist/{token-BLZcjQgt.js → token-DC9EQiet.js} +15 -13
  408. package/dist/{tool-refusal-CMLN5xhn.js → tool-refusal-Qpjs_W6M.js} +5 -2
  409. package/dist/trpc/index.d.ts +11 -8
  410. package/dist/trpc/index.js +15 -9
  411. package/dist/type-helpers-BIPIDa6a.d.ts +50 -0
  412. package/dist/{types-8RnwUAvE.d.ts → types-AMJ5qoU_.d.ts} +49 -30
  413. package/dist/{types-C_IiEs64.d.ts → types-B2mlGuhW.d.ts} +14 -11
  414. package/dist/{types-_WhUP7ud.d.ts → types-B4uqKDnp.d.ts} +25 -12
  415. package/dist/{types-CX7AvRqP.d.ts → types-BJk0BKRX.d.ts} +24 -14
  416. package/dist/types-B_d_g7T3.d.ts +262 -0
  417. package/dist/{types-BdtSDbBO.d.ts → types-B_tiYvk8.d.ts} +59 -32
  418. package/dist/{types-DqzsnlSA.js → types-CcUXciSj.js} +17 -5
  419. package/dist/{types-Dfv0aTER.d.ts → types-DC1Loeh3.d.ts} +449 -168
  420. package/dist/{types-lUlG3TVd.d.ts → types-DHKPcqzr.d.ts} +2 -2
  421. package/dist/{types-C791HFu9.d.ts → types-DjuMBNF9.d.ts} +68 -32
  422. package/dist/{types-C863OQiR.d.ts → types-HzqOtvsJ.d.ts} +23 -12
  423. package/dist/{types-Druu9wLM.d.ts → types-ZzRGoViD.d.ts} +77 -24
  424. package/dist/{types-BzzaHxt3.d.ts → types-ah9NlU6j.d.ts} +137 -48
  425. package/dist/{types-CVZSNocA.d.ts → types-djVzLlIV.d.ts} +31 -14
  426. package/dist/{types-BtHtcKMr.d.ts → types-yUuPR5Hy.d.ts} +47 -24
  427. package/dist/unplugin/index.d.ts +1 -1
  428. package/dist/unplugin/index.js +2 -2
  429. package/dist/usage-CYeXek0p.js +1 -0
  430. package/dist/{usage-SveTRTyr.js → usage-CxEzS4ey.js} +50 -26
  431. package/dist/{usage-CRVl9V3t.js → usage-ab0PdusX.js} +2 -2
  432. package/dist/{validation-error-ZfUHbKvf.js → validation-error-CYg2kJEw.js} +4 -1
  433. package/dist/{validation-error-DHUrAkku.d.ts → validation-error-YGg2mXyH.d.ts} +5 -2
  434. package/dist/{validity-CZ7-y0Cj.js → validity-Buc7w8E9.js} +13 -10
  435. package/dist/{verifier-DHuh-okk.js → verifier-DUV6pLqZ.js} +10 -4
  436. package/dist/{verify-BaAKx7t6.js → verify-DZeqoWu5.js} +417 -489
  437. package/dist/version-DMQ0dnN5.js +48 -0
  438. package/dist/version-DizhmD72.js +1 -0
  439. package/dist/{views-DTIIO-oQ.js → views-CD8wuL4Z.js} +19 -6
  440. package/dist/vocabulary-DEJ5j4mR.d.ts +70 -0
  441. package/dist/vue/index.d.ts +71 -21
  442. package/dist/vue/index.js +50 -13
  443. package/dist/{waiter-B986w1oK.d.ts → waiter-Bu9zAlE0.d.ts} +13 -10
  444. package/dist/{waiter-BGUtGYoU.js → waiter-aBLM63_f.js} +1 -1
  445. package/dist/{watch-vea54PRw.js → watch-Cx_WciLE.js} +8 -8
  446. package/dist/watch-DB42U1ib.js +1 -0
  447. package/dist/webmcp/index.d.ts +11 -11
  448. package/dist/webmcp/index.js +8 -5
  449. package/dist/{where-scope-C9vfEphl.js → where-scope-DlCdCM_8.js} +56 -30
  450. package/dist/{who-can-DWBAcaCQ.js → who-can-rI7dCR9Q.js} +23 -20
  451. package/dist/workflow/index.d.ts +3 -3
  452. package/dist/workflow/index.js +13 -13
  453. package/dist/workflow-sdk/index.d.ts +19 -13
  454. package/dist/workflow-sdk/index.js +6 -6
  455. package/dist/{wrap-gX61EtpB.js → wrap-B2wziJI_.js} +11 -5
  456. package/dist/{wrap-C5jC6sKI.d.ts → wrap-DlJE3gS_.d.ts} +8 -5
  457. package/package.json +25 -14
  458. package/schemas/catalog-v1.json +3 -0
  459. package/schemas/cli-result-v1.json +1 -0
  460. package/schemas/cloud-api-v1.json +152 -7
  461. package/schemas/connector-v1.json +8 -0
  462. package/schemas/supabase-claims-v1.json +5 -0
  463. package/schemas/supabase-manifest-v1.json +308 -75
  464. package/skills/permdock/SKILL.md +1 -0
  465. package/skills/permdock-agents/references/runtimes.md +3 -1
  466. package/skills/permdock-approvals/references/stores-and-resume.md +2 -1
  467. package/skills/permdock-credentials/references/tokens-keys-links.md +8 -6
  468. package/skills/permdock-data/references/queries-and-rls.md +6 -2
  469. package/skills/permdock-tenancy/SKILL.md +1 -1
  470. package/skills/permdock-tenancy/references/ownership-and-custom-roles.md +2 -1
  471. package/skills/permdock-wire/references/adapters.md +28 -22
  472. package/skills/permdock-wire/references/policy-options.md +1 -0
  473. package/dist/catalog-BoWglDPI.js +0 -1
  474. package/dist/collect-D7eQDffE.js +0 -1
  475. package/dist/config-bs_kZeYq.js +0 -162
  476. package/dist/denied-text-CiFQNuMV.d.ts +0 -19
  477. package/dist/fields-OYG_JR78.js +0 -203
  478. package/dist/generate-CNU1pIOo.js +0 -171
  479. package/dist/headers-BIjfMaoC.js +0 -8
  480. package/dist/headers-CIbDTUTK.d.ts +0 -7
  481. package/dist/index-Cp_ilKi1.d.ts +0 -91
  482. package/dist/index-CueFnxQc.d.ts +0 -110
  483. package/dist/load-BxEtwlBS.js +0 -1
  484. package/dist/output-files-B3IZJksC.js +0 -1
  485. package/dist/package-root-Dk_ETdkp.js +0 -31
  486. package/dist/parse-Cxq0WxrA.js +0 -53
  487. package/dist/parse-local-manifest-5ODwqu8b.d.ts +0 -10
  488. package/dist/parse-local-manifest-CVSs6J9I.js +0 -34
  489. package/dist/peer-C8swJ2GH.js +0 -50
  490. package/dist/policies-DP2nqRoF.js +0 -295
  491. package/dist/powersync-BkWWCS1K.js +0 -538
  492. package/dist/powersync-MMJ2mI2q.js +0 -1
  493. package/dist/problem-details-B45V5d2b.d.ts +0 -7
  494. package/dist/prompt-DfZLJLIn.js +0 -32
  495. package/dist/protected-RFGpakN9.d.ts +0 -38
  496. package/dist/pull-CvpINcMK.js +0 -1
  497. package/dist/run-DDwUaJDU.js +0 -1
  498. package/dist/sources-Xc4zkszI.d.ts +0 -190
  499. package/dist/stores-BUGNbJTt.d.ts +0 -30
  500. package/dist/supabase-hook-CCw5m35C.js +0 -1
  501. package/dist/supautils-B6HYL9eL.js +0 -91
  502. package/dist/text-diff-BQcUapOb.js +0 -15
  503. package/dist/types-B3_ooZgZ.d.ts +0 -172
  504. package/dist/usage-CZpSQP6L.js +0 -1
  505. package/dist/version-CEbQ5UeM.js +0 -18
  506. package/dist/version-DXkYUISx.js +0 -1
  507. package/dist/watch--ovlPOc0.js +0 -1
@@ -1,17 +1,15 @@
1
- import { a as sole, n as isReadonlyArray, r as isRecord } from "./compact-CAf557ZB.js";
2
- import { n as isForbiddenKey } from "./paths-DXSHAMIa.js";
3
- import { l as isSqlFunctionField } from "./refs-K6eWUJ0H.js";
4
- import { u as isoAt } from "./time-DkWjdL98.js";
5
- import { c as rootScope, d as scopeList, l as scopeChain, o as resolveScope } from "./scopes-CUPSjo9q.js";
6
- import { t as isoInstant } from "./instant-BluqDvyS.js";
7
- import { m as findRole, v as listRoles } from "./grantee-hxrUsbpM.js";
8
- import { r as relationCondition, t as inheritCondition } from "./grantee-conditions-CcXf8hMM.js";
9
- import { l as partitionsOf, p as scopeField } from "./tenancy-DN2QAx9z.js";
10
- import { i as configError } from "./errors-CWTY6uih.js";
11
- import { i as scopeMembershipTable, o as relatedSql, r as scopeColumn } from "./compile-CMqMx_IN.js";
12
- import { i as qualifySqlTable, t as SQL_IDENT, u as sqlTextArray } from "./sql-B5T1IL83.js";
13
- import { d as keptKeys, l as roleColumn, u as roleManifest } from "./sources-vs5dGOsS.js";
14
- import { A as declaredArray, D as sqlType, E as scopeTypeOf, F as quoteQualified, I as quoteTable, L as textArrayLiteral, M as qualified, N as quoteIdent, O as tenantTypeOf, P as quoteLiteral, S as CLAIM, _ as subjectClaimJsonSql, b as tenantClaimSql, d as keyTenantSql, f as kindFilterSql, h as roleKindSql, i as allowEntry, j as helperSchema, k as CLI, m as memberVia, n as GLOBAL_ROLES, o as rootName, p as liveSessionSql, r as HELPERS, s as signedIn, t as CUSTOM_ROLES, u as globalKindFilterSql, v as subjectClaimSql, x as userIdHelperSql, y as subjectIdSql } from "./shared-BAK8t4Uo.js";
1
+ import { c as listPermissions, h as formerKeys } from "./permission-registry-B8tYxPIt.js";
2
+ import { a as expandRelation, c as isEdgeRelation, d as isSelfParented, l as isFieldRelation, o as groupEntries, p as restrictedFor, u as isPrincipalRelation } from "./resource-relations-8-pCScBO.js";
3
+ import { f as scopeList, l as rootScope, s as resolveScope, u as scopeChain } from "./scopes-0Up_UA2t.js";
4
+ import { o as flattenGrantee } from "./grantee-Dke-9rd1.js";
5
+ import { r as relationCondition, t as inheritCondition } from "./grantee-conditions-BrYPe-w0.js";
6
+ import { l as partitionsOf, p as scopeField } from "./tenancy-5d1zVBvK.js";
7
+ import { a as delegatedScopeMap } from "./permdock-L-_I_3zP.js";
8
+ import { i as configError } from "./errors-Vpu6rKHl.js";
9
+ import { d as sqlTextArray, i as qualifySqlTable, t as SQL_IDENT } from "./sql-0WjxIp9g.js";
10
+ import { a as scopeMembershipTable, d as restrictedRowsSql, i as scopeColumn, l as relationArmSql } from "./compile-EzEMB-7v.js";
11
+ import { d as keptKeys, l as roleColumn, u as roleManifest } from "./sources-CMAM-2A3.js";
12
+ import { A as quoteQualified, D as qualified, E as helperSchema, I as scopeTypeOf, O as quoteIdent, R as tenantTypeOf, _ as subjectClaimJsonSql, d as keyTenantSql, f as kindFilterSql, h as roleKindSql, i as allowEntry, j as quoteTable, k as quoteLiteral, m as memberVia, n as GLOBAL_ROLES, o as rootName, r as HELPERS, s as signedIn, t as CUSTOM_ROLES, u as globalKindFilterSql, v as subjectClaimSql, w as CLI, x as userIdHelperSql, y as subjectIdSql } from "./shared-Afctg7B0.js";
15
13
  //#region src/cli/sql/active.ts
16
14
  function checkActiveRow(label, row) {
17
15
  quoteTable(row.table);
@@ -84,16 +82,19 @@ function activeUserSql(ctx, user = subjectIdSql(ctx)) {
84
82
  const row = ctx.suspension?.users;
85
83
  return row === void 0 ? [] : [activeRowSql(row, user)];
86
84
  }
87
- /** The permission of a grant key held in `grant`: `key#group` and `key@level` name `key`. */
85
+ /**
86
+ * The permission of a grant key held in `grant`: `key#group` and `key@level`
87
+ * name `key`.
88
+ */
88
89
  function grantPermissionSql(grant) {
89
90
  return { sql: `split_part(split_part(${grant}, '#', 1), '@', 1)` };
90
91
  }
91
92
  /**
92
- * The active-instance check for every suspendable scope on `scope`'s chain:
93
- * its own and each ancestor's. `idOf` gives the SQL for the id the membership
94
- * holds for a scope on that chain. A scope's `keep` lets `permission` through
95
- * a suspended instance: a kept key known now drops its check, and one read
96
- * at run time is compared with the kept keys.
93
+ * The active-instance check for every suspendable scope on `scope`'s chain: its
94
+ * own and each ancestor's. `idOf` gives the SQL for the id the membership holds
95
+ * for a scope on that chain. A scope's `keep` lets `permission` through a
96
+ * suspended instance: a kept key known now drops its check, and one read at run
97
+ * time is compared with the kept keys.
97
98
  */
98
99
  function activeInstancesSql(ctx, scope, idOf, permission = ctx.permission === void 0 ? void 0 : { key: ctx.permission }) {
99
100
  const parts = [];
@@ -115,8 +116,8 @@ function activeInstancesSql(ctx, scope, idOf, permission = ctx.permission === vo
115
116
  const CLOSURE = { table: "permdock_closure" };
116
117
  /**
117
118
  * The snake_case SQL name of a resource or link (`chatThread` becomes
118
- * `chat_thread`), or `undefined` when it is not made of ASCII letters,
119
- * digits and underscores starting with a letter.
119
+ * `chat_thread`), or `undefined` when it is not made of ASCII letters, digits
120
+ * and underscores starting with a letter.
120
121
  */
121
122
  function graphSqlName(name) {
122
123
  if (!/^[A-Za-z][A-Za-z0-9_]*$/u.test(name)) return;
@@ -127,21 +128,30 @@ function sqlNameOf(name, label) {
127
128
  if (sql === void 0) throw configError(`PermDock CLI: ${label} is not a SQL name (letters, digits and underscores, starting with a letter), so rls generate cannot name its helper`);
128
129
  return sql;
129
130
  }
130
- /** The helper returning the ids of `resource` the subject holds a relation on: `permitted_<snake_case resource>_ids`. */
131
+ /**
132
+ * The helper returning the ids of `resource` the subject holds a relation on:
133
+ * `permitted_<snake_case resource>_ids`.
134
+ */
131
135
  function graphHelper(resource) {
132
136
  return permittedIdsHelper(sqlNameOf(resource, `resource '${resource}'`));
133
137
  }
134
138
  function inheritedRowsHelper(resource) {
135
139
  return `permitted_${sqlNameOf(resource, `resource '${resource}'`)}_rows`;
136
140
  }
137
- /** The helper returning the ids of `resource` whose `link` points into the ids it is given. */
141
+ /**
142
+ * The helper returning the ids of `resource` whose `link` points into the ids
143
+ * it is given.
144
+ */
138
145
  function linkHelper(resource, link) {
139
146
  return `permdock_link_${sqlNameOf(resource, `resource '${resource}'`)}_${sqlNameOf(link, `link '${link}' on ${resource}`)}`;
140
147
  }
141
148
  function restrictedHelper(resource) {
142
149
  return `permdock_restricted_${sqlNameOf(resource, `resource '${resource}'`)}`;
143
150
  }
144
- /** Graph SQL parts as RLS text: values inline as literals, the subject from the dialect's claim. */
151
+ /**
152
+ * Graph SQL parts as RLS text: values inline as literals, the subject from the
153
+ * dialect's claim.
154
+ */
145
155
  function graphSqlText(parts, ctx) {
146
156
  return parts.map((part) => {
147
157
  if ("text" in part) return part.text;
@@ -168,23 +178,51 @@ function permittedIdsHelper(name) {
168
178
  if (!/^[a-z][a-z0-9_]*$/u.test(name)) throw configError(`PermDock CLI: unsafe scope name '${name}'`);
169
179
  return `permitted_${name}_ids`;
170
180
  }
171
- /** `permitted_<scope>_ids_for(p_user, p_grant)`: the same ids for a user the caller names. */
181
+ /**
182
+ * `permitted_<scope>_ids_for(p_user, p_grant)`: the same ids for a user the
183
+ * caller names.
184
+ */
172
185
  function permittedForHelper(name) {
173
186
  return `${permittedIdsHelper(name)}_for`;
174
187
  }
175
- /** The helper returning the ids of scope `name` the subject holds any live membership of. */
188
+ /**
189
+ * The helper returning the ids of scope `name` the subject holds any live
190
+ * membership of.
191
+ */
176
192
  function memberIdsHelper(name) {
177
193
  if (!/^[a-z][a-z0-9_]*$/u.test(name)) throw configError(`PermDock CLI: unsafe scope name '${name}'`);
178
194
  return `member_${name}_ids`;
179
195
  }
180
- /** `member_<scope>_ids_for(p_user uuid)`: the same rows for a user the caller names; the token hook's helper. */
196
+ /**
197
+ * `member_<scope>_ids_for(p_user uuid)`: the same rows for a user the caller
198
+ * names; the token hook's helper.
199
+ */
181
200
  function memberForHelper(name) {
182
201
  return `${memberIdsHelper(name)}_for`;
183
202
  }
184
203
  /**
185
- * Whether `member_<scope>_ids_for` is generated for `name`: Supabase only
186
- * (its one caller is `supabase_auth_admin`), and only where `memberships` maps
187
- * a table for the scope or a membership source can hold it.
204
+ * `member_<scope>_ids_of(p_user uuid)`: `member_<scope>_ids_for(p_user)` when
205
+ * `p_user` is the caller and no rows otherwise, so `authenticated` may execute
206
+ * it; better-supabase's `teamIdsOf` template calls it.
207
+ */
208
+ function memberOfHelper(name) {
209
+ return `${memberIdsHelper(name)}_of`;
210
+ }
211
+ /** A scope better-supabase reads as its teams: `team` or `teams` (doctor BS417). */
212
+ function isTeamScope(name) {
213
+ return /^teams?$/u.test(name);
214
+ }
215
+ /**
216
+ * Whether `member_<scope>_ids_of` is generated: a team scope with
217
+ * `member_<scope>_ids_for`.
218
+ */
219
+ function hasMemberOf(input, name) {
220
+ return isTeamScope(name) && hasMemberFor(input, name);
221
+ }
222
+ /**
223
+ * Whether `member_<scope>_ids_for` is generated for `name`: Supabase only (its
224
+ * one caller is `supabase_auth_admin`), and only where `memberships` maps a
225
+ * table for the scope or a membership source can hold it.
188
226
  */
189
227
  function hasMemberFor(input, name) {
190
228
  if (input.dialect !== "supabase") return false;
@@ -199,26 +237,36 @@ function hasPermittedUsers(input, name) {
199
237
  const sources = memberForSources(input, name);
200
238
  return sources.length > 0 && sources.every((source) => source.sql.holders !== void 0);
201
239
  }
202
- /** The table `member_<scope>_ids_for` reads for scope `name`, when `memberships` maps one with its id column. */
240
+ /**
241
+ * The table `member_<scope>_ids_for` reads for scope `name`, when `memberships`
242
+ * maps one with its id column.
243
+ */
203
244
  function memberForTable(input, name) {
204
245
  const table = scopeMembershipTable(input.memberships, input.scopes, name);
205
246
  return table !== void 0 && scopeColumn(table, input.scopes, name) !== void 0 ? table : void 0;
206
247
  }
207
- /** The sources `member_<scope>_ids_for` reads: none when a table is mapped, as in `database` mode. */
248
+ /**
249
+ * The sources `member_<scope>_ids_for` reads: none when a table is mapped, as
250
+ * in `database` mode.
251
+ */
208
252
  function memberForSources(input, name) {
209
253
  if (memberForTable(input, name) !== void 0) return [];
210
254
  return (input.memberSources ?? []).filter((source) => source.sql.scope === void 0 || source.sql.scope === name);
211
255
  }
212
256
  /**
213
- * The membership sources that can hold scope `name` when `memberships` maps
214
- * no table for it: every `tableMembershipSource` source and the `junctionMembershipSource` sources of
215
- * that scope. Empty outside `database` mode.
257
+ * The membership sources that can hold scope `name` when `memberships` maps no
258
+ * table for it: every `tableMembershipSource` source and the
259
+ * `junctionMembershipSource` sources of that scope. Empty outside `database`
260
+ * mode.
216
261
  */
217
262
  function scopeSources(ctx, name) {
218
263
  if (ctx.authorize !== "database" || scopeTable(ctx, name) !== void 0) return [];
219
264
  return (ctx.sources ?? []).filter((source) => source.sql.scope === void 0 || source.sql.scope === name);
220
265
  }
221
- /** The membership table mapped for scope `name`, with the column of its id and of the first scope's id. */
266
+ /**
267
+ * The membership table mapped for scope `name`, with the column of its id and
268
+ * of the first scope's id.
269
+ */
222
270
  function scopeTable(ctx, name) {
223
271
  const table = scopeMembershipTable(ctx.memberships, ctx.scopes, name);
224
272
  if (table === void 0) return;
@@ -236,477 +284,6 @@ function scopeTable(ctx, name) {
236
284
  };
237
285
  }
238
286
  //#endregion
239
- //#region src/cli/sql/conditions.ts
240
- const MAX_CLAIM_DEPTH = 8;
241
- /**
242
- * The claim path of a `principal.claim.*` / `principal.claims.*` ref, one
243
- * segment per JSON key; `undefined` for any other ref.
244
- */
245
- function claimPath(ref) {
246
- const prefix = ["principal.claim.", "principal.claims."].find((item) => ref.startsWith(item));
247
- if (prefix === void 0) return;
248
- const segments = ref.slice(prefix.length).split(".");
249
- if (segments.length > MAX_CLAIM_DEPTH) throw configError(`PermDock CLI: claim path '${ref}' is deeper than ${MAX_CLAIM_DEPTH}`);
250
- for (const segment of segments) if (!CLAIM.test(segment) || isForbiddenKey(segment)) throw configError(`PermDock CLI: unsafe claim name '${segment}'`);
251
- return segments;
252
- }
253
- function jsonPath(base, keys, last) {
254
- let sql = base;
255
- for (const [index, key] of keys.entries()) sql += ` ${index === keys.length - 1 ? last : "->"} ${quoteLiteral(key)}`;
256
- return sql;
257
- }
258
- /**
259
- * The JSON document a claim path starts from and the keys to walk in it. The
260
- * `guc` dialect keeps each top-level claim in its own setting as JSON text.
261
- */
262
- function claimRoot(ctx, path) {
263
- const [head, ...rest] = path;
264
- if (head === void 0) throw configError("PermDock CLI: empty claim path");
265
- switch (ctx.dialect) {
266
- case "supabase": return {
267
- root: "(select auth.jwt())",
268
- keys: path
269
- };
270
- case "neon": return {
271
- root: "(select auth.session())",
272
- keys: path
273
- };
274
- case "guc": return {
275
- root: subjectClaimJsonSql(ctx, head),
276
- keys: rest
277
- };
278
- default: return ctx.dialect;
279
- }
280
- }
281
- /** The claim at `path` as `jsonb`. */
282
- function claimJsonSql(ctx, path) {
283
- const { root, keys } = claimRoot(ctx, path);
284
- return keys.length === 0 ? root : `(${jsonPath(root, keys, "->")})`;
285
- }
286
- /** The claim at `path` as text: `->>` on the last key, or the plain setting for a one-segment `guc` claim. */
287
- function claimTextSql(ctx, path) {
288
- const [head] = path;
289
- if (path.length === 1 && head !== void 0) return subjectClaimSql(ctx, head);
290
- const { root, keys } = claimRoot(ctx, path);
291
- return `(${jsonPath(root, keys, "->>")})`;
292
- }
293
- /** The JSON kind a claim must have to compare with a column of `type`. */
294
- function jsonKindOf(type) {
295
- if (type === "numeric") return "number";
296
- return type === "boolean" ? "boolean" : "string";
297
- }
298
- /**
299
- * A claim compared with a column of `type`. Numbers and booleans must be JSON
300
- * of that kind (anything else is `null`, so the comparison is false, as in
301
- * memory); other types cast the text form. A one-segment `guc` claim is text
302
- * and is cast as is.
303
- */
304
- function typedClaimSql(ctx, path, type) {
305
- const text = claimTextSql(ctx, path);
306
- if (type === void 0 || type === "text") return text;
307
- const cast = `${text}::${sqlType(type)}`;
308
- const kind = jsonKindOf(type);
309
- if (kind === "string" || ctx.dialect === "guc" && path.length === 1) return `(${cast})`;
310
- return `(case when jsonb_typeof(${claimJsonSql(ctx, path)}) = '${kind}' then ${cast} end)`;
311
- }
312
- /**
313
- * The elements of an array claim as a Postgres array of the column's type,
314
- * built once per statement (an uncorrelated `array(select ...)` is an InitPlan).
315
- * A missing or non-array claim is the empty array; elements of another JSON
316
- * kind are left out.
317
- */
318
- function claimArraySql(ctx, path, type) {
319
- const json = claimJsonSql(ctx, path);
320
- return `array(select ${type === void 0 || type === "text" ? `(e #>> '{}')` : `(e #>> '{}')::${sqlType(type)}`} from jsonb_array_elements(case when jsonb_typeof(${json}) = 'array' then ${json} else '[]'::jsonb end) e where jsonb_typeof(e) = '${jsonKindOf(type)}')`;
321
- }
322
- function sqlValue(value, ctx, field) {
323
- if (value !== null && typeof value === "object" && "ref" in value) return compileRef(value.ref, ctx, field);
324
- if (value !== null && typeof value === "object" && "date" in value) {
325
- const instant = isoInstant(value.date);
326
- return instant === void 0 ? "null" : quoteLiteral(isoAt(instant));
327
- }
328
- if (isReadonlyArray(value)) return `array[${value.map((item) => sqlValue(item, ctx)).join(", ")}]`;
329
- if (value === null) return "null";
330
- if (typeof value === "boolean") return value ? "true" : "false";
331
- if (typeof value === "number") return Number.isFinite(value) ? String(value) : "null";
332
- if (typeof value === "string") return quoteLiteral(value);
333
- throw configError("PermDock CLI: non-portable condition value");
334
- }
335
- function compileRef(ref, ctx, field) {
336
- if (ref === "principal.id") return subjectIdSql(ctx);
337
- if (ref === "now") return "now()";
338
- const path = claimPath(ref);
339
- if (ref === "principal.tenant" || path?.length === 1 && path[0] === ctx.tenantClaim) return tenantClaimSql(ctx);
340
- if (path !== void 0) return typedClaimSql(ctx, path, field === void 0 ? void 0 : ctx.columnTypes?.[field]);
341
- if (ref === "context" || ref.startsWith("context.")) throw configError(`PermDock CLI: '${ref}' is request context, which is not in the token; RLS cannot read it (permdock doctor PD027)`);
342
- throw configError(`PermDock CLI: non-portable subject ref '${ref}'`);
343
- }
344
- /** `value` with `\`, `%` and `_` escaped for `like ... escape '\'`, as `escapeLike` does in process. */
345
- function likeLiteralSql(value) {
346
- return `replace(replace(replace(${value}, '\\', '\\\\'), '%', '\\%'), '_', '\\_')`;
347
- }
348
- function compareSql(op, field, value) {
349
- const left = quoteIdent(field);
350
- switch (op) {
351
- case "eq": return `${left} = ${value}`;
352
- case "ne": return `${left} <> ${value}`;
353
- case "gt": return `${left} > ${value}`;
354
- case "gte": return `${left} >= ${value}`;
355
- case "lt": return `${left} < ${value}`;
356
- case "lte": return `${left} <= ${value}`;
357
- case "contains": return `${left}::text like '%' || ${likeLiteralSql(`${value}::text`)} || '%' escape '\\'`;
358
- default: return op;
359
- }
360
- }
361
- /**
362
- * Whether the row's `rowField` is among the ids in `table.rowColumn` the
363
- * subject holds a matching membership on. The row field stays outside the
364
- * subquery: inside it, an unqualified name resolves to the membership table
365
- * first, so `m.org_id = "org_id"` would compare the membership with itself.
366
- */
367
- function membershipSql(table, rowColumn, rowField, roles, ctx, tenantColumn, scope) {
368
- const parts = [`m.${quoteIdent(table.user)} = ${subjectIdSql(ctx)}`];
369
- const role = memberRoleOf(table);
370
- if (roles.length > 0) {
371
- const held = role.through === void 0 && !role.lateral ? `m.${quoteIdent(role.column)}` : role.lookup;
372
- parts.push(`${held} = any(${textArrayLiteral(roles)})`);
373
- const via = memberVia(table);
374
- const single = sole(roles);
375
- const kind = single === void 0 ? kindFilterSql(ctx, role.lookup, via) : roleKindSql(ctx, single, via);
376
- if (kind !== void 0) parts.push(kind);
377
- }
378
- if (table.expiresAt !== void 0) parts.push(`(m.${quoteIdent(table.expiresAt)} is null or m.${quoteIdent(table.expiresAt)} > now())`);
379
- parts.push(...activeMembershipSql(ctx, table.disabledAt === void 0 ? void 0 : `m.${quoteIdent(table.disabledAt)}`));
380
- if (tenantColumn !== void 0 && ctx.tenants !== "all") parts.push(`m.${quoteIdent(tenantColumn)} = ${tenantClaimSql(ctx)}`);
381
- parts.push(...activeUserSql(ctx));
382
- if (scope !== void 0) {
383
- const tenantOf = scope === rootScope(ctx.scopes) ? rowColumn : tenantColumn;
384
- const keyed = keyTenantSql(ctx, tenantOf === void 0 ? void 0 : `m.${quoteIdent(tenantOf)}`);
385
- if (keyed !== void 0) parts.push(keyed);
386
- parts.push(...activeInstancesSql(ctx, scope, (name) => {
387
- const held = scopeColumn(table, ctx.scopes, name);
388
- return held === void 0 ? void 0 : `m.${quoteIdent(held)}`;
389
- }));
390
- }
391
- const expand = roles.length > 0 && role.lateral ? role.join : "";
392
- return `${quoteIdent(rowField)} in (select m.${quoteIdent(rowColumn)} from ${quoteTable(table.table)} m${expand} where ${parts.join(" and ")})`;
393
- }
394
- function compileMemberOf(condition, ctx) {
395
- const scope = condition.scope === "resource" ? void 0 : resolveScope(ctx.scopes, condition.scope);
396
- if (condition.scope !== "resource" && scope === void 0) throw configError(`PermDock CLI: memberOf ${condition.scope} names a scope the policy does not declare`);
397
- const mapping = scope === void 0 ? condition.resource === void 0 ? void 0 : ctx.memberships?.resource?.[condition.resource] : scopeMembershipTable(ctx.memberships, ctx.scopes, scope);
398
- if (mapping !== void 0) {
399
- const named = scope === void 0 ? void 0 : scopeTable(ctx, scope);
400
- const rowColumn = scope === void 0 ? mapping.id : named?.column;
401
- if (rowColumn === void 0) throw configError(`PermDock CLI: memberships mapping for ${condition.scope} is missing the row column`);
402
- const tenantColumn = scope === void 0 || scope === rootScope(ctx.scopes) ? void 0 : named?.tenantColumn;
403
- const primary = membershipSql(mapping, rowColumn, condition.field, condition.roles, ctx, tenantColumn, scope);
404
- if (condition.scope !== "resource" || condition.parents === void 0) return primary;
405
- return `(${[primary, ...condition.parents.flatMap((parent) => {
406
- if (typeof parent === "string") return [membershipSql(mapping, rowColumn, parent, condition.roles, ctx, tenantColumn)];
407
- const hopTable = ctx.memberships?.resource?.[parent.resource];
408
- return hopTable?.id === void 0 ? [] : [membershipSql(hopTable, hopTable.id, parent.field, condition.roles, ctx)];
409
- })].join(" or ")})`;
410
- }
411
- if (scope !== void 0 && condition.roles.length === 0 && scopeSources(ctx, scope).length > 0) return `${quoteIdent(condition.field)} in (select ${qualified(ctx, memberIdsHelper(scope))}())`;
412
- if (scope !== void 0 && scope === rootScope(ctx.scopes)) {
413
- const parts = [
414
- ctx.tenants === "all" ? `${quoteIdent(condition.field)} in (select ${qualified(ctx, memberIdsHelper(scope))}())` : `${quoteIdent(condition.field)} = ${tenantClaimSql(ctx)}`,
415
- ...ctx.tenants === "all" ? [] : [keyTenantSql(ctx, quoteIdent(condition.field))].filter((part) => part !== void 0),
416
- ...activeUserSql(ctx),
417
- ...activeInstancesSql(ctx, scope, () => quoteIdent(condition.field))
418
- ];
419
- return parts.length === 1 ? parts.join("") : `(${parts.join(" and ")})`;
420
- }
421
- throw configError(`PermDock CLI: memberOf ${condition.scope} needs a memberships table mapping`);
422
- }
423
- function compileConditionSql(condition, ctx) {
424
- switch (condition.op) {
425
- case "eq":
426
- case "ne":
427
- case "gt":
428
- case "gte":
429
- case "lt":
430
- case "lte": return compareSql(condition.op, condition.field, sqlValue(condition.value, ctx, condition.field));
431
- case "contains": {
432
- const element = ctx.arrayColumns?.[condition.field];
433
- if (element !== void 0) return `${sqlValue(condition.value, {
434
- ...ctx,
435
- columnTypes: {
436
- ...ctx.columnTypes,
437
- [condition.field]: element
438
- }
439
- }, condition.field)} = any(${quoteIdent(condition.field)})`;
440
- return compareSql(condition.op, condition.field, sqlValue(condition.value, ctx, condition.field));
441
- }
442
- case "in":
443
- case "notIn": {
444
- const keyword = condition.op === "in" ? "in" : "not in";
445
- if (!Array.isArray(condition.value) && typeof condition.value === "object" && "ref" in condition.value) {
446
- const path = claimPath(condition.value.ref);
447
- if (path === void 0) {
448
- compileRef(condition.value.ref, ctx);
449
- throw configError(`PermDock CLI: non-portable ${condition.op} against '${condition.value.ref}'`);
450
- }
451
- const column = quoteIdent(condition.field);
452
- const list = claimArraySql(ctx, path, ctx.columnTypes?.[condition.field]);
453
- return condition.op === "in" ? `${column} = any (${list})` : `(${column} is not null and not (${column} = any (${list})))`;
454
- }
455
- const values = condition.value.map((item) => sqlValue(item, ctx));
456
- if (values.length === 0) return condition.op === "in" ? "false" : `${quoteIdent(condition.field)} is not null`;
457
- return `${quoteIdent(condition.field)} ${keyword} (${values.join(", ")})`;
458
- }
459
- case "isNull": return `${quoteIdent(condition.field)} is ${condition.value ? "" : "not "}null`;
460
- case "and": return `(${condition.conditions.map((item) => compileConditionSql(item, ctx)).join(" and ")})`;
461
- case "or":
462
- if (condition.conditions.length === 0) return "false";
463
- return `(${condition.conditions.map((item) => compileConditionSql(item, ctx)).join(" or ")})`;
464
- case "not": return `(${compileConditionSql(condition.condition, ctx)}) is not true`;
465
- case "memberOf": return compileMemberOf(condition, ctx);
466
- case "related": return compileRelatedSql(condition, ctx);
467
- case "sqlFunction":
468
- if (ctx.inlineFunctions === true) return compileConditionSql(condition.twin, ctx);
469
- return `${quoteTable(condition.name)}(${condition.args.map((arg) => isSqlFunctionField(arg) ? quoteIdent(arg.field) : sqlValue(arg, ctx)).join(", ")})`;
470
- case "opaque": return condition.sql;
471
- case "liveSession": return liveSessionSql(ctx);
472
- default: return condition;
473
- }
474
- }
475
- function andConditions(left, right) {
476
- if (left === void 0) return right;
477
- if (right === void 0) return left;
478
- return {
479
- op: "and",
480
- conditions: [left, right]
481
- };
482
- }
483
- function valueContextRefs(value) {
484
- if (Array.isArray(value)) return value.flatMap(valueContextRefs);
485
- if (isRecord(value) && typeof value["ref"] === "string" && (value["ref"] === "context" || value["ref"].startsWith("context."))) return [value["ref"]];
486
- return [];
487
- }
488
- /**
489
- * The `context.*` refs a condition reads. The request context is not in the
490
- * token, so RLS cannot evaluate them.
491
- */
492
- function contextRefs(condition) {
493
- if (condition === void 0) return [];
494
- switch (condition.op) {
495
- case "eq":
496
- case "ne":
497
- case "gt":
498
- case "gte":
499
- case "lt":
500
- case "lte":
501
- case "contains":
502
- case "in":
503
- case "notIn": return valueContextRefs(condition.value);
504
- case "and":
505
- case "or": return condition.conditions.flatMap((child) => contextRefs(child));
506
- case "not": return contextRefs(condition.condition);
507
- case "sqlFunction": return [...condition.args.flatMap(valueContextRefs), ...contextRefs(condition.twin)];
508
- case "isNull":
509
- case "memberOf":
510
- case "related":
511
- case "opaque":
512
- case "liveSession": return [];
513
- default: return condition;
514
- }
515
- }
516
- function sqlFunctionNames(condition) {
517
- if (condition === void 0) return [];
518
- switch (condition.op) {
519
- case "sqlFunction": return [condition.name, ...sqlFunctionNames(condition.twin)];
520
- case "and":
521
- case "or": return condition.conditions.flatMap((child) => sqlFunctionNames(child));
522
- case "not": return sqlFunctionNames(condition.condition);
523
- case "eq":
524
- case "ne":
525
- case "gt":
526
- case "gte":
527
- case "lt":
528
- case "lte":
529
- case "contains":
530
- case "in":
531
- case "notIn":
532
- case "isNull":
533
- case "memberOf":
534
- case "related":
535
- case "opaque":
536
- case "liveSession": return [];
537
- default: return condition;
538
- }
539
- }
540
- /**
541
- * A `related` condition as uncorrelated subqueries, so Postgres runs the
542
- * helper once per statement: the row's field in the closure's descendants of
543
- * the ids the subject holds the relation on (or those ids alone at depth 0).
544
- * The helper sits in `array(...)` so it stays an InitPlan; a plain `in` gets
545
- * pulled into a join that can rescan it per closure row.
546
- */
547
- function compileRelatedSql(condition, ctx) {
548
- if (condition.hops !== void 0 && condition.hops.length > 0) return compileHoppedSql(condition, ctx);
549
- const type = ctx.columnTypes?.[condition.field];
550
- const cast = type === void 0 || type === "text" ? "" : `::${type}`;
551
- const column = type === void 0 ? `${quoteIdent(condition.field)}::text` : quoteIdent(condition.field);
552
- const helper = condition.permission === void 0 ? `${qualified(ctx, graphHelper(condition.resource))}(${quoteLiteral(condition.relation)})` : `${qualified(ctx, inheritedRowsHelper(condition.resource))}(${quoteLiteral(condition.permission)})`;
553
- const cap = ctx.graph?.closures[condition.resource];
554
- let inner;
555
- if (condition.depth > 0 && cap !== void 0) {
556
- const depth = condition.depth < cap ? ` and depth <= ${String(condition.depth)}` : "";
557
- inner = `select descendant${cast} from ${qualified(ctx, CLOSURE.table)} where resource = ${quoteLiteral(condition.resource)}${depth} and ancestor = any (array(select ${helper}))`;
558
- } else inner = cast === "" ? `select ${helper}` : `select p.id${cast} from ${helper} as p(id)`;
559
- const reach = `${column} in (${inner})`;
560
- if (condition.parent === true && condition.restricted !== void 0) return `(${reach} and ${quoteIdent(condition.restricted)} is not true)`;
561
- return reach;
562
- }
563
- /**
564
- * A `related` condition that crosses link hops: the reached instances of the
565
- * last resource, carried back over each link by its security definer helper,
566
- * so the hop reads a table the subject may not see.
567
- */
568
- function compileHoppedSql(condition, ctx) {
569
- const resources = ctx.graph?.resources;
570
- if (resources === void 0) throw configError("PermDock CLI: a related condition with link hops needs the policy resources in the RLS context");
571
- const schema = quoteIdent(helperSchema(ctx));
572
- return graphSqlText(relatedSql(condition, {
573
- resources,
574
- ...ctx.graph?.tables === void 0 ? {} : { tables: ctx.graph.tables },
575
- closure: `${helperSchema(ctx)}.${CLOSURE.table}`,
576
- closureDepths: ctx.graph?.closures ?? {},
577
- qualify: qualifySqlTable,
578
- holders: (resource, relation) => [{ text: `select ${schema}.${graphHelper(resource)}(${quoteLiteral(relation)})` }],
579
- permitted: (resource, permission) => [{ text: `select ${schema}.${inheritedRowsHelper(resource)}(${quoteLiteral(permission)})` }],
580
- restrictedRows: (resource) => [{ text: `select ${schema}.${restrictedHelper(resource)}()` }],
581
- linked: (resource, link, targets) => [
582
- { text: `select ${schema}.${linkHelper(resource, link)}(array(` },
583
- ...targets,
584
- { text: "))" }
585
- ]
586
- }), ctx);
587
- }
588
- //#endregion
589
- //#region src/cli/sql/grants.ts
590
- /**
591
- * The `role_permissions` grant key of a break-glass grant. It differs from
592
- * the permission key, so `authorize()` and the table policies never count it;
593
- * only the break-glass read asks for it.
594
- */
595
- function breakGlassKey(permission) {
596
- return `${permission}#break-glass`;
597
- }
598
- /**
599
- * The role and scope holding a break-glass grant. `breakGlass()` sets
600
- * `to: authenticated`, so inside `role()` the role is on the grant itself.
601
- */
602
- function breakGlassHolder(item) {
603
- if (item.access.kind === "role") return {
604
- role: item.access.role,
605
- scope: item.access.scope
606
- };
607
- const { role, scope } = item.grant;
608
- return role === null || typeof scope !== "string" ? void 0 : {
609
- role,
610
- scope
611
- };
612
- }
613
- function granteeItems(to) {
614
- return Array.isArray(to) ? to : [to];
615
- }
616
- function relationWhere(policy, grant, grantee) {
617
- const node = policy.resources.get(grant.permission.resource);
618
- const where = relationCondition(grantee, node, scopeList(policy.scopes), {
619
- resources: policy.resources,
620
- now: { ref: "now" }
621
- });
622
- if (where === void 0) throw configError(`PermDock CLI: grant ${grant.permission.key} names relation '${grantee.relation}', which ${grant.permission.resource} does not declare`);
623
- return where;
624
- }
625
- function inheritWhere(policy, grant, grantee) {
626
- const where = inheritCondition(grantee, policy.resources.get(grant.permission.resource), policy.resources);
627
- if (where === void 0) throw configError(`PermDock CLI: grant ${grant.permission.key} inherits ${grantee.permission}, which ${grant.permission.resource} cannot reach`);
628
- return where;
629
- }
630
- /** Only a deny for `oauth-client` actors compiles: Postgres sees `client_id` and `act`, not other actor kinds. */
631
- function actorAccess(grant, grantee, others) {
632
- if (grant.effect !== "deny" || grantee.actor !== "oauth-client" || others > 0) throw configError(`PermDock CLI: grant ${grant.permission.key} is limited to an actor grantee; RLS compiles only deny(…, { to: actor('oauth-client') }) on its own, so enforce it in the application`);
633
- return {
634
- kind: "actor",
635
- actor: "oauth-client"
636
- };
637
- }
638
- function accessOf(grant, roles, anyone) {
639
- const [held, ...more] = roles;
640
- if (more.length > 0) throw configError(`PermDock CLI: grant ${grant.permission.key} requires several roles at once (${roles.map((item) => item.role).join(", ")}); RLS compiles one role per grant`);
641
- if (held === void 0) return anyone ? { kind: "anyone" } : { kind: "authenticated" };
642
- if (typeof held.scope === "object") return {
643
- kind: "resource",
644
- role: held.role,
645
- resource: held.scope.resource
646
- };
647
- return {
648
- kind: "role",
649
- role: held.role,
650
- scope: held.scope
651
- };
652
- }
653
- /**
654
- * Every grant RLS must enforce: role grants and top-level
655
- * `definePolicy({ grants })` alike (`policy.grants` holds both). A grantee
656
- * Postgres cannot see (plan, actor, assurance) fails with the permission named.
657
- */
658
- function collectGrants(policy) {
659
- return policy.grants.map((grant) => {
660
- const items = granteeItems(grant.to);
661
- const roles = [];
662
- let anyone = true;
663
- let where = grant.where;
664
- let delegated;
665
- for (const item of items) switch (item.kind) {
666
- case "role":
667
- roles.push(item);
668
- break;
669
- case "anyone": break;
670
- case "authenticated":
671
- anyone = false;
672
- break;
673
- case "relation":
674
- anyone = false;
675
- where = andConditions(where, relationWhere(policy, grant, item));
676
- break;
677
- case "inherit":
678
- anyone = false;
679
- where = andConditions(where, inheritWhere(policy, grant, item));
680
- break;
681
- case "actor":
682
- delegated = actorAccess(grant, item, items.length - 1);
683
- break;
684
- case "plan":
685
- case "assurance": throw configError(`PermDock CLI: grant ${grant.permission.key} is limited to a ${item.kind} grantee, which RLS cannot compile; enforce it in the application`);
686
- default: return item;
687
- }
688
- const access = delegated ?? accessOf(grant, roles, anyone);
689
- const label = access.kind === "role" || access.kind === "resource" ? access.role : access.kind;
690
- return where === void 0 ? {
691
- grant,
692
- label,
693
- access
694
- } : {
695
- grant,
696
- label,
697
- access,
698
- where
699
- };
700
- });
701
- }
702
- /** Declared role names (bindings and `defineRoles` leaves) plus any role a top-level grant names. */
703
- function roleNames(policy) {
704
- const names = new Set(policy.roles.map((item) => item.name));
705
- for (const leaf of listRoles(policy.vocabulary.roles)) names.add(leaf.key);
706
- for (const item of collectGrants(policy)) if (item.access.kind === "role" || item.access.kind === "resource") names.add(item.access.role);
707
- return [...names];
708
- }
709
- //#endregion
710
287
  //#region src/cli/rls/api-keys.ts
711
288
  const API_KEY_ALLOWS = "permdock_api_key_allows";
712
289
  const DEFAULTS = {
@@ -832,7 +409,10 @@ ${body}
832
409
  }
833
410
  //#endregion
834
411
  //#region src/cli/shared/compare.ts
835
- /** UTF-16 code-unit order, never `localeCompare`: generated SQL must match byte for byte on every machine. */
412
+ /**
413
+ * UTF-16 code-unit order, never `localeCompare`: generated SQL must match byte
414
+ * for byte on every machine.
415
+ */
836
416
  function byCodeUnit(a, b) {
837
417
  return a < b ? -1 : a > b ? 1 : 0;
838
418
  }
@@ -848,10 +428,10 @@ const READERS = {
848
428
  trusted: "permdock_trusted_role_permissions"
849
429
  };
850
430
  /**
851
- * The custom-role objects: the tables (`database` mode), the ceiling view
852
- * over the `assignable` declared roles, and `permdock_custom_keys`, which
853
- * resolves one custom role to the grant keys it holds in a scope. A row in
854
- * the tables can never reach a key outside the view.
431
+ * The custom-role objects: the tables (`database` mode), the ceiling view over
432
+ * the `assignable` declared roles, and `permdock_custom_keys`, which resolves
433
+ * one custom role to the grant keys it holds in a scope. A row in the tables
434
+ * can never reach a key outside the view.
855
435
  */
856
436
  function customRolesSql(ctx) {
857
437
  const custom = ctx.customRoles;
@@ -978,7 +558,10 @@ revoke execute on function ${keys}(text[], text[], text[], text) from public, an
978
558
  if (shared !== "") chunks.push(shared);
979
559
  return chunks.join("\n\n");
980
560
  }
981
- /** `permdock_is_shared_role(p_scope, p_role)` (`database` mode): whether a shared custom role of that name exists at the scope. */
561
+ /**
562
+ * `permdock_is_shared_role(p_scope, p_role)` (`database` mode): whether a
563
+ * shared custom role of that name exists at the scope.
564
+ */
982
565
  function sharedRoleSql(ctx) {
983
566
  if (ctx.authorize !== "database") return "";
984
567
  const perms = qualified(ctx, CUSTOM_ROLES.permissions);
@@ -1000,8 +583,8 @@ $$;
1000
583
  revoke execute on function ${shared}(text, text) from public, anon, authenticated;`;
1001
584
  }
1002
585
  /**
1003
- * `permdock_custom_keys` when resources declare levels. An allow entry is
1004
- * `key` or `key@level`. A level reaches only the ceiling's `<grant key>@<level>`
586
+ * `permdock_custom_keys` when resources declare levels. An allow entry is `key`
587
+ * or `key@level`. A level reaches only the ceiling's `<grant key>@<level>`
1005
588
  * keys, plus the ceiling denies of that permission; a level no ceiling key
1006
589
  * carries removes the permission, as `resolveCustomRole` drops it with
1007
590
  * `unknown-level`.
@@ -1106,8 +689,8 @@ function currentKeysSql(array) {
1106
689
  }
1107
690
  /**
1108
691
  * SQL fragments that read a stored former key as its current key inside
1109
- * `permdock_custom_keys`. Without renames they are the plain column and
1110
- * array reads.
692
+ * `permdock_custom_keys`. Without renames they are the plain column and array
693
+ * reads.
1111
694
  */
1112
695
  function renamesSql(renames) {
1113
696
  if (renames.length === 0) return {
@@ -1131,14 +714,14 @@ function renamesSql(renames) {
1131
714
  }
1132
715
  /**
1133
716
  * The catalog reader `permdock_permission_keys()` and the role reader
1134
- * `permdock_role_permissions(p_role, p_scope, p_tenant, p_scope_id)`, both
1135
- * for `authenticated`. A declared role answers from `role_permissions`, which
1136
- * is the catalog. A custom role (`database` mode) answers from the tenant's
1137
- * own rows through `permdock_custom_keys`, only to a member of the tenant or
1138
- * a holder of a `meta.manageRoles` permission for a platform role; a shared
1139
- * role (no tenant below global) to any signed-in caller. The
1140
- * effect is `deny` for a deny the role carries after the ceiling, as
1141
- * `resolveCustomRole` returns it, never for a deny that merely removes an allow.
717
+ * `permdock_role_permissions(p_role, p_scope, p_tenant, p_scope_id)`, both for
718
+ * `authenticated`. A declared role answers from `role_permissions`, which is
719
+ * the catalog. A custom role (`database` mode) answers from the tenant's own
720
+ * rows through `permdock_custom_keys`, only to a member of the tenant or a
721
+ * holder of a `meta.manageRoles` permission for a platform role; a shared role
722
+ * (no tenant below global) to any signed-in caller. The effect is `deny` for a
723
+ * deny the role carries after the ceiling, as `resolveCustomRole` returns it,
724
+ * never for a deny that merely removes an allow.
1142
725
  */
1143
726
  function roleReadersSql(ctx, permissions, trustedReaders) {
1144
727
  const rp = qualified(ctx, "role_permissions");
@@ -1205,7 +788,7 @@ end;`;
1205
788
  and ${qualified(ctx, HELPERS.has)}(m.grant_key)
1206
789
  )`;
1207
790
  const match = `c.tenant_id is not distinct from p_tenant and c.scope = p_scope and c.scope_id is not distinct from p_scope_id and c.role = p_role`;
1208
- const allowEntryOf = custom.levels === true ? "c.permission || coalesce('@' || c.level, '')" : "c.permission";
791
+ const allowEntryOf = `c.${allowEntry(ctx)}`;
1209
792
  const checked = (gate) => `begin
1210
793
  if p_role = any(${sqlTextArray(custom.declared)}) then
1211
794
  return query
@@ -1248,7 +831,7 @@ revoke execute on function ${reader}(text, text, ${tenantType}, text) from publi
1248
831
  grant execute on function ${reader}(text, text, ${tenantType}, text) to authenticated;`);
1249
832
  const trusted = qualified(ctx, READERS.trusted);
1250
833
  const signature = `text, text, ${tenantType}, text`;
1251
- const grants = trustedReaders === void 0 || trustedReaders.length === 0 ? "" : `\ngrant execute on function ${trusted}(${signature}) to ${trustedReaders.map(quoteIdent).join(", ")};`;
834
+ const grants = trustedReaders === void 0 || trustedReaders.length === 0 ? "" : `\n${trustedReaderGrants(ctx, `${trusted}(${signature})`, trustedReaders)}`;
1252
835
  chunks.push(`create or replace function ${trusted}(p_role text, p_scope text, p_tenant ${tenantType} default null, p_scope_id text default null)
1253
836
  returns table (permission text, effect text)
1254
837
  language plpgsql
@@ -1261,33 +844,42 @@ $$;
1261
844
  revoke execute on function ${trusted}(${signature}) from public, anon, authenticated;${grants}`);
1262
845
  return chunks.join("\n\n");
1263
846
  }
1264
- /** The stored definition of `p_role` at `p_scope` for `p_tenant`: the shared rows when a shared role has the name. */
847
+ /**
848
+ * The stored definition of `p_role` at `p_scope` for `p_tenant`: the shared
849
+ * rows when a shared role has the name.
850
+ */
1265
851
  function storedTenant(ctx) {
1266
852
  return `case when ${qualified(ctx, CUSTOM_ROLES.shared)}(p_scope, p_role) then c.tenant_id is null else c.tenant_id is not distinct from p_tenant end`;
1267
853
  }
1268
854
  /**
1269
- * The tenant half of a custom-role row match for a membership: the
1270
- * membership's tenant, or no tenant when a shared role of that name exists at
1271
- * the scope, so a shared role shadows a tenant role of the same name.
855
+ * The tenant half of a custom-role row match for a membership: the membership's
856
+ * tenant, or no tenant when a shared role of that name exists at the scope, so
857
+ * a shared role shadows a tenant role of the same name.
1272
858
  */
1273
859
  function customTenantMatch(ctx, tenant, scope, role) {
1274
860
  return `case when ${qualified(ctx, CUSTOM_ROLES.shared)}(${quoteLiteral(scope)}, ${role}) then c.tenant_id is null else c.tenant_id::text = ${tenant} end`;
1275
861
  }
1276
862
  /**
1277
- * The scope of a row of the application's roles table: its scope column (or
1278
- * the first scope) with a tenant; with no tenant, the same scope when the
1279
- * `shared` column is true, else `'global'`.
863
+ * The scope of a row of the application's roles table: its scope column (or the
864
+ * first scope) with a tenant; with no tenant, the same scope when the `shared`
865
+ * column is true, else `'global'`.
1280
866
  */
1281
867
  function rolesTableScope(table, tenant, root, column) {
1282
868
  const own = table.scope === void 0 ? quoteLiteral(root) : `${column(table.scope)}::text`;
1283
869
  return `case when ${tenant} is null then ${table.shared === void 0 ? "'global'" : `case when coalesce(${column(table.shared)}, false) then ${own} else 'global' end`} else ${own} end`;
1284
870
  }
1285
- /** `expr` read as its current key: a former key maps to the key it was renamed to. */
871
+ /**
872
+ * `expr` read as its current key: a former key maps to the key it was renamed
873
+ * to.
874
+ */
1286
875
  function currentKeyExpr(renames, expr) {
1287
876
  if (renames.length === 0) return expr;
1288
877
  return `(case ${expr} ${renames.map(([former, current]) => `when ${quoteLiteral(former)} then ${quoteLiteral(current)}`).join(" ")} else ${expr} end)`;
1289
878
  }
1290
- /** `case p_scope when '<scope>' then <sql for scope> ... else false end` over the declared scopes. */
879
+ /**
880
+ * `case p_scope when '<scope>' then <sql for scope> ... else false end` over
881
+ * the declared scopes.
882
+ */
1291
883
  function perScope(ctx, sql) {
1292
884
  const arms = ctx.scopes.map((scope) => `when ${quoteLiteral(scope.name)} then ${sql(scope.name)}`).join(" ");
1293
885
  return arms === "" ? "false" : `(case p_scope ${arms} else false end)`;
@@ -1301,6 +893,16 @@ ${indent} hint = ${quoteLiteral(hint)};`;
1301
893
  function hasCustomRoleChecksFor(ctx) {
1302
894
  return ctx.authorize === "database" && ctx.customRoles !== void 0 && ctx.scopes.length > 0 && ctx.scopes.every((scope) => hasMemberFor(ctx, scope.name));
1303
895
  }
896
+ /**
897
+ * The grants a `rls.trustedReaders` role needs for one reader: usage on the
898
+ * helper schema, which no client role reaches through these lines, and execute
899
+ * on the function.
900
+ */
901
+ function trustedReaderGrants(ctx, fn, readers) {
902
+ const to = readers.map(quoteIdent).join(", ");
903
+ return `grant usage on schema ${quoteIdent(helperSchema(ctx))} to ${to};
904
+ grant execute on function ${fn} to ${to};`;
905
+ }
1304
906
  //#endregion
1305
907
  //#region src/cli/sql/global-roles.ts
1306
908
  function qualify(name, schema) {
@@ -1310,7 +912,10 @@ function throughTenant(through, roles) {
1310
912
  if (through === void 0 || roles?.tenant === void 0) return;
1311
913
  return qualify(roles.table, "public") === through.table ? roles.tenant : void 0;
1312
914
  }
1313
- /** Resolves `source` against `schema`, aliasing the table `alias` and the roles table `<alias>k`. */
915
+ /**
916
+ * Resolves `source` against `schema`, aliasing the table `alias` and the roles
917
+ * table `<alias>k`.
918
+ */
1314
919
  function globalRoleSource(source, schema, alias, roles) {
1315
920
  const table = qualify(source.table, schema);
1316
921
  const user = source.user ?? "user_id";
@@ -1328,6 +933,7 @@ function globalRoleSource(source, schema, alias, roles) {
1328
933
  from: `${quoteTable(table)} ${alias}${role.join}${global}`,
1329
934
  userSql: `${alias}.${quoteIdent(user)}`,
1330
935
  roleSql: role.sql,
936
+ nullable: role.nullable,
1331
937
  role: roleManifest(role),
1332
938
  ...role.through === void 0 ? {} : { through: role.through },
1333
939
  ...tenant === void 0 ? {} : { tenant },
@@ -1343,11 +949,17 @@ function keyTenantLine(ctx, scope, tenant) {
1343
949
  const keyed = keyTenantSql(ctx, underRoot(ctx, scope) ? tenant : void 0);
1344
950
  return keyed === void 0 ? "" : `\n and ${keyed}`;
1345
951
  }
1346
- /** Whether the helper for `scope` narrows to the tenant claim: unless `rls.tenants` is `'all'`, when the first scope is on its chain. */
952
+ /**
953
+ * Whether the helper for `scope` narrows to the tenant claim: unless
954
+ * `rls.tenants` is `'all'`, when the first scope is on its chain.
955
+ */
1347
956
  function narrowsTo(ctx, scope) {
1348
957
  return ctx.tenants !== "all" && underRoot(ctx, scope);
1349
958
  }
1350
- /** Whether the active tenant narrows memberships of `scope`: the first scope is on its chain. */
959
+ /**
960
+ * Whether the active tenant narrows memberships of `scope`: the first scope is
961
+ * on its chain.
962
+ */
1351
963
  function underRoot(ctx, scope) {
1352
964
  const root = ctx.scopes[0]?.name;
1353
965
  let current = ctx.scopes.find((item) => item.name === scope);
@@ -1360,11 +972,17 @@ function underRoot(ctx, scope) {
1360
972
  }
1361
973
  return false;
1362
974
  }
1363
- /** `and exists (...)` lines for an active user; empty without `rls.suspension.users`. */
975
+ /**
976
+ * `and exists (...)` lines for an active user; empty without
977
+ * `rls.suspension.users`.
978
+ */
1364
979
  function userActive(ctx, indent, user) {
1365
980
  return activeUserSql(ctx, user).map((part) => `${indent}and ${part}`);
1366
981
  }
1367
- /** `and exists (...)` lines for every suspendable instance on the chain of `scope`'s membership. */
982
+ /**
983
+ * `and exists (...)` lines for every suspendable instance on the chain of
984
+ * `scope`'s membership.
985
+ */
1368
986
  function instancesActive(ctx, scope, idOf, indent, permission) {
1369
987
  return activeInstancesSql(ctx, scope, idOf, permission).map((part) => `${indent}and ${part}`);
1370
988
  }
@@ -1503,7 +1121,10 @@ function hasBody(ctx) {
1503
1121
  ${customKeysSql(ctx, "global", claimEntries("left(e, 1) not in ('-', '@')", "e"), claimEntries("left(e, 1) = '-'", "substr(e, 2)"), claimEntries("left(e, 1) = '@'", "substr(e, 2)")).trimStart()}
1504
1122
  ))`;
1505
1123
  }
1506
- /** The claim `jwt` mode reads platform custom roles from: `customRoleClaim(globalRoles)`. */
1124
+ /**
1125
+ * The claim `jwt` mode reads platform custom roles from:
1126
+ * `customRoleClaim(globalRoles)`.
1127
+ */
1507
1128
  const GLOBAL_GRANTS_CLAIM = "role_grants";
1508
1129
  function memberColumn(name) {
1509
1130
  return `m.${quoteIdent(name)}`;
@@ -1646,8 +1267,8 @@ ${customKeysSql(ctx, scope, claimEntries("left(e, 1) not in ('-', '@')", "e"), c
1646
1267
  }
1647
1268
  /**
1648
1269
  * The rows the membership sources select for the subject, one per instance
1649
- * (`scope`, `id`, `within`, `roles`, `via`), with each source's own expiry
1650
- * and suspension filters: the statements the token hook runs.
1270
+ * (`scope`, `id`, `within`, `roles`, `via`), with each source's own expiry and
1271
+ * suspension filters: the statements the token hook runs.
1651
1272
  */
1652
1273
  function sourcesKeep(sources) {
1653
1274
  return sources.some((source) => source.sql.keeps);
@@ -1671,7 +1292,10 @@ function sourcesBodyOf(sql, sources, user) {
1671
1292
  vars: sources.map((source, index) => `${sourceUser(index)} ${source.sql.userType} := ${user};`)
1672
1293
  };
1673
1294
  }
1674
- /** `language sql`, or `plpgsql` returning the query when the body declares variables. */
1295
+ /**
1296
+ * `language sql`, or `plpgsql` returning the query when the body declares
1297
+ * variables.
1298
+ */
1675
1299
  function functionBody(body) {
1676
1300
  if (body.vars.length === 0) return `language sql
1677
1301
  stable
@@ -1738,7 +1362,7 @@ ${rows}
1738
1362
  union
1739
1363
  select (ms.id)::${type}, ck.grant_key
1740
1364
  ${rows}
1741
- ${customKeysJoin(ctx, scope, entries(CUSTOM_ROLES.permissions, "permission", " and c.effect = 'allow'"), entries(CUSTOM_ROLES.permissions, "permission", " and c.effect = 'deny'"), entries(CUSTOM_ROLES.includes, "include_role", ""))}
1365
+ ${customKeysJoin(ctx, scope, entries(CUSTOM_ROLES.permissions, allowEntry(ctx), " and c.effect = 'allow'"), entries(CUSTOM_ROLES.permissions, "permission", " and c.effect = 'deny'"), entries(CUSTOM_ROLES.includes, "include_role", ""))}
1742
1366
  where ${signedIn(ctx)}
1743
1367
  and ms.scope = ${quoteLiteral(scope)}
1744
1368
  and not (r.role = any(${sqlTextArray(custom.declared)}))${narrow}${sourceFilters(ctx, scope, subjectIdSql(ctx), grantPermissionSql("ck.grant_key"), sourcesKeep(sources))}`, sources, subjectIdSql(ctx));
@@ -1749,7 +1373,7 @@ ${rows}
1749
1373
  where ${signedIn(ctx)}
1750
1374
  and ms.scope = ${quoteLiteral(scope)}
1751
1375
  and not (r.role = any(${sqlTextArray(custom.declared)}))${narrow}${sourceFilters(ctx, scope, subjectIdSql(ctx), grantPermissionSql("p_grant"), sourcesKeep(sources))}
1752
- ${customKeysSql(ctx, scope, entries(CUSTOM_ROLES.permissions, "permission", " and c.effect = 'allow'"), entries(CUSTOM_ROLES.permissions, "permission", " and c.effect = 'deny'"), entries(CUSTOM_ROLES.includes, "include_role", ""))}`, sources, subjectIdSql(ctx));
1376
+ ${customKeysSql(ctx, scope, entries(CUSTOM_ROLES.permissions, allowEntry(ctx), " and c.effect = 'allow'"), entries(CUSTOM_ROLES.permissions, "permission", " and c.effect = 'deny'"), entries(CUSTOM_ROLES.includes, "include_role", ""))}`, sources, subjectIdSql(ctx));
1753
1377
  }
1754
1378
  function scopedBody(ctx, scope, type) {
1755
1379
  if (ctx.authorize !== "database") return sqlBody(claimBody(ctx, scope, type));
@@ -1757,7 +1381,10 @@ function scopedBody(ctx, scope, type) {
1757
1381
  }
1758
1382
  //#endregion
1759
1383
  //#region src/cli/rls/capabilities.ts
1760
- /** The helper `--capabilities` adds: ids of one resource a link capability claim reaches. Part of the SQL contract. */
1384
+ /**
1385
+ * The helper `--capabilities` adds: ids of one resource a link capability claim
1386
+ * reaches. Part of the SQL contract.
1387
+ */
1761
1388
  const CAPABILITIES = { ids: "permdock_capability_ids" };
1762
1389
  /**
1763
1390
  * The `anon` branch access for a resource-scoped grant reached through a link
@@ -1799,6 +1426,137 @@ revoke execute on function ${fn}(text, text, text) from public;
1799
1426
  grant execute on function ${fn}(text, text, text) to anon, authenticated;`;
1800
1427
  }
1801
1428
  //#endregion
1429
+ //#region src/cli/rls/custom-role-by-id.ts
1430
+ const PREFIX = "PermDock CLI";
1431
+ const col = (name) => `r.${quoteIdent(name)}`;
1432
+ /**
1433
+ * Custom-role functions keyed by the application's role row
1434
+ * (`rls.customRoleWrites.roles.primaryKey`): the tenant, scope, instance and
1435
+ * name come from the row, never from the caller. The writers delegate to
1436
+ * `permdock_replace_custom_role_grants` (or its trusted variant), so every
1437
+ * caller and definition check applies, and keep the stored denies and includes.
1438
+ * The reader returns declared allow keys only and is executable by
1439
+ * `rls.trustedReaders` alone.
1440
+ */
1441
+ function customRoleByIdSql(ctx, trustedReaders) {
1442
+ const custom = ctx.customRoles;
1443
+ const roles = custom?.table;
1444
+ const root = ctx.scopes[0]?.name;
1445
+ if (custom === void 0 || roles?.primaryKey === void 0 || ctx.authorize !== "database" || root === void 0) return "";
1446
+ const pk = roles.primaryKey;
1447
+ if (pk === "") throw configError(`${PREFIX}: rls.customRoleWrites.roles.primaryKey names no column`);
1448
+ const leveled = custom.levels === true;
1449
+ const tenantType = tenantTypeOf(ctx);
1450
+ const table = quoteQualified(roles.table);
1451
+ const idType = `${table}.${quoteIdent(pk)}%type`;
1452
+ const perms = qualified(ctx, CUSTOM_ROLES.permissions);
1453
+ const includes = qualified(ctx, CUSTOM_ROLES.includes);
1454
+ const replace = qualified(ctx, CUSTOM_ROLES.replace);
1455
+ const trustedReplace = qualified(ctx, CUSTOM_ROLES.trustedReplace);
1456
+ const byId = qualified(ctx, CUSTOM_ROLES.replaceById);
1457
+ const trustedById = qualified(ctx, CUSTOM_ROLES.trustedReplaceById);
1458
+ const reader = qualified(ctx, CUSTOM_ROLES.trustedReadById);
1459
+ const renames = Object.entries(custom.renamed ?? {}).toSorted(byKey);
1460
+ const current = (expr) => currentKeyExpr(renames, expr);
1461
+ const declaredKeys = sqlTextArray(custom.permissions ?? []);
1462
+ const declaredRoles = sqlTextArray(custom.declared);
1463
+ const tenant = roles.tenant === void 0 ? `null::${tenantType}` : `${col(roles.tenant)}::${tenantType}`;
1464
+ const scope = rolesTableScope(roles, tenant, root, col);
1465
+ const scopeId = roles.id === void 0 || roles.tenant === void 0 ? "null::text" : `case when ${tenant} is null then null else ${col(roles.id)}::text end`;
1466
+ const skipped = roles.skip === void 0 || roles.tenant === void 0 ? "false" : `(${tenant} is not null and coalesce(${col(roles.skip)}, false))`;
1467
+ const match = "c.tenant_id is not distinct from v_tenant and c.scope = v_scope and c.scope_id is not distinct from v_scope_id and c.role = v_key";
1468
+ const audit = custom.audit;
1469
+ const auditCall = audit === void 0 ? "" : `
1470
+ perform ${quoteQualified(audit.function)}(${[
1471
+ "event_type => 'permdock.custom_role.replaced'",
1472
+ "target_type => 'role'",
1473
+ "record_id => p_role_id::text",
1474
+ "tenant => v_tenant",
1475
+ "metadata => jsonb_build_object('operation', 'replace', 'role', v_key, 'permissions', p_permissions)",
1476
+ ...Object.entries(audit.args ?? {}).toSorted(byKey).map(([name, value]) => `${quoteIdent(name)} => ${quoteLiteral(value)}`)
1477
+ ].join(", ")});`;
1478
+ const body = (write) => `declare
1479
+ v_tenant ${tenantType};
1480
+ v_scope text;
1481
+ v_scope_id text;
1482
+ v_key text;
1483
+ v_skipped boolean;
1484
+ v_deny text[];
1485
+ v_include text[];
1486
+ begin
1487
+ select ${tenant}, ${scope}, ${scopeId}, ${col(roles.key)}::text, ${skipped}
1488
+ into v_tenant, v_scope, v_scope_id, v_key, v_skipped
1489
+ from ${table} r
1490
+ where ${col(pk)} = p_role_id;
1491
+ if not found then
1492
+ ${raiseSql(" ", "P0002", "'permdock: no role row has id ' || coalesce(p_role_id::text, 'null')", "unknown-role")}
1493
+ end if;
1494
+ if v_skipped or v_key = any(${declaredRoles}) then
1495
+ ${raiseSql(" ", "22023", "'permdock: ' || coalesce(v_key, 'null') || ' is a declared role, not a custom role'", "declared-role")}
1496
+ end if;
1497
+ select array_agg(c.permission) into v_deny
1498
+ from ${perms} c
1499
+ where ${match} and c.effect = 'deny';
1500
+ select array_agg(c.include_role) into v_include
1501
+ from ${includes} c
1502
+ where ${match};
1503
+ perform ${write}(v_tenant, v_scope, v_scope_id, v_key, p_permissions, v_deny, v_include);${auditCall}
1504
+ end;`;
1505
+ const writer = (comment, name, write, member) => `-- ${comment}
1506
+ create or replace function ${name}(p_role_id ${idType}, p_permissions text[])
1507
+ returns void
1508
+ language plpgsql
1509
+ volatile
1510
+ security definer
1511
+ set search_path = ''
1512
+ as $$
1513
+ ${body(write)}
1514
+ $$;
1515
+ ${member ? `revoke execute on function ${name}(${idType}, text[]) from public, anon;
1516
+ grant execute on function ${name}(${idType}, text[]) to authenticated;` : `revoke execute on function ${name}(${idType}, text[]) from public, anon, authenticated;`}`;
1517
+ const readerEntry = leveled ? `${current("c.permission")} || coalesce('@' || c.level, '')` : current("c.permission");
1518
+ const readerGrant = trustedReaders === void 0 || trustedReaders.length === 0 ? "" : `\n${trustedReaderGrants(ctx, `${reader}(text[], ${tenantType})`, trustedReaders)}`;
1519
+ return `${writer("save a custom role by its row id: the row supplies tenant, scope, instance and name; the caller checks of permdock_replace_custom_role_grants apply, and stored denies and includes stay", byId, replace, true)}
1520
+
1521
+ ${writer("save a custom role by its row id for a trusted caller: the definition checks without the caller checks", trustedById, trustedReplace, false)}
1522
+
1523
+ -- the declared allow keys of custom roles by their row ids as text, for a trusted reader; with p_tenant, an id of another tenant is refused
1524
+ create or replace function ${reader}(p_role_ids text[], p_tenant ${tenantType} default null)
1525
+ returns table (role_id text, permission text)
1526
+ language plpgsql
1527
+ stable
1528
+ security definer
1529
+ set search_path = ''
1530
+ as $$
1531
+ begin
1532
+ if p_tenant is not null and exists (
1533
+ select 1 from ${table} r
1534
+ where ${col(pk)}::text = any(p_role_ids) and ${tenant} is distinct from p_tenant
1535
+ ) then
1536
+ ${raiseSql(" ", "42501", "'permdock: a role id belongs to another tenant than ' || p_tenant::text", "not-member")}
1537
+ end if;
1538
+ return query
1539
+ select distinct v.role_id, ${readerEntry}
1540
+ from (
1541
+ select ${col(pk)}::text as role_id, ${tenant} as tenant_id, ${scope} as scope, ${scopeId} as scope_id, ${col(roles.key)}::text as role
1542
+ from ${table} r
1543
+ where ${col(pk)}::text = any(p_role_ids)
1544
+ and not ${skipped}
1545
+ and not (${col(roles.key)}::text = any(${declaredRoles}))
1546
+ ) v
1547
+ join ${perms} c
1548
+ on c.tenant_id is not distinct from v.tenant_id
1549
+ and c.scope = v.scope
1550
+ and c.scope_id is not distinct from v.scope_id
1551
+ and c.role = v.role
1552
+ where c.effect = 'allow'
1553
+ and ${current("c.permission")} = any(${declaredKeys})
1554
+ order by 1, 2;
1555
+ end;
1556
+ $$;
1557
+ revoke execute on function ${reader}(text[], ${tenantType}) from public, anon, authenticated;${readerGrant}`;
1558
+ }
1559
+ //#endregion
1802
1560
  //#region src/cli/rls/custom-role-cascade.ts
1803
1561
  /** `row.column`, for a trigger's `old` or `new`. */
1804
1562
  function rowColumn(row, name) {
@@ -1811,12 +1569,11 @@ function cascadeMatch(prefix) {
1811
1569
  /**
1812
1570
  * The trigger on the application's roles table (`rls.customRoleWrites.roles`):
1813
1571
  * a rename, a move to another tenant, scope or instance, or a delete of a
1814
- * custom role's row carries its grants and includes along. A signed-in
1815
- * caller at trigger depth 1 passes the write functions' checks: authority
1816
- * over the stored definition where it was, and, after a move, membership
1817
- * and authority where it lands and entries inside that scope's ceiling.
1818
- * Other paths (a migration, a job, a nested trigger) move the rows as the
1819
- * function owner.
1572
+ * custom role's row carries its grants and includes along. A signed-in caller
1573
+ * at trigger depth 1 passes the write functions' checks: authority over the
1574
+ * stored definition where it was, and, after a move, membership and authority
1575
+ * where it lands and entries inside that scope's ceiling. Other paths (a
1576
+ * migration, a job, a nested trigger) move the rows as the function owner.
1820
1577
  */
1821
1578
  function customRoleCascadeSql(ctx) {
1822
1579
  const custom = ctx.customRoles;
@@ -1830,11 +1587,11 @@ function customRoleCascadeSql(ctx) {
1830
1587
  const target = quoteTable(qualifySqlTable(table.table));
1831
1588
  const tenant = (row) => table.tenant === void 0 ? `null::${tenantType}` : `${rowColumn(row, table.tenant)}::${tenantType}`;
1832
1589
  const scope = (row) => rolesTableScope(table, tenant(row), root, (name) => rowColumn(row, name));
1833
- const id = (row) => table.id === void 0 ? "null::text" : `${rowColumn(row, table.id)}::text`;
1834
- const skipped = (row) => table.skip === void 0 ? "false" : `coalesce(${rowColumn(row, table.skip)}, false)`;
1590
+ const id = (row) => table.id === void 0 || table.tenant === void 0 ? "null::text" : `case when ${tenant(row)} is null then null else ${rowColumn(row, table.id)}::text end`;
1591
+ const skipped = (row) => table.skip === void 0 || table.tenant === void 0 ? "false" : `(${tenant(row)} is not null and coalesce(${rowColumn(row, table.skip)}, false))`;
1835
1592
  const guard = qualified(ctx, CUSTOM_ROLES.guard);
1836
1593
  const entries = qualified(ctx, CUSTOM_ROLES.entries);
1837
- const allowEntryOf = custom.levels === true ? "c.permission || coalesce('@' || c.level, '')" : "c.permission";
1594
+ const allowEntryOf = `c.${allowEntry(ctx)}`;
1838
1595
  return `-- the application's roles table: renames, moves and deletes of a custom role carry its grants and includes
1839
1596
  create or replace function ${fn}()
1840
1597
  returns trigger
@@ -1913,6 +1670,58 @@ create trigger "permdock_custom_role_cascade"
1913
1670
  `;
1914
1671
  }
1915
1672
  //#endregion
1673
+ //#region src/cli/rls/custom-role-guard.ts
1674
+ /** A column of the roles-table row a trigger sees as `new`. */
1675
+ function column(name) {
1676
+ return `new.${quoteIdent(name)}`;
1677
+ }
1678
+ /**
1679
+ * The trigger on the application's roles table (`rls.customRoleWrites.roles`)
1680
+ * that refuses a tenant row named like a declared role (`declared-role`) or
1681
+ * like a shared custom role (`name-taken`). A row the `skip` column marks is a
1682
+ * system role of the tenant and passes. Empty without a tenant column: then no
1683
+ * row is a tenant row.
1684
+ */
1685
+ function customRoleNameGuardSql(ctx) {
1686
+ const custom = ctx.customRoles;
1687
+ const table = custom?.table;
1688
+ const root = ctx.scopes[0]?.name;
1689
+ if (custom === void 0 || table?.tenant === void 0 || root === void 0) return "";
1690
+ const tenant = `${column(table.tenant)}::${tenantTypeOf(ctx)}`;
1691
+ const skipped = table.skip === void 0 ? "false" : `coalesce(${column(table.skip)}, false)`;
1692
+ const fn = qualified(ctx, CUSTOM_ROLES.nameGuard);
1693
+ const target = quoteTable(qualifySqlTable(table.table));
1694
+ const key = `${column(table.key)}::text`;
1695
+ const scope = rolesTableScope(table, tenant, root, column);
1696
+ return `-- the application's roles table: a tenant row cannot take the name of a declared or a shared role
1697
+ create or replace function ${fn}()
1698
+ returns trigger
1699
+ language plpgsql
1700
+ volatile
1701
+ security definer
1702
+ set search_path = ''
1703
+ as $$
1704
+ begin
1705
+ if ${tenant} is null or ${skipped} then
1706
+ return new;
1707
+ end if;
1708
+ if ${key} = any(${sqlTextArray(custom.declared)}) then
1709
+ ${raiseSql(" ", "22023", `'permdock: ' || ${key} || ' is a declared role'`, "declared-role")}
1710
+ end if;
1711
+ if ${qualified(ctx, CUSTOM_ROLES.shared)}(${scope}, ${key}) then
1712
+ ${raiseSql(" ", "22023", `'permdock: ' || ${key} || ' is a shared role'`, "name-taken")}
1713
+ end if;
1714
+ return new;
1715
+ end;
1716
+ $$;
1717
+ revoke execute on function ${fn}() from public, anon, authenticated;
1718
+ drop trigger if exists "permdock_custom_role_name_guard" on ${target};
1719
+ create trigger "permdock_custom_role_name_guard"
1720
+ before insert or update on ${target}
1721
+ for each row execute function ${fn}();
1722
+ `;
1723
+ }
1724
+ //#endregion
1916
1725
  //#region src/cli/rls/custom-role-writes.ts
1917
1726
  function callerFunctionsSql(ctx, custom, root, levelReach, forUser) {
1918
1727
  const leveled = custom.levels === true;
@@ -2038,8 +1847,8 @@ revoke execute on function ${guard}(${userArg}${tenantType}, text, text, text) f
2038
1847
  };
2039
1848
  }
2040
1849
  /**
2041
- * The functions an application calls to save, rename and delete a custom
2042
- * role in \`database\` mode, with the rules of \`validateCustomRole\` and
1850
+ * The functions an application calls to save, rename and delete a custom role
1851
+ * in \`database\` mode, with the rules of \`validateCustomRole\` and
2043
1852
  * \`assignablePermissions\`: entries inside the ceiling of the role's scope,
2044
1853
  * includes naming declared roles, and only permissions and levels the caller
2045
1854
  * may hand out, for the new definition and the stored one alike. A platform
@@ -2063,6 +1872,7 @@ function customRoleWritesSql(ctx, levelReach) {
2063
1872
  const guard = qualified(ctx, CUSTOM_ROLES.guard);
2064
1873
  const shape = qualified(ctx, CUSTOM_ROLES.shape);
2065
1874
  const entries = qualified(ctx, CUSTOM_ROLES.entries);
1875
+ const problem = qualified(ctx, CUSTOM_ROLES.entryProblem);
2066
1876
  const renames = Object.entries(custom.renamed ?? {}).toSorted(byKey);
2067
1877
  const current = (expr) => currentKeyExpr(renames, expr);
2068
1878
  const known = sqlTextArray([...custom.permissions ?? [], ...renames.map(([former]) => former)]);
@@ -2150,6 +1960,38 @@ ${callerFunctionsSql(ctx, custom, root, levelReach, true).beyond}
2150
1960
 
2151
1961
  ${callerFunctionsSql(ctx, custom, root, levelReach, true).guard}
2152
1962
  ` : ""}
1963
+ -- null when an allow entry is a declared permission inside the ceiling of the scope${leveled ? ", at a level the resource declares" : ""}; else unknown-permission, outside-ceiling${leveled ? " or unknown-level" : ""}
1964
+ create or replace function ${problem}(p_scope text, p_entry text)
1965
+ returns text
1966
+ language plpgsql
1967
+ stable
1968
+ security definer
1969
+ set search_path = ''
1970
+ as $$
1971
+ declare
1972
+ v_key text := ${current(leveled ? "split_part(p_entry, '@', 1)" : "p_entry")};
1973
+ begin
1974
+ if p_entry is null or not (v_key = any(${known})) then
1975
+ return 'unknown-permission';
1976
+ end if;
1977
+ if not exists (
1978
+ select 1 from ${ceiling} c
1979
+ where c.scope = p_scope and c.effect = 'allow' and ${current("c.permission")} = v_key
1980
+ ) then
1981
+ return 'outside-ceiling';
1982
+ end if;${leveled ? `
1983
+ if strpos(p_entry, '@') > 0 and not exists (
1984
+ select 1 from ${ceiling} c
1985
+ where c.scope = p_scope and c.effect = 'allow' and ${current("c.permission")} = v_key
1986
+ and split_part(c.grant_key, '@', 2) = split_part(p_entry, '@', 2)
1987
+ ) then
1988
+ return 'unknown-level';
1989
+ end if;` : ""}
1990
+ return null;
1991
+ end;
1992
+ $$;
1993
+ revoke execute on function ${problem}(text, text) from public, anon, authenticated;
1994
+
2153
1995
  -- the entries of a definition: declared keys and levels inside the ceiling of the scope, and declared includes
2154
1996
  create or replace function ${entries}(p_scope text, p_allow text[], p_deny text[], p_include text[])
2155
1997
  returns void
@@ -2161,23 +2003,18 @@ as $$
2161
2003
  declare
2162
2004
  v_entry text;
2163
2005
  v_key text;
2006
+ v_problem text;
2164
2007
  begin
2165
2008
  foreach v_entry in array coalesce(p_allow, '{}'::text[]) loop
2166
2009
  v_key := ${current(leveled ? "split_part(v_entry, '@', 1)" : "v_entry")};
2167
- if not (v_key = any(${known})) then
2010
+ v_problem := ${problem}(p_scope, v_entry);
2011
+ if v_problem = 'unknown-permission' then
2168
2012
  ${raiseSql(" ", "22023", `'permdock: ' || v_entry || ' is not a declared permission'`, "unknown-permission")}
2169
2013
  end if;
2170
- if not exists (
2171
- select 1 from ${ceiling} c
2172
- where c.scope = p_scope and c.effect = 'allow' and ${current("c.permission")} = v_key
2173
- ) then
2014
+ if v_problem = 'outside-ceiling' then
2174
2015
  ${raiseSql(" ", "22023", `'permdock: ' || v_key || ' is outside the ceiling of ' || p_scope`, "outside-ceiling")}
2175
2016
  end if;${leveled ? `
2176
- if strpos(v_entry, '@') > 0 and not exists (
2177
- select 1 from ${ceiling} c
2178
- where c.scope = p_scope and c.effect = 'allow' and ${current("c.permission")} = v_key
2179
- and split_part(c.grant_key, '@', 2) = split_part(v_entry, '@', 2)
2180
- ) then
2017
+ if v_problem = 'unknown-level' then
2181
2018
  ${raiseSql(" ", "22023", `'permdock: ' || v_entry || ' names a level the resource does not declare'`, "unknown-level")}
2182
2019
  end if;` : ""}
2183
2020
  end loop;
@@ -2249,9 +2086,9 @@ end;`, false)}`;
2249
2086
  //#region src/cli/rls/global-roles.ts
2250
2087
  /**
2251
2088
  * `permdock_replace_global_roles(p_user, p_roles)`: sets the global roles of
2252
- * one user in `rls.roles` in one statement pair. It runs as the invoker, so
2253
- * the table's policies and the `rls.assignments` trigger judge every row it
2254
- * deletes or inserts.
2089
+ * one user in `rls.roles` in one statement pair. It runs as the invoker, so the
2090
+ * table's policies and the `rls.assignments` trigger judge every row it deletes
2091
+ * or inserts.
2255
2092
  */
2256
2093
  function globalRolesSql(ctx) {
2257
2094
  if (ctx.roles === void 0) return "";
@@ -2324,9 +2161,9 @@ grant execute on function ${fn}(${args}) to authenticated;`;
2324
2161
  //#endregion
2325
2162
  //#region src/cli/rls/helper-members.ts
2326
2163
  /**
2327
- * The body of `member_<scope>_ids()`: instances of `scope` the subject holds
2328
- * a live membership of, with any role. Not narrowed to the active tenant, and
2329
- * no cascade: a membership of a child scope is not one of its parent.
2164
+ * The body of `member_<scope>_ids()`: instances of `scope` the subject holds a
2165
+ * live membership of, with any role. Not narrowed to the active tenant, and no
2166
+ * cascade: a membership of a child scope is not one of its parent.
2330
2167
  */
2331
2168
  function memberBody(ctx, scope, type, roles) {
2332
2169
  if (ctx.authorize !== "database") return sqlBody(` select distinct (m ->> 'id')::${type}
@@ -2426,8 +2263,8 @@ ${grants}`;
2426
2263
  /**
2427
2264
  * `member_<scope>_ids_for(p_user uuid)`: `member_<scope>_ids()` for a user the
2428
2265
  * caller names, read from the membership sources (or the mapped table) in
2429
- * either mode, so a token hook can call it before any claim exists. Naming
2430
- * the user is why no client role may execute it; the hook's grants give
2266
+ * either mode, so a token hook can call it before any claim exists. Naming the
2267
+ * user is why no client role may execute it; the hook's grants give
2431
2268
  * `supabase_auth_admin` execute.
2432
2269
  */
2433
2270
  function memberForFunction(ctx, scope, type) {
@@ -2437,18 +2274,125 @@ returns setof ${type}
2437
2274
  ${functionBody(memberRowsBody(ctx, scope, type, "p_user", memberForSources(ctx, scope)))}
2438
2275
  revoke execute on function ${fn}(uuid) from public, anon, authenticated;`;
2439
2276
  }
2277
+ /**
2278
+ * `member_<scope>_ids_of(p_user uuid)`: `member_<scope>_ids_for(p_user)` for
2279
+ * the caller only. Any other `p_user` returns no rows, so a client role may
2280
+ * execute it without learning another user's memberships.
2281
+ */
2282
+ function memberOfFunction(ctx, scope, type) {
2283
+ const fn = qualified(ctx, memberOfHelper(scope));
2284
+ return `create or replace function ${fn}(p_user uuid)
2285
+ returns setof ${type}
2286
+ ${functionBody(sqlBody(` select m.id
2287
+ from ${qualified(ctx, memberForHelper(scope))}(p_user) as m(id)
2288
+ where p_user = ${subjectIdSql(ctx)}`))}
2289
+ revoke execute on function ${fn}(uuid) from public, anon;
2290
+ grant execute on function ${fn}(uuid) to authenticated;`;
2291
+ }
2292
+ //#endregion
2293
+ //#region src/cli/rls/token-scopes.ts
2294
+ const TOKEN_SCOPE_ALLOWS = "permdock_token_scope_allows";
2295
+ /**
2296
+ * `rls.tokenScopes` as the SQL generator needs it, or `undefined` when it is
2297
+ * off.
2298
+ */
2299
+ function tokenScopesPlan(setting, policy) {
2300
+ if (setting !== true) return;
2301
+ const permissions = listPermissions(policy.permissions).map((leaf) => ({
2302
+ key: leaf.key,
2303
+ scope: leaf.scope,
2304
+ renamedFrom: formerKeys(leaf)
2305
+ }));
2306
+ return { scopes: delegatedScopeMap(permissions, policy.oauthScopes) };
2307
+ }
2308
+ function tokenScopeAllowsCall(ctx, grant) {
2309
+ return `${qualified(ctx, TOKEN_SCOPE_ALLOWS)}(${grant})`;
2310
+ }
2311
+ /**
2312
+ * The token's scopes as rows of `s(scope)`: a space-separated string or,
2313
+ * outside `guc`, a JSON array.
2314
+ */
2315
+ function tokenScopeRows(ctx) {
2316
+ if (ctx.dialect === "guc") return `regexp_split_to_table(btrim(coalesce(${subjectClaimSql(ctx, "scope")}, '')), '\\s+') s(scope)`;
2317
+ const scope = subjectClaimJsonSql(ctx, "scope");
2318
+ return `unnest(case jsonb_typeof(${scope})
2319
+ when 'string' then regexp_split_to_array(btrim(${scope} #>> '{}'), '\\s+')
2320
+ when 'array' then array(select jsonb_array_elements_text(${scope}))
2321
+ else '{}'::text[]
2322
+ end) s(scope)`;
2323
+ }
2324
+ /**
2325
+ * `permdock_token_scope_allows(p_grant)`: a delegated token (a `client_id`, or
2326
+ * an `act` that is not a support or impersonation session) reaches a grant key
2327
+ * only when one of its scopes maps to the key's permission. better-supabase's
2328
+ * `cap_allows` reads the same tokens the same way.
2329
+ */
2330
+ function tokenScopeAllowsSql(ctx, plan, anonExecute) {
2331
+ const fn = qualified(ctx, TOKEN_SCOPE_ALLOWS);
2332
+ const act = subjectClaimJsonSql(ctx, "act");
2333
+ const grants = anonExecute ? `revoke execute on function ${fn}(text) from public;
2334
+ grant execute on function ${fn}(text) to anon, authenticated;` : `revoke execute on function ${fn}(text) from public, anon;
2335
+ grant execute on function ${fn}(text) to authenticated;`;
2336
+ return `-- Token scopes: a delegated token holds only the permissions its scope claim maps to; denies always apply, and a token without client_id or an agent act is not limited
2337
+ create or replace function ${fn}(p_grant text)
2338
+ returns boolean
2339
+ language sql
2340
+ stable
2341
+ security definer
2342
+ set search_path = ''
2343
+ as $$
2344
+ select case
2345
+ when coalesce(${subjectClaimSql(ctx, "client_id")}, '') = ''
2346
+ and (${act} is null
2347
+ or ${act} = 'null'::jsonb
2348
+ or (jsonb_typeof(${act}) = 'object'
2349
+ and ${act} ->> 'kind' in ('support', 'impersonation'))) then true
2350
+ when exists (
2351
+ select 1 from ${qualified(ctx, "role_permissions")} rp
2352
+ where rp.grant_key = p_grant and rp.effect = 'deny'
2353
+ ) then true
2354
+ else exists (
2355
+ select 1
2356
+ from ${tokenScopeRows(ctx)}
2357
+ where s.scope <> ''
2358
+ and coalesce(${quoteLiteral(JSON.stringify(plan.scopes))}::jsonb -> s.scope, '[]'::jsonb)
2359
+ ? split_part(split_part(p_grant, '#', 1), '@', 1)
2360
+ )
2361
+ end
2362
+ $$;
2363
+ ${grants}`;
2364
+ }
2440
2365
  //#endregion
2441
2366
  //#region src/cli/rls/helpers.ts
2442
2367
  /**
2443
- * The policy-side call for a grant key. Every form is uncorrelated, so
2444
- * Postgres runs the helper once per statement (an InitPlan, or a hashed
2445
- * SubPlan for `in`), never once per row.
2368
+ * The policy-side call for a grant key. Every form is uncorrelated, so Postgres
2369
+ * runs the helper once per statement (an InitPlan, or a hashed SubPlan for
2370
+ * `in`), never once per row.
2446
2371
  */
2447
- function accessSql(ctx, scope, grantKey, column) {
2372
+ function accessSql(ctx, scope, grantKey, column, helper) {
2448
2373
  const key = quoteLiteral(grantKey);
2449
2374
  if (scope === "global") return `(select ${qualified(ctx, HELPERS.has)}(${key}))`;
2450
2375
  if (column === void 0) throw configError(`PermDock CLI: ${scope}-scoped grant needs definePolicy({ scopes.${scope} })`);
2451
- return `${quoteIdent(column)} in (select ${qualified(ctx, permittedIdsHelper(scope))}(${key}))`;
2376
+ return `${quoteIdent(column)} in (select ${qualified(ctx, helper ?? permittedIdsHelper(scope))}(${key}))`;
2377
+ }
2378
+ /**
2379
+ * `permitted_<parent>_ids(p_grant)` for each \`rls.tables\` \`via\`: the parent
2380
+ * rows whose scope instance the grant reaches. \`security definer\`, so the
2381
+ * caller needs no read on the parent, and called uncorrelated like
2382
+ * \`permitted_<scope>_ids\`.
2383
+ */
2384
+ function viaHelpersSql(ctx, anonExecute) {
2385
+ const done = /* @__PURE__ */ new Set();
2386
+ const chunks = [];
2387
+ for (const via of Object.values(ctx.vias ?? {})) {
2388
+ if (done.has(via.helper)) continue;
2389
+ done.add(via.helper);
2390
+ chunks.push(`-- rows of ${via.table} whose ${via.column} is a ${via.scope} the grant reaches
2391
+ ${helperFunction(ctx, via.helper, `setof ${via.type}`, sqlBody(` select p.${quoteIdent(via.id)}
2392
+ from ${quoteQualified(via.table)} p
2393
+ where p.${quoteIdent(via.column)} in (select ${qualified(ctx, permittedIdsHelper(via.scope))}(p_grant))`), anonExecute)}`);
2394
+ }
2395
+ return chunks;
2452
2396
  }
2453
2397
  function userIdSql(ctx, anonExecute) {
2454
2398
  const fn = userIdHelperSql(ctx);
@@ -2482,11 +2426,11 @@ ${grants}`;
2482
2426
  * `permdock_has_for(p_user, p_grant)` and one
2483
2427
  * `permitted_<scope>_ids_for(p_user, p_grant)` per scope in `database` mode:
2484
2428
  * the bodies of `permdock_has` and `permitted_<scope>_ids` with `p_user` for
2485
- * the signed-in user and no tenant-claim narrowing, since a named user has
2486
- * no token. For trusted SQL that acts for a stored user (jobs, triggers,
2487
- * approvals resolved later). Naming the user is why no client role may
2488
- * execute them: `security definer` callers run them as their owner, and a
2489
- * backend role needs its own grant.
2429
+ * the signed-in user and no tenant-claim narrowing, since a named user has no
2430
+ * token. For trusted SQL that acts for a stored user (jobs, triggers, approvals
2431
+ * resolved later). Naming the user is why no client role may execute them:
2432
+ * `security definer` callers run them as their owner, and a backend role needs
2433
+ * its own grant.
2490
2434
  */
2491
2435
  function forUserSql(ctx) {
2492
2436
  if (ctx.authorize !== "database") return "";
@@ -2509,8 +2453,8 @@ function userIdType(ctx) {
2509
2453
  return ctx.dialect === "supabase" ? "uuid not null references auth.users on delete cascade" : "text not null";
2510
2454
  }
2511
2455
  /**
2512
- * The `role_permissions` rows the policy compiles to: an upsert and a delete
2513
- * of every other row. Data, not schema, so `supabase db diff` drops it from a
2456
+ * The `role_permissions` rows the policy compiles to: an upsert and a delete of
2457
+ * every other row. Data, not schema, so `supabase db diff` drops it from a
2514
2458
  * declarative schema; `--split ...,seeds` writes it as its own migration.
2515
2459
  */
2516
2460
  function seedSql(ctx, rows) {
@@ -2536,8 +2480,45 @@ ${rows.map((row) => ` (${[
2536
2480
  );`;
2537
2481
  }
2538
2482
  /**
2539
- * `role_permissions`, its seeds, and the three `security definer` helpers
2540
- * with `search_path = ''`. `execute` goes to `authenticated` only.
2483
+ * `role_permissions` as a view over the compiled rows (`rls.seeds: 'view'`).
2484
+ * `security_invoker` makes the view read as its caller, so it adds no
2485
+ * privilege; the table's revoke applies to the view the same way.
2486
+ */
2487
+ function seedViewSql(ctx, rows) {
2488
+ const view = qualified(ctx, "role_permissions");
2489
+ const columns = "role, permission, grant_key, scope, effect";
2490
+ return `-- rls.seeds: 'view': role_permissions is a view, so the rows travel with the schema
2491
+ create or replace view ${view} (${columns}) with (security_invoker = true) as
2492
+ ${rows.length === 0 ? `select null::text, null::text, null::text, null::text, null::text where false` : `select * from (values\n${rows.map((row) => ` (${[
2493
+ row.role,
2494
+ row.permission,
2495
+ row.grantKey,
2496
+ row.scope,
2497
+ row.effect
2498
+ ].map(quoteLiteral).join(", ")})`).join(",\n")}\n) as v(${columns})`};
2499
+ revoke all on table ${view} from anon, authenticated, public;`;
2500
+ }
2501
+ /**
2502
+ * `<schema>.seed()`: the same upsert and delete as `seedSql`, for a declarative
2503
+ * schema that carries no rows.
2504
+ */
2505
+ function seedFunctionSql(ctx, rows) {
2506
+ const fn = qualified(ctx, "seed");
2507
+ return `-- rls.seeds: 'function': the role_permissions rows are data, so a declarative diff leaves them out; run select ${fn}(); after each deploy
2508
+ create or replace function ${fn}()
2509
+ returns void
2510
+ language sql
2511
+ volatile
2512
+ security definer
2513
+ set search_path = ''
2514
+ as $permdock$
2515
+ ${seedSql(ctx, rows)}
2516
+ $permdock$;
2517
+ revoke execute on function ${fn}() from public, anon, authenticated;`;
2518
+ }
2519
+ /**
2520
+ * `role_permissions`, its seeds, and the three `security definer` helpers with
2521
+ * `search_path = ''`. `execute` goes to `authenticated` only.
2541
2522
  */
2542
2523
  function helpersSql(ctx, rows, options) {
2543
2524
  const schema = helperSchema(ctx);
@@ -2545,9 +2526,13 @@ function helpersSql(ctx, rows, options) {
2545
2526
  const rp = qualified(ctx, "role_permissions");
2546
2527
  const chunks = [`-- permdock helpers (${ctx.authorize === "database" ? "database: reads the membership and user_roles tables" : "jwt: reads the role and memberships claims"})
2547
2528
  -- policies call them uncorrelated, so Postgres evaluates each once per statement`];
2548
- if (schema !== "public") chunks.push(`create schema if not exists ${s};\nrevoke all on schema ${s} from public;\ngrant usage on schema ${s} to ${options.anonExecute === true ? "anon, authenticated" : "authenticated"};`);
2529
+ if (schema !== "public") {
2530
+ const anonUsage = options.anonExecute === true || ctx.capabilities === true;
2531
+ chunks.push(`create schema if not exists ${s};\nrevoke all on schema ${s} from public;\ngrant usage on schema ${s} to ${anonUsage ? "anon, authenticated" : "authenticated"};`);
2532
+ }
2549
2533
  if (ctx.dialect === "supabase") chunks.push(userIdSql(ctx, options.anonExecute === true));
2550
- chunks.push(`create table if not exists ${rp} (
2534
+ if (ctx.seeds === "view") chunks.push(seedViewSql(ctx, rows));
2535
+ else chunks.push(`create table if not exists ${rp} (
2551
2536
  role text not null,
2552
2537
  permission text not null,
2553
2538
  grant_key text not null,
@@ -2557,7 +2542,8 @@ function helpersSql(ctx, rows, options) {
2557
2542
  );
2558
2543
  alter table ${rp} enable row level security;
2559
2544
  revoke all on table ${rp} from anon, authenticated, public;`);
2560
- if (options.withoutSeeds !== true) chunks.push(seedSql(ctx, rows));
2545
+ if (ctx.seeds === "function") chunks.push(seedFunctionSql(ctx, rows));
2546
+ else if (options.withoutSeeds !== true && ctx.seeds !== "view") chunks.push(seedSql(ctx, rows));
2561
2547
  if (ctx.authorize === "database" && options.userRoles && ctx.roles === void 0) {
2562
2548
  const ur = qualified(ctx, "user_roles");
2563
2549
  chunks.push(`create table if not exists ${ur} (
@@ -2573,6 +2559,7 @@ revoke all on table ${ur} from anon, authenticated, public;`);
2573
2559
  const anon = options.anonExecute === true;
2574
2560
  const keys = ctx.apiKeys;
2575
2561
  if (keys !== void 0) chunks.push(apiKeyAllowsSql(ctx, keys, anon));
2562
+ if (ctx.tokenScopes !== void 0) chunks.push(tokenScopeAllowsSql(ctx, ctx.tokenScopes, anon));
2576
2563
  const bodies = grantBodies(ctx);
2577
2564
  chunks.push(helperFunction(ctx, HELPERS.has, "boolean", bodies.has, anon));
2578
2565
  const capabilities = capabilitiesSql(ctx);
@@ -2583,7 +2570,9 @@ revoke all on table ${ur} from anon, authenticated, public;`);
2583
2570
  chunks.push(memberFunction(ctx, scope.name, type, anon));
2584
2571
  if (ctx.roleMemberScopes?.includes(scope.name) === true) chunks.push(memberRolesFunction(ctx, scope.name, type, anon));
2585
2572
  if (hasMemberFor(ctx, scope.name)) chunks.push(memberForFunction(ctx, scope.name, type));
2573
+ if (hasMemberOf(ctx, scope.name)) chunks.push(memberOfFunction(ctx, scope.name, type));
2586
2574
  }
2575
+ chunks.push(...viaHelpersSql(ctx, anon));
2587
2576
  const forUser = forUserSql(ctx);
2588
2577
  if (forUser !== "") chunks.push(forUser);
2589
2578
  const writes = customRoleWritesSql(ctx, options.levelReach ?? []);
@@ -2591,6 +2580,12 @@ revoke all on table ${ur} from anon, authenticated, public;`);
2591
2580
  chunks.push(writes);
2592
2581
  const cascade = customRoleCascadeSql(ctx);
2593
2582
  if (cascade !== "") chunks.push(cascade);
2583
+ const sync = ctx.customRoles?.sync;
2584
+ if (sync !== void 0) chunks.push(sync);
2585
+ const byId = customRoleByIdSql(ctx, options.trustedReaders);
2586
+ if (byId !== "") chunks.push(byId);
2587
+ const nameGuard = customRoleNameGuardSql(ctx);
2588
+ if (nameGuard !== "") chunks.push(nameGuard);
2594
2589
  }
2595
2590
  const globalRoles = globalRolesSql(ctx);
2596
2591
  if (globalRoles !== "") chunks.push(globalRoles);
@@ -2600,20 +2595,26 @@ revoke all on table ${ur} from anon, authenticated, public;`);
2600
2595
  }
2601
2596
  function grantBodies(ctx) {
2602
2597
  const keys = ctx.apiKeys;
2598
+ const scoped = ctx.tokenScopes !== void 0;
2603
2599
  const set = ctx.grantSet === true;
2604
- const allows = apiKeyAllowsCall(ctx, set ? "ids.grant_key" : "p_grant");
2605
- const keyTenant = keyTenantSql(ctx, void 0) ?? "true";
2600
+ const ceiling = (grant) => {
2601
+ const calls = [...keys === void 0 ? [] : [apiKeyAllowsCall(ctx, grant)], ...scoped ? [tokenScopeAllowsCall(ctx, grant)] : []];
2602
+ return calls.length === 0 ? void 0 : calls.join(" and ");
2603
+ };
2604
+ const allows = ceiling(set ? "ids.grant_key" : "p_grant");
2605
+ const keyTenant = keys === void 0 ? "" : ` and ${keyTenantSql(ctx, void 0) ?? "true"}`;
2606
+ const hasAllows = ceiling(set ? "g.grant_key" : "p_grant");
2606
2607
  return {
2607
- has: sqlBody(keys === void 0 ? hasBody(ctx) : set ? ` select g.grant_key
2608
+ has: sqlBody(hasAllows === void 0 ? hasBody(ctx) : set ? ` select g.grant_key
2608
2609
  from (
2609
2610
  ${hasBody(ctx)}
2610
2611
  ) g(grant_key)
2611
- where ${apiKeyAllowsCall(ctx, "g.grant_key")} and ${keyTenant}` : ceilingHasSql(hasBody(ctx), `${allows} and ${keyTenant}`)),
2612
+ where ${hasAllows}${keyTenant}` : ceilingHasSql(hasBody(ctx), `${hasAllows}${keyTenant}`)),
2612
2613
  ids: (scope) => {
2613
2614
  const type = scopeTypeOf(ctx, scope);
2614
2615
  const body = scopedBody(ctx, scope, type);
2615
- if (keys === void 0) return body;
2616
- const sql = scope === rootName(ctx) ? `${body.sql}\n union\n${serviceKeyIdsSql(ctx, keys, scope, type)}` : body.sql;
2616
+ if (allows === void 0) return body;
2617
+ const sql = keys !== void 0 && scope === rootName(ctx) ? `${body.sql}\n union\n${serviceKeyIdsSql(ctx, keys, scope, type)}` : body.sql;
2617
2618
  return {
2618
2619
  sql: set ? ` select ids.id, ids.grant_key
2619
2620
  from (
@@ -2626,7 +2627,7 @@ ${sql}
2626
2627
  };
2627
2628
  }
2628
2629
  function forUserContext(ctx) {
2629
- const { apiKeys: _apiKeys, ...unkeyed } = ctx;
2630
+ const { apiKeys: _apiKeys, tokenScopes: _tokenScopes, ...unkeyed } = ctx;
2630
2631
  return {
2631
2632
  ...unkeyed,
2632
2633
  subjectId: "p_user",
@@ -2635,18 +2636,27 @@ function forUserContext(ctx) {
2635
2636
  }
2636
2637
  //#endregion
2637
2638
  //#region src/cli/rls/permission-keys.ts
2638
- /** The helpers that take a permission key instead of a grant key. Names are part of the SQL contract. */
2639
+ /**
2640
+ * The helpers that take a permission key instead of a grant key. Names are part
2641
+ * of the SQL contract.
2642
+ */
2639
2643
  const PERMISSION_HELPERS = {
2640
2644
  grantKeys: "grant_keys",
2641
2645
  has: "permdock_has_permission",
2642
2646
  hasFor: "permdock_has_permission_for"
2643
2647
  };
2644
- /** `permitted_<scope>_permission_keys(p_id)`: every permission key the subject holds on one instance. */
2648
+ /**
2649
+ * `permitted_<scope>_permission_keys(p_id)`: every permission key the subject
2650
+ * holds on one instance.
2651
+ */
2645
2652
  function permissionKeysHelper(scope) {
2646
2653
  permittedIdsHelper(scope);
2647
2654
  return `permitted_${scope}_permission_keys`;
2648
2655
  }
2649
- /** `permitted_<scope>_permission_keys_for(p_user, p_id)`, written in `database` mode. */
2656
+ /**
2657
+ * `permitted_<scope>_permission_keys_for(p_user, p_id)`, written in `database`
2658
+ * mode.
2659
+ */
2650
2660
  function permissionKeysForHelper(scope) {
2651
2661
  return `${permissionKeysHelper(scope)}_for`;
2652
2662
  }
@@ -2666,7 +2676,10 @@ function requiresSql(ctx, key, resource) {
2666
2676
  return `${quoteIdent(field)} in (select ${qualified(ctx, checkName(permittedByPermissionHelper(scope)))}(${literal}))`;
2667
2677
  })].join(" or ")})`;
2668
2678
  }
2669
- /** Postgres truncates an identifier past 63 bytes, which would merge two helpers. */
2679
+ /**
2680
+ * Postgres truncates an identifier past 63 bytes, which would merge two
2681
+ * helpers.
2682
+ */
2670
2683
  function checkName(name) {
2671
2684
  if (new TextEncoder().encode(name).length > 63) throw configError(`PermDock CLI: the helper name ${name} is longer than 63 bytes; use a shorter scope name`);
2672
2685
  return name;
@@ -2717,17 +2730,17 @@ ${access}`;
2717
2730
  }
2718
2731
  /**
2719
2732
  * The permission-key forms of the helpers: `grant_keys`, which maps a
2720
- * permission key to the grant keys of its unconditional allows on a scope
2721
- * (or every deny key, or the conditioned allow or deny keys),
2733
+ * permission key to the grant keys of its unconditional allows on a scope (or
2734
+ * every deny key, or the conditioned allow or deny keys),
2722
2735
  * `permdock_has_permission(p_permission)` and one
2723
2736
  * `permitted_<scope>_ids_by_permission(p_permission)` per scope, plus their
2724
2737
  * `_for(p_user, ...)` forms in `database` mode. They answer with the
2725
- * unconditional allows minus any deny, so a permission whose allows all carry
2726
- * a row condition answers nothing. The
2738
+ * unconditional allows minus any deny, so a permission whose allows all carry a
2739
+ * row condition answers nothing. The
2727
2740
  * `permitted_<scope>_ids_by_permission(p_permission, p_conditioned)` overload
2728
2741
  * with `true` also lists the instances a conditioned allow reaches and
2729
- * subtracts only unconditional denies, leaving the row condition to the
2730
- * caller, as `permittedIds(..., { conditioned: true })` does in process.
2742
+ * subtracts only unconditional denies, leaving the row condition to the caller,
2743
+ * as `permittedIds(..., { conditioned: true })` does in process.
2731
2744
  */
2732
2745
  function permissionHelpersSql(ctx, grants, renamed, anonExecute) {
2733
2746
  const keys = qualified(ctx, PERMISSION_HELPERS.grantKeys);
@@ -2851,7 +2864,10 @@ ${keysFunction(keysFor, `p_user ${user}, p_id ${type}, p_keys text[]`, keysBody(
2851
2864
  }
2852
2865
  //#endregion
2853
2866
  //#region src/cli/rls/ownership-names.ts
2854
- /** Objects the ownership rules add next to the helpers. Names are part of the SQL contract. */
2867
+ /**
2868
+ * Objects the ownership rules add next to the helpers. Names are part of the
2869
+ * SQL contract.
2870
+ */
2855
2871
  const OWNERSHIP = {
2856
2872
  canAssign: "permdock_can_assign",
2857
2873
  canAssignFor: "permdock_can_assign_for",
@@ -2867,231 +2883,14 @@ const ASSIGNMENTS = {
2867
2883
  trigger: "permdock_assignment"
2868
2884
  };
2869
2885
  //#endregion
2870
- //#region src/cli/rls/assignments.ts
2871
- /** The client roles whose writes the assignment triggers check; any other role is a trusted path. */
2872
- const CLIENT_ROLES = [
2873
- "anon",
2874
- "anonymous",
2875
- "authenticated"
2876
- ];
2877
- /** The mapped membership tables, `rls.assignments.tables` and the global-roles table `rls.roles`. */
2878
- function assignedTables(ctx) {
2879
- const out = [];
2880
- for (const { name } of ctx.scopes) {
2881
- const mapped = scopeTable(ctx, name);
2882
- if (mapped === void 0) continue;
2883
- out.push({
2884
- table: mapped.table.table,
2885
- scope: name,
2886
- id: mapped.column,
2887
- tenant: mapped.tenantColumn ?? mapped.column,
2888
- role: mapped.table.role,
2889
- user: mapped.table.user
2890
- });
2891
- }
2892
- if (ctx.roles !== void 0) out.push({
2893
- table: ctx.roles.table,
2894
- scope: "global",
2895
- role: ctx.roles.role ?? "role",
2896
- user: ctx.roles.user ?? "user_id"
2897
- });
2898
- for (const extra of ctx.assignments?.tables ?? []) {
2899
- const scope = resolveScope(ctx.scopes, extra.scope);
2900
- if (scope === void 0) throw configError(`PermDock CLI: rls.assignments.tables names scope '${extra.scope}', which the policy does not declare`);
2901
- if (scope !== ctx.scopes[0]?.name && extra.tenant === void 0) throw configError(`PermDock CLI: rls.assignments.tables entry ${extra.table} assigns at ${scope}, below the first scope: name its tenant column`);
2902
- out.push({
2903
- table: extra.table,
2904
- scope,
2905
- id: extra.id,
2906
- tenant: extra.tenant ?? extra.id,
2907
- role: extra.role,
2908
- ...extra.user === void 0 ? {} : { user: extra.user }
2909
- });
2910
- }
2911
- return out;
2912
- }
2913
- /** The tables `rls.assignments.ownRole` refuses a caller's own rows on, by table key. */
2914
- function ownRoleTables(ctx, tables) {
2915
- const own = ctx.assignments?.ownRole;
2916
- if (own === void 0) return /* @__PURE__ */ new Set();
2917
- if (own === "refuse") return new Set(tables.filter((entry) => entry.user !== void 0).map((entry) => qualifySqlTable(entry.table)));
2918
- const out = /* @__PURE__ */ new Set();
2919
- for (const [name, value] of Object.entries(own)) {
2920
- const entry = tables.find((table) => qualifySqlTable(table.table) === qualifySqlTable(name));
2921
- if (entry === void 0) throw configError(`PermDock CLI: rls.assignments.ownRole names ${name}, which no assignment trigger guards`);
2922
- if (value !== "refuse") throw configError(`PermDock CLI: rls.assignments.ownRole.${name} must be 'refuse'`);
2923
- if (entry.user === void 0) throw configError(`PermDock CLI: rls.assignments.ownRole names ${name}, which has no user column: set its user in rls.assignments.tables`);
2924
- out.add(qualifySqlTable(entry.table));
2925
- }
2926
- return out;
2927
- }
2928
- /**
2929
- * The tables `rls.assignments` guards, schema-qualified: each scope's mapped
2930
- * membership table, the global-roles table (`rls.roles`, else
2931
- * `supabase.hook.roles`) and `rls.assignments.tables`. Empty without
2932
- * `rls.assignments`.
2933
- */
2934
- function guardedTables(config, scopes) {
2935
- const rls = config.rls;
2936
- if (rls?.assignments === void 0) return [];
2937
- const hook = config.supabase?.hook?.roles;
2938
- const roles = rls.roles ?? (hook === false ? void 0 : hook);
2939
- const tables = [
2940
- ...scopes.flatMap((scope) => {
2941
- const mapped = memberForTable({
2942
- scopes,
2943
- ...rls.memberships === void 0 ? {} : { memberships: rls.memberships }
2944
- }, scope.name);
2945
- return mapped === void 0 ? [] : [mapped.table];
2946
- }),
2947
- ...roles === void 0 ? [] : [roles.table],
2948
- ...(rls.assignments === true ? [] : rls.assignments.tables ?? []).map((entry) => entry.table)
2949
- ];
2950
- return [...new Set(tables.map(qualifySqlTable))];
2951
- }
2952
- function isRoleList(role) {
2953
- return Array.isArray(role);
2954
- }
2955
- /** The role keys a row variable holds, one scalar per role source. */
2956
- function rowRoles(entry, row) {
2957
- return (isRoleList(entry.role) ? entry.role : [entry.role]).map((part) => roleColumn(part, qualifySqlTable(entry.table), row, {
2958
- label: `rls.assignments ${entry.table} role`,
2959
- prefix: "PermDock CLI"
2960
- }).lookup);
2961
- }
2962
- /**
2963
- * `permdock_can_assign_custom_role`: whether the signed-in caller may assign a
2964
- * custom role of the tenant at the instance, by the checks the custom-role
2965
- * write functions run on its stored definition.
2966
- */
2967
- function canAssignCustomSql(ctx, forUser = false) {
2968
- const tenantType = tenantTypeOf(ctx);
2969
- const fn = qualified(ctx, forUser ? ASSIGNMENTS.customFor : ASSIGNMENTS.custom);
2970
- const perms = qualified(ctx, CUSTOM_ROLES.permissions);
2971
- const includes = qualified(ctx, CUSTOM_ROLES.includes);
2972
- const guard = qualified(ctx, forUser ? CUSTOM_ROLES.guardFor : CUSTOM_ROLES.guard);
2973
- const userParam = forUser ? "p_user uuid, " : "";
2974
- const userArg = forUser ? "uuid, " : "";
2975
- const stored = `${storedTenant(ctx)} and c.scope = p_scope and c.role = p_role and (c.scope_id is null or c.scope_id = p_scope_id)`;
2976
- const grants = forUser ? `revoke execute on function ${fn}(${userArg}${tenantType}, text, text, text) from public, anon, authenticated;` : `revoke execute on function ${fn}(${tenantType}, text, text, text) from public, anon;
2977
- grant execute on function ${fn}(${tenantType}, text, text, text) to authenticated;`;
2978
- return `-- whether ${forUser ? "user p_user" : "the caller"} may assign custom role p_role at instance p_scope_id (a platform role: null tenant, scope global): the custom-role write checks on its stored definition${forUser ? "; no client role may execute it" : ""}
2979
- create or replace function ${fn}(${userParam}p_tenant ${tenantType}, p_scope text, p_scope_id text, p_role text)
2980
- returns boolean
2981
- language plpgsql
2982
- stable
2983
- security definer
2984
- set search_path = ''
2985
- as $$
2986
- declare
2987
- v_scope_id text;
2988
- v_found boolean := false;
2989
- begin
2990
- select true, c.scope_id into v_found, v_scope_id
2991
- from (
2992
- select c.scope_id from ${perms} c where ${stored}
2993
- union all
2994
- select c.scope_id from ${includes} c where ${stored}
2995
- ) c
2996
- order by c.scope_id nulls first
2997
- limit 1;
2998
- if not coalesce(v_found, false) then
2999
- return false;
3000
- end if;
3001
- perform ${guard}(${forUser ? "p_user, " : ""}p_tenant, p_scope, v_scope_id, p_role);
3002
- return true;
3003
- exception
3004
- when insufficient_privilege or invalid_parameter_value then
3005
- return false;
3006
- end;
3007
- $$;
3008
- ${grants}`;
3009
- }
3010
- /**
3011
- * The assignment triggers (\`rls.assignments\`): before a client role inserts,
3012
- * updates or deletes a row that assigns a role, every role the old and new
3013
- * row hold must be one the caller may assign at that instance, by
3014
- * \`permdock_can_assign\` for a declared role and
3015
- * \`permdock_can_assign_custom_role\` for a custom one. The functions run as
3016
- * the invoker, so \`current_user\` tells a client write from a trusted one.
3017
- */
3018
- function assignmentSql(ctx) {
3019
- const own = ctx.ownership;
3020
- if (ctx.assignments === void 0 || own === void 0 || own.assigns.length === 0) return "";
3021
- const canAssign = qualified(ctx, OWNERSHIP.canAssign);
3022
- const custom = ctx.customRoles !== void 0 && ctx.authorize === "database";
3023
- const declared = ctx.customRoles?.declared;
3024
- const tenantType = tenantTypeOf(ctx);
3025
- const chunks = [];
3026
- const clients = `current_user::text = any(array[${CLIENT_ROLES.map(quoteLiteral).join(", ")}])`;
3027
- const tables = assignedTables(ctx);
3028
- const ownRole = ownRoleTables(ctx, tables);
3029
- for (const entry of tables) {
3030
- const table = quoteQualified(entry.table);
3031
- const suffix = entry.table.replaceAll(/[^a-z0-9_]/gu, "_");
3032
- const fn = qualified(ctx, quoteIdent(`${ASSIGNMENTS.trigger}_${suffix}`));
3033
- const ownUser = entry.user !== void 0 && ownRole.has(qualifySqlTable(entry.table)) ? entry.user : void 0;
3034
- const ownCheck = (row, reason) => ownUser === void 0 ? "" : ` if ${row}.${quoteIdent(ownUser)}::text = (${subjectIdSql(ctx)})::text then
3035
- raise exception using
3036
- errcode = '42501',
3037
- message = 'permdock: the caller may not change their own roles',
3038
- hint = '${reason}';
3039
- end if;
3040
- `;
3041
- const check = (row) => {
3042
- const id = entry.id === void 0 ? "null::text" : `${row}.${quoteIdent(entry.id)}::text`;
3043
- const customFn = qualified(ctx, ASSIGNMENTS.custom);
3044
- let allowed = `${canAssign}(v_role, ${id})`;
3045
- if (custom && declared !== void 0) {
3046
- const platform = `${customFn}(null, 'global', null, v_role)`;
3047
- allowed = entry.id === void 0 || entry.tenant === void 0 ? `case when v_role = any(${declaredArray(declared)}) then ${canAssign}(v_role, ${id}) else ${platform} end` : `case when v_role = any(${declaredArray(declared)}) then ${canAssign}(v_role, ${id}) when ${id} is null then ${platform} else ${customFn}(${row}.${quoteIdent(entry.tenant)}::${tenantType}, ${quoteLiteral(entry.scope)}, ${id}, v_role) end`;
3048
- }
3049
- return ` foreach v_role in array array[${rowRoles(entry, row).join(", ")}]::text[] loop
3050
- if v_role is not null and not coalesce(${allowed}, false) then
3051
- raise exception using
3052
- errcode = '42501',
3053
- message = 'permdock: the caller may not assign ' || v_role || ' in ' || coalesce(${id}, 'null'),
3054
- hint = 'not-assignable-by';
3055
- end if;
3056
- end loop;`;
3057
- };
3058
- const what = entry.id === void 0 ? "global roles" : `${entry.scope} roles`;
3059
- chunks.push(`-- ${entry.table}: a client role may write only the ${what} it may assign${ownUser === void 0 ? "" : ", and none on its own rows"}
3060
- create or replace function ${fn}()
3061
- returns trigger
3062
- language plpgsql
3063
- volatile
3064
- set search_path = ''
3065
- as $$
3066
- declare
3067
- v_role text;
3068
- begin
3069
- if not (${clients}) then
3070
- return coalesce(new, old);
3071
- end if;
3072
- if tg_op in ('UPDATE', 'DELETE') then
3073
- ${ownCheck("old", "self-demotion")}${check("old")}
3074
- end if;
3075
- if tg_op in ('INSERT', 'UPDATE') then
3076
- ${ownCheck("new", "not-assignable-by")}${check("new")}
3077
- end if;
3078
- return coalesce(new, old);
3079
- end;
3080
- $$;
3081
- revoke execute on function ${fn}() from public, anon, authenticated;
3082
- drop trigger if exists "permdock_assignment" on ${table};
3083
- create trigger "permdock_assignment"
3084
- before insert or update or delete on ${table}
3085
- for each row execute function ${fn}();`);
3086
- }
3087
- return `${chunks.join("\n\n")}\n`;
3088
- }
3089
- //#endregion
3090
2886
  //#region src/cli/rls/ownership-triggers.ts
3091
2887
  function objectName(prefix, scope) {
3092
2888
  return quoteIdent(`${prefix}_${scope}`);
3093
2889
  }
3094
- /** Live holders of `roleExpr` in instance `idExpr`, with the same kind and expiry rules as the helpers. */
2890
+ /**
2891
+ * Live holders of `roleExpr` in instance `idExpr`, with the same kind and
2892
+ * expiry rules as the helpers.
2893
+ */
3095
2894
  function holdersSql(ctx, table, column, idExpr, role, into) {
3096
2895
  const roleExpr = typeof role === "string" ? quoteLiteral(role) : role.expr;
3097
2896
  const held = memberRoleOf(table, "m", " ");
@@ -3168,10 +2967,10 @@ create constraint trigger ${objectName(OWNERSHIP.holders, scope)}
3168
2967
  for each row execute function ${fn}();`;
3169
2968
  }
3170
2969
  /**
3171
- * `transferOnly` as statement triggers over transition tables: a statement
3172
- * that changes a role's holder count in an instance that had holders before
3173
- * and still has some after is refused. Creating the first holder and removing
3174
- * the last (which `min` guards) are not transfers.
2970
+ * `transferOnly` as statement triggers over transition tables: a statement that
2971
+ * changes a role's holder count in an instance that had holders before and
2972
+ * still has some after is refused. Creating the first holder and removing the
2973
+ * last (which `min` guards) are not transfers.
3175
2974
  */
3176
2975
  function transferTriggerSql(ctx, scope, table, column, roles) {
3177
2976
  const fn = qualified(ctx, `${OWNERSHIP.transferOnly}_${scope}`);
@@ -3234,8 +3033,8 @@ ${trigger("delete")}`;
3234
3033
  //#endregion
3235
3034
  //#region src/cli/rls/ownership-sources.ts
3236
3035
  /**
3237
- * The membership sources holding scope `name`: `undefined` when there are
3238
- * none, `'not-tables'` when one of them carries no table to put a trigger on.
3036
+ * The membership sources holding scope `name`: `undefined` when there are none,
3037
+ * `'not-tables'` when one of them carries no table to put a trigger on.
3239
3038
  */
3240
3039
  function holderSources(ctx, name) {
3241
3040
  const sources = memberForSources(ctx, name);
@@ -3253,7 +3052,10 @@ function holderSources(ctx, name) {
3253
3052
  function indent(sql, by) {
3254
3053
  return sql.replaceAll(/^/gmu, by);
3255
3054
  }
3256
- /** Every source's rows of one instance, each compared with its own typed `v_key_<n>`. */
3055
+ /**
3056
+ * Every source's rows of one instance, each compared with its own typed
3057
+ * `v_key_<n>`.
3058
+ */
3257
3059
  function sourceRowsOf(scope, sources) {
3258
3060
  return sources.map((source, index) => indent(source.holders.rows(scope, { id: `v_key_${String(index)}` }), " ")).join("\n union all\n");
3259
3061
  }
@@ -3269,7 +3071,10 @@ function regclassOf(table) {
3269
3071
  function sourceTables(sources) {
3270
3072
  return [...new Set(sources.map((source) => quoteQualified(source.table)))];
3271
3073
  }
3272
- /** `min` and `max` over membership sources: the same commit-time check as on a mapped table, counted over every source of the scope. */
3074
+ /**
3075
+ * `min` and `max` over membership sources: the same commit-time check as on a
3076
+ * mapped table, counted over every source of the scope.
3077
+ */
3273
3078
  function sourceHoldersTriggerSql(ctx, scope, sources, counted) {
3274
3079
  const fn = qualified(ctx, `${OWNERSHIP.holders}_${scope}`);
3275
3080
  const keys = sourceKeys(sources);
@@ -3342,7 +3147,10 @@ ${triggers.join("\n")}`;
3342
3147
  function collectChangesSql(changed) {
3343
3148
  return ` v_changes := v_changes || coalesce((select jsonb_agg(to_jsonb(c)) from (${changed}) c), '[]'::jsonb);`;
3344
3149
  }
3345
- /** `transferOnly` over membership sources: each source table's statement triggers diff its transition tables, and the count after reads every source. */
3150
+ /**
3151
+ * `transferOnly` over membership sources: each source table's statement
3152
+ * triggers diff its transition tables, and the count after reads every source.
3153
+ */
3346
3154
  function sourceTransferTriggerSql(ctx, scope, sources, roles) {
3347
3155
  const fn = qualified(ctx, `${OWNERSHIP.transferOnly}_${scope}`);
3348
3156
  const keys = sourceKeys(sources);
@@ -3420,278 +3228,410 @@ function sourceRowsSql(sources, user) {
3420
3228
  return sources.map((source) => source.sql.select(user(source), keep).replaceAll(/^/gmu, " ")).join("\n union all\n");
3421
3229
  }
3422
3230
  //#endregion
3423
- //#region src/cli/rls/can-assign.ts
3424
- /** Whether the signed-in user (or `p_user` in the `_for` form) holds, in instance `p_scope_id`, a role whose `assigns` lists `p_role`. */
3425
- function canAssignSql(base, own, forUser = false) {
3426
- const ctx = forUser ? {
3427
- ...base,
3428
- subjectId: "p_user",
3429
- tenants: "all"
3430
- } : base;
3431
- const fn = qualified(ctx, forUser ? OWNERSHIP.canAssignFor : OWNERSHIP.canAssign);
3432
- const parts = [];
3433
- const used = [];
3434
- const userOf = (source) => {
3435
- const index = used.includes(source) ? used.indexOf(source) : used.push(source) - 1;
3436
- return `v_user_${String(index)}`;
3231
+ //#region src/cli/rls/graph.ts
3232
+ function tableFor(resource, tables) {
3233
+ return tables?.[resource] ?? resource;
3234
+ }
3235
+ /**
3236
+ * Every `related` condition the policy's grants compile to, with the resource
3237
+ * graph they need, groups included.
3238
+ */
3239
+ function graphPlan(policy) {
3240
+ const scopes = scopeList(policy.scopes);
3241
+ const plan = /* @__PURE__ */ new Map();
3242
+ const entryFor = (node) => {
3243
+ const entry = plan.get(node.name) ?? {
3244
+ node,
3245
+ relations: /* @__PURE__ */ new Set(),
3246
+ links: /* @__PURE__ */ new Set()
3247
+ };
3248
+ plan.set(node.name, entry);
3249
+ return entry;
3437
3250
  };
3438
- const global = own.assigns.filter((pair) => pair.scope === "global");
3439
- for (const [instance, pairs] of [["p_scope_id is not null", global.filter((pair) => pair.at !== "global")], ["p_scope_id is null", global.filter((pair) => pair.at === "global")]]) {
3440
- if (pairs.length === 0) continue;
3441
- if (ctx.authorize === "database") {
3442
- const ur = globalRoleRows(ctx);
3443
- const kind = globalKindFilterSql(ctx, ur.roleSql);
3444
- parts.push(`${instance} and exists (
3445
- select 1 from ${ur.from}
3446
- where ${ur.userSql} = ${subjectIdSql(ctx)}
3447
- and (${ur.roleSql}, p_role) in (${pairsSql(pairs)})${kind === void 0 ? "" : `\n and ${kind}`}
3448
- )`);
3449
- } else {
3450
- const kind = globalKindFilterSql(ctx, "r.role");
3451
- parts.push(`${instance} and exists (
3452
- select 1 from ${roleRows(ctx)}
3453
- where (r.role, p_role) in (${pairsSql(pairs)})${kind === void 0 ? "" : `\n and ${kind}`}
3454
- )`);
3251
+ const addRelation = (node, relation) => {
3252
+ const entry = entryFor(node);
3253
+ if (entry.relations.has(relation)) return;
3254
+ entry.relations.add(relation);
3255
+ for (const name of expandRelation(node, relation)) {
3256
+ const spec = node.relations[name];
3257
+ if (!isEdgeRelation(spec) || spec.groups === void 0) continue;
3258
+ for (const { resource, relation: groupRelation } of groupEntries(spec)) {
3259
+ const target = policy.resources.get(resource);
3260
+ if (target !== void 0 && target.name !== node.name) addRelation(target, groupRelation);
3261
+ }
3455
3262
  }
3456
- }
3457
- for (const { name } of ctx.scopes) {
3458
- const pairs = own.assigns.filter((pair) => pair.scope === name);
3459
- if (pairs.length === 0) continue;
3460
- const sources = scopeSources(ctx, name);
3461
- if (sources.length > 0) {
3462
- const kind = kindFilterSql(ctx, "r.role", "ms.via");
3463
- parts.push(`exists (
3464
- select 1
3465
- from (
3466
- ${sourceRowsSql(sources, userOf)}
3467
- ) ms
3468
- cross join lateral jsonb_array_elements_text(
3469
- case jsonb_typeof(ms.roles) when 'array' then ms.roles else '[]'::jsonb end
3470
- ) r(role)
3471
- where ms.scope = ${quoteLiteral(name)}
3472
- and ms.id = p_scope_id
3473
- and (r.role, p_role) in (${pairsSql(pairs)})${kind === void 0 ? "" : `\n and ${kind}`}${sourceFilters(ctx, name, void 0, void 0, sourcesKeep(sources)).replaceAll("\n and ", "\n and ")}
3474
- )`);
3475
- continue;
3263
+ };
3264
+ const addLinks = (hops) => {
3265
+ for (let index = 1; index < hops.length; index += 1) {
3266
+ const from = policy.resources.get(hops[index - 1]?.resource ?? "");
3267
+ const hop = hops[index];
3268
+ if (from !== void 0 && hop !== void 0) entryFor(from).links.add(hop.link);
3476
3269
  }
3477
- if (ctx.authorize === "database") {
3478
- const mapped = scopeTable(ctx, name);
3479
- if (mapped === void 0) continue;
3480
- const { table, column } = mapped;
3481
- const role = memberRoleOf(table, "m", " ");
3482
- const filters = [
3483
- `${memberColumn(table.user)} = ${subjectIdSql(ctx)}`,
3484
- `${memberColumn(column)}::text = p_scope_id`,
3485
- `(${role.sql}, p_role) in (${pairsSql(pairs)})`
3486
- ];
3487
- if (table.expiresAt !== void 0) {
3488
- const expires = memberColumn(table.expiresAt);
3489
- filters.push(`(${expires} is null or ${expires} > now())`);
3270
+ };
3271
+ const addAbove = (above) => {
3272
+ const node = above === void 0 ? void 0 : policy.resources.get(above.resource);
3273
+ if (above === void 0 || node === void 0) return;
3274
+ const entry = entryFor(node);
3275
+ entry.closure = Math.max(entry.closure ?? 0, above.depth);
3276
+ entry.restrictedAncestors = above;
3277
+ };
3278
+ for (const grant of policy.grants) {
3279
+ const row = policy.resources.get(grant.permission.resource);
3280
+ for (const item of flattenGrantee(grant.to)) {
3281
+ if (item.kind === "inherit") {
3282
+ const inherited = inheritCondition(item, row, policy.resources);
3283
+ addLinks(inherited?.hops ?? []);
3284
+ addAbove(inherited?.restrictedAncestors);
3285
+ continue;
3490
3286
  }
3491
- const disabled = disabledColumn(table);
3492
- if (disabled !== void 0) filters.push(`${disabled} is null`);
3493
- const kind = kindFilterSql(ctx, role.sql, memberVia(table));
3494
- if (kind !== void 0) filters.push(kind);
3495
- parts.push(`exists (
3496
- select 1 from ${quoteQualified(table.table)} m${role.join}
3497
- where ${filters.join("\n and ")}
3498
- )`);
3499
- } else {
3500
- const kind = kindFilterSql(ctx, "r.role", "m ->> 'via'");
3501
- parts.push(`exists (
3502
- select 1 from ${membershipRows(ctx)}
3503
- where m ->> 'scope' = ${quoteLiteral(name)}
3504
- and m ->> 'id' = p_scope_id
3505
- and (r.role, p_role) in (${pairsSql(pairs)})
3506
- and case jsonb_typeof(m -> 'expires_at')
3507
- when 'number' then (m ->> 'expires_at')::numeric > extract(epoch from now())
3508
- else true
3509
- end${kind === void 0 ? "" : `\n and ${kind}`}${claimKeptLines(ctx, " ").map((line) => `\n${line}`).join("")}
3510
- )`);
3287
+ if (item.kind !== "relation") continue;
3288
+ const condition = relationCondition(item, row, scopes, { resources: policy.resources });
3289
+ if (condition?.op !== "related") continue;
3290
+ const node = policy.resources.get(condition.resource);
3291
+ if (node === void 0) continue;
3292
+ addRelation(node, condition.relation);
3293
+ const entry = entryFor(node);
3294
+ if (condition.depth > 0 && isSelfParented(node)) entry.closure = Math.max(entry.closure ?? 0, condition.depth);
3295
+ addLinks(condition.hops ?? []);
3296
+ addAbove(condition.restrictedAncestors);
3511
3297
  }
3512
3298
  }
3513
- const body = parts.length === 0 ? "false" : parts.join("\n or ");
3514
- const language = used.length === 0 ? `language sql
3299
+ return plan;
3300
+ }
3301
+ /** The closure depth per walked resource, as `RlsSqlContext.graph` carries it. */
3302
+ function closureDepths(plan) {
3303
+ const out = {};
3304
+ for (const [name, entry] of plan) if (entry.closure !== void 0) out[name] = entry.closure;
3305
+ return out;
3306
+ }
3307
+ /**
3308
+ * The `permitted_<resource>_ids(relation)` arm for one concrete relation: ids
3309
+ * of `resource` the subject holds it on, directly or through a group. `asked`
3310
+ * are the relation names that include it, so a call for any of them (or for
3311
+ * all, with null) reads this arm.
3312
+ */
3313
+ function relationArm(ctx, plan, node, relation, asked, tables) {
3314
+ const spec = node.relations[relation];
3315
+ if (!isEdgeRelation(spec) && !isPrincipalRelation(spec) && (!isFieldRelation(spec) || spec.memberOf !== void 0)) throw configError(`PermDock CLI: relation '${relation}' on ${node.name} is not a graph relation RLS can compile`);
3316
+ const guard = asked.length === 1 ? `(p_relation is null or p_relation = ${quoteLiteral(asked[0] ?? relation)})` : `(p_relation is null or p_relation in (${asked.map(quoteLiteral).join(", ")}))`;
3317
+ return ` select a.id from (${graphSqlText(relationArmSql(node.name, relation, {
3318
+ resources: new Map([...plan].map(([name, item]) => [name, item.node])),
3319
+ ...tables === void 0 ? {} : { tables },
3320
+ qualify: qualifySqlTable,
3321
+ holders: (resource, groupRelation) => [{ text: `select ${qualified(ctx, graphHelper(resource))}(${quoteLiteral(groupRelation)})` }]
3322
+ }), ctx)}) a\n where ${guard}${activeUserSql(ctx).map((part) => ` and ${part}`).join("")}`;
3323
+ }
3324
+ function permittedSql(ctx, plan, entry, tables) {
3325
+ const fn = qualified(ctx, graphHelper(entry.node.name));
3326
+ const asked = [...entry.relations].toSorted();
3327
+ const arms = [...new Set(asked.flatMap((name) => expandRelation(entry.node, name)))].toSorted().map((relation) => relationArm(ctx, plan, entry.node, relation, asked.filter((name) => expandRelation(entry.node, name).includes(relation)), tables));
3328
+ if (arms.length === 0) arms.push(" select null::text where false");
3329
+ return `-- ${entry.node.name}: ids the subject holds a relation on (all relations when p_relation is null)
3330
+ create or replace function ${fn}(p_relation text)
3331
+ returns setof text
3332
+ language sql
3515
3333
  stable
3516
3334
  security definer
3517
3335
  set search_path = ''
3518
3336
  as $$
3519
- select ${signedIn(ctx)} and (
3520
- ${body}
3521
- )
3522
- $$;` : `language plpgsql
3337
+ ${arms.join("\n union\n")}
3338
+ $$;
3339
+ revoke execute on function ${fn}(text) from public, anon;
3340
+ grant execute on function ${fn}(text) to authenticated;`;
3341
+ }
3342
+ /**
3343
+ * `permdock_link_<resource>_<link>(ids)`: ids of `resource` whose link points
3344
+ * into `ids`, read past the table's own policies.
3345
+ */
3346
+ function linkSql(ctx, node, link, tables) {
3347
+ const spec = Object.hasOwn(node.links, link) ? node.links[link] : void 0;
3348
+ if (spec === void 0) throw configError(`PermDock CLI: ${node.name} has no link '${link}'`);
3349
+ const fn = qualified(ctx, linkHelper(node.name, link));
3350
+ return `-- ${node.name}.${link}: rows whose ${spec.field} points at one of the given ${spec.resource} ids
3351
+ create or replace function ${fn}(p_ids text[])
3352
+ returns setof text
3353
+ language sql
3523
3354
  stable
3524
3355
  security definer
3525
3356
  set search_path = ''
3357
+ as $$
3358
+ select t.${quoteIdent(node.id)}::text from ${quoteQualified(tableFor(node.name, tables))} t
3359
+ where t.${quoteIdent(spec.field)}::text = any(p_ids)
3360
+ $$;
3361
+ revoke execute on function ${fn}(text[]) from public, anon;
3362
+ grant execute on function ${fn}(text[]) to authenticated;`;
3363
+ }
3364
+ function closureTableSql(ctx) {
3365
+ const table = qualified(ctx, CLOSURE.table);
3366
+ return `-- the object graph: one row per (ancestor, descendant) within each walked resource's depth,
3367
+ -- never crossing a restricted row; kept current by statement triggers on each self-parented table
3368
+ create table if not exists ${table} (
3369
+ resource text not null,
3370
+ ancestor text not null,
3371
+ descendant text not null,
3372
+ depth integer not null check (depth >= 0),
3373
+ primary key (resource, descendant, ancestor)
3374
+ );
3375
+ create index if not exists ${quoteIdent(`${CLOSURE.table}_ancestor`)} on ${table} (resource, ancestor);
3376
+ alter table ${table} enable row level security;
3377
+ revoke all on table ${table} from anon, authenticated, public;
3378
+ grant select on table ${table} to authenticated;`;
3379
+ }
3380
+ function closureObjectsSql(ctx, entry, depth, tables) {
3381
+ const { node } = entry;
3382
+ const parent = node.parent;
3383
+ if (parent === void 0) return "";
3384
+ const name = node.name;
3385
+ const sqlName = graphSqlName(name) ?? name;
3386
+ const closure = qualified(ctx, CLOSURE.table);
3387
+ const table = quoteQualified(tableFor(name, tables));
3388
+ const id = quoteIdent(node.id);
3389
+ const up = quoteIdent(parent.field);
3390
+ const restricted = restrictedFor(node, "parent");
3391
+ const stop = restricted === void 0 ? "false" : `coalesce(t.${quoteIdent(restricted)}, false)`;
3392
+ const stopParent = restricted === void 0 ? "false" : `coalesce(p.${quoteIdent(restricted)}, false)`;
3393
+ const refresh = qualified(ctx, `${CLOSURE.table}_${sqlName}`);
3394
+ const changed = qualified(ctx, `${CLOSURE.table}_${sqlName}_changed`);
3395
+ const literal = quoteLiteral(name);
3396
+ const restrictedChanged = restricted === void 0 ? "" : ` or n.${quoteIdent(restricted)} is distinct from o.${quoteIdent(restricted)}`;
3397
+ const trigger = (suffix) => quoteIdent(`${CLOSURE.table}_${sqlName}_${suffix}`);
3398
+ return `-- ${name}: ${restricted === void 0 ? `rows reach their ancestors up to depth ${String(depth)}` : `rows reach their ancestors up to depth ${String(depth)}, stopping at a restricted row`}
3399
+ drop policy if exists ${quoteIdent(`${CLOSURE.table}_${sqlName}`)} on ${closure};
3400
+ create policy ${quoteIdent(`${CLOSURE.table}_${sqlName}`)} on ${closure}
3401
+ for select to authenticated
3402
+ using (resource = ${literal} and ancestor in (select ${qualified(ctx, graphHelper(name))}(null)));
3403
+ create or replace function ${refresh}(p_ids text[])
3404
+ returns void
3405
+ language plpgsql
3406
+ security definer
3407
+ set search_path = ''
3526
3408
  as $$
3527
3409
  declare
3528
- ${used.map((source, index) => ` v_user_${String(index)} ${source.sql.userType} := ${subjectIdSql(ctx)};`).join("\n")}
3410
+ v_cycle text;
3411
+ v_affected text[];
3529
3412
  begin
3530
- return ${signedIn(ctx)} and (
3531
- ${body}
3532
- );
3533
- end;
3534
- $$;`;
3535
- if (forUser) {
3536
- const user = ctx.dialect === "supabase" ? "uuid" : "text";
3537
- return `-- who may assign, for a user the caller names: trusted SQL acting later for a stored user; no client role may execute it
3538
- create or replace function ${fn}(p_user ${user}, p_role text, p_scope_id text)
3539
- returns boolean
3540
- ${language}
3541
- revoke execute on function ${fn}(${user}, text, text) from public, anon, authenticated;`;
3542
- }
3543
- return `-- who may assign: a held role whose assigns lists p_role, in instance p_scope_id (its scope or an ancestor's); a null p_scope_id assigns a global role, which only global assigners may
3544
- create or replace function ${fn}(p_role text, p_scope_id text)
3545
- returns boolean
3546
- ${language}
3547
- revoke execute on function ${fn}(text, text) from public, anon;
3548
- grant execute on function ${fn}(text, text) to authenticated;`;
3549
- }
3550
- /** Whether the helpers keep custom roles in tables, so assigning one is checked against its stored definition. */
3551
- function assignsCustomRoles(ctx) {
3552
- return ctx.customRoles !== void 0 && ctx.authorize === "database";
3553
- }
3554
- /** Whether `permdock_can_assign_any_for` is written: every function it calls has a `_for` form. */
3555
- function hasCanAssignAnyFor(ctx) {
3556
- return ctx.authorize === "database" && (!assignsCustomRoles(ctx) || hasCustomRoleChecksFor(ctx));
3557
- }
3558
- /**
3559
- * `permdock_can_assign_any`: one check for a declared or a custom role, so
3560
- * SQL that assigns roles calls one function: `permdock_can_assign` for a
3561
- * declared role, `permdock_can_assign_custom_role` for a custom one. `p_scope`
3562
- * `'global'` (no tenant, no instance) assigns a platform role.
3563
- */
3564
- function canAssignAnySql(ctx, custom, forUser = false) {
3565
- const tenantType = tenantTypeOf(ctx);
3566
- const fn = qualified(ctx, forUser ? OWNERSHIP.canAssignAnyFor : OWNERSHIP.canAssignAny);
3567
- const declaredFn = qualified(ctx, forUser ? OWNERSHIP.canAssignFor : OWNERSHIP.canAssign);
3568
- const customFn = qualified(ctx, forUser ? ASSIGNMENTS.customFor : ASSIGNMENTS.custom);
3569
- const userType = ctx.dialect === "supabase" ? "uuid" : "text";
3570
- const userParam = forUser ? `p_user ${userType}, ` : "";
3571
- const userArg = forUser ? `${userType}, ` : "";
3572
- const user = forUser ? "p_user, " : "";
3573
- const declared = `${declaredFn}(${user}p_role, case when p_scope = 'global' then null else p_scope_id end)`;
3574
- const body = custom && ctx.customRoles !== void 0 ? `case
3575
- when p_role = any(${declaredArray(ctx.customRoles.declared)}) then ${declared}
3576
- when p_scope = 'global' then ${customFn}(${user}null, 'global', null, p_role)
3577
- else ${customFn}(${user}p_tenant, p_scope, p_scope_id, p_role)
3578
- end` : declared;
3579
- const grants = forUser ? `revoke execute on function ${fn}(${userArg}text, ${tenantType}, text, text) from public, anon, authenticated;` : `revoke execute on function ${fn}(text, ${tenantType}, text, text) from public, anon;
3580
- grant execute on function ${fn}(text, ${tenantType}, text, text) to authenticated;`;
3581
- return `-- whether ${forUser ? "user p_user" : "the caller"} may assign p_role at instance p_scope_id of p_scope in tenant p_tenant, declared or custom (p_scope 'global' for a platform role)${forUser ? "; no client role may execute it" : ""}
3582
- create or replace function ${fn}(${userParam}p_role text, p_tenant ${tenantType}, p_scope text, p_scope_id text)
3583
- returns boolean
3413
+ with recursive walk(start, next, path) as (
3414
+ select t.${id}, t.${up}, array[t.${id}::text]
3415
+ from ${table} t
3416
+ where t.${id}::text = any(p_ids)
3417
+ union all
3418
+ select walk.start, t.${up}, walk.path || t.${id}::text
3419
+ from walk join ${table} t on t.${id} = walk.next
3420
+ where not (walk.next::text = any(walk.path))
3421
+ )
3422
+ select walk.start::text into v_cycle
3423
+ from walk
3424
+ where walk.next::text = any(walk.path)
3425
+ limit 1;
3426
+ if v_cycle is not null then
3427
+ raise exception 'permdock: % % is its own ancestor', ${literal}, v_cycle
3428
+ using errcode = 'check_violation';
3429
+ end if;
3430
+ with recursive down(id, level) as (
3431
+ select t.${id}, 0
3432
+ from ${table} t
3433
+ where t.${id}::text = any(p_ids) or t.${up}::text = any(p_ids)
3434
+ union
3435
+ select t.${id}, down.level + 1
3436
+ from down join ${table} t on t.${up} = down.id
3437
+ where down.level < ${String(depth)}
3438
+ )
3439
+ select coalesce(array_agg(distinct down.id::text), '{}') into v_affected from down;
3440
+ delete from ${closure} c
3441
+ where c.resource = ${literal}
3442
+ and (c.descendant = any(p_ids) or c.descendant = any(v_affected));
3443
+ insert into ${closure} (resource, ancestor, descendant, depth)
3444
+ with recursive walk(descendant, ancestor, parent, depth, stop) as (
3445
+ select t.${id}::text, t.${id}, t.${up}, 0, ${stop}
3446
+ from ${table} t
3447
+ where t.${id}::text = any(v_affected)
3448
+ union all
3449
+ select walk.descendant, p.${id}, p.${up}, walk.depth + 1, ${stopParent}
3450
+ from walk join ${table} p on p.${id} = walk.parent
3451
+ where not walk.stop and walk.depth < ${String(depth)}
3452
+ )
3453
+ select ${literal}, walk.ancestor::text, walk.descendant, walk.depth from walk
3454
+ on conflict do nothing;
3455
+ end
3456
+ $$;
3457
+ revoke execute on function ${refresh}(text[]) from public, anon, authenticated;
3458
+ create or replace function ${changed}()
3459
+ returns trigger
3460
+ language plpgsql
3461
+ security definer
3462
+ set search_path = ''
3463
+ as $$
3464
+ begin
3465
+ if tg_op = 'TRUNCATE' then
3466
+ delete from ${closure} where resource = ${literal};
3467
+ elsif tg_op = 'INSERT' then
3468
+ perform ${refresh}(array(select n.${id}::text from new_rows n));
3469
+ elsif tg_op = 'DELETE' then
3470
+ perform ${refresh}(array(select o.${id}::text from old_rows o));
3471
+ else
3472
+ perform ${refresh}(array(
3473
+ select n.${id}::text
3474
+ from new_rows n left join old_rows o on o.${id} = n.${id}
3475
+ where o.${id} is null or n.${up} is distinct from o.${up}${restrictedChanged}
3476
+ union
3477
+ select o.${id}::text
3478
+ from old_rows o left join new_rows n on n.${id} = o.${id}
3479
+ where n.${id} is null
3480
+ ));
3481
+ end if;
3482
+ return null;
3483
+ end
3484
+ $$;
3485
+ revoke execute on function ${changed}() from public, anon, authenticated;
3486
+ drop trigger if exists ${trigger("insert")} on ${table};
3487
+ create trigger ${trigger("insert")} after insert on ${table}
3488
+ referencing new table as new_rows
3489
+ for each statement execute function ${changed}();
3490
+ drop trigger if exists ${trigger("update")} on ${table};
3491
+ create trigger ${trigger("update")} after update on ${table}
3492
+ referencing old table as old_rows new table as new_rows
3493
+ for each statement execute function ${changed}();
3494
+ drop trigger if exists ${trigger("delete")} on ${table};
3495
+ create trigger ${trigger("delete")} after delete on ${table}
3496
+ referencing old table as old_rows
3497
+ for each statement execute function ${changed}();
3498
+ drop trigger if exists ${trigger("truncate")} on ${table};
3499
+ create trigger ${trigger("truncate")} after truncate on ${table}
3500
+ for each statement execute function ${changed}();
3501
+ select ${refresh}(array(select t.${id}::text from ${table} t));`;
3502
+ }
3503
+ function restrictedRowsHelperSql(ctx, plan, entry, tables) {
3504
+ const above = entry.restrictedAncestors;
3505
+ if (above === void 0) return "";
3506
+ const fn = qualified(ctx, restrictedHelper(entry.node.name));
3507
+ const body = graphSqlText(restrictedRowsSql(above, {
3508
+ resources: new Map([...plan].map(([name, item]) => [name, item.node])),
3509
+ ...tables === void 0 ? {} : { tables },
3510
+ qualify: qualifySqlTable,
3511
+ closure: `${helperSchema(ctx)}.${CLOSURE.table}`,
3512
+ closureDepths: closureDepths(plan)
3513
+ }), ctx);
3514
+ return `-- ${entry.node.name}: rows a stopped link does not reach below: restricted, under a restricted row within ${String(above.depth - 1)} levels, or deeper
3515
+ create or replace function ${fn}()
3516
+ returns setof text
3584
3517
  language sql
3585
3518
  stable
3586
3519
  security definer
3587
3520
  set search_path = ''
3588
3521
  as $$
3589
- select coalesce(${body}, false)
3522
+ ${body}
3590
3523
  $$;
3591
- ${grants}`;
3524
+ revoke execute on function ${fn}() from public, anon;
3525
+ grant execute on function ${fn}() to authenticated;`;
3592
3526
  }
3593
- //#endregion
3594
- //#region src/cli/rls/ownership.ts
3595
- function roleScope(policy, scopes, name) {
3596
- const on = policy.rolesByName.get(name)?.on ?? findRole(policy.vocabulary.roles, name)?.on;
3597
- return typeof on === "string" ? resolveScope(scopes, on) : void 0;
3598
- }
3599
- function isGlobalRole(policy, name) {
3600
- const binding = policy.rolesByName.get(name);
3601
- if (binding !== void 0) return binding.on === void 0;
3602
- const leaf = findRole(policy.vocabulary.roles, name);
3603
- return leaf !== void 0 && leaf.on === void 0;
3604
- }
3605
- /** The rules `rls generate` enforces; `undefined` when no role declares one. */
3606
- function ownershipRules(policy, scopes) {
3607
- const kinds = {};
3608
- const assigns = [];
3609
- const counted = [];
3610
- for (const binding of policy.roles) {
3611
- const scope = roleScope(policy, scopes, binding.name);
3612
- if (binding.for !== void 0) kinds[binding.name] = binding.for;
3613
- const assigner = scope ?? (binding.on === void 0 ? "global" : void 0);
3614
- for (const target of binding.assigns ?? []) {
3615
- const at = roleScope(policy, scopes, target);
3616
- if (assigner !== void 0 && at !== void 0 && (assigner === "global" || scopeChain(scopes, at).includes(assigner))) assigns.push({
3617
- assigner: binding.name,
3618
- scope: assigner,
3619
- role: target,
3620
- at
3621
- });
3622
- else if (assigner === "global" && isGlobalRole(policy, target)) assigns.push({
3623
- assigner: binding.name,
3624
- scope: "global",
3625
- role: target,
3626
- at: "global"
3627
- });
3628
- }
3629
- const min = binding.min ?? 0;
3630
- const transferOnly = binding.transferOnly === true;
3631
- if (scope !== void 0 && (min > 0 || binding.max !== void 0 || transferOnly)) counted.push(binding.max === void 0 ? {
3632
- role: binding.name,
3633
- scope,
3634
- min,
3635
- transferOnly
3636
- } : {
3637
- role: binding.name,
3638
- scope,
3639
- min,
3640
- max: binding.max,
3641
- transferOnly
3642
- });
3527
+ /** Group resources a resource's asked relations name, other than itself. */
3528
+ function groupTargets(entry) {
3529
+ const out = /* @__PURE__ */ new Set();
3530
+ for (const asked of entry.relations) for (const name of expandRelation(entry.node, asked)) {
3531
+ const spec = entry.node.relations[name];
3532
+ const resources = isEdgeRelation(spec) ? groupEntries(spec).map((group) => group.resource) : [];
3533
+ for (const resource of resources) if (resource !== entry.node.name) out.add(resource);
3643
3534
  }
3644
- if (Object.keys(kinds).length === 0 && assigns.length === 0 && counted.length === 0) return;
3645
- return {
3646
- kinds,
3647
- assigns,
3648
- counted
3535
+ return [...out].toSorted();
3536
+ }
3537
+ /**
3538
+ * Plan entries by name, each after the group resources its helper calls (SQL
3539
+ * function bodies are checked on create).
3540
+ */
3541
+ function dependencyOrder(plan) {
3542
+ const out = [];
3543
+ const done = /* @__PURE__ */ new Set();
3544
+ const visit = (name) => {
3545
+ const entry = plan.get(name);
3546
+ if (entry === void 0 || done.has(name)) return;
3547
+ done.add(name);
3548
+ for (const target of groupTargets(entry)) visit(target);
3549
+ out.push(entry);
3649
3550
  };
3551
+ for (const name of [...plan.keys()].toSorted()) visit(name);
3552
+ return out;
3650
3553
  }
3651
3554
  /**
3652
- * The ownership objects: holder-count and transfer-only triggers on each
3653
- * scope's membership table, and `permdock_can_assign` for the application's
3654
- * own policies on those tables. Empty when no role declares a rule.
3555
+ * The graph preamble: `permitted_<resource>_ids` for every resource a graph
3556
+ * grant reads, and the closure table with its triggers for every self-parented
3557
+ * resource a grant walks. Empty when no grant is a graph grant.
3655
3558
  */
3656
- function ownershipSql(ctx) {
3657
- const own = ctx.ownership;
3658
- if (own === void 0) return "";
3659
- const chunks = [];
3660
- const skipped = ctx.skipOwnershipTriggers;
3661
- for (const { name } of ctx.scopes) {
3662
- const counted = own.counted.filter((rule) => rule.scope === name);
3663
- if (counted.length === 0) continue;
3664
- if (skipped === "all" || skipped?.includes(name) === true) {
3665
- chunks.push(`-- ${name}: rls.ownershipTriggers leaves out the triggers, so min, max and transferOnly are checked only by decideRoleChange`);
3666
- continue;
3667
- }
3668
- const transfer = counted.filter((rule) => rule.transferOnly).map((rule) => rule.role);
3669
- const mapped = scopeTable(ctx, name);
3670
- if (mapped === void 0) {
3671
- const sources = holderSources(ctx, name);
3672
- if (sources === void 0) chunks.push(`-- ${name}: no memberships table configured, so min, max and transferOnly are checked only by decideRoleChange`);
3673
- else if (sources === "not-tables") chunks.push(`-- ${name}: a membership source is not a table, so min, max and transferOnly are checked only by decideRoleChange`);
3674
- else {
3675
- chunks.push(sourceHoldersTriggerSql(ctx, name, sources, counted));
3676
- if (transfer.length > 0) chunks.push(sourceTransferTriggerSql(ctx, name, sources, transfer));
3677
- }
3678
- continue;
3679
- }
3680
- chunks.push(holdersTriggerSql(ctx, name, mapped.table, mapped.column, counted));
3681
- if (transfer.length > 0) chunks.push(transferTriggerSql(ctx, name, mapped.table, mapped.column, transfer));
3559
+ function graphSql(ctx, plan, tables) {
3560
+ if (plan.size === 0) return "";
3561
+ const sqlNames = /* @__PURE__ */ new Map();
3562
+ for (const name of plan.keys()) {
3563
+ const helper = graphHelper(name);
3564
+ const sql = graphSqlName(name) ?? name;
3565
+ if (ctx.scopes.some((scope) => scope.name === sql)) throw configError(`PermDock CLI: resource '${name}' has graph relations and shares its SQL name with the ${sql} scope, so ${helper} would replace the scope helper; rename the resource (permdock doctor PD032)`);
3566
+ const other = sqlNames.get(sql);
3567
+ if (other !== void 0) throw configError(`PermDock CLI: resources '${other}' and '${name}' both name the SQL helper ${helper}; rename one (permdock doctor PD032)`);
3568
+ sqlNames.set(sql, name);
3682
3569
  }
3683
- if (own.assigns.length > 0) {
3684
- chunks.push(canAssignSql(ctx, own));
3685
- if (ctx.authorize === "database") chunks.push(canAssignSql(ctx, own, true));
3686
- const custom = assignsCustomRoles(ctx);
3687
- if (custom) {
3688
- chunks.push(canAssignCustomSql(ctx));
3689
- if (hasCustomRoleChecksFor(ctx)) chunks.push(canAssignCustomSql(ctx, true));
3570
+ const entries = dependencyOrder(plan);
3571
+ const chunks = entries.flatMap((entry) => [...entry.relations.size === 0 && entry.closure === void 0 ? [] : [permittedSql(ctx, plan, entry, tables)], ...[...entry.links].toSorted().map((link) => linkSql(ctx, entry.node, link, tables))]);
3572
+ const walked = entries.filter((entry) => entry.closure !== void 0);
3573
+ if (walked.length > 0) {
3574
+ chunks.push(closureTableSql(ctx));
3575
+ for (const entry of walked) chunks.push(closureObjectsSql(ctx, entry, entry.closure ?? 0, tables));
3576
+ for (const entry of walked) {
3577
+ const helper = restrictedRowsHelperSql(ctx, plan, entry, tables);
3578
+ if (helper !== "") chunks.push(helper);
3690
3579
  }
3691
- chunks.push(canAssignAnySql(ctx, custom));
3692
- if (hasCanAssignAnyFor(ctx)) chunks.push(canAssignAnySql(ctx, custom, true));
3693
3580
  }
3694
- return chunks.length === 0 ? "" : `${chunks.join("\n\n")}\n`;
3581
+ return `${chunks.join("\n\n")}\n`;
3582
+ }
3583
+ //#endregion
3584
+ //#region src/cli/sql/tables.ts
3585
+ /**
3586
+ * The `schema.table` an `rls.tables` entry names, or `resource` when it names
3587
+ * none or is `false`.
3588
+ */
3589
+ function tableNameOf(entry, resource) {
3590
+ if (entry === void 0 || entry === false) return resource;
3591
+ return typeof entry === "string" ? entry : entry.table ?? resource;
3592
+ }
3593
+ /**
3594
+ * `rls.tables` as resource to table name, the `via` objects reduced to their
3595
+ * `table` and the `false` entries left out.
3596
+ */
3597
+ function tableNames(tables) {
3598
+ return tables === void 0 ? void 0 : Object.fromEntries(Object.entries(tables).filter(([, entry]) => entry !== false).map(([resource, entry]) => [resource, tableNameOf(entry, resource)]));
3599
+ }
3600
+ /** The resources `rls.tables` marks `false`: no table, so no policies. */
3601
+ function tablelessResources(tables) {
3602
+ return new Set(Object.entries(tables ?? {}).filter(([, entry]) => entry === false).map(([resource]) => resource));
3603
+ }
3604
+ function singular(name) {
3605
+ if (name.endsWith("ies")) return `${name.slice(0, -3)}y`;
3606
+ if (name.endsWith("sses")) return name.slice(0, -2);
3607
+ return name.endsWith("s") && !name.endsWith("ss") ? name.slice(0, -1) : name;
3608
+ }
3609
+ /** The `via` entries of `rls.tables`, by resource. */
3610
+ function resolveVias(tables, scopes, dialect) {
3611
+ const out = {};
3612
+ for (const [resource, entry] of Object.entries(tables ?? {})) {
3613
+ if (typeof entry === "string" || entry === false) continue;
3614
+ const { via } = entry;
3615
+ const scope = via.scope ?? scopes[0]?.name;
3616
+ if (scope === void 0 || !scopes.some((item) => item.name === scope)) throw configError(`PermDock CLI: rls.tables.${resource}.via.scope '${scope ?? ""}' is not a scope of the policy (${scopes.map((item) => item.name).join(", ")})`);
3617
+ const name = singular(via.table.slice(via.table.lastIndexOf(".") + 1));
3618
+ out[resource] = {
3619
+ helper: permittedIdsHelper(name),
3620
+ key: via.key ?? `${name}_id`,
3621
+ scope,
3622
+ table: via.table.includes(".") ? via.table : `public.${via.table}`,
3623
+ column: via.column,
3624
+ id: via.id ?? "id",
3625
+ type: via.type ?? (dialect === "supabase" ? "uuid" : "text")
3626
+ };
3627
+ }
3628
+ const helpers = /* @__PURE__ */ new Map();
3629
+ for (const [resource, via] of Object.entries(out)) {
3630
+ const seen = helpers.get(via.helper);
3631
+ if (seen !== void 0 && (seen.table !== via.table || seen.column !== via.column || seen.id !== via.id || seen.scope !== via.scope || seen.type !== via.type)) throw configError(`PermDock CLI: rls.tables.${resource}.via names ${via.helper}, which another resource defines over a different parent; give the parents different table names`);
3632
+ helpers.set(via.helper, via);
3633
+ }
3634
+ return Object.keys(out).length === 0 ? void 0 : out;
3695
3635
  }
3696
3636
  //#endregion
3697
- export { activeUserSql as $, collectGrants as A, hasPermittedUsers as B, byKey as C, apiKeysPlan as D, apiKeyFields as E, CLOSURE as F, memberIdsHelper as G, memberForHelper as H, graphHelper as I, permittedIdsHelper as J, memberRoleOf as K, graphSqlName as L, compileConditionSql as M, contextRefs as N, breakGlassHolder as O, sqlFunctionNames as P, activeRowSql as Q, graphSqlText as R, byCodeUnit as S, apiKeyAllowsCall as T, memberForSources as U, linkHelper as V, memberForTable as W, scopeSources as X, restrictedHelper as Y, scopeTable as Z, capabilityAccessSql as _, guardedTables as a, currentKeyExpr as b, PERMISSION_HELPERS as c, permissionKeysForHelper as d, checkSuspension as et, permittedByPermissionHelper as f, seedSql as g, helpersSql as h, assignmentSql as i, roleNames as j, breakGlassKey as k, checkName as l, accessSql as m, ownershipSql as n, ASSIGNMENTS as o, requiresSql as p, permittedForHelper as q, holderSources as r, OWNERSHIP as s, ownershipRules as t, permissionHelpersSql as u, globalRoleRows as v, API_KEY_ALLOWS as w, rolesTableScope as x, globalRoleSource as y, hasMemberFor as z };
3637
+ export { graphSqlText as $, claimKeptLines as A, hasCustomRoleChecksFor as B, accessSql as C, tokenScopeAllowsCall as D, TOKEN_SCOPE_ALLOWS as E, globalRoleRows as F, byKey as G, rolesTableScope as H, membershipRows as I, apiKeyFields as J, API_KEY_ALLOWS as K, roleRows as L, memberColumn as M, sourceFilters as N, tokenScopesPlan as O, sourcesKeep as P, graphSqlName as Q, globalRoleSource as R, requiresSql as S, seedSql as T, storedTenant as U, raiseSql as V, byCodeUnit as W, CLOSURE as X, apiKeysPlan as Y, graphHelper as Z, PERMISSION_HELPERS as _, activeMembershipSql as _t, closureDepths as a, memberForHelper as at, permissionKeysForHelper as b, checkSuspension as bt, holderSources as c, memberIdsHelper as ct, sourceRowsSql as d, permittedForHelper as dt, hasMemberFor as et, sourceTransferTriggerSql as f, permittedIdsHelper as ft, OWNERSHIP as g, activeInstancesSql as gt, ASSIGNMENTS as h, scopeTable as ht, tablelessResources as i, linkHelper as it, disabledColumn as j, capabilityAccessSql as k, pairsSql as l, memberOfHelper as lt, transferTriggerSql as m, scopeSources as mt, tableNameOf as n, inheritedRowsHelper as nt, graphPlan as o, memberForSources as ot, holdersTriggerSql as p, restrictedHelper as pt, apiKeyAllowsCall as q, tableNames as r, isTeamScope as rt, graphSql as s, memberForTable as st, resolveVias as t, hasPermittedUsers as tt, sourceHoldersTriggerSql as u, memberRoleOf as ut, checkName as v, activeRowSql as vt, helpersSql as w, permittedByPermissionHelper as x, permissionHelpersSql as y, activeUserSql as yt, currentKeyExpr as z };