permdock 0.3.1 → 0.5.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 (552) hide show
  1. package/README.md +37 -35
  2. package/dist/{openapi-schema--ARjyfQr.js → 2020-C9VN-x8c.js} +5 -132
  3. package/dist/a2a/index.d.ts +8 -6
  4. package/dist/a2a/index.js +21 -31
  5. package/dist/{adapter-context-mlWsVGGg.js → adapter-context-CRhqyzCk.js} +1 -1
  6. package/dist/adoption-scan-B4R5fnMU.js +107 -0
  7. package/dist/agent/index.d.ts +3 -0
  8. package/dist/agent/index.js +1 -0
  9. package/dist/ai-sdk/index.d.ts +18 -23
  10. package/dist/ai-sdk/index.js +38 -21
  11. package/dist/app-data-MuyzVqX8.js +60 -0
  12. package/dist/approval-header-CJACtHkv.js +23 -0
  13. package/dist/approval-prompt-Dg5LeAcT.js +62 -0
  14. package/dist/approval-prompt-Djo_PyZc.d.ts +57 -0
  15. package/dist/approvals/index.d.ts +5 -3
  16. package/dist/approvals/index.js +8 -5
  17. package/dist/approvals-DJ6Yd5z1.js +995 -0
  18. package/dist/{approvers-DT4AVyIg.js → approvers-CqPOLD-o.js} +1 -1
  19. package/dist/arazzo-B6lebmiQ.js +36 -0
  20. package/dist/{arazzo-BAgw9ap6.js → arazzo-BWhNF-k0.js} +24 -17
  21. package/dist/args-IIolxxKA.js +197 -0
  22. package/dist/astro/index.d.ts +43 -0
  23. package/dist/astro/index.js +27 -0
  24. package/dist/audience-Dg8XsRaO.js +55 -0
  25. package/dist/auth-DRLEizbL.js +178 -0
  26. package/dist/authzen/index.d.ts +26 -6
  27. package/dist/authzen/index.js +228 -75
  28. package/dist/automation-BMZPIVUM.d.ts +114 -0
  29. package/dist/automation-DVjOckjV.js +157 -0
  30. package/dist/better-auth/index.d.ts +8 -6
  31. package/dist/better-auth/index.js +11 -23
  32. package/dist/better-supabase/index.d.ts +438 -86
  33. package/dist/better-supabase/index.js +1027 -184
  34. package/dist/bind--9EFVYWF.d.ts +38 -0
  35. package/dist/{bind-CuzGytRf.js → bind-CZzhx-Vj.js} +22 -10
  36. package/dist/body-CH5of8Ne.js +10 -0
  37. package/dist/body-text-QcXok_SI.js +52 -0
  38. package/dist/build-instance-BMnAudHn.js +52 -0
  39. package/dist/{canonical-json-DemiK4Vp.js → canonical-json-BC8_nMTw.js} +6 -6
  40. package/dist/{capability-2kYTMWDv.d.ts → capability-BSslw1QV.d.ts} +44 -25
  41. package/dist/{capability-4qPdO9gk.js → capability-N1K9IpqS.js} +69 -42
  42. package/dist/casbin-Cr7RLtA3.js +30 -0
  43. package/dist/casbin-Dyu2gnUD.js +234 -0
  44. package/dist/casl-64gpT-8x.js +24 -0
  45. package/dist/casl-DjEDhTCP.js +299 -0
  46. package/dist/catalog/index.d.ts +74 -2
  47. package/dist/catalog/index.js +51 -2
  48. package/dist/catalog-BECMPrwc.js +100 -0
  49. package/dist/catalog-DjhMYYCq.js +1 -0
  50. package/dist/catalog-R_A__RsH.js +52 -0
  51. package/dist/{catalog-doc-DdGm82XT.js → catalog-doc-C9wjsfNY.js} +54 -25
  52. package/dist/catalog-fingerprint-CCP0-yQS.js +153 -0
  53. package/dist/chat/index.d.ts +2 -0
  54. package/dist/chat/index.js +224 -0
  55. package/dist/chat-sdk/index.d.ts +87 -0
  56. package/dist/chat-sdk/index.js +197 -0
  57. package/dist/claims-map-7oQ3ue4X.js +13 -0
  58. package/dist/claims-map-Ca-QX8C6.d.ts +32 -0
  59. package/dist/claude-agent/index.d.ts +7 -14
  60. package/dist/claude-agent/index.js +26 -30
  61. package/dist/clerk/index.d.ts +9 -8
  62. package/dist/clerk/index.js +15 -45
  63. package/dist/cli/bin.js +36 -17
  64. package/dist/cli/index.d.ts +20 -6
  65. package/dist/cli/index.js +164 -4
  66. package/dist/cloud/index.d.ts +102 -63
  67. package/dist/cloud/index.js +240 -347
  68. package/dist/cloud-CIoHNevm.js +87 -0
  69. package/dist/codemod-Bmj9dWMH.js +37 -0
  70. package/dist/codemod-Cxvvc70T.js +2084 -0
  71. package/dist/{collect-DSTE3ml9.js → collect-CrZJJnEF.js} +79 -17
  72. package/dist/collect-DloHbDVa.js +55 -0
  73. package/dist/collect-FlXJlhus.js +1 -0
  74. package/dist/command-L8EstFNm.js +6 -0
  75. package/dist/{compact-y6vfu2_f.js → compact-CAf557ZB.js} +11 -1
  76. package/dist/compile/index.d.ts +1 -1
  77. package/dist/compile/index.js +1 -2
  78. package/dist/{compile-Ba4blROR.js → compile-CMqMx_IN.js} +13 -9
  79. package/dist/{compile-BkRyBDBw.d.ts → compile-Ckm2HqBZ.d.ts} +5 -3
  80. package/dist/completion-BqhRBJD8.js +143 -0
  81. package/dist/config-B46rm9sc.js +26 -0
  82. package/dist/{config-BJiM-xAm.js → config-bs_kZeYq.js} +15 -10
  83. package/dist/{config-report-CdCgbRnV.js → config-report-DUgj62Ai.js} +17 -8
  84. package/dist/{connection-CU8MQxRi.js → connection-DjhIJSs2.js} +8 -6
  85. package/dist/constants-DcJABbLW.js +20 -0
  86. package/dist/{context-Bcy85rGi.js → context-bbFHR-0D.js} +16 -5
  87. package/dist/convex/index.d.ts +11 -10
  88. package/dist/convex/index.js +18 -18
  89. package/dist/create-0VZ7Pdiu.d.ts +107 -0
  90. package/dist/create-BDS7wc3y.d.ts +138 -0
  91. package/dist/create-BFfm17UA.d.ts +110 -0
  92. package/dist/create-BWRSXgOF.js +403 -0
  93. package/dist/create-C3b4B5eF.js +130 -0
  94. package/dist/create-DnrmLnLW.js +323 -0
  95. package/dist/{credential-8P81ff4u.js → credential-DM2um3ha.js} +153 -32
  96. package/dist/credentials-DPyHmYz5.js +74 -0
  97. package/dist/deciding-columns-Sq12mEA6.js +123 -0
  98. package/dist/delegation-DaXVdDhM.js +286 -0
  99. package/dist/delegation-input-FTxMK3ji.js +19 -0
  100. package/dist/delivery/index.d.ts +5 -0
  101. package/dist/delivery/index.js +5 -0
  102. package/dist/delivery-DCAgNyCn.js +499 -0
  103. package/dist/delivery-event-xX4obB8F.js +40 -0
  104. package/dist/denied-text-D8jDHgsl.d.ts +19 -0
  105. package/dist/{describe-DMYiCjJV.d.ts → describe-BZUsixiL.d.ts} +11 -2
  106. package/dist/{describe-B_gVZEVi.js → describe-BybMRY7A.js} +10 -5
  107. package/dist/device-BARQ8eYG.js +193 -0
  108. package/dist/diff-CxZo9pTq.js +37 -0
  109. package/dist/{diff-2VaeChzX.js → diff-DCePiN6M.js} +61 -55
  110. package/dist/doctor-CxiwSfqH.js +44 -0
  111. package/dist/doctor-DFjUxf2f.js +2361 -0
  112. package/dist/drizzle/index.d.ts +5 -3
  113. package/dist/drizzle/index.js +6 -14
  114. package/dist/elysia/index.d.ts +16 -13
  115. package/dist/elysia/index.js +14 -13
  116. package/dist/env-DJZrPjRi.js +8 -0
  117. package/dist/errors-BDUwg2qC.js +27 -0
  118. package/dist/{errors-Cz5zn0mW.js → errors-BKy0EfhN.js} +6 -10
  119. package/dist/errors-BWXBMaKK.d.ts +11 -0
  120. package/dist/{errors-DPdqI50v.d.ts → errors-C2PXIv4c.d.ts} +2 -2
  121. package/dist/errors-CWTY6uih.js +143 -0
  122. package/dist/evaluate-_R8QkWaf.js +1732 -0
  123. package/dist/evaluations-16UqqQ6W.d.ts +32 -0
  124. package/dist/{evaluations-tz6D9A2G.js → evaluations-DZ2CbV5k.js} +181 -32
  125. package/dist/eve/index.d.ts +12 -16
  126. package/dist/eve/index.js +23 -29
  127. package/dist/event-time-CsegxXxB.js +23 -0
  128. package/dist/expiry-D4_YKRKD.js +52 -0
  129. package/dist/express/index.d.ts +13 -10
  130. package/dist/express/index.js +14 -12
  131. package/dist/fastify/index.d.ts +26 -13
  132. package/dist/fastify/index.js +17 -36
  133. package/dist/fetch-adapter-g0ypLkID.js +120 -0
  134. package/dist/fetch-adapter-qKJBuRqy.d.ts +31 -0
  135. package/dist/fields-D9tPVUUS.js +203 -0
  136. package/dist/files-C5ZI27eV.js +124 -0
  137. package/dist/{files-DWUN9PzI.js → files-D_JIzUrP.js} +12 -3
  138. package/dist/{fixtures-ht28Cplr.js → fixtures-B7XrLeY7.js} +12 -12
  139. package/dist/flags-C3DktBDY.js +86 -0
  140. package/dist/{from-snapshot-CTLijvdm.js → from-snapshot-D2lZt4Dd.js} +78 -65
  141. package/dist/{generate-CH7ThJ14.js → generate-CNU1pIOo.js} +1 -3
  142. package/dist/grantee-Cn1HyUsI.js +254 -0
  143. package/dist/grantee-conditions-CcXf8hMM.js +271 -0
  144. package/dist/grantee-match-DHArqY82.js +142 -0
  145. package/dist/h3/index.d.ts +37 -0
  146. package/dist/h3/index.js +35 -0
  147. package/dist/headers-BIjfMaoC.js +8 -0
  148. package/dist/headers-CIbDTUTK.d.ts +7 -0
  149. package/dist/{helpers-H_KARDVC.js → helpers-DTcZOzhF.js} +47 -39
  150. package/dist/hono/index.d.ts +12 -9
  151. package/dist/hono/index.js +14 -13
  152. package/dist/{hosted-DJQw46hn.js → hosted-BZIAYvon.js} +64 -15
  153. package/dist/hosted-DBDX0M-P.d.ts +78 -0
  154. package/dist/{http-SO7qGttf.js → http-BBoKefeS.js} +35 -2
  155. package/dist/{http-DPuDaChb.d.ts → http-B_LphE5Y.d.ts} +1 -2
  156. package/dist/http-DXJ37RDA.js +46 -0
  157. package/dist/{http-BIhmLfCc.js → http-ujKm0JXE.js} +3 -26
  158. package/dist/{openapi-import-cmwetbzw.js → import-CNV7I2Dw.js} +30 -26
  159. package/dist/{rls-import-4lLW_pbb.js → import-DMmOG27O.js} +271 -128
  160. package/dist/index-BcHcPyIo.d.ts +110 -0
  161. package/dist/index-CaxrMD1A.d.ts +1 -0
  162. package/dist/index-Chrs5fQ-.d.ts +210 -0
  163. package/dist/index-CxGUswIC.d.ts +431 -0
  164. package/dist/index-D8EovfsP.d.ts +91 -0
  165. package/dist/index-DOVf7RJv.d.ts +900 -0
  166. package/dist/{index-C8z4RjQV.d.ts → index-SGdIQyzc.d.ts} +34 -16
  167. package/dist/index.d.ts +20 -14
  168. package/dist/index.js +33 -23
  169. package/dist/init-DeMYsnYJ.js +96 -0
  170. package/dist/init-GG6Jh8HD.js +95 -0
  171. package/dist/{instance-options-jARdwfPN.d.ts → instance-options-Omfd793B.d.ts} +2 -2
  172. package/dist/instance-snapshot-Cxq4ygFc.js +600 -0
  173. package/dist/instant-BluqDvyS.js +19 -0
  174. package/dist/internals-Dk8A8fX3.js +17 -0
  175. package/dist/{json-schema-BsAflUGW.js → json-schema-B-vAwlde.js} +46 -9
  176. package/dist/json-schema-CN_iLFH2.d.ts +40 -0
  177. package/dist/jwt/index.d.ts +18 -11
  178. package/dist/jwt/index.js +166 -145
  179. package/dist/{kernel-_RRHmnMY.js → kernel-5JatXyND.js} +120 -43
  180. package/dist/kysely/index.d.ts +5 -3
  181. package/dist/kysely/index.js +6 -14
  182. package/dist/{live-options-H2eG8dfJ.js → live-options-BaAFCIiu.js} +16 -3
  183. package/dist/load-BxEtwlBS.js +1 -0
  184. package/dist/{load-DhWEB9P1.js → load-DFS_ry0W.js} +31 -7
  185. package/dist/{load-jose-BL6LH4bL.js → load-jose-DZ4dnjri.js} +1 -4
  186. package/dist/login-Dfe5RlYk.js +68 -0
  187. package/dist/login-imhuwvn3.js +254 -0
  188. package/dist/manifest-C8IzW3Do.js +42 -0
  189. package/dist/manifest-CtKR3GXL.js +370 -0
  190. package/dist/manifest-json-CAGzls-C.js +9 -0
  191. package/dist/{manifest-uDS9y5de.d.ts → manifest-mx5ghSFM.d.ts} +9 -2
  192. package/dist/manifest-schema-C0kK_8V0.js +399 -0
  193. package/dist/{map-claims-Z4QZOiFF.js → map-claims-D3oRmTk0.js} +19 -25
  194. package/dist/{map-error-pgUWNNtG.js → map-error-TCAdw-eW.js} +5 -5
  195. package/dist/{markers-B0hYLvMD.js → markers-C6joDI3o.js} +8 -3
  196. package/dist/mcp/index.d.ts +36 -9
  197. package/dist/mcp/index.js +476 -448
  198. package/dist/{rls-migrate-CN4TlJTO.js → migrate-BShP7Vf4.js} +119 -20
  199. package/dist/nest/index.d.ts +12 -9
  200. package/dist/nest/index.js +20 -20
  201. package/dist/next/index.d.ts +40 -20
  202. package/dist/next/index.js +37 -39
  203. package/dist/next/plugin.d.ts +1 -1
  204. package/dist/next/plugin.js +2 -2
  205. package/dist/node/index.d.ts +10 -10
  206. package/dist/node/index.js +7 -9
  207. package/dist/ocsf-BFp-PZZA.js +126 -0
  208. package/dist/openai/index.d.ts +9 -16
  209. package/dist/openai/index.js +18 -25
  210. package/dist/openapi/index.d.ts +3 -3
  211. package/dist/openapi/index.js +1 -1
  212. package/dist/{openapi-BqRBvAng.js → openapi-B7HZ0f-r.js} +9 -12
  213. package/dist/{openapi-CqqEbHsL.js → openapi-DTaEmTSW.js} +23 -26
  214. package/dist/openapi-ahr8rHu3.js +138 -0
  215. package/dist/operation-findings-DgATDpvh.js +36 -0
  216. package/dist/operations-CxybZErY.d.ts +59 -0
  217. package/dist/orpc/index.d.ts +13 -10
  218. package/dist/orpc/index.js +23 -20
  219. package/dist/otel/index.d.ts +3 -2
  220. package/dist/otel/index.js +53 -27
  221. package/dist/output-Dyw8fyPF.js +88 -0
  222. package/dist/output-files-B3IZJksC.js +1 -0
  223. package/dist/output-files-DhRBBE4n.js +138 -0
  224. package/dist/{rls-fields-CZOMlwHl.js → ownership-BatFgAA4.js} +2393 -2349
  225. package/dist/{package-root-zGkxO3oN.js → package-root-Dk_ETdkp.js} +4 -3
  226. package/dist/{parse-BuyjxhZV.js → parse-Cxq0WxrA.js} +3 -3
  227. package/dist/{parse-local-manifest-BrMozJCo.js → parse-local-manifest-CVSs6J9I.js} +2 -4
  228. package/dist/{parse-local-manifest-JLnaBH0S.d.ts → parse-local-manifest-DgqtruPn.d.ts} +1 -1
  229. package/dist/parse-request-D_tUXG6M.js +26 -0
  230. package/dist/paths-DXSHAMIa.js +42 -0
  231. package/dist/pdp/index.d.ts +7 -3
  232. package/dist/pdp/index.js +47 -29
  233. package/dist/{peer-BOr_kJE1.js → peer-C8swJ2GH.js} +23 -4
  234. package/dist/peer-CRvC7Hg1.js +1 -0
  235. package/dist/permdock-B9JxkBwC.js +868 -0
  236. package/dist/permdock-D7eeLJER.d.ts +502 -0
  237. package/dist/permission-registry-Br4jSNVi.js +385 -0
  238. package/dist/permissions-B-wCP5ZC.js +30 -0
  239. package/dist/{permissions-RJrovw98.js → permissions-CZKiUfFM.js} +4 -3
  240. package/dist/{permissions-CDgvKG5L.d.ts → permissions-oEWfl-kl.d.ts} +1 -2
  241. package/dist/policies-CO1oOymN.js +295 -0
  242. package/dist/{policy-D2W7uv19.d.ts → policy-Czo4n3Hs.d.ts} +2 -2
  243. package/dist/policy-DLZJ5gAp.js +399 -0
  244. package/dist/policy-from-catalog-DzhEc3Y9.js +450 -0
  245. package/dist/policy-module-CTlwMvLI.js +89 -0
  246. package/dist/{policy-IB6yxpC3.d.ts → policy-types-DzwivBgR.d.ts} +939 -1057
  247. package/dist/postgrest-CW0IqC-U.js +566 -0
  248. package/dist/postgrest-NUqZT-RB.d.ts +100 -0
  249. package/dist/powersync-BHfuehf3.js +62 -0
  250. package/dist/powersync-CbzkqL_M.js +1 -0
  251. package/dist/{powersync-Dv1_aWF7.js → powersync-bYUDLOAO.js} +30 -32
  252. package/dist/preset/index.d.ts +146 -0
  253. package/dist/preset/index.js +1 -0
  254. package/dist/preset-DZdU0tne.js +1768 -0
  255. package/dist/principal-source-pHneukrx.js +103 -0
  256. package/dist/prisma/index.d.ts +6 -3
  257. package/dist/prisma/index.js +6 -12
  258. package/dist/{problem-B43ztKIm.js → problem-CTviyH5L.js} +18 -27
  259. package/dist/{problem-details-DX075l9V.d.ts → problem-details-DXlt-vks.d.ts} +1 -1
  260. package/dist/{problem-details-_AMOYz-w.js → problem-details-cn5X30gZ.js} +45 -42
  261. package/dist/problem-types-CJ2325SZ.js +5 -0
  262. package/dist/{prompt-CXYhLfsS.js → prompt-DfZLJLIn.js} +2 -2
  263. package/dist/protected-DEPHc2xV.d.ts +38 -0
  264. package/dist/{protected-utU-SLuy.js → protected-DfjY5mLA.js} +37 -8
  265. package/dist/{provider-CSQd7m92.js → provider-CC-u3bYP.js} +23 -10
  266. package/dist/{provider-DZY281jO.d.ts → provider-DMVt6yYn.d.ts} +1 -1
  267. package/dist/{provider-store-Cfbvo4DE.js → provider-store-B5Yrkhzr.js} +6 -3
  268. package/dist/pull-BgIfno6w.js +256 -0
  269. package/dist/pull-ByOks_Tg.js +1 -0
  270. package/dist/push-CmFVBOeI.js +116 -0
  271. package/dist/pushed-policy-BImMTTIM.d.ts +57 -0
  272. package/dist/pushed-policy-DobP7AHI.js +238 -0
  273. package/dist/react/index.d.ts +8 -8
  274. package/dist/react/index.js +6 -6
  275. package/dist/react/provider-client.d.ts +1 -1
  276. package/dist/react/provider-client.js +1 -2
  277. package/dist/react-native/index.d.ts +24 -63
  278. package/dist/react-native/index.js +45 -27
  279. package/dist/{refs-KYb4ntaz.js → refs-K6eWUJ0H.js} +1 -1
  280. package/dist/relation-walk-3OyDBF0S.js +186 -0
  281. package/dist/relations-BnCWnJGI.js +163 -0
  282. package/dist/renames-CGj7YwcQ.js +175 -0
  283. package/dist/{replay-CYyiLiwO.d.ts → replay-D7iKcLFe.d.ts} +1 -1
  284. package/dist/{replay-BsOyTrdV.js → replay-Du1tJjG7.js} +2 -1
  285. package/dist/request-context-BTRTG7OT.js +71 -0
  286. package/dist/request-context-C2_1lchh.d.ts +5 -0
  287. package/dist/request-schema-CMreVuFM.js +148 -0
  288. package/dist/resend/index.d.ts +126 -0
  289. package/dist/resend/index.js +379 -0
  290. package/dist/resource-relations-D_jw0BFx.js +243 -0
  291. package/dist/retire-CALjIQ7e.js +240 -0
  292. package/dist/{revocations-Cy06JNx2.d.ts → revocations-R07lFBnX.d.ts} +2 -0
  293. package/dist/rls-BA8an5Ap.js +336 -0
  294. package/dist/{rls-DiinlRML.js → rls-j6t43t7n.js} +1066 -1068
  295. package/dist/role-builder-CXbis5-X.js +623 -0
  296. package/dist/{rpc-DL9Sqvb1.js → rpc-aPyNi6tA.js} +18 -8
  297. package/dist/run-CNsApDpd.js +1 -0
  298. package/dist/run-DOfMtc9V.js +723 -0
  299. package/dist/run-ticket-BjhTOi6u.d.ts +124 -0
  300. package/dist/run-ticket-sxFI6LAk.js +337 -0
  301. package/dist/{runtime-CB8gQFvS.js → runtime-D3VTAiCA.js} +1 -1
  302. package/dist/{saas-DhpJjnBA.js → saas-FO-Q8oj0.js} +18 -15
  303. package/dist/scan-BOI5tJ-6.js +29 -0
  304. package/dist/{schema-D3MnDMxa.js → schema-B4v0Qrac.js} +106 -10
  305. package/dist/schema-yLuBWWsh.js +130 -0
  306. package/dist/scim/index.d.ts +2 -2
  307. package/dist/scim/index.js +1 -2
  308. package/dist/{scim-BFGBBAqR.js → scim-ClHtpyV3.js} +213 -87
  309. package/dist/{scopes-CVdPrWN-.js → scopes-CUPSjo9q.js} +20 -7
  310. package/dist/server/index.d.ts +13 -134
  311. package/dist/server/index.js +13 -10
  312. package/dist/server-DqCT4CgZ.js +453 -0
  313. package/dist/{create-B1d519XK.d.ts → server-types-BLd4MnBX.d.ts} +89 -70
  314. package/dist/{sha256-BO0swRXL.js → sha256-D9ihqmvd.js} +8 -1
  315. package/dist/shared-CD11nBbC.js +304 -0
  316. package/dist/{shared-DRu8VFVM.js → shared-DvvFzYXe.js} +8 -9
  317. package/dist/signals-D3NcTKUS.js +32 -0
  318. package/dist/sink-BEZSlMS_.js +957 -0
  319. package/dist/sink-Bj_lDUDq.d.ts +225 -0
  320. package/dist/{skills-Biq5uup3.js → skills-B6lVdD_W.js} +16 -8
  321. package/dist/skills-CTYwkl44.js +51 -0
  322. package/dist/snapshot-CXUWfsNy.js +269 -0
  323. package/dist/{snapshot-cache-D6jLelln.js → snapshot-cache-DZHQ0KF6.js} +11 -5
  324. package/dist/{snapshot-cache-B-LxUahj.d.ts → snapshot-cache-DqK9eDmM.d.ts} +7 -5
  325. package/dist/solid/index.d.ts +24 -25
  326. package/dist/solid/index.js +41 -11
  327. package/dist/solid-start/index.d.ts +52 -0
  328. package/dist/solid-start/index.js +49 -0
  329. package/dist/{sources-BdOYLw9j.d.ts → sources-BV9j0DeZ.d.ts} +11 -13
  330. package/dist/{sources-4p0-4xQm.js → sources-vs5dGOsS.js} +147 -142
  331. package/dist/sql-B5T1IL83.js +55 -0
  332. package/dist/src-DLfUWFAI.js +1851 -0
  333. package/dist/ssf/index.d.ts +4 -4
  334. package/dist/ssf/index.js +440 -106
  335. package/dist/standard-webhooks-6lg8v_is.d.ts +88 -0
  336. package/dist/standard-webhooks-DgFFD-Ga.js +228 -0
  337. package/dist/storage-C2bWdvJH.js +132 -0
  338. package/dist/{store-DMAWwAJV.js → store-4u_hxjWa.js} +42 -55
  339. package/dist/{store-options-ByhZq1jV.js → store-options-BqEakYRa.js} +38 -24
  340. package/dist/{stores-BT2pSCub.js → stores-BbsD4fAR.js} +7 -3
  341. package/dist/stores-ClZUgeoP.d.ts +30 -0
  342. package/dist/{stream-D1RXubw2.d.ts → stream-7-xmnf3U.d.ts} +2 -2
  343. package/dist/{stream-DDpvynSB.js → stream-CvdUS9V1.js} +10 -2
  344. package/dist/style-DiKWnAvn.js +1 -0
  345. package/dist/{style-CTm1AXDT.js → style-Uao76wO7.js} +3 -2
  346. package/dist/subject-BGoM5o8B.js +17 -0
  347. package/dist/{subject-settings-BwBmCdTR.js → subject-settings-CkEwrw3W.js} +23 -5
  348. package/dist/supabase/index.d.ts +23 -106
  349. package/dist/supabase/index.js +5 -4
  350. package/dist/supabase/middleware.d.ts +13 -6
  351. package/dist/supabase/middleware.js +16 -11
  352. package/dist/supabase-9pqI57pW.js +92 -0
  353. package/dist/{supabase-C0qESOa-.js → supabase-Dxlp8A94.js} +124 -247
  354. package/dist/supabase-hook-CXcGpHqq.js +1 -0
  355. package/dist/{supabase-hook-ntETYCQu.js → supabase-hook-DdDQhMlw.js} +1659 -819
  356. package/dist/{supautils-u8jXTGrt.js → supautils-B6HYL9eL.js} +2 -2
  357. package/dist/support-session-C6xJQYut.js +158 -0
  358. package/dist/svelte/Protected.svelte +11 -4
  359. package/dist/svelte/index.d.ts +7 -7
  360. package/dist/svelte/index.js +26 -12
  361. package/dist/svelte/runtime.d.ts +10 -3
  362. package/dist/svelte/runtime.js +4 -3
  363. package/dist/svelte/source.d.ts +7 -7
  364. package/dist/svelte/source.js +5 -5
  365. package/dist/sveltekit/index.d.ts +51 -0
  366. package/dist/sveltekit/index.js +27 -0
  367. package/dist/tanstack-start/index.d.ts +51 -0
  368. package/dist/tanstack-start/index.js +40 -0
  369. package/dist/target-Bw37TFNA.js +51 -0
  370. package/dist/{validity-uOp8UpYn.js → tenancy-DN2QAx9z.js} +6 -42
  371. package/dist/terminal/index.d.ts +11 -9
  372. package/dist/terminal/index.js +36 -316
  373. package/dist/testing/index.d.ts +391 -106
  374. package/dist/testing/index.js +4574 -1286
  375. package/dist/testing/saas/index.d.ts +9 -8
  376. package/dist/testing/saas/index.js +2 -2
  377. package/dist/testing/saas/permissions.d.ts +1 -1
  378. package/dist/testing/saas/permissions.js +1 -2
  379. package/dist/text-BMFRfBqc.js +8 -0
  380. package/dist/{text-diff-BdkkYSbi.js → text-diff-BQcUapOb.js} +1 -1
  381. package/dist/thenable-yD7SabqG.js +31 -0
  382. package/dist/time-DkWjdL98.js +71 -0
  383. package/dist/{token-CIWxJOCh.js → token-BLZcjQgt.js} +15 -6
  384. package/dist/tool-refusal-CMLN5xhn.js +41 -0
  385. package/dist/trpc/index.d.ts +12 -9
  386. package/dist/trpc/index.js +18 -17
  387. package/dist/{types-DbjRN55U.d.ts → types-5Ut_pbKy.d.ts} +141 -13
  388. package/dist/types-BNORmDM8.d.ts +172 -0
  389. package/dist/types-BjV-fqcA.d.ts +57 -0
  390. package/dist/{types-QCuYJh4q.d.ts → types-BqAKcRNW.d.ts} +2 -1
  391. package/dist/types-C8-KNsFf.d.ts +107 -0
  392. package/dist/{types-aqOHTHcp.d.ts → types-CCpc2Dcv.d.ts} +77 -17
  393. package/dist/{types-Dnemwd77.d.ts → types-CZTnPFzM.d.ts} +50 -11
  394. package/dist/{types-C97IskBw.d.ts → types-CdlxsYQh.d.ts} +23 -11
  395. package/dist/{types-DxiCGmmd.d.ts → types-ClZaWzfb.d.ts} +9 -6
  396. package/dist/types-DX0LaKTb.d.ts +157 -0
  397. package/dist/types-DiQjupG2.d.ts +332 -0
  398. package/dist/types-DqzsnlSA.js +67 -0
  399. package/dist/types-_sHy8BQf.d.ts +65 -0
  400. package/dist/types-fbZ7JEXr.d.ts +89 -0
  401. package/dist/types-tIgIeIKJ.d.ts +66 -0
  402. package/dist/unplugin/index.d.ts +1 -1
  403. package/dist/unplugin/index.js +2 -2
  404. package/dist/usage-CHOtwL19.js +1 -0
  405. package/dist/usage-DHtE8j4E.js +32 -0
  406. package/dist/{usage-ItS7nLcl.js → usage-T5mXq98v.js} +56 -13
  407. package/dist/{validation-error-Ba264OaJ.d.ts → validation-error-Bg5Tpluk.d.ts} +1 -1
  408. package/dist/{validation-error-CnjZ4t92.js → validation-error-ZfUHbKvf.js} +1 -1
  409. package/dist/validity-CZ7-y0Cj.js +79 -0
  410. package/dist/{verifier-BxBo6I44.js → verifier-DHuh-okk.js} +68 -21
  411. package/dist/verify-DbCL9bnB.js +1770 -0
  412. package/dist/{version-BvacVTZR.js → version-CEbQ5UeM.js} +4 -3
  413. package/dist/version-DXkYUISx.js +1 -0
  414. package/dist/{views-Soe7bGSC.js → views-DTIIO-oQ.js} +41 -2
  415. package/dist/{permissions-DttnqqxC.d.ts → vocabulary-D6lPdb4Z.d.ts} +120 -29
  416. package/dist/vue/index.d.ts +25 -21
  417. package/dist/vue/index.js +25 -9
  418. package/dist/waiter-BGUtGYoU.js +67 -0
  419. package/dist/waiter-CKy4MQSY.d.ts +36 -0
  420. package/dist/watch-Cb-NcaUw.js +1 -0
  421. package/dist/{watch-DVBJ9cBq.js → watch-CszoTJMq.js} +47 -8
  422. package/dist/webmcp/index.d.ts +2 -2
  423. package/dist/webmcp/index.js +13 -37
  424. package/dist/where-scope-Dv-WTP-w.js +306 -0
  425. package/dist/who-can-D0QoYbEb.js +364 -0
  426. package/dist/wire-delegation-Cv3W2eaM.js +36 -0
  427. package/dist/wire-denial-C7PaVYEM.js +27 -0
  428. package/dist/workflow/index.d.ts +5 -0
  429. package/dist/workflow/index.js +223 -0
  430. package/dist/workflow-sdk/index.d.ts +56 -0
  431. package/dist/workflow-sdk/index.js +78 -0
  432. package/dist/wrap-Bd1Tejur.d.ts +15 -0
  433. package/dist/wrap-gX61EtpB.js +27 -0
  434. package/package.json +140 -41
  435. package/schemas/access-event-v1.json +118 -0
  436. package/schemas/approval-request-v1.json +34 -1
  437. package/schemas/approval-signature-v1.json +94 -0
  438. package/schemas/automation-event-v1.json +113 -0
  439. package/schemas/capability-v1.json +84 -0
  440. package/schemas/catalog-event-v1.json +87 -0
  441. package/schemas/catalog-v1.json +129 -12
  442. package/schemas/cli-commands-v1.json +164 -0
  443. package/schemas/cli-result-v1.json +96 -0
  444. package/schemas/cloud-api-v1.json +5407 -0
  445. package/schemas/connector-v1.json +408 -0
  446. package/schemas/credential-event-v1.json +81 -0
  447. package/schemas/credential-v1.json +134 -0
  448. package/schemas/decision-event-v1.json +288 -0
  449. package/schemas/delivery-event-v1.json +117 -0
  450. package/schemas/directory-event-v1.json +85 -0
  451. package/schemas/evidence-export-v1.json +54 -0
  452. package/schemas/membership-event-v1.json +93 -0
  453. package/schemas/policy-v1.json +68 -0
  454. package/schemas/pushed-policy-v1.json +236 -0
  455. package/schemas/run-v1.json +162 -0
  456. package/schemas/snapshot-v1.json +138 -0
  457. package/schemas/supabase-claims-v1.json +4 -4
  458. package/schemas/supabase-manifest-v1.json +5 -1
  459. package/schemas/support-session-v1.json +111 -0
  460. package/skills/permdock/SKILL.md +16 -12
  461. package/skills/permdock-agents/SKILL.md +40 -23
  462. package/skills/permdock-agents/references/operations-automations-chat.md +221 -0
  463. package/skills/permdock-agents/references/runtimes.md +22 -18
  464. package/skills/permdock-approvals/SKILL.md +29 -13
  465. package/skills/permdock-approvals/references/delivery-and-waiters.md +172 -0
  466. package/skills/permdock-approvals/references/stores-and-resume.md +21 -7
  467. package/skills/permdock-audit/SKILL.md +3 -3
  468. package/skills/permdock-credentials/SKILL.md +5 -5
  469. package/skills/permdock-credentials/references/tokens-keys-links.md +19 -10
  470. package/skills/permdock-data/SKILL.md +6 -6
  471. package/skills/permdock-data/references/queries-and-rls.md +13 -8
  472. package/skills/permdock-data/references/relationships.md +3 -3
  473. package/skills/permdock-tenancy/SKILL.md +20 -7
  474. package/skills/permdock-tenancy/references/contacts-and-support-sessions.md +122 -0
  475. package/skills/permdock-tenancy/references/ownership-and-custom-roles.md +6 -4
  476. package/skills/permdock-wire/SKILL.md +18 -12
  477. package/skills/permdock-wire/references/adapters.md +146 -50
  478. package/skills/permdock-wire/references/policy-options.md +15 -6
  479. package/skills/permdock-wire/references/presets-and-recipes.md +58 -0
  480. package/dist/approvals-DbYAuNkG.js +0 -224
  481. package/dist/arazzo-C9chp3ST.js +0 -54
  482. package/dist/bind-C_ob3LKa.d.ts +0 -25
  483. package/dist/catalog-CRW5Mx_B.js +0 -2
  484. package/dist/catalog-CYj-mX2O.js +0 -48
  485. package/dist/catalog-DWF5cYqB.js +0 -55
  486. package/dist/cloud-CfwZ39Rl.js +0 -50
  487. package/dist/cloud-DWLXO2af.js +0 -118
  488. package/dist/collect-CHB22Sx4.js +0 -83
  489. package/dist/collect-DSWOk19R.js +0 -2
  490. package/dist/commands-BeaGNj_o.js +0 -97
  491. package/dist/config-ir40y8m0.js +0 -36
  492. package/dist/create-CMwIl-w2.js +0 -253
  493. package/dist/delegation-D8pUEuce.js +0 -103
  494. package/dist/diff-C9RDjjhs.js +0 -48
  495. package/dist/doctor-Djh6JDEk.js +0 -53
  496. package/dist/doctor-DvH0IQff.js +0 -1194
  497. package/dist/doctor-collect-CjdO0wQX.js +0 -665
  498. package/dist/duration-xK2jGFVc.js +0 -25
  499. package/dist/errors-CWfoNg9N.js +0 -47
  500. package/dist/errors-DRnGZHzF.js +0 -30
  501. package/dist/evaluation-items-CfCCejQd.js +0 -40
  502. package/dist/expiry-BoBJ8Tvc.js +0 -7
  503. package/dist/fields-BMl1RxoX.js +0 -26
  504. package/dist/grantee-Cv5Sogfv.js +0 -602
  505. package/dist/headers-BZSDieNT.js +0 -6
  506. package/dist/headers-Cf-zWvsO.d.ts +0 -6
  507. package/dist/ids-DC4V4CFu.js +0 -18
  508. package/dist/index-0nNqHspG.d.ts +0 -6
  509. package/dist/index-BNc7Wer_.d.ts +0 -483
  510. package/dist/index-C7Tw7MaW.d.ts +0 -201
  511. package/dist/instance-options-Dhhcnm3y.js +0 -22
  512. package/dist/load-DwJUafYN.js +0 -2
  513. package/dist/manifest-iaoBXHKc.js +0 -783
  514. package/dist/map-AcxK9o_D.js +0 -147
  515. package/dist/may-use-DndQ5tQ-.js +0 -77
  516. package/dist/openapi-CQTsgKoB.js +0 -164
  517. package/dist/permdock-CbgvLnuo.js +0 -3479
  518. package/dist/permissions-DVoF6dMt.js +0 -662
  519. package/dist/policy-xyWg7QdR.js +0 -1122
  520. package/dist/powersync-CrBX8hY6.js +0 -2
  521. package/dist/powersync-DE19b9dT.js +0 -62
  522. package/dist/protected-BdnIEFkY.d.ts +0 -27
  523. package/dist/protected-xAr24FEC.d.ts +0 -40
  524. package/dist/relations-cjOL01hZ.js +0 -505
  525. package/dist/rls-P-BeBx0n.js +0 -238
  526. package/dist/rls-graph-CHwx0V8q.js +0 -435
  527. package/dist/rls-ownership-DcYlZ09B.js +0 -843
  528. package/dist/rls-shared-Cewd_-zI.js +0 -495
  529. package/dist/rls-verify-CMv9H3wV.js +0 -746
  530. package/dist/run-BWc9qqvE.js +0 -174
  531. package/dist/server-DlZ28Oz5.js +0 -268
  532. package/dist/sink-B1fZzIMf.d.ts +0 -104
  533. package/dist/sink-Bd9PSyfq.js +0 -141
  534. package/dist/skills-C5CbhhpG.js +0 -56
  535. package/dist/snapshot-Enxd0n-s.js +0 -975
  536. package/dist/sql-xdg5f4Fi.js +0 -27
  537. package/dist/src-BDwEZWrZ.js +0 -568
  538. package/dist/store-jOBYpiXC.d.ts +0 -36
  539. package/dist/stores-C0IMQrGI.d.ts +0 -52
  540. package/dist/subject-C6dLxqHU.js +0 -24
  541. package/dist/supabase-config-Bj3iDH2I.js +0 -472
  542. package/dist/supabase-hook-gd3s8bxB.js +0 -2
  543. package/dist/supabase-s4o9s1O1.js +0 -83
  544. package/dist/thenable-okc2xDsj.js +0 -10
  545. package/dist/types-BnIEz1sU.d.ts +0 -48
  546. package/dist/types-Bvc4oWT6.d.ts +0 -9
  547. package/dist/types-CKNSyIf_.d.ts +0 -52
  548. package/dist/types-Cq_H8N_X.d.ts +0 -197
  549. package/dist/usage-B0YuTQRO.js +0 -42
  550. package/dist/usage-DorAJiU_.js +0 -2
  551. package/dist/vocabulary-DA8a8zif.d.ts +0 -56
  552. package/dist/watch-BwxLTYMZ.js +0 -2
@@ -1,27 +1,33 @@
1
- import { t as compact } from "./compact-y6vfu2_f.js";
2
- import { _ as listPermissions, y as renamedKeys } from "./permissions-DVoF6dMt.js";
3
- import { d as scopeList, o as resolveScope } from "./scopes-CVdPrWN-.js";
4
- import { E as listRoles, s as flattenGrantee } from "./grantee-Cv5Sogfv.js";
5
- import { h as scopeField } from "./validity-uOp8UpYn.js";
6
- import { i as describeError, o as usageResult } from "./errors-CWfoNg9N.js";
1
+ import { t as compact } from "./compact-CAf557ZB.js";
2
+ import { c as listPermissions, u as renamedKeys } from "./permission-registry-Br4jSNVi.js";
3
+ import { i as dateAt } from "./time-DkWjdL98.js";
4
+ import { d as scopeList, o as resolveScope } from "./scopes-CUPSjo9q.js";
5
+ import { o as flattenGrantee, v as listRoles } from "./grantee-Cn1HyUsI.js";
6
+ import { p as scopeField } from "./tenancy-DN2QAx9z.js";
7
+ import { a as describeError, i as configError, o as exitCodeOfError, u as usageResult } from "./errors-CWTY6uih.js";
7
8
  import { t as policyRowConditionKeys } from "./row-conditions-h2HythXQ.js";
8
- import { a as loadModule, n as asPolicy, o as pickNamed } from "./load-DhWEB9P1.js";
9
+ import { c as requirePolicy } from "./load-DFS_ry0W.js";
9
10
  import "./budget-NbEtbJC_.js";
10
- import { n as escapeSqlIdent } from "./sql-xdg5f4Fi.js";
11
- import "./sources-4p0-4xQm.js";
12
- import { i as SEEDS_MARKER, r as INDEXES_MARKER, t as GRANTS_MARKER } from "./markers-B0hYLvMD.js";
13
- import { n as requirePeer } from "./peer-BOr_kJE1.js";
14
- import { f as splitUndiffed, n as supabaseConfig } from "./supabase-config-Bj3iDH2I.js";
15
- import { $ as sessionLiveHelperSql, G as quoteIdent$1, K as quoteLiteral, Q as scopeTypeOf, R as memberIdsHelper, T as graphSqlName, U as permittedIdsHelper, V as parseMembershipsFlag, W as qualifiedTable$1, X as scopeSources, Z as scopeTable, f as SESSION_LIVE_HELPER, o as qualified$1, q as quoteTable, r as HELPERS, rt as subjectIdSql, tt as subjectClaimJsonSql, w as graphHelper, y as checkSuspension } from "./rls-shared-Cewd_-zI.js";
16
- import { A as helpersSql, B as breakGlassHolder, C as readTableIndexFacts, E as accessSql, F as seedSql, H as collectGrants, S as pruneIndexTargets, T as connectPgPool, U as roleNames, V as breakGlassKey, a as fieldViewsSql, b as indexTargets, c as assemblePolicies, d as readOnlyActorPolicies, f as branchClauses, g as permissionHelpersSql, h as tableFor, i as fieldViews, l as orSql, m as compileGrants, n as columnGrantSql, o as rowBranches, q as apiKeysPlan, r as columnRevokeSql, u as readOnlyActorKinds, v as permittedByPermissionHelper, x as indexesSql, y as COLUMNS_SQL } from "./rls-fields-CZOMlwHl.js";
17
- import { c as helperCallKeys, i as HELPER_TABLES, n as graphPlan, r as graphSql, s as callsHelper, t as closureDepths } from "./rls-graph-CHwx0V8q.js";
18
- import { a as ownershipRules, o as ownershipSql, r as assignmentSql } from "./rls-ownership-DcYlZ09B.js";
19
- import { _ as seedsMigration, d as resolveAuthorize, f as driftOf, g as pgDeltaPath, h as partPath, l as parseRbacAuthorize, m as parseSplit, o as supabaseHookSql, p as isSeedsDirectory, t as grantsLabel, u as rbacScaffold, v as writeSqlFiles } from "./supabase-hook-ntETYCQu.js";
20
- import { n as runRlsVerify } from "./rls-verify-CMv9H3wV.js";
21
- import { t as supautilsRejections } from "./supautils-u8jXTGrt.js";
22
- import { basename, delimiter, join, resolve } from "node:path";
23
- import { spawnSync } from "node:child_process";
24
- //#region src/cli/rls-advisors.ts
11
+ import { i as qualifySqlTable, r as escapeSqlIdent, u as sqlTextArray } from "./sql-B5T1IL83.js";
12
+ import { l as roleColumn } from "./sources-vs5dGOsS.js";
13
+ import { t as policyFromCatalog } from "./policy-from-catalog-DzhEc3Y9.js";
14
+ import { t as beforeFirst } from "./text-BMFRfBqc.js";
15
+ import { i as INDEXES_MARKER, n as GRANTS_MARKER, o as SEEDS_MARKER, t as BACKFILL_MARKER } from "./markers-C6joDI3o.js";
16
+ import { i as requirePeer, n as packageManagerOf, t as addLine } from "./peer-C8swJ2GH.js";
17
+ import { t as POLICY_NOT_PUBLISHED } from "./problem-types-CJ2325SZ.js";
18
+ import { t as fetchProjectPolicy } from "./pull-BgIfno6w.js";
19
+ import { a as isSeedsDirectory, c as parseSplit, d as seedsMigration, f as writeOutputFiles, l as partPath, n as dataMigrationFile, r as driftOf, u as pgDeltaPath } from "./output-files-DhRBBE4n.js";
20
+ import { c as approvalsSql, d as resolveAuthorize, h as schemasRefusal, l as parseRbacAuthorize, m as resolveSchemas, o as supabaseHookSql, p as isSupabaseSchemas, s as permissionHoldersSql, t as grantsLabel, u as rbacScaffold } from "./supabase-hook-DdDQhMlw.js";
21
+ import { C as parseMembershipsFlag, E as scopeTypeOf, F as quoteQualified, I as quoteTable, M as qualified, N as quoteIdent$1, O as tenantTypeOf, P as quoteLiteral, _ as subjectClaimJsonSql, a as customRolesOn, c as SESSION_LIVE_HELPER, g as sessionLiveHelperSql, r as HELPERS, t as CUSTOM_ROLES, y as subjectIdSql } from "./shared-CD11nBbC.js";
22
+ import { a as splitUndiffed } from "./deciding-columns-Sq12mEA6.js";
23
+ import { a as readOnlyActorKinds, c as branchClauses, d as connectPgPool, i as orSql, n as actorMayWriteSql, o as readOnlyActorPolicies, r as assemblePolicies, t as ACTOR_MAY_WRITE } from "./policies-CO1oOymN.js";
24
+ import { $ as checkSuspension, A as roleNames, D as breakGlassHolder, E as apiKeysPlan, F as graphHelper, I as graphSqlName, O as breakGlassKey, S as byKey, W as memberIdsHelper, X as scopeTable, Y as scopeSources, _ as currentKeyExpr, f as permittedByPermissionHelper, g as seedSql, h as helpersSql, i as assignmentSql, k as collectGrants, m as accessSql, n as ownershipSql, q as permittedIdsHelper, t as ownershipRules, u as permissionHelpersSql, x as byCodeUnit } from "./ownership-BatFgAA4.js";
25
+ import { _ as indexesSql, a as graphSql, c as callsHelper, g as indexTargets, h as COLUMNS_SQL, i as graphPlan, l as helperCallKeys, m as tableFor, n as runRlsVerify, o as HELPER_TABLES, p as compileGrants, r as closureDepths, v as pruneIndexTargets, y as readTableIndexFacts } from "./verify-DbCL9bnB.js";
26
+ import { a as fieldViewsSql, i as fieldViews, n as columnGrantSql, o as rowBranches, r as columnRevokeSql } from "./fields-D9tPVUUS.js";
27
+ import { t as supautilsRejections } from "./supautils-B6HYL9eL.js";
28
+ import { basename, delimiter, join } from "node:path";
29
+ import { spawn } from "node:child_process";
30
+ //#region src/cli/rls/advisors.ts
25
31
  /** The PermDock doctor check that reports the same problem in migrations. */
26
32
  const ADVISOR_CODES = {
27
33
  function_search_path_mutable: "PD048",
@@ -31,19 +37,41 @@ const ADVISOR_CODES = {
31
37
  anon_security_definer_function_executable: "PD049",
32
38
  authenticated_security_definer_function_executable: "PD046"
33
39
  };
34
- const defaultExec = (command, args, options) => {
35
- const result = spawnSync(command, [...args], {
40
+ const defaultExec = (command, args, options) => new Promise((resolve) => {
41
+ const child = spawn(command, [...args], {
36
42
  cwd: options.cwd,
37
43
  env: options.env,
38
- encoding: "utf8"
44
+ stdio: [
45
+ "ignore",
46
+ "pipe",
47
+ "pipe"
48
+ ],
49
+ ...options.signal === void 0 ? {} : { signal: options.signal }
39
50
  });
40
- return {
41
- status: result.status,
42
- stdout: result.stdout ?? "",
43
- stderr: result.stderr ?? "",
44
- ...result.error === void 0 ? {} : { error: result.error }
45
- };
46
- };
51
+ let stdout = "";
52
+ let stderr = "";
53
+ child.stdout.setEncoding("utf8").on("data", (chunk) => {
54
+ stdout += chunk;
55
+ });
56
+ child.stderr.setEncoding("utf8").on("data", (chunk) => {
57
+ stderr += chunk;
58
+ });
59
+ child.once("error", (error) => {
60
+ resolve({
61
+ status: null,
62
+ stdout,
63
+ stderr,
64
+ error
65
+ });
66
+ });
67
+ child.once("close", (status) => {
68
+ resolve({
69
+ status,
70
+ stdout,
71
+ stderr
72
+ });
73
+ });
74
+ });
47
75
  function text(value, key) {
48
76
  const field = typeof value === "object" && value !== null ? Reflect.get(value, key) : void 0;
49
77
  return typeof field === "string" ? field : "";
@@ -64,16 +92,19 @@ function findingsOf(value) {
64
92
  return code === void 0 ? finding : Object.assign(finding, { code });
65
93
  });
66
94
  }
67
- const INSTALL = "rls verify --advisors runs the Supabase CLI: install it with npm install --save-dev supabase (or brew install supabase/tap/supabase)";
95
+ function installHint(agent) {
96
+ return `rls verify --advisors runs the Supabase CLI: install it with ${addLine(agent, ["supabase"], { dev: true })} (or brew install supabase/tap/supabase)`;
97
+ }
68
98
  /**
69
- * `supabase db advisors --type security` against `--db` or the local stack.
70
- * Exit 1 on a WARN or ERROR row, 2 when the CLI is missing or fails.
99
+ * `supabase db advisors --type security` against `--db-url` or the local stack,
100
+ * from the project's `node_modules/.bin` first, then `PATH`. Exit 1 on a WARN
101
+ * or ERROR row, 2 when the CLI is missing or fails.
71
102
  */
72
- function runRlsAdvisors(input) {
103
+ async function runRlsAdvisors(input) {
73
104
  const exec = input.exec ?? defaultExec;
74
105
  const bin = join(input.cwd, "node_modules", ".bin");
75
106
  const path = input.env["PATH"];
76
- const result = exec("supabase", [
107
+ const result = await exec("supabase", [
77
108
  "db",
78
109
  "advisors",
79
110
  ...input.db === void 0 ? ["--local"] : ["--db-url", input.db],
@@ -86,11 +117,12 @@ function runRlsAdvisors(input) {
86
117
  env: {
87
118
  ...input.env,
88
119
  PATH: path === void 0 ? bin : `${bin}${delimiter}${path}`
89
- }
120
+ },
121
+ ...input.signal === void 0 ? {} : { signal: input.signal }
90
122
  });
91
123
  if (result.error !== void 0) return {
92
124
  code: 2,
93
- output: INSTALL
125
+ output: installHint(await packageManagerOf(input.cwd))
94
126
  };
95
127
  let parsed;
96
128
  try {
@@ -127,578 +159,487 @@ function runRlsAdvisors(input) {
127
159
  };
128
160
  }
129
161
  //#endregion
130
- //#region src/cli/approval-schema.ts
131
- /** An RFC 3339 instant as `Temporal.Instant` prints it; pg_jsonschema asserts no `format`. */
132
- const INSTANT = String.raw`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$`;
133
- const STRINGS = {
134
- type: "array",
135
- items: { type: "string" }
136
- };
137
- /**
138
- * `schemas/approval-request-v1.json`: the `ApprovalRequest` an approval store
139
- * holds. Self-contained (`$defs`, no remote `$ref`) so `rls.jsonSchema` can
140
- * inline it into a pg_jsonschema check constraint.
141
- */
142
- const APPROVAL_REQUEST_SCHEMA = {
143
- $schema: "https://json-schema.org/draft/2020-12/schema",
144
- $id: "https://permdock.com/schemas/approval-request-v1.json",
145
- title: "PermDock approval request",
146
- description: "An ApprovalRequest as an ApprovalStore holds it, wire format v1. Unknown properties are allowed so a v1 minor release can add one.",
147
- type: "object",
148
- required: [
149
- "v",
150
- "token",
151
- "permission",
152
- "scope",
153
- "resource",
154
- "subject",
155
- "detail",
156
- "createdAt",
157
- "expiresAt",
158
- "status"
159
- ],
160
- properties: {
161
- v: { const: 1 },
162
- token: {
163
- type: "string",
164
- minLength: 1
165
- },
166
- permission: {
167
- type: "string",
168
- minLength: 1
169
- },
170
- scope: { type: "string" },
171
- resource: {
172
- type: "object",
173
- required: ["type"],
174
- properties: {
175
- type: { type: "string" },
176
- id: { type: "string" }
177
- }
178
- },
179
- subject: { $ref: "#/$defs/subject" },
180
- membership: {
181
- type: "object",
182
- required: ["roles"],
183
- properties: { roles: STRINGS }
184
- },
185
- approvers: { type: "object" },
186
- detail: { type: "string" },
187
- adapter: { type: "string" },
188
- createdAt: { $ref: "#/$defs/instant" },
189
- expiresAt: { $ref: "#/$defs/instant" },
190
- status: { enum: [
191
- "pending",
192
- "approved",
193
- "rejected",
194
- "expired"
195
- ] },
196
- approvals: {
197
- type: "array",
198
- items: { $ref: "#/$defs/signature" }
199
- },
200
- resolvedAt: { $ref: "#/$defs/instant" },
201
- resolvedBy: { type: "string" },
202
- vouched: { type: "string" },
203
- note: { type: "string" },
204
- consumedAt: { $ref: "#/$defs/instant" }
205
- },
206
- $defs: {
207
- instant: {
208
- type: "string",
209
- pattern: INSTANT
210
- },
211
- subject: {
212
- type: "object",
213
- required: ["principal"],
214
- properties: {
215
- principal: { anyOf: [{ type: "null" }, {
216
- type: "object",
217
- required: ["id", "roles"],
218
- properties: {
219
- id: { type: "string" },
220
- roles: STRINGS,
221
- tenant: { type: "string" }
222
- }
223
- }] },
224
- actor: {
225
- type: "object",
226
- required: ["id", "kind"],
227
- properties: {
228
- id: { type: "string" },
229
- kind: { type: "string" }
230
- }
231
- },
232
- session: { type: "string" },
233
- delegation: { type: "object" }
234
- }
162
+ //#region src/cli/rls/generate-context.ts
163
+ /** The scopes an `rls.migrate` `role` helper names, so `rls generate` writes their role-filtered member helper. */
164
+ function roleMemberScopesOf(migrate) {
165
+ const scopes = [...new Set(Object.values(migrate?.helpers ?? {}).flatMap((helper) => helper.form === "role" ? [helper.scope] : []))].toSorted();
166
+ return scopes.length === 0 ? {} : { roleMemberScopes: scopes };
167
+ }
168
+ /** `rls.customRoleWrites.requires` as permission keys, or why it names none. */
169
+ function writeRequirement(policy, requires) {
170
+ if (requires === void 0) return {};
171
+ const leaves = listPermissions(policy.permissions);
172
+ const keys = requires === "manageRoles" ? leaves.filter((leaf) => leaf.meta.manageRoles === true).map((leaf) => leaf.key) : [...requires];
173
+ if (keys.length === 0) return { error: requires === "manageRoles" ? "rls.customRoleWrites.requires is 'manageRoles', but no permission declares meta.manageRoles" : "rls.customRoleWrites.requires names no permission" };
174
+ const known = new Set(leaves.map((leaf) => leaf.key));
175
+ const unknown = keys.filter((key) => !known.has(key));
176
+ if (unknown.length > 0) return { error: `rls.customRoleWrites.requires names undeclared permissions: ${unknown.join(", ")}` };
177
+ return { keys: [...new Set(keys)].toSorted() };
178
+ }
179
+ /** Declared role names, and those a tenant admin may compose into custom roles. */
180
+ function customRoleNames(policy, requires) {
181
+ const declared = [...roleNames(policy)].toSorted();
182
+ const assignable = declared.filter((name) => policy.rolesByName.get(name)?.assignable ?? listRoles(policy.vocabulary.roles).some((leaf) => leaf.key === name && leaf.assignable));
183
+ const renamed = renamedKeys(policy.vocabulary.permissions);
184
+ const leaves = listPermissions(policy.permissions);
185
+ const manage = leaves.filter((leaf) => leaf.meta.manageRoles === true).map((leaf) => leaf.key);
186
+ return {
187
+ declared,
188
+ assignable,
189
+ ...renamed.size === 0 ? {} : { renamed: Object.fromEntries(renamed) },
190
+ permissions: leaves.map((leaf) => leaf.key).toSorted(),
191
+ ...manage.length === 0 ? {} : { manage: manage.toSorted() },
192
+ ...requires === void 0 ? {} : { requires },
193
+ ...policy.levels === void 0 ? {} : { levels: true }
194
+ };
195
+ }
196
+ function skippedOwnershipTriggers(triggers, scopes) {
197
+ if (triggers === false) return "all";
198
+ return Object.keys(triggers).map((key) => {
199
+ const name = resolveScope(scopes, key);
200
+ if (name === void 0) throw configError(`PermDock CLI: rls.ownershipTriggers.${key} names a scope the policy does not declare`);
201
+ return name;
202
+ });
203
+ }
204
+ /** The SQL context every emitter reads, from the flags, `rls` config and the policy. */
205
+ function buildContext(input, policy, options) {
206
+ const rls = input.config.rls;
207
+ const memberships = parseMembershipsFlag(input.memberships) ?? rls?.memberships;
208
+ if (input.rbac && input.dialect !== "supabase") return { refusal: `rls generate --rbac supabase needs --dialect supabase (got ${input.dialect})` };
209
+ const schema = input.rbacSchema ?? rls?.schema ?? rls?.rbac?.schema ?? "permdock";
210
+ const authorize = resolveAuthorize(input.config, {
211
+ authorize: input.authorize,
212
+ rbac: input.rbac,
213
+ memberships
214
+ });
215
+ const memberSources = rls?.membershipSources ?? input.config.supabase?.hook?.memberships;
216
+ const sources = authorize === "database" ? memberSources : void 0;
217
+ const scopes = scopeList(policy.scopes);
218
+ const suspension = checkSuspension(rls?.suspension, scopes);
219
+ const hookRoles = input.config.supabase?.hook?.roles;
220
+ const roles = rls?.roles ?? (hookRoles === false ? void 0 : hookRoles);
221
+ if (roles !== void 0 && input.rbac) return { refusal: "rls generate --rbac supabase creates its own user_roles (user_id, role app_role); drop rls.roles (or supabase.hook.roles) or the --rbac flag" };
222
+ const requires = writeRequirement(policy, rls?.customRoleWrites?.requires);
223
+ if (requires.error !== void 0) return { refusal: requires.error };
224
+ const readers = rls?.trustedReaders;
225
+ if (readers !== void 0 && (!Array.isArray(readers) || !readers.every((name) => typeof name === "string" && name !== ""))) return { refusal: "rls.trustedReaders must list Postgres role names, such as ['service_role']" };
226
+ const ownership = ownershipRules(policy, scopes);
227
+ const graph = graphPlan(policy);
228
+ const apiKeys = apiKeysPlan(rls?.apiKeys, new Set(roleNames(policy)), Object.fromEntries(renamedKeys(policy.vocabulary.permissions)));
229
+ return {
230
+ ctx: {
231
+ dialect: input.dialect,
232
+ scopes,
233
+ tenantClaim: rls?.tenantClaim ?? "tenant_id",
234
+ ...roleMemberScopesOf(rls?.migrate),
235
+ ...rls?.roleValues === void 0 ? {} : { roleValues: rls.roleValues },
236
+ ...rls?.tenants === "all" ? { tenants: "all" } : {},
237
+ gucPrefix: input.gucPrefix ?? rls?.gucPrefix ?? "app",
238
+ ...rls?.actions === void 0 ? {} : { actions: rls.actions },
239
+ inlineFunctions: input.inlineFunctions || rls?.inlineFunctions === true,
240
+ schema,
241
+ authorize,
242
+ roleClaim: rls?.roleClaim ?? "user_role",
243
+ tenantType: input.tenantType ?? rls?.tenantType ?? "uuid",
244
+ ...rls?.teamType === void 0 ? {} : { teamType: rls.teamType },
245
+ ...rls?.scopeTypes === void 0 ? {} : { scopeTypes: rls.scopeTypes },
246
+ ...memberships === void 0 ? {} : { memberships },
247
+ ...sources === void 0 || sources.length === 0 ? {} : { sources },
248
+ ...memberSources === void 0 || memberSources.length === 0 ? {} : { memberSources },
249
+ ...suspension === void 0 ? {} : { suspension },
250
+ ...roles === void 0 ? {} : { roles },
251
+ ...input.customRoles === true || customRolesOn(rls) ? { customRoles: {
252
+ ...customRoleNames(policy, requires.keys),
253
+ ...rls?.customRoleWrites?.roles === void 0 ? {} : { table: rls.customRoleWrites.roles }
254
+ } } : {},
255
+ ...input.capabilities === true || rls?.capabilities === true ? { capabilities: true } : {},
256
+ ...apiKeys === void 0 ? {} : { apiKeys },
257
+ ...rls?.anonymousSignIns === "deny" ? { anonymousSignIns: "deny" } : {},
258
+ ...ownership === void 0 ? {} : { ownership },
259
+ ...rls?.ownershipTriggers === void 0 ? {} : { skipOwnershipTriggers: skippedOwnershipTriggers(rls.ownershipTriggers, scopes) },
260
+ ...rls?.assignments === void 0 ? {} : { assignments: rls.assignments === true ? { tables: [] } : {
261
+ tables: rls.assignments.tables ?? [],
262
+ ...rls.assignments.ownRole === void 0 ? {} : { ownRole: rls.assignments.ownRole }
263
+ } },
264
+ ...options.fieldsMode === void 0 ? {} : { fields: options.fieldsMode },
265
+ ...graph.size === 0 ? {} : { graph: {
266
+ closures: closureDepths(graph),
267
+ resources: policy.resources,
268
+ ...rls?.tables === void 0 ? {} : { tables: rls.tables }
269
+ } }
235
270
  },
236
- signature: {
237
- type: "object",
238
- required: ["by", "at"],
239
- properties: {
240
- by: { type: "string" },
241
- at: { $ref: "#/$defs/instant" },
242
- stage: {
243
- type: "integer",
244
- minimum: 0
245
- },
246
- vouched: { type: "string" }
247
- }
248
- }
249
- }
250
- };
251
- //#endregion
252
- //#region src/cli/rls-approvals.ts
253
- /** The objects `rls.approvals` adds. Names are part of the SQL contract `supabaseApprovalStore` calls. */
254
- const APPROVAL_STORE = {
255
- table: "approval_requests",
256
- open: "permdock_approval_open",
257
- get: "permdock_approval_get",
258
- resolve: "permdock_approval_resolve",
259
- consume: "permdock_approval_consume",
260
- list: "permdock_approval_list",
261
- expire: "permdock_approval_expire",
262
- cancel: "permdock_approval_cancel"
263
- };
264
- /** `body` as the row's filter columns hold it. */
265
- const COLUMNS = `p_request ->> 'token',
266
- p_request,
267
- p_request ->> 'status',
268
- p_request #>> '{subject,principal,tenant}',
269
- p_request #>> '{subject,principal,id}',
270
- p_request #>> '{subject,actor,id}',
271
- p_request #>> '{subject,session}',
272
- coalesce(jsonb_array_length(p_request -> 'approvals'), 0),
273
- p_request ->> 'createdAt',
274
- (p_request ->> 'expiresAt')::timestamptz,
275
- (p_request ->> 'consumedAt')::timestamptz`;
276
- /** The `ApprovalListFilter` in `p_filter`, over the row aliased `a`. */
277
- const FILTER = `(p_filter ->> 'status' is null or a.status = p_filter ->> 'status')
278
- and (p_filter ->> 'tenant' is null or a.tenant = p_filter ->> 'tenant')
279
- and (p_filter ->> 'principalId' is null or a.principal_id = p_filter ->> 'principalId')
280
- and (p_filter ->> 'actorId' is null or a.actor_id = p_filter ->> 'actorId')
281
- and (p_filter ->> 'session' is null or a.session = p_filter ->> 'session')`;
282
- function serverOnly(fn, args) {
283
- return `revoke execute on function ${fn}(${args}) from public, anon, authenticated;`;
284
- }
285
- const BODY_CHECK = `${APPROVAL_STORE.table}_body_schema`;
286
- /**
287
- * `rls.jsonSchema`: a pg_jsonschema check that `body` matches
288
- * `approval-request-v1.json`, added `not valid` and then validated. `auto`
289
- * adds it only where the extension is available.
290
- */
291
- function bodySchemaSql(table, mode, body = "body", check = BODY_CHECK) {
292
- const statements = [
293
- "create schema if not exists extensions;",
294
- "create extension if not exists pg_jsonschema with schema extensions;",
295
- `alter table ${table} drop constraint if exists "${check}";`,
296
- `alter table ${table} add constraint "${check}" check (extensions.jsonb_matches_schema(${quoteLiteral(JSON.stringify(APPROVAL_REQUEST_SCHEMA))}::json, ${body})) not valid;`,
297
- `alter table ${table} validate constraint "${check}";`
298
- ];
299
- const comment = "-- approval store: body matches schemas/approval-request-v1.json through pg_jsonschema";
300
- if (mode === true) return `\n${comment}\n${statements.join("\n")}\n`;
301
- return `\n${comment}, where the extension is available
302
- do $permdock$
303
- begin
304
- if exists (select 1 from pg_available_extensions where name = 'pg_jsonschema') then
305
- ${statements.map((statement) => ` ${statement}`).join("\n")}
306
- end if;
307
- end
308
- $permdock$;\n`;
271
+ graph,
272
+ ownership,
273
+ schema,
274
+ authorize
275
+ };
309
276
  }
310
- /**
311
- * The approval store (`rls.approvals`): one table of `ApprovalRequest`
312
- * bodies with the columns a list filters on, and one `security definer`
313
- * function per `ApprovalStore` method, each one atomic statement. No client
314
- * role may read the table or execute the functions: approvals are opened and
315
- * resolved by the server, which grants its own role.
316
- */
317
- function approvalStoreSql(ctx, jsonSchema = false) {
318
- const table = qualified$1(ctx, APPROVAL_STORE.table);
319
- const fn = (name) => qualified$1(ctx, name);
320
- const index = (name) => `"${APPROVAL_STORE.table}_${name}"`;
321
- return `-- approval store: ApprovalRequest bodies, opened, resolved and consumed through the functions below; supabaseApprovalStore calls them
322
- create table if not exists ${table} (
323
- token text primary key,
324
- body jsonb not null,
325
- status text not null check (status in ('pending', 'approved', 'rejected', 'expired')),
326
- tenant text,
327
- principal_id text,
328
- actor_id text,
329
- session text,
330
- approvals integer not null default 0,
331
- created_at text not null,
332
- expires_at timestamptz not null,
333
- consumed_at timestamptz
334
- );
335
- create index if not exists ${index("page")} on ${table} (created_at collate "C", token collate "C");
336
- create index if not exists ${index("pending")} on ${table} (status, expires_at);
337
- alter table ${table} enable row level security;
338
- revoke all on table ${table} from anon, authenticated, public;
339
- ${jsonSchema === false ? "" : bodySchemaSql(table, jsonSchema)}
340
- -- open a request; a repeated ask keeps the open record and replaces only an expired one
341
- create or replace function ${fn(APPROVAL_STORE.open)}(p_request jsonb)
342
- returns void
343
- language sql
344
- volatile
345
- security definer
346
- set search_path = ''
347
- as $$
348
- insert into ${table} as a (token, body, status, tenant, principal_id, actor_id, session, approvals, created_at, expires_at, consumed_at)
349
- values (
350
- ${COLUMNS}
351
- )
352
- on conflict (token) do update
353
- set body = excluded.body, status = excluded.status, tenant = excluded.tenant,
354
- principal_id = excluded.principal_id, actor_id = excluded.actor_id, session = excluded.session,
355
- approvals = excluded.approvals, created_at = excluded.created_at,
356
- expires_at = excluded.expires_at, consumed_at = excluded.consumed_at
357
- where a.status = 'expired' or a.expires_at <= now()
358
- $$;
359
- ${serverOnly(fn(APPROVAL_STORE.open), "jsonb")}
360
-
361
- create or replace function ${fn(APPROVAL_STORE.get)}(p_token text)
362
- returns jsonb
363
- language sql
364
- stable
365
- security definer
366
- set search_path = ''
367
- as $$
368
- select a.body from ${table} a where a.token = p_token
369
- $$;
370
- ${serverOnly(fn(APPROVAL_STORE.get), "text")}
371
-
372
- -- write a verdict over the record it was computed from: still pending, unexpired, with p_seen approvals
373
- create or replace function ${fn(APPROVAL_STORE.resolve)}(p_token text, p_next jsonb, p_seen integer)
374
- returns jsonb
375
- language sql
376
- volatile
377
- security definer
378
- set search_path = ''
379
- as $$
380
- update ${table} a
381
- set body = p_next,
382
- status = p_next ->> 'status',
383
- approvals = coalesce(jsonb_array_length(p_next -> 'approvals'), 0)
384
- where a.token = p_token
385
- and a.status = 'pending'
386
- and a.expires_at > now()
387
- and a.approvals = p_seen
388
- returning a.body
389
- $$;
390
- ${serverOnly(fn(APPROVAL_STORE.resolve), "text, jsonb, integer")}
391
-
392
- -- consume an approved, unexpired, unconsumed request once
393
- create or replace function ${fn(APPROVAL_STORE.consume)}(p_token text, p_now text)
394
- returns jsonb
395
- language sql
396
- volatile
397
- security definer
398
- set search_path = ''
399
- as $$
400
- update ${table} a
401
- set consumed_at = p_now::timestamptz,
402
- body = a.body || jsonb_build_object('consumedAt', p_now)
403
- where a.token = p_token
404
- and a.status = 'approved'
405
- and a.consumed_at is null
406
- and a.expires_at > p_now::timestamptz
407
- returning a.body
408
- $$;
409
- ${serverOnly(fn(APPROVAL_STORE.consume), "text, text")}
410
-
411
- -- one page in (createdAt, token) order after the cursor position
412
- create or replace function ${fn(APPROVAL_STORE.list)}(p_filter jsonb, p_after_created text, p_after_token text, p_limit integer)
413
- returns setof jsonb
414
- language sql
415
- stable
416
- security definer
417
- set search_path = ''
418
- as $$
419
- select a.body
420
- from ${table} a
421
- where ${FILTER}
422
- and (p_after_created is null
423
- or (a.created_at collate "C", a.token collate "C") > (p_after_created collate "C", p_after_token collate "C"))
424
- order by a.created_at collate "C", a.token collate "C"
425
- limit p_limit
426
- $$;
427
- ${serverOnly(fn(APPROVAL_STORE.list), "jsonb, text, text, integer")}
428
-
429
- create or replace function ${fn(APPROVAL_STORE.expire)}(p_now text)
430
- returns integer
431
- language sql
432
- volatile
433
- security definer
434
- set search_path = ''
435
- as $$
436
- with expired as (
437
- update ${table} a
438
- set status = 'expired', body = a.body || '{"status":"expired"}'::jsonb
439
- where a.status = 'pending' and a.expires_at <= p_now::timestamptz
440
- returning 1
441
- )
442
- select count(*)::integer from expired
443
- $$;
444
- ${serverOnly(fn(APPROVAL_STORE.expire), "text")}
445
-
446
- -- reject every pending request the filter matches, as system:<by>
447
- create or replace function ${fn(APPROVAL_STORE.cancel)}(p_filter jsonb, p_by text, p_note text, p_now text)
448
- returns integer
449
- language sql
450
- volatile
451
- security definer
452
- set search_path = ''
453
- as $$
454
- with cancelled as (
455
- update ${table} a
456
- set status = 'rejected',
457
- body = (a.body - 'note') || jsonb_strip_nulls(jsonb_build_object(
458
- 'status', 'rejected', 'resolvedAt', p_now, 'resolvedBy', 'system:' || p_by, 'note', p_note
459
- ))
460
- where a.status = 'pending'
461
- and ${FILTER.replaceAll("\n ", "\n ")}
462
- returning 1
463
- )
464
- select count(*)::integer from cancelled
465
- $$;
466
- ${serverOnly(fn(APPROVAL_STORE.cancel), "jsonb, text, text, text")}
467
- `;
277
+ //#endregion
278
+ //#region src/cli/rls/generate-options.ts
279
+ function isRefusal(value) {
280
+ return "refusal" in value;
468
281
  }
469
- /** The request fields an adopted table may mirror into its own columns, as SQL over a body. */
470
- const MIRRORS = {
471
- status: (body) => `${body} ->> 'status'`,
472
- permission: (body) => `${body} ->> 'permission'`,
473
- tenant: (body) => `${body} #>> '{subject,principal,tenant}'`,
474
- principalId: (body) => `${body} #>> '{subject,principal,id}'`,
475
- actorId: (body) => `${body} #>> '{subject,actor,id}'`,
476
- session: (body) => `${body} #>> '{subject,session}'`,
477
- approvals: (body) => `coalesce(jsonb_array_length(${body} -> 'approvals'), 0)`,
478
- createdAt: (body) => `${body} ->> 'createdAt'`,
479
- expiresAt: (body) => `${body} ->> 'expiresAt'`,
480
- resolvedAt: (body) => `${body} ->> 'resolvedAt'`,
481
- resolvedBy: (body) => `${body} ->> 'resolvedBy'`,
482
- consumedAt: (body) => `${body} ->> 'consumedAt'`
483
- };
484
- function mirrorsOf(adopt) {
485
- const out = [];
486
- for (const [field, column] of Object.entries(adopt.mirror ?? {})) {
487
- if (!Object.hasOwn(MIRRORS, field)) throw new Error(`PermDock CLI: rls.approvals.mirror.${field} is not a request field; use ${Object.keys(MIRRORS).join(", ")}`);
488
- if (typeof column !== "string") throw new TypeError(`PermDock CLI: rls.approvals.mirror.${field} must be a column name`);
489
- out.push([column, field]);
490
- }
491
- return out;
282
+ /** The checks that need no policy: `--fields` and `--revoke-columns`. */
283
+ function generateOptions(input) {
284
+ const rls = input.config.rls;
285
+ const fieldsMode = input.fields ?? rls?.fields;
286
+ if (fieldsMode !== void 0 && fieldsMode !== "views") return { refusal: `rls generate --fields must be views (got ${fieldsMode})` };
287
+ const revokeColumns = input.revokeColumns === true || rls?.revokeColumns === true;
288
+ if (revokeColumns && fieldsMode === void 0) return { refusal: "rls generate --revoke-columns needs --fields views" };
289
+ return {
290
+ fieldsMode,
291
+ revokeColumns
292
+ };
492
293
  }
493
- /** One row of `table` from `values` (column, SQL), each converted to the column's own type. */
494
- function populate(table, values) {
495
- return `pg_catalog.jsonb_populate_record(null::${table}, pg_catalog.jsonb_build_object(${values.map(([column, sql]) => `${quoteLiteral(column)}, ${sql}`).join(", ")}))`;
294
+ /** Whether `--split` names `part`, which then leaves the helpers or the warnings. */
295
+ function splitsPart(raw, part) {
296
+ const split = parseSplit(raw);
297
+ return typeof split !== "string" && split?.includes(part) === true;
496
298
  }
497
- /** The mirrored columns' values for the request `body`. */
498
- function mirrorRow(table, mirror, body) {
499
- return populate(table, mirror.map(([column, field]) => [column, MIRRORS[field](body)]));
299
+ //#endregion
300
+ //#region src/cli/rls/generate-output.ts
301
+ function moveUndiffed(sql, label, part) {
302
+ const { kept, moved } = splitUndiffed(sql);
303
+ const [first = "", ...rest] = kept.split("\n");
304
+ return {
305
+ sql: [
306
+ first,
307
+ `-- what supabase db diff drops from this part is in ${label}`,
308
+ ...rest
309
+ ].join("\n"),
310
+ grants: `-- the privileges and view options supabase db diff drops from the ${part} part\n${moved.join("\n")}`
311
+ };
500
312
  }
501
- /** `, (<mirrored columns>) = (select …)` for an update that writes `body`; empty without mirrors. */
502
- function mirrorSet(table, mirror, body) {
503
- if (mirror.length === 0) return "";
504
- const columns = mirror.map(([column]) => quoteIdent$1(column));
505
- const picked = columns.map((column) => `r.${column}`).join(", ");
506
- return `,\n ${columns.length === 1 ? columns[0] ?? "" : `(${columns.join(", ")})`} = (select ${picked} from ${mirrorRow(table, mirror, body)} r)`;
313
+ /** Why `--split` cannot be written for `mode`, or `undefined`. */
314
+ function splitRefusal(mode, plan) {
315
+ const seedsWithoutOut = plan.split.includes("seeds") && plan.seedsOut === void 0;
316
+ const helpersWithoutSeeds = plan.split.includes("helpers") && !plan.split.includes("seeds");
317
+ switch (mode) {
318
+ case "pg-delta":
319
+ if (plan.explicitOut && !plan.placeholder) return `rls generate --split needs {part} in --out, or no --out for pg-delta's per-schema layout (got ${plan.outRel})`;
320
+ if (plan.grantsOut !== void 0) return "rls generate --grants-out is for --schemas migra: pg-delta keeps the grants in each part, so drop --grants-out";
321
+ if (helpersWithoutSeeds) return "rls generate --split helpers under pg-delta needs the seeds part with --seeds-out: pg-delta rejects the role_permissions rows in a declarative file";
322
+ if (seedsWithoutOut) return "rls generate --split seeds under pg-delta needs --seeds-out: pg-delta does not diff the role_permissions rows, so they go in a migration";
323
+ return;
324
+ case "migra":
325
+ if (!plan.placeholder) return `rls generate --split under migra needs {part} in --out, for example supabase/schemas/identity/056_permdock_{part}.sql (got ${plan.outRel})`;
326
+ if (helpersWithoutSeeds) return "rls generate --split helpers under migra needs the seeds part with --seeds-out: supabase db diff does not carry the role_permissions rows from a schema file";
327
+ if (seedsWithoutOut) return "rls generate --split seeds under migra needs --seeds-out: supabase db diff does not carry the role_permissions rows, so they go in a migration";
328
+ return;
329
+ case "migrations":
330
+ if (!plan.placeholder) return `rls generate --split needs {part} in --out, for example supabase/migrations/20260101000000_permdock_{part}.sql (got ${plan.outRel})`;
331
+ if (plan.grantsOut !== void 0) return "rls generate --grants-out is for --schemas migra: a hand-written migration keeps its grants, so drop --grants-out";
332
+ return;
333
+ default: return mode;
334
+ }
507
335
  }
508
- /**
509
- * The approval store over a table the app already has (`rls.approvals:
510
- * { table, token?, body?, mirror? }`): the same `permdock_approval_*`
511
- * functions, reading every field from the request body in `body`, keyed by
512
- * the text column `token`, and copying the fields `mirror` names into the
513
- * app's own columns, converted to their types, on every write. The table's
514
- * grants and policies stay the app's.
515
- */
516
- function adoptedApprovalStoreSql(ctx, adopt, jsonSchema = false) {
517
- const table = quoteTable(qualifiedTable$1(adopt.table));
518
- const base = adopt.table.split(".").at(-1) ?? adopt.table;
519
- const token = quoteIdent$1(adopt.token ?? "token");
520
- const body = quoteIdent$1(adopt.body ?? "body");
521
- const mirror = mirrorsOf(adopt);
522
- for (const [column] of mirror) if (column === (adopt.token ?? "token") || column === (adopt.body ?? "body")) throw new Error(`PermDock CLI: rls.approvals.mirror names the ${column} column, which already holds the token or the body`);
523
- const open = adopt.open ?? "insert";
524
- if (open !== "insert" && open !== "attach") throw new Error(`PermDock CLI: rls.approvals.open must be 'insert' or 'attach', got '${String(open)}'`);
525
- const schema = adopt.schema;
526
- if (schema !== void 0 && (typeof schema !== "string" || schema === "")) throw new Error("PermDock CLI: rls.approvals.schema must be a schema name");
527
- const fn = (name) => schema === void 0 ? qualified$1(ctx, name) : `${quoteIdent$1(schema)}.${name}`;
528
- const index = (name) => quoteIdent$1(`${base}_permdock_${name}`);
529
- const status = `a.${body} ->> 'status'`;
530
- const expires = `(a.${body} ->> 'expiresAt')::timestamptz`;
531
- const created = `(a.${body} ->> 'createdAt')`;
532
- const filter = `(p_filter ->> 'status' is null or ${status} = p_filter ->> 'status')
533
- and (p_filter ->> 'tenant' is null or a.${body} #>> '{subject,principal,tenant}' = p_filter ->> 'tenant')
534
- and (p_filter ->> 'principalId' is null or a.${body} #>> '{subject,principal,id}' = p_filter ->> 'principalId')
535
- and (p_filter ->> 'actorId' is null or a.${body} #>> '{subject,actor,id}' = p_filter ->> 'actorId')
536
- and (p_filter ->> 'session' is null or a.${body} #>> '{subject,session}' = p_filter ->> 'session')`;
537
- const columns = [
538
- token,
539
- body,
540
- ...mirror.map(([column]) => quoteIdent$1(column))
541
- ];
542
- const inserted = populate(table, [
543
- [adopt.token ?? "token", "p_request ->> 'token'"],
544
- [adopt.body ?? "body", "p_request"],
545
- ...mirror.map(([column, field]) => [column, MIRRORS[field]("p_request")])
546
- ]);
547
- const insertSql = `-- open a request; a repeated ask keeps the open record and replaces only an expired one
548
- create or replace function ${fn(APPROVAL_STORE.open)}(p_request jsonb)
549
- returns void
550
- language sql
551
- volatile
552
- security definer
553
- set search_path = ''
554
- as $$
555
- insert into ${table} as a (${columns.join(", ")})
556
- select ${columns.map((column) => `r.${column}`).join(", ")}
557
- from ${inserted} r
558
- on conflict (${token}) do update
559
- set ${columns.filter((column) => column !== token).map((column) => `${column} = excluded.${column}`).join(", ")}
560
- where ${status} = 'expired' or ${expires} <= now()
561
- $$;`;
562
- const attachSql = `create or replace function ${fn(APPROVAL_STORE.open)}(p_request jsonb)
563
- returns void
564
- language plpgsql
565
- volatile
566
- security definer
567
- set search_path = ''
568
- as $$
569
- begin
570
- update ${table} a
571
- set ${body} = p_request${mirrorSet(table, mirror, "p_request")}
572
- where a.${token} = p_request ->> 'token'
573
- and (a.${body} is null or ${status} = 'expired' or ${expires} <= now());
574
- if not found and not exists (select 1 from ${table} a where a.${token} = p_request ->> 'token') then
575
- raise exception using
576
- errcode = 'P0002',
577
- message = 'permdock: no row of ${adopt.table.replaceAll("'", "''")} holds approval token ' || (p_request ->> 'token'),
578
- hint = 'insert the application row with this token before opening the request';
579
- end if;
580
- end;
581
- $$;`;
582
- return `-- approval store adopted onto ${adopt.table}: ApprovalRequest bodies in ${adopt.body ?? "body"}, keyed by ${adopt.token ?? "token"}; supabaseApprovalStore calls the functions below
583
- alter table ${table} add column if not exists ${token} text;
584
- alter table ${table} add column if not exists ${body} jsonb;
585
- create unique index if not exists ${index("token")} on ${table} (${token});
586
- create index if not exists ${index("page")} on ${table} ((${body} ->> 'createdAt') collate "C", ${token} collate "C");
587
- create index if not exists ${index("status")} on ${table} ((${body} ->> 'status'));
588
- ${jsonSchema === false ? "" : bodySchemaSql(table, jsonSchema, body, `${base}_permdock_body_schema`)}
589
- ${open === "attach" ? attachSql : insertSql}
590
- ${serverOnly(fn(APPROVAL_STORE.open), "jsonb")}
591
-
592
- create or replace function ${fn(APPROVAL_STORE.get)}(p_token text)
593
- returns jsonb
594
- language sql
595
- stable
596
- security definer
597
- set search_path = ''
598
- as $$
599
- select a.${body} from ${table} a where a.${token} = p_token
600
- $$;
601
- ${serverOnly(fn(APPROVAL_STORE.get), "text")}
602
-
603
- -- write a verdict over the record it was computed from: still pending, unexpired, with p_seen approvals
604
- create or replace function ${fn(APPROVAL_STORE.resolve)}(p_token text, p_next jsonb, p_seen integer)
605
- returns jsonb
606
- language sql
607
- volatile
608
- security definer
609
- set search_path = ''
610
- as $$
611
- update ${table} a
612
- set ${body} = p_next${mirrorSet(table, mirror, "p_next")}
613
- where a.${token} = p_token
614
- and ${status} = 'pending'
615
- and ${expires} > now()
616
- and coalesce(jsonb_array_length(a.${body} -> 'approvals'), 0) = p_seen
617
- returning a.${body}
618
- $$;
619
- ${serverOnly(fn(APPROVAL_STORE.resolve), "text, jsonb, integer")}
620
-
621
- -- consume an approved, unexpired, unconsumed request once
622
- create or replace function ${fn(APPROVAL_STORE.consume)}(p_token text, p_now text)
623
- returns jsonb
624
- language sql
625
- volatile
626
- security definer
627
- set search_path = ''
628
- as $$
629
- update ${table} a
630
- set ${body} = a.${body} || jsonb_build_object('consumedAt', p_now)${mirrorSet(table, mirror, `(a.${body} || jsonb_build_object('consumedAt', p_now))`)}
631
- where a.${token} = p_token
632
- and ${status} = 'approved'
633
- and a.${body} ->> 'consumedAt' is null
634
- and ${expires} > p_now::timestamptz
635
- returning a.${body}
636
- $$;
637
- ${serverOnly(fn(APPROVAL_STORE.consume), "text, text")}
638
-
639
- -- one page in (createdAt, token) order after the cursor position
640
- create or replace function ${fn(APPROVAL_STORE.list)}(p_filter jsonb, p_after_created text, p_after_token text, p_limit integer)
641
- returns setof jsonb
642
- language sql
643
- stable
644
- security definer
645
- set search_path = ''
646
- as $$
647
- select a.${body}
648
- from ${table} a
649
- where a.${body} is not null
650
- and ${filter}
651
- and (p_after_created is null
652
- or (${created} collate "C", a.${token} collate "C") > (p_after_created collate "C", p_after_token collate "C"))
653
- order by ${created} collate "C", a.${token} collate "C"
654
- limit p_limit
655
- $$;
656
- ${serverOnly(fn(APPROVAL_STORE.list), "jsonb, text, text, integer")}
657
-
658
- create or replace function ${fn(APPROVAL_STORE.expire)}(p_now text)
659
- returns integer
660
- language sql
661
- volatile
662
- security definer
663
- set search_path = ''
664
- as $$
665
- with expired as (
666
- update ${table} a
667
- set ${body} = a.${body} || '{"status":"expired"}'::jsonb${mirrorSet(table, mirror, `(a.${body} || '{"status":"expired"}'::jsonb)`)}
668
- where ${status} = 'pending' and ${expires} <= p_now::timestamptz
669
- returning 1
670
- )
671
- select count(*)::integer from expired
672
- $$;
673
- ${serverOnly(fn(APPROVAL_STORE.expire), "text")}
674
-
675
- -- reject every pending request the filter matches, as system:<by>
676
- create or replace function ${fn(APPROVAL_STORE.cancel)}(p_filter jsonb, p_by text, p_note text, p_now text)
677
- returns integer
678
- language sql
679
- volatile
680
- security definer
681
- set search_path = ''
682
- as $$
683
- with cancelled as (
684
- update ${table} a
685
- set ${body} = (a.${body} - 'note') || jsonb_strip_nulls(jsonb_build_object(
686
- 'status', 'rejected', 'resolvedAt', p_now, 'resolvedBy', 'system:' || p_by, 'note', p_note
687
- ))${mirrorSet(table, mirror, `((a.${body} - 'note') || jsonb_strip_nulls(jsonb_build_object('status', 'rejected', 'resolvedAt', p_now, 'resolvedBy', 'system:' || p_by, 'note', p_note)))`)}
688
- where ${status} = 'pending'
689
- and ${filter.replaceAll("\n ", "\n ")}
690
- returning 1
691
- )
692
- select count(*)::integer from cancelled
693
- $$;
694
- ${serverOnly(fn(APPROVAL_STORE.cancel), "jsonb, text, text, text")}
336
+ /** The files `generate` writes: one, or one per `--split` part, plus the hook's grants under migra. */
337
+ function outputFiles(plan) {
338
+ const { input } = plan;
339
+ const split = parseSplit(input.split);
340
+ if (typeof split === "string") return split;
341
+ if (split?.includes("policies") === true && plan.helpersOnly) return "rls generate --helpers-only writes no policies part; drop it from --split";
342
+ if (split === void 0) {
343
+ if (input.grantsOut !== void 0) return "rls generate --grants-out needs --split with the helpers or hook part";
344
+ if (input.seedsOut !== void 0) return "rls generate --seeds-out needs --split with the seeds part";
345
+ return [{
346
+ part: "rls",
347
+ rel: plan.outRel,
348
+ text: plan.text
349
+ }];
350
+ }
351
+ if (input.target !== "sql") return "rls generate --split needs --target sql";
352
+ const schemas = resolveSchemas(input.cwd, input.config, input.schemas);
353
+ const pgDelta = schemas.mode === "pg-delta" ? schemas : void 0;
354
+ const placeholder = plan.outRel.includes("{part}");
355
+ const refused = splitRefusal(schemas.mode, {
356
+ split,
357
+ placeholder,
358
+ explicitOut: input.out !== void 0,
359
+ outRel: plan.outRel,
360
+ grantsOut: input.grantsOut,
361
+ seedsOut: input.seedsOut
362
+ });
363
+ if (refused !== void 0) return refused;
364
+ const at = (part, schema = plan.schema) => pgDelta === void 0 || placeholder ? partPath(plan.outRel, part) : pgDeltaPath(pgDelta.schemaDir, part, schema);
365
+ if (input.grantsOut !== void 0 && !split.includes("hook") && !split.includes("helpers")) return "rls generate --grants-out needs the helpers or hook part in --split";
366
+ if (input.seedsOut !== void 0 && !split.includes("seeds")) return "rls generate --seeds-out needs the seeds part in --split";
367
+ if (split.includes("hook") && input.config.supabase?.hook === void 0) return "rls generate --split hook needs supabase.hook in permdock.config.ts";
368
+ const sql = plan.sql();
369
+ const relocated = pgDelta === void 0 ? void 0 : sql.servicePolicies;
370
+ if (relocated !== void 0 && split.includes("policies") && !split.includes("seeds")) return "rls generate --split policies under pg-delta with rls.realtime or rls.storage needs the seeds part with --seeds-out: the declarative sync shadow database has no realtime.messages or storage.objects, so their policies go in that migration";
371
+ const seeds = relocated === void 0 || !split.includes("policies") ? sql.seeds : `${sql.seeds}\n${relocated}`;
372
+ const files = [];
373
+ const label = grantsLabel(input.grantsOut);
374
+ let grantsMarker = `${GRANTS_MARKER} schema=${plan.schema}`;
375
+ const grants = [];
376
+ for (const part of split) switch (part) {
377
+ case "helpers": {
378
+ if (label === void 0) {
379
+ files.push({
380
+ part,
381
+ rel: at(part),
382
+ text: sql.helpers
383
+ });
384
+ break;
385
+ }
386
+ const moved = moveUndiffed(sql.helpers, label, part);
387
+ files.push({
388
+ part,
389
+ rel: at(part),
390
+ text: moved.sql
391
+ });
392
+ grants.push(moved.grants);
393
+ break;
394
+ }
395
+ case "seeds": {
396
+ const out = input.seedsOut;
397
+ if (out !== void 0 && isSeedsDirectory(input.cwd, out)) {
398
+ const planned = seedsMigration(input.cwd, out, SEEDS_MARKER, seeds, input.io.now?.() ?? dateAt());
399
+ if (!planned.current || input.check) files.push({
400
+ part,
401
+ rel: planned.rel,
402
+ text: seeds
403
+ });
404
+ break;
405
+ }
406
+ files.push({
407
+ part,
408
+ rel: out ?? at(part),
409
+ text: seeds
410
+ });
411
+ break;
412
+ }
413
+ case "indexes":
414
+ files.push({
415
+ part,
416
+ rel: at(part),
417
+ text: sql.indexes
418
+ });
419
+ break;
420
+ case "policies":
421
+ files.push({
422
+ part,
423
+ rel: at(part),
424
+ text: pgDelta === void 0 ? sql.policies : sql.declarativePolicies
425
+ });
426
+ break;
427
+ case "hook": {
428
+ const hook = supabaseHookSql(plan.scopes, input.config, {}, label);
429
+ files.push({
430
+ part,
431
+ rel: at(part, hook.manifest.hook.schema),
432
+ text: hook.sql
433
+ });
434
+ const [marker = grantsMarker, ...rest] = hook.grants.split("\n");
435
+ grantsMarker = marker;
436
+ grants.push(rest.join("\n").trimEnd());
437
+ break;
438
+ }
439
+ default: return part;
440
+ }
441
+ if (input.grantsOut !== void 0) files.push({
442
+ part: "grants",
443
+ rel: input.grantsOut,
444
+ text: `${[grantsMarker, ...grants].join("\n")}\n`
445
+ });
446
+ return files;
447
+ }
448
+ //#endregion
449
+ //#region src/cli/rls/backfill.ts
450
+ const PREFIX$1 = "PermDock CLI";
451
+ /** A column of the roles-table row `r`. */
452
+ function col(name) {
453
+ return `r.${quoteIdent$1(name)}`;
454
+ }
455
+ function schemaOf(table) {
456
+ return table.includes(".") ? beforeFirst(table, ".") : "public";
457
+ }
458
+ /** The roles-table column a reference joins on, after checking it points at `roles`. */
459
+ function roleRef(role, table, roles, label) {
460
+ const pairs = Object.entries(role.on);
461
+ const [pair] = pairs;
462
+ if (pair === void 0 || pairs.length !== 1) throw configError(`${PREFIX$1}: ${label}.on must map exactly one column of ${table} to ${role.through}, for example { role_id: 'id' }`);
463
+ const through = role.through.includes(".") ? role.through : `${schemaOf(table)}.${role.through}`;
464
+ if (qualifySqlTable(through) !== qualifySqlTable(roles.table)) throw configError(`${PREFIX$1}: ${label}.through must be rls.customRoleWrites.roles.table (${roles.table}), got ${role.through}`);
465
+ if (role.column !== roles.key) throw configError(`${PREFIX$1}: ${label}.column must be rls.customRoleWrites.roles.key (${roles.key}), got ${role.column}`);
466
+ const [ref, id] = pair;
467
+ return {
468
+ ref,
469
+ id
470
+ };
471
+ }
472
+ /**
473
+ * A legacy key as SQL the way `mapKey` maps it: `rls.migrate.keys`, then
474
+ * `renamed`, then the longest `rls.migrate.prefixes` match, and a former key
475
+ * read as its current key.
476
+ */
477
+ function legacyKeySql(expr, migrate, renamed) {
478
+ const exact = new Map(Object.entries(renamed));
479
+ for (const [from, to] of Object.entries(migrate?.keys ?? {})) exact.set(from, to);
480
+ const prefixes = Object.entries(migrate?.prefixes ?? {}).toSorted(([a], [b]) => b.length - a.length || byCodeUnit(a, b));
481
+ const prefixed = prefixes.length === 0 ? expr : `(case ${prefixes.map(([from, to]) => `when left(${expr}, ${String(from.length)}) = ${quoteLiteral(from)} then ${quoteLiteral(to)} || substr(${expr}, ${String(from.length + 1)})`).join(" ")} else ${expr} end)`;
482
+ const mapped = exact.size === 0 ? prefixed : `(case ${expr} ${[...exact.entries()].toSorted(byKey).map(([from, to]) => `when ${quoteLiteral(from)} then ${quoteLiteral(to)}`).join(" ")} else ${prefixed} end)`;
483
+ const renames = Object.entries(renamed).toSorted(byKey);
484
+ return currentKeyExpr(renames, mapped);
485
+ }
486
+ function sourceSql(source, table, label) {
487
+ return roleColumn(source, table, "x", {
488
+ label,
489
+ prefix: PREFIX$1
490
+ }).lookup;
491
+ }
492
+ /**
493
+ * The `--backfill-out` migration: every custom role in the application's
494
+ * roles table that has no rows in `custom_role_permissions` or
495
+ * `custom_role_includes` yet is saved through
496
+ * `permdock_trusted_replace_custom_role_grants`, with the entries PermDock
497
+ * cannot hold left out and listed in one warning. Running it again copies
498
+ * nothing, so a role edited after the copy keeps its edits.
499
+ */
500
+ function backfillSql(ctx, from, migrate) {
501
+ const custom = ctx.customRoles;
502
+ const root = ctx.scopes[0]?.name;
503
+ if (custom === void 0 || root === void 0) throw configError(`${PREFIX$1}: rls.customRoles.from needs custom roles on`);
504
+ if (ctx.authorize !== "database") throw configError(`${PREFIX$1}: rls.customRoles.from needs database mode; in jwt mode custom roles ride the token, so build the grants claim with customRoleClaim`);
505
+ const roles = custom.table;
506
+ if (roles === void 0) throw configError(`${PREFIX$1}: rls.customRoles.from needs rls.customRoleWrites.roles, the roles table each row belongs to`);
507
+ const leveled = custom.levels === true;
508
+ if (from.permissions.level !== void 0 && !leveled) throw configError(`${PREFIX$1}: rls.customRoles.from.permissions.level is set, but no resource declares levels`);
509
+ const tenantType = tenantTypeOf(ctx);
510
+ const rolesTable = quoteQualified(roles.table);
511
+ const tenant = roles.tenant === void 0 ? `null::${tenantType}` : `${col(roles.tenant)}::${tenantType}`;
512
+ const scope = roles.scope === void 0 ? `case when ${tenant} is null then 'global' else ${quoteLiteral(root)} end` : `case when ${tenant} is null then 'global' else ${col(roles.scope)}::text end`;
513
+ const scopeId = roles.id === void 0 ? "null::text" : `${col(roles.id)}::text`;
514
+ const skipped = roles.skip === void 0 ? "false" : `coalesce(${col(roles.skip)}, false)`;
515
+ const renamed = custom.renamed ?? {};
516
+ const renames = Object.entries(renamed).toSorted(byKey);
517
+ const current = (expr) => currentKeyExpr(renames, expr);
518
+ const known = sqlTextArray([...custom.permissions ?? [], ...renames.map(([former]) => former)]);
519
+ const declared = sqlTextArray(custom.declared);
520
+ const scopes = sqlTextArray(ctx.scopes.map((item) => item.name));
521
+ const ceiling = qualified(ctx, CUSTOM_ROLES.ceiling);
522
+ const rp = qualified(ctx, "role_permissions");
523
+ const perms = qualified(ctx, CUSTOM_ROLES.permissions);
524
+ const includes = qualified(ctx, CUSTOM_ROLES.includes);
525
+ const replace = qualified(ctx, CUSTOM_ROLES.trustedReplace);
526
+ const p = from.permissions;
527
+ const pTable = qualifySqlTable(p.table);
528
+ const pRef = roleRef(p.role, p.table, roles, "rls.customRoles.from.permissions.role");
529
+ const legacy = sourceSql(p.permission, pTable, "rls.customRoles.from.permissions.permission");
530
+ const effect = p.effect === void 0 ? "'allow'" : `coalesce(lower(x.${quoteIdent$1(p.effect)}::text), 'allow')`;
531
+ const level = p.level === void 0 ? "null::text" : `nullif(x.${quoteIdent$1(p.level)}::text, '')`;
532
+ const inCeiling = (key) => `exists (select 1 from ${ceiling} c where c.scope = v.scope and c.effect = 'allow' and ${current("c.permission")} = ${key})`;
533
+ const levelCheck = leveled ? `
534
+ when m.effect = 'allow' and m.level is not null and not exists (
535
+ select 1 from ${ceiling} c
536
+ where c.scope = v.scope and c.effect = 'allow' and ${current("c.permission")} = m.key
537
+ and split_part(c.grant_key, '@', 2) = m.level
538
+ ) then 'level ' || m.level || ' is not declared'` : "";
539
+ const allowEntry = leveled ? "j.key || coalesce('@' || j.level, '')" : "j.key";
540
+ const i = from.includes;
541
+ const iTable = i === void 0 ? void 0 : qualifySqlTable(i.table);
542
+ const iRef = i === void 0 || iTable === void 0 ? void 0 : roleRef(i.role, i.table, roles, "rls.customRoles.from.includes.role");
543
+ const included = i === void 0 || iTable === void 0 ? void 0 : sourceSql(i.include, iTable, "rls.customRoles.from.includes.include");
544
+ const refs = [`${col(pRef.id)} as permissions_ref`, ...iRef === void 0 ? [] : [`${col(iRef.id)} as includes_ref`]].join(", ");
545
+ const includeBlock = i === void 0 || iTable === void 0 || iRef === void 0 || included === void 0 ? " v_include := null;" : ` with raw as (
546
+ select ${included} as role
547
+ from ${quoteTable(iTable)} x
548
+ where x.${quoteIdent$1(iRef.ref)} = v.includes_ref
549
+ ),
550
+ judged as (
551
+ select n.role, case
552
+ when n.role is null or not (n.role = any(${declared})) then 'not a declared role'
553
+ when exists (
554
+ select 1 from ${rp} rp
555
+ where rp.role = n.role and rp.effect = 'allow'
556
+ and not ${inCeiling(current("rp.permission"))}
557
+ ) then 'allows permissions outside the ceiling of ' || v.scope
558
+ end as dropped
559
+ from raw n
560
+ )
561
+ select array_agg(distinct j.role) filter (where j.dropped is null),
562
+ array_agg(v.role || ': includes ' || coalesce(j.role, 'null') || ', ' || j.dropped) filter (where j.dropped is not null)
563
+ into v_include, v_lost
564
+ from judged j;
565
+ v_dropped := v_dropped || coalesce(v_lost, '{}'::text[]);`;
566
+ return `${BACKFILL_MARKER} schema=${ctx.schema ?? "permdock"}
567
+ -- custom roles copied from ${qualifySqlTable(roles.table)} (rls.customRoles.from) through ${replace}
568
+ -- a role that already has rows is left as it is; entries PermDock cannot hold are dropped and listed in one warning
569
+ do $permdock$
570
+ declare
571
+ v record;
572
+ v_allow text[];
573
+ v_deny text[];
574
+ v_include text[];
575
+ v_lost text[];
576
+ v_dropped text[] := '{}'::text[];
577
+ v_copied integer := 0;
578
+ v_present integer := 0;
579
+ begin
580
+ for v in
581
+ select ${tenant} as tenant_id, ${scope} as scope, ${scopeId} as scope_id, ${col(roles.key)}::text as role, ${refs}
582
+ from ${rolesTable} r
583
+ where not ${skipped} and not (${col(roles.key)}::text = any(${declared}))
584
+ order by 4, 2, 1, 3
585
+ loop
586
+ if v.role is null or not (v.scope = 'global' or v.scope = any(${scopes})) then
587
+ v_dropped := v_dropped || (coalesce(v.role, 'null') || ': scope ' || coalesce(v.scope, 'null') || ' is not declared');
588
+ continue;
589
+ end if;
590
+ if exists (
591
+ select 1 from ${perms} c
592
+ where c.tenant_id is not distinct from v.tenant_id and c.scope = v.scope and c.scope_id is not distinct from v.scope_id and c.role = v.role
593
+ ) or exists (
594
+ select 1 from ${includes} c
595
+ where c.tenant_id is not distinct from v.tenant_id and c.scope = v.scope and c.scope_id is not distinct from v.scope_id and c.role = v.role
596
+ ) then
597
+ v_present := v_present + 1;
598
+ continue;
599
+ end if;
600
+ with raw as (
601
+ select ${legacy} as legacy, ${effect} as effect, ${level} as level
602
+ from ${quoteTable(pTable)} x
603
+ where x.${quoteIdent$1(pRef.ref)} = v.permissions_ref
604
+ ),
605
+ mapped as (
606
+ select n.legacy, ${legacyKeySql("n.legacy", migrate, renamed)} as key, n.effect, n.level
607
+ from raw n
608
+ ),
609
+ judged as (
610
+ select m.*, case
611
+ when m.effect not in ('allow', 'deny') then 'effect ' || m.effect || ' is not allow or deny'
612
+ when m.key is null or not (m.key = any(${known})) then 'not a declared permission'
613
+ when m.effect = 'allow' and not ${inCeiling("m.key")} then 'outside the ceiling of ' || v.scope${levelCheck}
614
+ end as dropped
615
+ from mapped m
616
+ )
617
+ select array_agg(distinct ${allowEntry}) filter (where j.dropped is null and j.effect = 'allow'),
618
+ array_agg(distinct j.key) filter (where j.dropped is null and j.effect = 'deny'),
619
+ array_agg(v.role || ': ' || coalesce(j.legacy, 'null') || ', ' || j.dropped) filter (where j.dropped is not null)
620
+ into v_allow, v_deny, v_lost
621
+ from judged j;
622
+ v_dropped := v_dropped || coalesce(v_lost, '{}'::text[]);
623
+ ${includeBlock}
624
+ if v_allow is null and v_deny is null and v_include is null then
625
+ continue;
626
+ end if;
627
+ perform ${replace}(v.tenant_id, v.scope, v.scope_id, v.role, v_allow, v_deny, v_include);
628
+ v_copied := v_copied + 1;
629
+ end loop;
630
+ raise notice 'permdock backfill: copied % custom role(s), % already present', v_copied, v_present;
631
+ if cardinality(v_dropped) > 0 then
632
+ raise warning 'permdock backfill: dropped % entr(ies) PermDock cannot hold: %', cardinality(v_dropped), array_to_string(v_dropped[1:50], '; ');
633
+ end if;
634
+ end
635
+ $permdock$;
695
636
  `;
696
637
  }
697
638
  //#endregion
698
- //#region src/cli/rls-break-glass.ts
639
+ //#region src/cli/rls/break-glass.ts
699
640
  /** The audit table every generated break-glass read writes to. Part of the SQL contract. */
700
641
  const BREAK_GLASS_AUDIT = "permdock_break_glass_audit";
701
- const IDENT = /^[a-z][a-z0-9_]*$/u;
642
+ const IDENT$1 = /^[a-z][a-z0-9_]*$/u;
702
643
  /**
703
644
  * The resources a `breakGlass` grant targets, with the table each reads. A
704
645
  * break-glass grant is non-portable, so `compileGrants` never turns it into a
@@ -747,22 +688,19 @@ function rowsFilter(ctx, entry) {
747
688
  const asked = `p_permission = ${quoteLiteral(grant.permission)}`;
748
689
  if (grant.scope === "anyone") {
749
690
  if (grant.root === void 0) return [`(${asked})`];
750
- return grant.column === void 0 ? [] : [`(${asked} and ${quoteIdent$1(grant.column)} in (select ${qualified$1(ctx, memberIdsHelper(grant.root))}()))`];
691
+ return grant.column === void 0 ? [] : [`(${asked} and ${quoteIdent$1(grant.column)} in (select ${qualified(ctx, memberIdsHelper(grant.root))}()))`];
751
692
  }
752
693
  if (grant.scope !== "global" && grant.column === void 0) return [];
753
694
  return [`(${asked} and ${accessSql(ctx, grant.scope, breakGlassKey(grant.permission), grant.column)})`];
754
695
  });
755
696
  return arms.length === 0 ? "false" : [...new Set(arms)].join("\n or ");
756
697
  }
757
- function functionName(resource) {
758
- if (!IDENT.test(resource)) throw new Error(`PermDock CLI: unsafe break-glass resource '${resource}'`);
698
+ function functionName$1(resource) {
699
+ if (!IDENT$1.test(resource)) throw configError(`PermDock CLI: unsafe break-glass resource '${resource}'`);
759
700
  return `permdock_break_glass_${resource}`;
760
701
  }
761
- function qualifiedTable(name) {
762
- return quoteTable(name.includes(".") ? name : `public.${name}`);
763
- }
764
- function auditSql(ctx) {
765
- const audit = qualified$1(ctx, quoteIdent$1(BREAK_GLASS_AUDIT));
702
+ function auditSql$1(ctx) {
703
+ const audit = qualified(ctx, quoteIdent$1(BREAK_GLASS_AUDIT));
766
704
  return `-- break-glass audit: one row per session that read restricted data
767
705
  -- RLS never compiles a break-glass override, so restricted rows stay denied to a direct read
768
706
  create table if not exists ${audit} (
@@ -777,9 +715,9 @@ alter table ${audit} enable row level security;
777
715
  revoke all on table ${audit} from anon, authenticated, public;`;
778
716
  }
779
717
  function readFunctionSql(ctx, entry) {
780
- const fn = qualified$1(ctx, functionName(entry.resource));
781
- const audit = qualified$1(ctx, quoteIdent$1(BREAK_GLASS_AUDIT));
782
- const table = qualifiedTable(entry.table);
718
+ const fn = qualified(ctx, functionName$1(entry.resource));
719
+ const audit = qualified(ctx, quoteIdent$1(BREAK_GLASS_AUDIT));
720
+ const table = quoteQualified(entry.table);
783
721
  const session = subjectClaimJsonSql(ctx, "break_glass");
784
722
  const subject = subjectIdSql(ctx);
785
723
  return `-- ${entry.resource}: reads restricted rows through a checked, audited break-glass session
@@ -797,8 +735,8 @@ begin
797
735
  or session ->> 'v' <> '1'
798
736
  or session ->> 'permission' <> p_permission
799
737
  or coalesce(session ->> 'reason', '') = ''
800
- or jsonb_typeof(session -> 'expiresAt') <> 'number'
801
- or (session ->> 'expiresAt')::numeric <= extract(epoch from now())
738
+ or jsonb_typeof(session -> 'expires_at') <> 'number'
739
+ or (session ->> 'expires_at')::numeric <= extract(epoch from now())
802
740
  then
803
741
  raise exception 'PermDock: no valid break-glass session for %', p_permission
804
742
  using errcode = '42501';
@@ -827,10 +765,10 @@ grant execute on function ${fn}(text) to authenticated;`;
827
765
  */
828
766
  function breakGlassSql(ctx, entries) {
829
767
  if (entries.length === 0) return "";
830
- return [auditSql(ctx), ...entries.map((entry) => readFunctionSql(ctx, entry))].join("\n\n");
768
+ return [auditSql$1(ctx), ...entries.map((entry) => readFunctionSql(ctx, entry))].join("\n\n");
831
769
  }
832
770
  //#endregion
833
- //#region src/cli/rls-emit.ts
771
+ //#region src/cli/rls/emit.ts
834
772
  const HEADER = "-- generated by permdock rls generate\n-- fail-closed: never target a bypass role\n";
835
773
  const TRUSTED_READER_GRANT = /^grant execute on function [^\n]+\.permdock_trusted_role_permissions\([^)\n]*\) to [^\n;]+;$/gmu;
836
774
  function assertNoServiceRole(text) {
@@ -855,7 +793,7 @@ function tablesOf(policies) {
855
793
  /** Enable (and force) row level security, then the table grants for the commands a policy allows. */
856
794
  function tableSql(table, policies, force, views) {
857
795
  const closed = views.find((view) => view.table === table && view.companion !== void 0);
858
- const target = quoteTable(qualifiedTable$1(table));
796
+ const target = quoteQualified(table);
859
797
  const lines = [`alter table ${target} enable row level security;`];
860
798
  if (force) lines.push(`alter table ${target} force row level security;`);
861
799
  lines.push(`revoke all on table ${target} from anon, authenticated;`);
@@ -875,7 +813,7 @@ function tableSql(table, policies, force, views) {
875
813
  }
876
814
  /** `drop policy if exists` and `create policy` for one policy, without the table's grants. */
877
815
  function policySql(item) {
878
- const target = quoteTable(qualifiedTable$1(item.table));
816
+ const target = quoteQualified(item.table);
879
817
  const lines = [
880
818
  `drop policy if exists ${quoteIdent$1(item.name)} on ${target};`,
881
819
  `create policy ${quoteIdent$1(item.name)}`,
@@ -930,7 +868,7 @@ function pascalCase(name) {
930
868
  }
931
869
  const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
932
870
  function identifier(name, what) {
933
- if (!IDENTIFIER.test(name)) throw new Error(`PermDock CLI: ${what} ${JSON.stringify(name)} is not an identifier`);
871
+ if (!IDENTIFIER.test(name)) throw configError(`PermDock CLI: ${what} ${JSON.stringify(name)} is not an identifier`);
934
872
  return name;
935
873
  }
936
874
  function quoted(text) {
@@ -994,7 +932,7 @@ function emitPrisma(policies, options = {}) {
994
932
  ""
995
933
  ];
996
934
  for (const item of policies) {
997
- if (item.effect === "deny") throw new Error(`PermDock CLI: Prisma policy blocks are permissive only, so ${item.name} (a deny) needs --target sql`);
935
+ if (item.effect === "deny") throw configError(`PermDock CLI: Prisma policy blocks are permissive only, so ${item.name} (a deny) needs --target sql`);
998
936
  lines.push(`policy_${item.command} ${identifier(item.name, "policy name")} {`, ` target = ${modelOf(item.table)}`, ` roles = [${item.roles.join(", ")}]`);
999
937
  if (item.using !== void 0) lines.push(` using = ${JSON.stringify(item.using)}`);
1000
938
  if (item.check !== void 0) lines.push(` withCheck = ${JSON.stringify(item.check)}`);
@@ -1018,24 +956,24 @@ function defaultOut(target) {
1018
956
  }
1019
957
  }
1020
958
  //#endregion
1021
- //#region src/cli/rls-realtime-storage.ts
959
+ //#region src/cli/rls/realtime-storage.ts
1022
960
  const SCOPE_SEGMENT = /^\{([a-z][a-z0-9_]*)\}$/u;
1023
961
  const LITERAL_SEGMENT = /^[^{}:\s]+$/u;
1024
962
  const TOPIC = "(select realtime.topic())";
1025
963
  function policyName(prefix, name, command) {
1026
964
  const full = `permdock_${prefix}_${name.replaceAll(/[^A-Za-z0-9]+/gu, "_").replaceAll(/^_|_$/gu, "")}_${command}`;
1027
- if (new TextEncoder().encode(full).length > 63) throw new Error(`PermDock CLI: the ${prefix} policy name ${full} is longer than Postgres's 63 bytes; shorten '${name}'`);
965
+ if (new TextEncoder().encode(full).length > 63) throw configError(`PermDock CLI: the ${prefix} policy name ${full} is longer than Postgres's 63 bytes; shorten '${name}'`);
1028
966
  return full;
1029
967
  }
1030
968
  function grantKey(facts, ref, where, warnings) {
1031
969
  if (ref === void 0) return;
1032
970
  const key = typeof ref === "object" && ref !== null ? Reflect.get(ref, "key") : ref;
1033
- if (typeof key !== "string" || !facts.keys.has(key)) throw new Error(`PermDock CLI: ${where} must be a permission reference from the definitions (got ${JSON.stringify(key)})`);
971
+ if (typeof key !== "string" || !facts.keys.has(key)) throw configError(`PermDock CLI: ${where} must be a permission reference from the definitions (got ${JSON.stringify(key)})`);
1034
972
  if (facts.rowConditions.has(key)) warnings.push(`${where} is ${key}, whose grants carry row conditions (a relation, a where or another condition): the policy admits only the instances its role allows without a condition reach, through permitted_<scope>_ids_by_permission`);
1035
973
  return key;
1036
974
  }
1037
975
  function declaredScope(facts, scope, where) {
1038
- if (!facts.scopes.has(scope)) throw new Error(`PermDock CLI: ${where} names the scope '${scope}', which definePolicy({ scopes }) does not declare`);
976
+ if (!facts.scopes.has(scope)) throw configError(`PermDock CLI: ${where} names the scope '${scope}', which definePolicy({ scopes }) does not declare`);
1039
977
  return scope;
1040
978
  }
1041
979
  /**
@@ -1044,7 +982,7 @@ function declaredScope(facts, scope, where) {
1044
982
  * relationship or a `where` on the same permission never widens it.
1045
983
  */
1046
984
  function permitted(ctx, value, scope, key) {
1047
- return `${value} in (select x::text from ${qualified$1(ctx, permittedByPermissionHelper(scope))}(${quoteLiteral(key)}) x)`;
985
+ return `${value} in (select x::text from ${qualified(ctx, permittedByPermissionHelper(scope))}(${quoteLiteral(key)}) x)`;
1048
986
  }
1049
987
  function escapeRegex(text) {
1050
988
  return text.replaceAll(/[.*+?^${}()|[\]\\]/gu, String.raw`\$&`);
@@ -1061,7 +999,7 @@ function realtimePolicies(ctx, facts, realtime, warnings) {
1061
999
  }];
1062
1000
  });
1063
1001
  const [placeholder] = placeholders;
1064
- if (placeholder === void 0 || placeholders.length > 1 || segments.some((segment, index) => index !== placeholder.index && !LITERAL_SEGMENT.test(segment))) throw new Error(`PermDock CLI: ${where} must be ':'-separated segments with exactly one '{<scope>}' segment, such as 'org:{organization}:chat'`);
1002
+ if (placeholder === void 0 || placeholders.length > 1 || segments.some((segment, index) => index !== placeholder.index && !LITERAL_SEGMENT.test(segment))) throw configError(`PermDock CLI: ${where} must be ':'-separated segments with exactly one '{<scope>}' segment, such as 'org:{organization}:chat'`);
1065
1003
  const scope = declaredScope(facts, placeholder.scope, where);
1066
1004
  const regex = `^${segments.map((segment, index) => index === placeholder.index ? "[^:]+" : escapeRegex(segment)).join(":")}$`;
1067
1005
  const id = `split_part(${TOPIC}, ':', ${String(placeholder.index + 1)})`;
@@ -1070,7 +1008,7 @@ function realtimePolicies(ctx, facts, realtime, warnings) {
1070
1008
  and ${permitted(ctx, id, scope, key)}`;
1071
1009
  const read = grantKey(facts, topic.read, `${where}.read`, warnings);
1072
1010
  const write = grantKey(facts, topic.write, `${where}.write`, warnings);
1073
- if (read === void 0) throw new Error(`PermDock CLI: ${where}.read is required`);
1011
+ if (read === void 0) throw configError(`PermDock CLI: ${where}.read is required`);
1074
1012
  const policies = [{
1075
1013
  name: policyName("realtime", pattern, "select"),
1076
1014
  table: "realtime.messages",
@@ -1092,7 +1030,7 @@ function realtimePolicies(ctx, facts, realtime, warnings) {
1092
1030
  }
1093
1031
  function folderOf(bucket, where) {
1094
1032
  const folder = bucket.folder ?? 1;
1095
- if (!Number.isInteger(folder) || folder < 1) throw new Error(`PermDock CLI: ${where}.folder must be a positive integer (got ${String(folder)})`);
1033
+ if (!Number.isInteger(folder) || folder < 1) throw configError(`PermDock CLI: ${where}.folder must be a positive integer (got ${String(folder)})`);
1096
1034
  return folder;
1097
1035
  }
1098
1036
  function storagePolicies(ctx, facts, storage, warnings) {
@@ -1137,7 +1075,7 @@ function storagePolicies(ctx, facts, storage, warnings) {
1137
1075
  ...command === "select" || command === "delete" ? {} : { check: expression }
1138
1076
  }];
1139
1077
  });
1140
- if (policies.length === 0) throw new Error(`PermDock CLI: ${where} needs read, write or delete`);
1078
+ if (policies.length === 0) throw configError(`PermDock CLI: ${where} needs read, write or delete`);
1141
1079
  return policies;
1142
1080
  });
1143
1081
  }
@@ -1151,22 +1089,22 @@ function storagePolicies(ctx, facts, storage, warnings) {
1151
1089
  */
1152
1090
  function realtimeStoragePolicies(ctx, facts, config, warnings = []) {
1153
1091
  if (config.realtime === void 0 && config.storage === void 0) return [];
1154
- if (ctx.dialect !== "supabase") throw new Error(`PermDock CLI: rls.realtime and rls.storage need --dialect supabase (got ${ctx.dialect})`);
1092
+ if (ctx.dialect !== "supabase") throw configError(`PermDock CLI: rls.realtime and rls.storage need --dialect supabase (got ${ctx.dialect})`);
1155
1093
  const policies = [...config.realtime === void 0 ? [] : realtimePolicies(ctx, facts, config.realtime, warnings), ...config.storage === void 0 ? [] : storagePolicies(ctx, facts, config.storage, warnings)];
1156
1094
  const seen = /* @__PURE__ */ new Set();
1157
1095
  for (const { name } of policies) {
1158
- if (seen.has(name)) throw new Error(`PermDock CLI: two rls.realtime or rls.storage entries both name the policy ${name}; rename one`);
1096
+ if (seen.has(name)) throw configError(`PermDock CLI: two rls.realtime or rls.storage entries both name the policy ${name}; rename one`);
1159
1097
  seen.add(name);
1160
1098
  }
1161
1099
  return policies;
1162
1100
  }
1163
1101
  //#endregion
1164
- //#region src/cli/rls-rows.ts
1102
+ //#region src/cli/rls/rows.ts
1165
1103
  /** `permitted_<resource>_rows(p_permission)`: the resource's row ids the subject may act on with one permission. */
1166
1104
  function rowsHelper(resource) {
1167
1105
  const name = graphSqlName(resource);
1168
1106
  const helper = `permitted_${name ?? resource}_rows`;
1169
- if (name === void 0 || new TextEncoder().encode(`${helper}_for`).length > 63) throw new Error(`PermDock CLI: rls.rowHelpers cannot name a helper for resource '${resource}'; use a SQL name of at most 45 bytes`);
1107
+ if (name === void 0 || new TextEncoder().encode(`${helper}_for`).length > 63) throw configError(`PermDock CLI: rls.rowHelpers cannot name a helper for resource '${resource}'; use a SQL name of at most 45 bytes`);
1170
1108
  return helper;
1171
1109
  }
1172
1110
  /** The branches a row check of a permission reads: its allows and denies on rows that exist, for signed-in callers. */
@@ -1243,7 +1181,7 @@ function inheritTargets(policy) {
1243
1181
  return [...out].toSorted();
1244
1182
  }
1245
1183
  function stubSql(ctx, resource) {
1246
- return `create or replace function ${qualified$1(ctx, rowsHelper(resource))}(p_permission text)
1184
+ return `create or replace function ${qualified(ctx, rowsHelper(resource))}(p_permission text)
1247
1185
  returns setof text
1248
1186
  language sql
1249
1187
  stable
@@ -1254,7 +1192,7 @@ as $$
1254
1192
  $$;`;
1255
1193
  }
1256
1194
  function rowSql(ctx, rows, table, where) {
1257
- const fn = qualified$1(ctx, rows.replace(/_rows$/u, "_row"));
1195
+ const fn = qualified(ctx, rows.replace(/_rows$/u, "_row"));
1258
1196
  return `create or replace function ${fn}(p_row ${table}, p_permission text)
1259
1197
  returns boolean
1260
1198
  language sql
@@ -1269,8 +1207,8 @@ grant execute on function ${fn}(${table}, text) to authenticated;`;
1269
1207
  }
1270
1208
  function resourceSql(ctx, node, branches, tables) {
1271
1209
  const name = rowsHelper(node.name);
1272
- const fn = qualified$1(ctx, name);
1273
- const table = quoteTable(qualifiedTable$1(tableFor(node.name, tables)));
1210
+ const fn = qualified(ctx, name);
1211
+ const table = quoteQualified(tableFor(node.name, tables));
1274
1212
  const cases = [...branches.keys()].toSorted().flatMap((key) => {
1275
1213
  const sql = permissionCase(key, branches.get(key) ?? []);
1276
1214
  return sql === void 0 ? [] : [sql];
@@ -1293,7 +1231,7 @@ ${rowSql(ctx, name, table, where)}`;
1293
1231
  if (ctx.dialect === "neon") return rows;
1294
1232
  return `${rows}
1295
1233
  -- the same rows for a user the caller names, with claims it supplies; no client role may execute it
1296
- ${forUserSql(ctx, qualified$1(ctx, `${name}_for`), fn)}`;
1234
+ ${forUserSql(ctx, qualified(ctx, `${name}_for`), fn)}`;
1297
1235
  }
1298
1236
  /**
1299
1237
  * `permitted_<resource>_rows(p_permission)` for each resource `select`
@@ -1313,14 +1251,96 @@ function rowHelpersSql(ctx, policy, branches, select, tables, warnings) {
1313
1251
  const chunks = called.map((name) => stubSql(ctx, name));
1314
1252
  for (const name of names) {
1315
1253
  const node = policy.resources.get(name);
1316
- if (node === void 0) throw new Error(`PermDock CLI: rls.rowHelpers names '${name}', which the policy does not declare`);
1254
+ if (node === void 0) throw configError(`PermDock CLI: rls.rowHelpers names '${name}', which the policy does not declare`);
1317
1255
  chunks.push(resourceSql(ctx, node, rowBranchesOf(branches, name), tables));
1318
1256
  }
1319
1257
  if (chunks.length > 0 && ctx.dialect === "neon") warnings.push("rls.rowHelpers: the neon dialect reads the subject from auth.session(), which SQL cannot set, so no permitted_<resource>_rows_for is written");
1320
1258
  return chunks.length === 0 ? "" : `${chunks.join("\n\n")}\n`;
1321
1259
  }
1322
1260
  //#endregion
1323
- //#region src/cli/rls-shims.ts
1261
+ //#region src/cli/rls/generate-policies.ts
1262
+ const SERVICE_TABLES_HEADER = "-- Supabase Realtime and Storage: rls.realtime and rls.storage. RLS on both tables is Supabase's.";
1263
+ /**
1264
+ * A `realtime.messages` or `storage.objects` policy that applies only where
1265
+ * the table exists. The Realtime and Storage services create those tables, so
1266
+ * a local stack with the service off, and pg-delta's shadow database, have
1267
+ * none.
1268
+ */
1269
+ function guardedPolicySql(item) {
1270
+ const body = policySql(item).trimEnd().split("\n").map((line) => ` ${line}`).join("\n");
1271
+ return `do $permdock$
1272
+ begin
1273
+ if to_regclass(${quoteLiteral(qualifySqlTable(item.table))}) is not null then
1274
+ ${body}
1275
+ end if;
1276
+ end
1277
+ $permdock$;
1278
+ `;
1279
+ }
1280
+ /** Appends the `realtime.messages` and `storage.objects` policies, which never get `tableSql`. */
1281
+ function withHelperTables(sql, helperTables) {
1282
+ if (helperTables === void 0) return sql;
1283
+ const tables = `${helperTables.trimEnd()}\n`;
1284
+ return sql.trim() === "" ? tables : `${sql.trimEnd()}\n\n${tables}`;
1285
+ }
1286
+ function databaseModeWarnings(policy, { ctx }, warnings) {
1287
+ if (ctx.authorize !== "database") return;
1288
+ for (const { name } of ctx.scopes) if (policy.grants.some((grant) => grant.scope === name) && scopeTable(ctx, name) === void 0 && scopeSources(ctx, name).length === 0) warnings.push(`${name}-scoped grants read the ${name} memberships table in database mode: set rls.memberships.scopes.${name}, rls.membershipSources or supabase.hook.memberships (or --memberships for the first scope), otherwise they deny`);
1289
+ }
1290
+ /** The grants compiled into table policies, and the Realtime and Storage policies beside them. */
1291
+ function generatePolicies(input, policy, options, built, warnings) {
1292
+ const rls = input.config.rls;
1293
+ const { ctx } = built;
1294
+ const { fieldsMode, revokeColumns } = options;
1295
+ databaseModeWarnings(policy, built, warnings);
1296
+ const compiled = compileGrants(policy, ctx, rls?.tables, warnings, input.skipClosures, rls?.rowHelpers !== void 0 || inheritTargets(policy).length > 0);
1297
+ const helpersOnly = input.helpersOnly === true || rls?.helpersOnly === true;
1298
+ if (helpersOnly && (input.target !== "sql" || revokeColumns)) return { refusal: "rls generate --helpers-only needs --target sql and no --revoke-columns: the table grants are hand-written, so revoke the restricted columns there" };
1299
+ const views = fieldsMode === void 0 ? [] : fieldViews(policy, ctx, compiled.branches, {
1300
+ revokeColumns,
1301
+ helpersOnly
1302
+ }, warnings);
1303
+ const force = input.force === true || rls?.force === true;
1304
+ if (force && views.some((view) => view.companion !== void 0)) warnings.push("--force with --revoke-columns: each <table>_visible_fields companion reads as its owner, so its owner needs BYPASSRLS or the restricted columns read as null");
1305
+ const policyName = input.policyName ?? rls?.policyName;
1306
+ const assembled = helpersOnly ? [] : assemblePolicies(fieldsMode === void 0 ? compiled.branches : rowBranches(compiled.branches), {
1307
+ perRole: input.policyPerRole === true || rls?.policyPerRole === true,
1308
+ ...policyName === void 0 ? {} : { name: policyName }
1309
+ });
1310
+ const actorKinds = readOnlyActorKinds(rls?.readOnlyActors);
1311
+ if (actorKinds !== void 0 && helpersOnly) warnings.push(`rls.readOnlyActors with --helpers-only writes no policies: add \`and (select ${qualified(ctx, ACTOR_MAY_WRITE)}())\` to each hand-written insert, update and delete policy, or a restrictive policy that calls it`);
1312
+ const withActors = (items) => actorKinds === void 0 ? items : [...items, ...readOnlyActorPolicies(items, subjectClaimJsonSql(ctx, "act"), actorKinds)];
1313
+ let tablePolicies;
1314
+ try {
1315
+ tablePolicies = realtimeStoragePolicies(ctx, {
1316
+ scopes: new Set(ctx.scopes.map((scope) => scope.name)),
1317
+ keys: new Set(listPermissions(policy.vocabulary.permissions).map((leaf) => leaf.key)),
1318
+ rowConditions: new Set(policyRowConditionKeys(policy))
1319
+ }, compact({
1320
+ realtime: rls?.realtime,
1321
+ storage: rls?.storage
1322
+ }), warnings);
1323
+ } catch (cause) {
1324
+ return { refusal: describeError(cause) };
1325
+ }
1326
+ if (tablePolicies.length > 0 && helpersOnly) warnings.push("rls.realtime and rls.storage add policies, and --helpers-only writes none: no realtime.messages or storage.objects policy is written");
1327
+ const servicePolicies = tablePolicies.length === 0 || helpersOnly ? [] : withActors(tablePolicies);
1328
+ const generated = {
1329
+ compiled,
1330
+ helpersOnly,
1331
+ force,
1332
+ views,
1333
+ policies: withActors(assembled)
1334
+ };
1335
+ if (servicePolicies.length === 0) return generated;
1336
+ return {
1337
+ ...generated,
1338
+ helperTables: [SERVICE_TABLES_HEADER, ...servicePolicies.map(policySql)].join("\n"),
1339
+ guardedTables: [`${SERVICE_TABLES_HEADER} A stack without the service has no table, so each policy is skipped there.`, ...servicePolicies.map(guardedPolicySql)].join("\n")
1340
+ };
1341
+ }
1342
+ //#endregion
1343
+ //#region src/cli/rls/shims.ts
1324
1344
  const NAME = /^[a-z_][a-z0-9_]*$/u;
1325
1345
  /**
1326
1346
  * The key a legacy caller passes, mapped the way `rls migrate` maps it:
@@ -1337,7 +1357,7 @@ function mappedKey(config, renamed) {
1337
1357
  return `coalesce(${quoteLiteral(JSON.stringify(exact))}::jsonb ->> p_key, ${fallback})`;
1338
1358
  }
1339
1359
  function scopeOf(ctx, scope) {
1340
- if (!ctx.scopes.some((item) => item.name === scope)) throw new Error(`PermDock CLI: rls.migrate helper scope '${scope}' is not a declared scope`);
1360
+ if (!ctx.scopes.some((item) => item.name === scope)) throw configError(`PermDock CLI: rls.migrate helper scope '${scope}' is not a declared scope`);
1341
1361
  return scope;
1342
1362
  }
1343
1363
  function addKey(list, key) {
@@ -1404,7 +1424,7 @@ function grantKeys(keys, scope, effect) {
1404
1424
  }
1405
1425
  /** The instances of `scope` the caller holds an unconditional allow of the key in, minus those it holds a deny in. */
1406
1426
  function idsSql(ctx, keys, scope) {
1407
- const helper = qualified$1(ctx, permittedIdsHelper(scope));
1427
+ const helper = qualified(ctx, permittedIdsHelper(scope));
1408
1428
  return ` select a.id
1409
1429
  from ${grantKeys(keys, scope, "allow")}
1410
1430
  cross join lateral ${helper}(g.grant_key) a(id)
@@ -1414,7 +1434,7 @@ function idsSql(ctx, keys, scope) {
1414
1434
  cross join lateral ${helper}(g.grant_key) d(id)`;
1415
1435
  }
1416
1436
  function hasSql(ctx, keys) {
1417
- const has = qualified$1(ctx, HELPERS.has);
1437
+ const has = qualified(ctx, HELPERS.has);
1418
1438
  return `exists (select 1 from ${grantKeys(keys, "global", "allow")} where ${has}(g.grant_key))
1419
1439
  and not exists (select 1 from ${grantKeys(keys, "global", "deny")} where ${has}(g.grant_key))`;
1420
1440
  }
@@ -1432,7 +1452,14 @@ function shim(ctx, config, keys, fn, helper) {
1432
1452
  case "membership": {
1433
1453
  const scope = scopeOf(ctx, helper.scope);
1434
1454
  const type = scopeTypeOf(ctx, scope);
1435
- return shimFunction(fn, `p_id ${type}`, type, "boolean", ` select coalesce(p_id in (select ${qualified$1(ctx, memberIdsHelper(scope))}()), false)`);
1455
+ return shimFunction(fn, `p_id ${type}`, type, "boolean", ` select coalesce(p_id in (select ${qualified(ctx, memberIdsHelper(scope))}()), false)`);
1456
+ }
1457
+ case "role": {
1458
+ const scope = scopeOf(ctx, helper.scope);
1459
+ const type = scopeTypeOf(ctx, scope);
1460
+ const values = Object.entries(ctx.roleValues ?? {});
1461
+ const role = values.length === 0 ? "p_role" : `case p_role ${values.map(([from, to]) => `when ${quoteLiteral(from)} then ${quoteLiteral(to)}`).join(" ")} else p_role end`;
1462
+ return shimFunction(fn, `p_id ${type}, p_role text`, `${type}, text`, "boolean", ` select coalesce(p_id in (select ${qualified(ctx, memberIdsHelper(scope))}(array[${role}])), false)`);
1436
1463
  }
1437
1464
  case "global": return shimFunction(fn, "p_key text", "text", "boolean", ` select ${hasSql(ctx, keys)}`);
1438
1465
  case "scoped": {
@@ -1461,229 +1488,118 @@ function shimsSql(ctx, config, shims, renamed, grants) {
1461
1488
  map: (scope) => `${quoteLiteral(shimMap(grants, scope))}::jsonb`
1462
1489
  };
1463
1490
  const chunks = [`-- permdock shims: legacy helper names over the permdock helpers; drop each once doctor reports no caller (PD056)`];
1464
- for (const [name, helper] of Object.entries(config.helpers).toSorted(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
1465
- if (!NAME.test(name)) throw new Error(`PermDock CLI: unsafe rls.migrate helper name '${name}'`);
1491
+ for (const [name, helper] of Object.entries(config.helpers).toSorted(byKey)) {
1492
+ if (!NAME.test(name)) throw configError(`PermDock CLI: unsafe rls.migrate helper name '${name}'`);
1466
1493
  chunks.push(shim(ctx, config, keys, `${quoteIdent$1(schema)}.${name}`, helper));
1467
1494
  }
1468
1495
  return `${chunks.join("\n\n")}\n`;
1469
1496
  }
1470
1497
  //#endregion
1471
- //#region src/cli/rls-generate.ts
1472
- async function loadPolicy(cwd, config, from) {
1473
- const policyPath = from === "drizzle" ? config.policy : from ?? config.policy;
1474
- if (policyPath === void 0) throw new Error("PermDock CLI: rls generate needs policy in permdock.config.ts or --from");
1475
- return asPolicy(pickNamed(await loadModule(resolve(cwd, policyPath)), ["policy"]));
1476
- }
1477
- /** `rls.customRoleWrites.requires` as permission keys, or why it names none. */
1478
- function writeRequirement(policy, requires) {
1479
- if (requires === void 0) return {};
1480
- const leaves = listPermissions(policy.permissions);
1481
- const keys = requires === "manageRoles" ? leaves.filter((leaf) => leaf.meta.manageRoles === true).map((leaf) => leaf.key) : [...requires];
1482
- if (keys.length === 0) return { error: requires === "manageRoles" ? "rls.customRoleWrites.requires is 'manageRoles', but no permission declares meta.manageRoles" : "rls.customRoleWrites.requires names no permission" };
1483
- const known = new Set(leaves.map((leaf) => leaf.key));
1484
- const unknown = keys.filter((key) => !known.has(key));
1485
- if (unknown.length > 0) return { error: `rls.customRoleWrites.requires names undeclared permissions: ${unknown.join(", ")}` };
1486
- return { keys: [...new Set(keys)].toSorted() };
1487
- }
1488
- /** Declared role names, and those a tenant admin may compose into custom roles. */
1489
- function customRoleNames(policy, requires) {
1490
- const declared = [...roleNames(policy)].toSorted();
1491
- const assignable = declared.filter((name) => policy.rolesByName.get(name)?.assignable ?? listRoles(policy.vocabulary.roles).some((leaf) => leaf.key === name && leaf.assignable));
1492
- const renamed = renamedKeys(policy.vocabulary.permissions);
1493
- const leaves = listPermissions(policy.permissions);
1494
- const manage = leaves.filter((leaf) => leaf.meta.manageRoles === true).map((leaf) => leaf.key);
1495
- return {
1496
- declared,
1497
- assignable,
1498
- ...renamed.size === 0 ? {} : { renamed: Object.fromEntries(renamed) },
1499
- permissions: leaves.map((leaf) => leaf.key).toSorted(),
1500
- ...manage.length === 0 ? {} : { manage: manage.toSorted() },
1501
- ...requires === void 0 ? {} : { requires },
1502
- ...policy.levels === void 0 ? {} : { levels: true }
1503
- };
1504
- }
1505
- function moveUndiffed(sql, label, part) {
1506
- const { kept, moved } = splitUndiffed(sql);
1507
- const [first = "", ...rest] = kept.split("\n");
1508
- return {
1509
- sql: [
1510
- first,
1511
- `-- what supabase db diff drops from this part is in ${label}`,
1512
- ...rest
1513
- ].join("\n"),
1514
- grants: `-- the privileges and view options supabase db diff drops from the ${part} part\n${moved.join("\n")}`
1515
- };
1498
+ //#region src/cli/rls/triggers.ts
1499
+ const PREFIX = "PermDock CLI";
1500
+ const EVENTS = /* @__PURE__ */ new Set([
1501
+ "insert",
1502
+ "update",
1503
+ "delete",
1504
+ "truncate"
1505
+ ]);
1506
+ const AUDITED = [
1507
+ CUSTOM_ROLES.permissions,
1508
+ CUSTOM_ROLES.includes,
1509
+ "user_roles"
1510
+ ];
1511
+ const IDENT = /^[a-z_][a-z0-9_$]*$/u;
1512
+ /** The tables `sql` creates in `schema`, by name without the schema. */
1513
+ function generatedTables(sql, schema) {
1514
+ const prefix = `create table if not exists ${quoteIdent$1(schema)}.`;
1515
+ const found = /* @__PURE__ */ new Set();
1516
+ for (const line of sql.split("\n")) {
1517
+ if (!line.startsWith(prefix)) continue;
1518
+ const name = /^"?([A-Za-z_][\w$]*)"?/u.exec(line.slice(prefix.length))?.[1];
1519
+ if (name !== void 0) found.add(name);
1520
+ }
1521
+ return [...found].toSorted();
1522
+ }
1523
+ function functionName(name, label) {
1524
+ if (typeof name !== "string" || name.split(".").length !== 2) throw configError(`${PREFIX}: ${label} must be a schema-qualified function such as audit.log_change, got ${JSON.stringify(name)}`);
1525
+ return quoteTable(name);
1526
+ }
1527
+ function knownTable(table, tables, label) {
1528
+ if (!tables.includes(table)) throw configError(`${PREFIX}: ${label} names ${table}, which rls generate does not create with this config; it creates ${tables.join(", ")}`);
1529
+ }
1530
+ function triggerSql(table, trigger, label) {
1531
+ if (typeof trigger.name !== "string" || trigger.name === "" || trigger.name.toLowerCase().startsWith("permdock_")) throw configError(`${PREFIX}: ${label}.name must be a name outside permdock_, which generated triggers use, got ${JSON.stringify(trigger.name)}`);
1532
+ if (trigger.when !== "before" && trigger.when !== "after") throw configError(`${PREFIX}: ${label}.when must be before or after`);
1533
+ const events = trigger.events;
1534
+ if (events.length === 0 || !events.every((event) => EVENTS.has(event))) throw configError(`${PREFIX}: ${label}.events must list insert, update, delete or truncate`);
1535
+ const level = trigger.level ?? "row";
1536
+ if (level !== "row" && level !== "statement") throw configError(`${PREFIX}: ${label}.level must be row or statement`);
1537
+ if (level === "row" && events.includes("truncate")) throw configError(`${PREFIX}: ${label} fires on truncate, which Postgres allows only with level: 'statement'`);
1538
+ const fn = functionName(trigger.function, `${label}.function`);
1539
+ const args = (trigger.args ?? []).map((arg) => quoteLiteral(arg));
1540
+ const name = quoteIdent$1(trigger.name);
1541
+ return `drop trigger if exists ${name} on ${table};
1542
+ create trigger ${name}
1543
+ ${trigger.when} ${[...new Set(events)].join(" or ")} on ${table}
1544
+ for each ${level} execute function ${fn}(${args.join(", ")});`;
1516
1545
  }
1517
1546
  /**
1518
- * The row columns that belong to a table. With `rls.tables` set, a resource
1519
- * it does not name is a table only when generated policies are written for
1520
- * it; with `--helpers-only` it has none, so it gets no index.
1547
+ * `rls.triggers`: the application's triggers on generated tables, after the
1548
+ * tables. A table this run does not create is an error, so a trigger never
1549
+ * silently goes missing.
1521
1550
  */
1522
- function tableColumns(rowColumns, tables, helpersOnly, warnings) {
1523
- if (tables === void 0) return rowColumns;
1524
- const unmapped = [...new Set(rowColumns.map((item) => item.resource).filter((resource) => !Object.hasOwn(tables, resource)))].toSorted();
1525
- if (unmapped.length === 0) return rowColumns;
1526
- if (helpersOnly) {
1527
- warnings.push(`no index is suggested for resources without an rls.tables entry: ${unmapped.join(", ")}; map each one that is a table`);
1528
- return rowColumns.filter((item) => Object.hasOwn(tables, item.resource));
1551
+ function triggersSql(triggers, schema, tables) {
1552
+ if (triggers === void 0) return "";
1553
+ const chunks = [];
1554
+ for (const [table, list] of Object.entries(triggers).toSorted(byKey)) {
1555
+ knownTable(table, tables, `rls.triggers.${table}`);
1556
+ const target = `${quoteIdent$1(schema)}.${quoteIdent$1(table)}`;
1557
+ for (const [index, trigger] of list.entries()) chunks.push(triggerSql(target, trigger, `rls.triggers.${table}[${String(index)}]`));
1529
1558
  }
1530
- warnings.push(`policies for resources without an rls.tables entry target a table named after the resource: ${unmapped.map((resource) => qualifiedTable$1(resource)).join(", ")}; map each one to its table`);
1531
- return rowColumns;
1559
+ return chunks.length === 0 ? "" : `-- rls.triggers: triggers the application owns on generated tables\n${chunks.join("\n")}\n`;
1532
1560
  }
1533
- async function runRlsGenerate(input) {
1534
- const rls = input.config.rls;
1535
- const fieldsMode = input.fields ?? rls?.fields;
1536
- if (fieldsMode !== void 0 && fieldsMode !== "views") return {
1537
- code: 2,
1538
- output: `rls generate --fields must be views (got ${fieldsMode})`,
1539
- text: ""
1540
- };
1541
- const revokeColumns = input.revokeColumns === true || rls?.revokeColumns === true;
1542
- if (revokeColumns && fieldsMode === void 0) return {
1543
- code: 2,
1544
- output: "rls generate --revoke-columns needs --fields views",
1545
- text: ""
1546
- };
1547
- const policy = await loadPolicy(input.cwd, input.config, input.from);
1548
- const memberships = parseMembershipsFlag(input.memberships) ?? rls?.memberships;
1549
- if (input.rbac && input.dialect !== "supabase") return {
1550
- code: 2,
1551
- output: `rls generate --rbac supabase needs --dialect supabase (got ${input.dialect})`,
1552
- text: ""
1553
- };
1554
- const schema = input.rbacSchema ?? rls?.schema ?? rls?.rbac?.schema ?? "permdock";
1555
- const authorize = resolveAuthorize(input.config, {
1556
- authorize: input.authorize,
1557
- rbac: input.rbac,
1558
- memberships
1559
- });
1560
- const memberSources = rls?.membershipSources ?? input.config.supabase?.hook?.memberships;
1561
- const sources = authorize === "database" ? memberSources : void 0;
1562
- const scopes = scopeList(policy.scopes);
1563
- const suspension = checkSuspension(rls?.suspension, scopes);
1564
- const hookRoles = input.config.supabase?.hook?.roles;
1565
- const roles = rls?.roles ?? (hookRoles === false ? void 0 : hookRoles);
1566
- if (roles !== void 0 && input.rbac) return {
1567
- code: 2,
1568
- output: "rls generate --rbac supabase creates its own user_roles (user_id, role app_role); drop rls.roles (or supabase.hook.roles) or the --rbac flag",
1569
- text: ""
1570
- };
1571
- const requires = writeRequirement(policy, rls?.customRoleWrites?.requires);
1572
- if (requires.error !== void 0) return {
1573
- code: 2,
1574
- output: requires.error,
1575
- text: ""
1576
- };
1577
- const readers = rls?.trustedReaders;
1578
- if (readers !== void 0 && (!Array.isArray(readers) || !readers.every((name) => typeof name === "string" && name !== ""))) return {
1579
- code: 2,
1580
- output: "rls.trustedReaders must list Postgres role names, such as ['service_role']",
1581
- text: ""
1582
- };
1583
- const ownership = ownershipRules(policy, scopes);
1584
- const graph = graphPlan(policy);
1585
- const apiKeys = apiKeysPlan(rls?.apiKeys, new Set(roleNames(policy)), Object.fromEntries(renamedKeys(policy.vocabulary.permissions)));
1586
- const ctx = {
1587
- dialect: input.dialect,
1588
- scopes,
1589
- tenantClaim: rls?.tenantClaim ?? "tenant_id",
1590
- ...rls?.tenants === "all" ? { tenants: "all" } : {},
1591
- gucPrefix: input.gucPrefix ?? rls?.gucPrefix ?? "app",
1592
- ...rls?.actions === void 0 ? {} : { actions: rls.actions },
1593
- inlineFunctions: input.inlineFunctions || rls?.inlineFunctions === true,
1594
- schema,
1595
- authorize,
1596
- roleClaim: rls?.roleClaim ?? "user_role",
1597
- tenantType: input.tenantType ?? rls?.tenantType ?? "uuid",
1598
- ...rls?.teamType === void 0 ? {} : { teamType: rls.teamType },
1599
- ...rls?.scopeTypes === void 0 ? {} : { scopeTypes: rls.scopeTypes },
1600
- ...memberships === void 0 ? {} : { memberships },
1601
- ...sources === void 0 || sources.length === 0 ? {} : { sources },
1602
- ...memberSources === void 0 || memberSources.length === 0 ? {} : { memberSources },
1603
- ...suspension === void 0 ? {} : { suspension },
1604
- ...roles === void 0 ? {} : { roles },
1605
- ...input.customRoles === true || rls?.customRoles === true ? { customRoles: {
1606
- ...customRoleNames(policy, requires.keys),
1607
- ...rls?.customRoleWrites?.roles === void 0 ? {} : { table: rls.customRoleWrites.roles }
1608
- } } : {},
1609
- ...input.capabilities === true || rls?.capabilities === true ? { capabilities: true } : {},
1610
- ...apiKeys === void 0 ? {} : { apiKeys },
1611
- ...rls?.anonymousSignIns === "deny" ? { anonymousSignIns: "deny" } : {},
1612
- ...ownership === void 0 ? {} : { ownership },
1613
- ...rls?.ownershipTriggers === void 0 ? {} : { skipOwnershipTriggers: rls.ownershipTriggers === false ? "all" : Object.keys(rls.ownershipTriggers).map((key) => {
1614
- const name = resolveScope(scopes, key);
1615
- if (name === void 0) throw new Error(`PermDock CLI: rls.ownershipTriggers.${key} names a scope the policy does not declare`);
1616
- return name;
1617
- }) },
1618
- ...rls?.assignments === void 0 ? {} : { assignments: rls.assignments === true ? { tables: [] } : {
1619
- tables: rls.assignments.tables ?? [],
1620
- ...rls.assignments.ownRole === void 0 ? {} : { ownRole: rls.assignments.ownRole }
1621
- } },
1622
- ...fieldsMode === void 0 ? {} : { fields: fieldsMode },
1623
- ...graph.size === 0 ? {} : { graph: {
1624
- closures: closureDepths(graph),
1625
- resources: policy.resources,
1626
- ...rls?.tables === void 0 ? {} : { tables: rls.tables }
1627
- } }
1628
- };
1629
- if (ctx.anonymousSignIns === "deny" && ctx.dialect !== "supabase") return {
1630
- code: 2,
1631
- output: `rls.anonymousSignIns needs --dialect supabase (got ${ctx.dialect})`,
1632
- text: ""
1633
- };
1634
- const jsonSchema = rls?.jsonSchema;
1635
- if (jsonSchema !== void 0 && jsonSchema !== "auto" && typeof jsonSchema !== "boolean") return {
1636
- code: 2,
1637
- output: `rls.jsonSchema must be 'auto', true or false (got ${JSON.stringify(jsonSchema)})`,
1638
- text: ""
1639
- };
1640
- const warnings = [];
1641
- if (jsonSchema !== void 0 && jsonSchema !== false && (rls?.approvals === void 0 || rls.approvals === false)) warnings.push("rls.jsonSchema constrains the approval store's body, which needs rls.approvals: true; nothing else is jsonb");
1642
- if (authorize === "database") {
1643
- for (const { name } of ctx.scopes) if (policy.grants.some((grant) => grant.scope === name) && scopeTable(ctx, name) === void 0 && scopeSources(ctx, name).length === 0) warnings.push(`${name}-scoped grants read the ${name} memberships table in database mode: set rls.memberships.scopes.${name}, rls.membershipSources or supabase.hook.memberships (or --memberships for the first scope), otherwise they deny`);
1644
- }
1645
- const compiled = compileGrants(policy, ctx, rls?.tables, warnings, input.skipClosures, rls?.rowHelpers !== void 0 || inheritTargets(policy).length > 0);
1646
- const helpersOnly = input.helpersOnly === true || rls?.helpersOnly === true;
1647
- if (helpersOnly && (input.target !== "sql" || revokeColumns)) return {
1648
- code: 2,
1649
- output: "rls generate --helpers-only needs --target sql and no --revoke-columns: the table grants are hand-written, so revoke the restricted columns there",
1650
- text: ""
1651
- };
1652
- const views = fieldsMode === void 0 ? [] : fieldViews(policy, ctx, compiled.branches, {
1653
- revokeColumns,
1654
- helpersOnly
1655
- }, warnings);
1656
- const force = input.force === true || rls?.force === true;
1657
- if (force && views.some((view) => view.companion !== void 0)) warnings.push("--force with --revoke-columns: each <table>_visible_fields companion reads as its owner, so its owner needs BYPASSRLS or the restricted columns read as null");
1658
- const policyName = input.policyName ?? rls?.policyName;
1659
- const assembled = helpersOnly ? [] : assemblePolicies(fieldsMode === void 0 ? compiled.branches : rowBranches(compiled.branches), {
1660
- perRole: input.policyPerRole === true || rls?.policyPerRole === true,
1661
- ...policyName === void 0 ? {} : { name: policyName }
1561
+ /**
1562
+ * `rls.audit`: one call of the audit function per generated table, which
1563
+ * registers the table with the application's audit module. Statements that
1564
+ * run code, so they travel with the seeds.
1565
+ */
1566
+ function auditSql(audit, schema, tables) {
1567
+ if (audit === void 0) return "";
1568
+ const fn = functionName(audit.function, "rls.audit.function");
1569
+ const listed = audit.tables;
1570
+ if (listed !== void 0) for (const table of listed) knownTable(table, tables, "rls.audit.tables");
1571
+ const targets = listed ?? AUDITED.filter((table) => tables.includes(table));
1572
+ const args = Object.entries(audit.args ?? {}).map(([name, value]) => {
1573
+ if (!IDENT.test(name)) throw configError(`${PREFIX}: rls.audit.args names ${JSON.stringify(name)}, which is not a lower-case parameter name`);
1574
+ return `, ${quoteIdent$1(name)} => ${quoteLiteral(value)}`;
1662
1575
  });
1663
- const actorKinds = readOnlyActorKinds(rls?.readOnlyActors);
1664
- if (actorKinds !== void 0 && helpersOnly) warnings.push("rls.readOnlyActors adds policies, and --helpers-only writes none: add the restrictive read-only policies to the hand-written ones");
1665
- const policies = actorKinds === void 0 ? assembled : [...assembled, ...readOnlyActorPolicies(assembled, subjectClaimJsonSql(ctx, "act"), actorKinds)];
1666
- let tablePolicies;
1667
- try {
1668
- tablePolicies = realtimeStoragePolicies(ctx, {
1669
- scopes: new Set(ctx.scopes.map((scope) => scope.name)),
1670
- keys: new Set(listPermissions(policy.vocabulary.permissions).map((leaf) => leaf.key)),
1671
- rowConditions: new Set(policyRowConditionKeys(policy))
1672
- }, compact({
1673
- realtime: rls?.realtime,
1674
- storage: rls?.storage
1675
- }), warnings);
1676
- } catch (cause) {
1677
- return {
1678
- code: 2,
1679
- output: describeError(cause),
1680
- text: ""
1681
- };
1576
+ if (targets.length === 0) return "";
1577
+ return `-- rls.audit: generated tables registered with ${audit.function}
1578
+ ${targets.map((table) => `select ${fn}(${quoteLiteral(`${quoteIdent$1(schema)}.${quoteIdent$1(table)}`)}::regclass${args.join("")});`).join("\n")}
1579
+ `;
1580
+ }
1581
+ //#endregion
1582
+ //#region src/cli/rls/generate-sql.ts
1583
+ /** The checks that need the SQL context: `--backfill-out`, `rls.anonymousSignIns`, `rls.jsonSchema`. */
1584
+ function preflight(input, { ctx }) {
1585
+ const rls = input.config.rls;
1586
+ const from = typeof rls?.customRoles === "object" ? rls.customRoles.from : void 0;
1587
+ if (input.backfillOut !== void 0 && from === void 0) return { refusal: "rls generate --backfill-out needs rls.customRoles.from in permdock.config.ts" };
1588
+ let backfill;
1589
+ if (from !== void 0) try {
1590
+ backfill = backfillSql(ctx, from, rls?.migrate);
1591
+ } catch (cause) {
1592
+ return { refusal: describeError(cause) };
1682
1593
  }
1683
- if (tablePolicies.length > 0 && helpersOnly) warnings.push("rls.realtime and rls.storage add policies, and --helpers-only writes none: no realtime.messages or storage.objects policy is written");
1684
- const servicePolicies = tablePolicies.length === 0 || helpersOnly ? [] : [...tablePolicies, ...actorKinds === void 0 ? [] : readOnlyActorPolicies(tablePolicies, subjectClaimJsonSql(ctx, "act"), actorKinds)];
1685
- const helperTables = servicePolicies.length === 0 ? void 0 : [SERVICE_TABLES_HEADER, ...servicePolicies.map(policySql)].join("\n");
1686
- const guardedTables = servicePolicies.length === 0 ? void 0 : [`${SERVICE_TABLES_HEADER} A stack without the service has no table, so each policy is skipped there.`, ...servicePolicies.map(guardedPolicySql)].join("\n");
1594
+ if (ctx.anonymousSignIns === "deny" && ctx.dialect !== "supabase") return { refusal: `rls.anonymousSignIns needs --dialect supabase (got ${ctx.dialect})` };
1595
+ const jsonSchema = rls?.jsonSchema;
1596
+ if (jsonSchema !== void 0 && jsonSchema !== "auto" && typeof jsonSchema !== "boolean") return { refusal: `rls.jsonSchema must be 'auto', true or false (got ${JSON.stringify(jsonSchema)})` };
1597
+ return {
1598
+ backfill,
1599
+ jsonSchema
1600
+ };
1601
+ }
1602
+ function rbacPart(input, policy, { ctx, schema, authorize }, warnings) {
1687
1603
  const rootMapped = ctx.scopes[0] === void 0 ? void 0 : scopeTable(ctx, ctx.scopes[0].name);
1688
1604
  const rootTable = rootMapped === void 0 ? void 0 : {
1689
1605
  ...rootMapped.table,
@@ -1702,51 +1618,67 @@ async function runRlsGenerate(input) {
1702
1618
  }) : void 0;
1703
1619
  if (rbac !== void 0) warnings.push(...rbac.warnings);
1704
1620
  if (input.rbac) warnings.push("the token hook that writes user_role and memberships comes from permdock supabase hook generate");
1621
+ return rbac;
1622
+ }
1623
+ /** The ownership, approval store, graph, row-helper and break-glass sections. */
1624
+ function sectionsSql(input, policy, options, built, generated, jsonSchema, warnings) {
1625
+ const rls = input.config.rls;
1626
+ const { ctx, graph, ownership } = built;
1627
+ const { compiled } = generated;
1705
1628
  const owned = [ownershipSql(ctx), assignmentSql(ctx)].filter((part) => part !== "").join("\n");
1706
1629
  if (ctx.assignments !== void 0 && (ownership === void 0 || ownership.assigns.length === 0)) warnings.push("rls.assignments needs a role that declares assigns: no assignment trigger is written");
1707
1630
  let approvals;
1708
1631
  try {
1709
- approvals = rls?.approvals === true ? approvalStoreSql(ctx, jsonSchema ?? false) : typeof rls?.approvals === "object" ? adoptedApprovalStoreSql(ctx, rls.approvals, jsonSchema ?? false) : void 0;
1632
+ approvals = approvalsSql(ctx, rls?.approvals, jsonSchema ?? false);
1710
1633
  } catch (cause) {
1711
- return {
1712
- code: 2,
1713
- output: describeError(cause),
1714
- text: ""
1715
- };
1634
+ return { refusal: describeError(cause) };
1716
1635
  }
1717
1636
  const graphed = graphSql(ctx, graph, rls?.tables);
1718
1637
  let rowHelpers = "";
1719
1638
  if (rls?.rowHelpers !== void 0 || inheritTargets(policy).length > 0) try {
1720
- rowHelpers = rowHelpersSql(ctx, policy, [...fieldsMode === void 0 ? compiled.branches : rowBranches(compiled.branches), ...compiled.actionBranches], rls?.rowHelpers ?? [], rls?.tables, warnings);
1639
+ rowHelpers = rowHelpersSql(ctx, policy, [...options.fieldsMode === void 0 ? compiled.branches : rowBranches(compiled.branches), ...compiled.actionBranches], rls?.rowHelpers ?? [], rls?.tables, warnings);
1721
1640
  } catch (cause) {
1722
- return {
1723
- code: 2,
1724
- output: describeError(cause),
1725
- text: ""
1726
- };
1641
+ return { refusal: describeError(cause) };
1727
1642
  }
1728
1643
  const breakGlass = breakGlassSql(ctx, breakGlassEntries(policy, rls?.tables));
1729
- const configured = rls?.shims;
1730
- const shimsConfig = typeof configured === "object" ? configured : configured === true || input.shims === true ? {} : void 0;
1731
- if (shimsConfig !== void 0 && rls?.migrate === void 0) return {
1732
- code: 2,
1733
- output: "PermDock CLI: rls generate --shims needs rls.migrate.helpers in permdock.config.ts",
1734
- text: ""
1735
- };
1736
- const renamed = Object.fromEntries(renamedKeys(policy.vocabulary.permissions));
1737
- const grants = shimGrants(compiled.rolePermissions, compiled.conditionedKeys, renamed);
1738
- const anonExecute = rls?.anonExecute === true || views.some((view) => view.roles.includes("anon")) || compiled.branches.some((branch) => branch.roles.includes("anon") && [
1644
+ return [
1645
+ approvals,
1646
+ owned === "" ? void 0 : owned,
1647
+ graphed === "" ? void 0 : graphed,
1648
+ breakGlass === "" ? void 0 : breakGlass,
1649
+ rowHelpers === "" ? void 0 : rowHelpers
1650
+ ];
1651
+ }
1652
+ /** Whether `anon` calls a helper: `rls.anonExecute`, an anon field view or an anon branch reading the user id. */
1653
+ function anonExecutes(input, { compiled, views }) {
1654
+ return input.config.rls?.anonExecute === true || views.some((view) => view.roles.includes("anon")) || compiled.branches.some((branch) => branch.roles.includes("anon") && [
1739
1655
  branch.access,
1740
1656
  branch.using,
1741
1657
  branch.check
1742
1658
  ].some((sql) => sql?.includes("permdock_user_id") === true));
1659
+ }
1660
+ /** Everything before the table policies: helpers, sections, shims, scaffold and triggers. */
1661
+ function preambleSql(input, policy, options, built, generated, jsonSchema, warnings) {
1662
+ const rls = input.config.rls;
1663
+ const { ctx } = built;
1664
+ const { compiled } = generated;
1665
+ const rbac = rbacPart(input, policy, built, warnings);
1666
+ const sections = sectionsSql(input, policy, options, built, generated, jsonSchema, warnings);
1667
+ if (isRefusal(sections)) return sections;
1668
+ const configured = rls?.shims;
1669
+ const shimsConfig = typeof configured === "object" ? configured : configured === true || input.shims === true ? {} : void 0;
1670
+ if (shimsConfig !== void 0 && rls?.migrate === void 0) return { refusal: "PermDock CLI: rls generate --shims needs rls.migrate.helpers in permdock.config.ts" };
1671
+ const renamed = Object.fromEntries(renamedKeys(policy.vocabulary.permissions));
1672
+ const grants = shimGrants(compiled.rolePermissions, compiled.conditionedKeys, renamed);
1673
+ const anonExecute = anonExecutes(input, generated);
1674
+ const actorKinds = readOnlyActorKinds(rls?.readOnlyActors);
1743
1675
  const usesLiveSession = compiled.branches.some((branch) => [
1744
1676
  branch.access,
1745
1677
  branch.using,
1746
1678
  branch.check
1747
1679
  ].some((sql) => sql?.includes(SESSION_LIVE_HELPER) === true));
1748
1680
  const shims = shimsConfig === void 0 || rls?.migrate === void 0 ? void 0 : shimsSql(ctx, rls.migrate, shimsConfig, renamed, grants);
1749
- const preamble = [
1681
+ const core = [
1750
1682
  rbac?.head,
1751
1683
  helpersSql(ctx, compiled.rolePermissions, {
1752
1684
  levelReach: compiled.levelReach,
@@ -1757,19 +1689,59 @@ async function runRlsGenerate(input) {
1757
1689
  ...rls?.trustedReaders === void 0 ? {} : { trustedReaders: rls.trustedReaders }
1758
1690
  }),
1759
1691
  permissionHelpersSql(ctx, grants, renamed, anonExecute),
1692
+ permissionHoldersSql(ctx) || void 0,
1693
+ actorKinds === void 0 ? void 0 : actorMayWriteSql(ctx, actorKinds),
1760
1694
  usesLiveSession ? sessionLiveHelperSql(ctx) : void 0,
1761
- approvals,
1762
- owned === "" ? void 0 : owned,
1763
- graphed === "" ? void 0 : graphed,
1764
- breakGlass === "" ? void 0 : breakGlass,
1765
- rowHelpers === "" ? void 0 : rowHelpers,
1695
+ ...sections,
1766
1696
  shims,
1767
1697
  rbac?.tail
1768
1698
  ].filter((part) => part !== void 0).join("\n");
1769
- if (force) for (const entry of graph.values()) {
1699
+ const created = generatedTables(core, built.schema);
1700
+ let ownTriggers;
1701
+ let audit;
1702
+ try {
1703
+ ownTriggers = triggersSql(rls?.triggers, built.schema, created);
1704
+ audit = auditSql(rls?.audit, built.schema, created);
1705
+ } catch (cause) {
1706
+ return { refusal: describeError(cause) };
1707
+ }
1708
+ const seedsSplit = splitsPart(input.split, "seeds");
1709
+ return {
1710
+ preamble: [
1711
+ core,
1712
+ ownTriggers,
1713
+ seedsSplit ? "" : audit
1714
+ ].filter((part) => part !== "").join("\n"),
1715
+ audit
1716
+ };
1717
+ }
1718
+ function forceWarnings({ graph }, warnings) {
1719
+ for (const entry of graph.values()) {
1770
1720
  const own = [...entry.relations].filter((name) => !("edge" in (entry.node.relations[name] ?? {})));
1771
1721
  if (own.length > 0) warnings.push(`--force: ${entry.node.name} relations ${own.join(", ")} are read from the ${entry.node.name} table itself, so ${graphHelper(entry.node.name)} would recurse through its own policy; keep ${entry.node.name} unforced or move them to edge tables`);
1772
1722
  }
1723
+ }
1724
+ /**
1725
+ * The row columns that belong to a table. With `rls.tables` set, a resource
1726
+ * it does not name is a table only when generated policies are written for
1727
+ * it; with `--helpers-only` it has none, so it gets no index.
1728
+ */
1729
+ function tableColumns(rowColumns, tables, helpersOnly, warnings) {
1730
+ if (tables === void 0) return rowColumns;
1731
+ const unmapped = [...new Set(rowColumns.map((item) => item.resource).filter((resource) => !Object.hasOwn(tables, resource)))].toSorted();
1732
+ if (unmapped.length === 0) return rowColumns;
1733
+ if (helpersOnly) {
1734
+ warnings.push(`no index is suggested for resources without an rls.tables entry: ${unmapped.join(", ")}; map each one that is a table`);
1735
+ return rowColumns.filter((item) => Object.hasOwn(tables, item.resource));
1736
+ }
1737
+ warnings.push(`policies for resources without an rls.tables entry target a table named after the resource: ${unmapped.map((resource) => qualifySqlTable(resource)).join(", ")}; map each one to its table`);
1738
+ return rowColumns;
1739
+ }
1740
+ /** The `--target` file, and for Drizzle and Prisma the SQL migration beside it. */
1741
+ function emitTarget(input, built, generated, preamble) {
1742
+ const rls = input.config.rls;
1743
+ const { ctx } = built;
1744
+ const { policies, force, views, helperTables } = generated;
1773
1745
  const outRel = input.out ?? rls?.out ?? defaultOut(input.target);
1774
1746
  const migration = input.target === "sql" ? "" : dialectRoles(migrationSql(policies, withHelperTables(preamble, helperTables), force, views), ctx.dialect);
1775
1747
  const migrationRel = migration === "" ? void 0 : migrationOut(outRel);
@@ -1795,14 +1767,130 @@ async function runRlsGenerate(input) {
1795
1767
  break;
1796
1768
  default: return input.target;
1797
1769
  }
1798
- let indexes = indexTargets(ctx, tableColumns(compiled.rowColumns, rls?.tables, helpersOnly, warnings));
1770
+ return {
1771
+ text,
1772
+ migration,
1773
+ migrationRel,
1774
+ outRel
1775
+ };
1776
+ }
1777
+ /** The generated SQL for one run, before `--db-url-stdin` prunes the index targets. */
1778
+ function generateSql(input, policy, options, built, warnings) {
1779
+ const rls = input.config.rls;
1780
+ const { ctx } = built;
1781
+ const checked = preflight(input, built);
1782
+ if (isRefusal(checked)) return checked;
1783
+ const { jsonSchema } = checked;
1784
+ if (jsonSchema !== void 0 && jsonSchema !== false && (rls?.approvals === void 0 || rls.approvals === false)) warnings.push("rls.jsonSchema constrains the approval store's body, which needs rls.approvals: true; nothing else is jsonb");
1785
+ const generated = generatePolicies(input, policy, options, built, warnings);
1786
+ if (isRefusal(generated)) return generated;
1787
+ const assembled = preambleSql(input, policy, options, built, generated, jsonSchema, warnings);
1788
+ if (isRefusal(assembled)) return assembled;
1789
+ const { preamble, audit } = assembled;
1790
+ if (generated.force) forceWarnings(built, warnings);
1791
+ const emitted = emitTarget(input, built, generated, preamble);
1792
+ const { policies, force, views, helperTables, guardedTables, helpersOnly } = generated;
1793
+ const seedsSplit = splitsPart(input.split, "seeds");
1794
+ return {
1795
+ ...generated,
1796
+ ...emitted,
1797
+ backfill: checked.backfill,
1798
+ indexes: indexTargets(ctx, tableColumns(generated.compiled.rowColumns, rls?.tables, helpersOnly, warnings)),
1799
+ parts: (indexes) => ({
1800
+ policies: dialectRoles(withHelperTables(emitSql(policies, "", force, views), helperTables), ctx.dialect),
1801
+ declarativePolicies: dialectRoles(emitSql(policies, "", force, views), ctx.dialect),
1802
+ ...guardedTables === void 0 ? {} : { servicePolicies: dialectRoles(guardedTables, ctx.dialect) },
1803
+ helpers: dialectRoles(emitSql([], preamble, false, helpersOnly ? views : []), ctx.dialect),
1804
+ seeds: `${SEEDS_MARKER} schema=${built.schema}\n${seedSql(ctx, generated.compiled.rolePermissions)}\n${seedsSplit ? audit : ""}`,
1805
+ indexes: `${INDEXES_MARKER}\n${indexesSql(indexes)}\n`
1806
+ })
1807
+ };
1808
+ }
1809
+ //#endregion
1810
+ //#region src/cli/rls/generate.ts
1811
+ function loadPolicy(cwd, config, from) {
1812
+ const policyPath = from === "drizzle" ? config.policy : from ?? config.policy;
1813
+ return requirePolicy(cwd, policyPath, "rls generate needs policy in permdock.config.ts or --policy");
1814
+ }
1815
+ const PROJECT_COMMAND = "permdock rls generate";
1816
+ /**
1817
+ * The policy `policyFromCatalog` rebuilds from the project's pushed catalog
1818
+ * and policy; a project with no pushed policy fails with `policy-not-published`.
1819
+ */
1820
+ async function projectPolicyOf(input, project) {
1821
+ const fetched = await fetchProjectPolicy({
1822
+ command: PROJECT_COMMAND,
1823
+ url: input.url,
1824
+ project,
1825
+ json: input.json === true,
1826
+ env: input.io.env ?? process.env,
1827
+ io: input.io
1828
+ });
1829
+ if (!fetched.ok) return {
1830
+ ...fetched.result,
1831
+ text: ""
1832
+ };
1833
+ const { catalog, policy } = fetched.value;
1834
+ if (policy === void 0) {
1835
+ const detail = `project ${project} has a pushed catalog but no policy; push from a project whose permdock.config.ts names one`;
1836
+ return {
1837
+ code: 1,
1838
+ output: input.json === true ? JSON.stringify({
1839
+ type: POLICY_NOT_PUBLISHED,
1840
+ title: "Policy not published",
1841
+ detail,
1842
+ command: PROJECT_COMMAND,
1843
+ exitCode: 1
1844
+ }, null, 2) : `${PROJECT_COMMAND}: ${detail}`,
1845
+ text: ""
1846
+ };
1847
+ }
1848
+ try {
1849
+ return policyFromCatalog(catalog, policy);
1850
+ } catch (cause) {
1851
+ return {
1852
+ code: 1,
1853
+ output: `${PROJECT_COMMAND}: the pushed policy does not rebuild: ${describeError(cause)}`,
1854
+ text: ""
1855
+ };
1856
+ }
1857
+ }
1858
+ function isOutcome(value) {
1859
+ return "text" in value && "code" in value;
1860
+ }
1861
+ function refused(output) {
1862
+ return {
1863
+ code: 2,
1864
+ output,
1865
+ text: ""
1866
+ };
1867
+ }
1868
+ async function runRlsGenerate(input) {
1869
+ const options = generateOptions(input);
1870
+ if (isRefusal(options)) return refused(options.refusal);
1871
+ if (input.project !== void 0 && input.from !== void 0 && input.from !== "drizzle") return refused("rls generate takes the policy from --project or from a --policy module, not both");
1872
+ let policy;
1873
+ if (input.project === void 0) policy = await loadPolicy(input.cwd, input.config, input.from);
1874
+ else {
1875
+ const loaded = await projectPolicyOf(input, input.project);
1876
+ if (isOutcome(loaded)) return loaded;
1877
+ policy = loaded;
1878
+ }
1879
+ const built = buildContext(input, policy, options);
1880
+ if (isRefusal(built)) return refused(built.refusal);
1881
+ const warnings = [];
1882
+ const generated = generateSql(input, policy, options, built, warnings);
1883
+ if (isRefusal(generated)) return refused(generated.refusal);
1884
+ const { ctx } = built;
1885
+ const { text, migration, migrationRel, helpersOnly } = generated;
1886
+ let indexes = generated.indexes;
1799
1887
  if (input.db !== void 0) try {
1800
1888
  const pruned = pruneIndexTargets(indexes, await readTableIndexFacts(input.db, [...new Set(indexes.map((target) => target.table))], input.connect));
1801
1889
  indexes = pruned.targets;
1802
1890
  warnings.push(...pruned.warnings);
1803
1891
  } catch (cause) {
1804
1892
  return {
1805
- code: 2,
1893
+ code: exitCodeOfError(cause),
1806
1894
  output: describeError(cause),
1807
1895
  text: ""
1808
1896
  };
@@ -1813,33 +1901,26 @@ async function runRlsGenerate(input) {
1813
1901
  code: 0,
1814
1902
  output: warnings.join("\n"),
1815
1903
  text,
1816
- policies,
1817
- seeds: compiled.rolePermissions,
1904
+ policies: generated.policies,
1905
+ seeds: generated.compiled.rolePermissions,
1818
1906
  indexes,
1819
1907
  keys: {
1820
1908
  permissions: listPermissions(policy.vocabulary.permissions).map((leaf) => leaf.key),
1821
1909
  rowConditions: [...policyRowConditionKeys(policy)],
1822
1910
  renamed: Object.fromEntries(renamedKeys(policy.vocabulary.permissions))
1823
1911
  },
1824
- schema,
1912
+ schema: built.schema,
1825
1913
  helpersOnly,
1826
1914
  ...extras
1827
1915
  };
1828
1916
  const planned = outputFiles({
1829
1917
  input,
1830
- outRel,
1918
+ outRel: generated.outRel,
1831
1919
  text,
1832
1920
  helpersOnly,
1833
- sql: () => ({
1834
- policies: dialectRoles(withHelperTables(emitSql(policies, "", force, views), helperTables), ctx.dialect),
1835
- declarativePolicies: dialectRoles(emitSql(policies, "", force, views), ctx.dialect),
1836
- ...guardedTables === void 0 ? {} : { servicePolicies: dialectRoles(guardedTables, ctx.dialect) },
1837
- helpers: dialectRoles(emitSql([], preamble, false, helpersOnly ? views : []), ctx.dialect),
1838
- seeds: `${SEEDS_MARKER} schema=${schema}\n${seedSql(ctx, compiled.rolePermissions)}\n`,
1839
- indexes: `${INDEXES_MARKER}\n${indexesSql(indexes)}\n`
1840
- }),
1841
- scopes,
1842
- schema
1921
+ sql: () => generated.parts(indexes),
1922
+ scopes: ctx.scopes,
1923
+ schema: built.schema
1843
1924
  });
1844
1925
  if (typeof planned === "string") return {
1845
1926
  code: 2,
@@ -1853,6 +1934,19 @@ async function runRlsGenerate(input) {
1853
1934
  rel: migrationRel,
1854
1935
  text: migration
1855
1936
  });
1937
+ if (input.backfillOut !== void 0 && generated.backfill !== void 0) {
1938
+ const file = dataMigrationFile({
1939
+ cwd: input.cwd,
1940
+ out: input.backfillOut,
1941
+ part: "backfill",
1942
+ marker: BACKFILL_MARKER,
1943
+ text: generated.backfill,
1944
+ now: input.io.now?.() ?? dateAt(),
1945
+ check: input.check,
1946
+ reserved: files.map((other) => other.rel)
1947
+ });
1948
+ if (file !== void 0) files.push(file);
1949
+ }
1856
1950
  if (input.check) {
1857
1951
  const drift = driftOf(input.cwd, files);
1858
1952
  return drift.length === 0 ? {
@@ -1867,150 +1961,18 @@ async function runRlsGenerate(input) {
1867
1961
  ...extras
1868
1962
  };
1869
1963
  }
1870
- const written = writeSqlFiles(input.cwd, files);
1871
- const extra = warnings.length === 0 ? "" : `\n${warnings.join("\n")}`;
1964
+ const written = writeOutputFiles(input.cwd, files);
1965
+ const notes = [...written.wrote.length === 0 ? [] : [`wrote ${written.wrote.join(", ")}`], ...warnings];
1966
+ if (written.printed !== "") for (const note of notes) input.io.stderr(note);
1872
1967
  return {
1873
1968
  code: 0,
1874
- output: `wrote ${written.wrote.join(", ")}${written.printed === "" ? "" : `\n${written.printed}`}${extra}`,
1969
+ output: written.printed === "" ? notes.join("\n") : written.printed,
1875
1970
  text,
1876
1971
  ...extras
1877
1972
  };
1878
1973
  }
1879
- /** Whether `--split` names `part`, which then leaves the helpers or the warnings. */
1880
- const SERVICE_TABLES_HEADER = "-- Supabase Realtime and Storage: rls.realtime and rls.storage. RLS on both tables is Supabase's.";
1881
- /**
1882
- * A `realtime.messages` or `storage.objects` policy that applies only where
1883
- * the table exists. The Realtime and Storage services create those tables, so
1884
- * a local stack with the service off, and pg-delta's shadow database, have
1885
- * none.
1886
- */
1887
- function guardedPolicySql(item) {
1888
- const body = policySql(item).trimEnd().split("\n").map((line) => ` ${line}`).join("\n");
1889
- return `do $permdock$
1890
- begin
1891
- if to_regclass(${quoteLiteral(qualifiedTable$1(item.table))}) is not null then
1892
- ${body}
1893
- end if;
1894
- end
1895
- $permdock$;
1896
- `;
1897
- }
1898
- /** Appends the `realtime.messages` and `storage.objects` policies, which never get `tableSql`. */
1899
- function withHelperTables(sql, helperTables) {
1900
- if (helperTables === void 0) return sql;
1901
- const tables = `${helperTables.trimEnd()}\n`;
1902
- return sql.trim() === "" ? tables : `${sql.trimEnd()}\n\n${tables}`;
1903
- }
1904
- function splitsPart(raw, part) {
1905
- const split = parseSplit(raw);
1906
- return typeof split !== "string" && split?.includes(part) === true;
1907
- }
1908
- /** The files `generate` writes: one, or one per `--split` part, plus the hook's grants. */
1909
- function outputFiles(plan) {
1910
- const { input } = plan;
1911
- const split = parseSplit(input.split);
1912
- if (typeof split === "string") return split;
1913
- if (split?.includes("policies") === true && plan.helpersOnly) return "rls generate --helpers-only writes no policies part; drop it from --split";
1914
- if (split === void 0) {
1915
- if (input.grantsOut !== void 0) return "rls generate --grants-out needs --split with the helpers or hook part";
1916
- if (input.seedsOut !== void 0) return "rls generate --seeds-out needs --split with the seeds part";
1917
- return [{
1918
- part: "rls",
1919
- rel: plan.outRel,
1920
- text: plan.text
1921
- }];
1922
- }
1923
- if (input.target !== "sql") return "rls generate --split needs --target sql";
1924
- const pgDelta = input.out === void 0 && input.config.rls?.out === void 0 ? supabaseConfig(input.cwd).pgDelta : void 0;
1925
- if (pgDelta === void 0 && !plan.outRel.includes("{part}")) return `rls generate --split needs {part} in --out, for example supabase/schemas/identity/056_permdock_{part}.sql (got ${plan.outRel})`;
1926
- if (pgDelta !== void 0 && split.includes("helpers") && !split.includes("seeds")) return "rls generate --split helpers under pg-delta needs the seeds part with --seeds-out: pg-delta rejects the role_permissions rows in a declarative file";
1927
- if (pgDelta !== void 0 && split.includes("seeds") && input.seedsOut === void 0) return "rls generate --split seeds under pg-delta needs --seeds-out: pg-delta does not diff the role_permissions rows, so they go in a migration";
1928
- const at = (part, schema = plan.schema) => pgDelta === void 0 ? partPath(plan.outRel, part) : pgDeltaPath(pgDelta.schemaDir, part, schema);
1929
- if (input.grantsOut !== void 0 && !split.includes("hook") && !split.includes("helpers")) return "rls generate --grants-out needs the helpers or hook part in --split";
1930
- if (input.seedsOut !== void 0 && !split.includes("seeds")) return "rls generate --seeds-out needs the seeds part in --split";
1931
- if (split.includes("hook") && input.config.supabase?.hook === void 0) return "rls generate --split hook needs supabase.hook in permdock.config.ts";
1932
- const sql = plan.sql();
1933
- const relocated = pgDelta === void 0 ? void 0 : sql.servicePolicies;
1934
- if (relocated !== void 0 && split.includes("policies") && !split.includes("seeds")) return "rls generate --split policies under pg-delta with rls.realtime or rls.storage needs the seeds part with --seeds-out: the shadow database has no realtime.messages or storage.objects, so their policies go in that migration";
1935
- const seeds = relocated === void 0 || !split.includes("policies") ? sql.seeds : `${sql.seeds}\n${relocated}`;
1936
- const files = [];
1937
- const label = grantsLabel(input.grantsOut);
1938
- let grantsMarker = `${GRANTS_MARKER} schema=${plan.schema}`;
1939
- const grants = [];
1940
- for (const part of split) switch (part) {
1941
- case "helpers": {
1942
- if (label === void 0) {
1943
- files.push({
1944
- part,
1945
- rel: at(part),
1946
- text: sql.helpers
1947
- });
1948
- break;
1949
- }
1950
- const moved = moveUndiffed(sql.helpers, label, part);
1951
- files.push({
1952
- part,
1953
- rel: at(part),
1954
- text: moved.sql
1955
- });
1956
- grants.push(moved.grants);
1957
- break;
1958
- }
1959
- case "seeds": {
1960
- const out = input.seedsOut;
1961
- if (out !== void 0 && isSeedsDirectory(input.cwd, out)) {
1962
- const planned = seedsMigration(input.cwd, out, SEEDS_MARKER, seeds, input.io.now?.() ?? /* @__PURE__ */ new Date());
1963
- if (!planned.current || input.check) files.push({
1964
- part,
1965
- rel: planned.rel,
1966
- text: seeds
1967
- });
1968
- break;
1969
- }
1970
- files.push({
1971
- part,
1972
- rel: out ?? at(part),
1973
- text: seeds
1974
- });
1975
- break;
1976
- }
1977
- case "indexes":
1978
- files.push({
1979
- part,
1980
- rel: at(part),
1981
- text: sql.indexes
1982
- });
1983
- break;
1984
- case "policies":
1985
- files.push({
1986
- part,
1987
- rel: at(part),
1988
- text: pgDelta === void 0 ? sql.policies : sql.declarativePolicies
1989
- });
1990
- break;
1991
- case "hook": {
1992
- const hook = supabaseHookSql(plan.scopes, input.config, {}, label);
1993
- files.push({
1994
- part,
1995
- rel: at(part, hook.manifest.hook.schema),
1996
- text: hook.sql
1997
- });
1998
- const [marker = grantsMarker, ...rest] = hook.grants.split("\n");
1999
- grantsMarker = marker;
2000
- grants.push(rest.join("\n").trimEnd());
2001
- break;
2002
- }
2003
- default: return part;
2004
- }
2005
- if (input.grantsOut !== void 0) files.push({
2006
- part: "grants",
2007
- rel: input.grantsOut,
2008
- text: `${[grantsMarker, ...grants].join("\n")}\n`
2009
- });
2010
- return files;
2011
- }
2012
1974
  //#endregion
2013
- //#region src/cli/rls-introspect.ts
1975
+ //#region src/cli/rls/introspect.ts
2014
1976
  const ROLES = ["anon", "authenticated"];
2015
1977
  /** Neon's `anonymous` is the role `rls generate` writes as `anon` for the other dialects. */
2016
1978
  function liveRole(role) {
@@ -2023,7 +1985,7 @@ const PRIVILEGES = [
2023
1985
  "delete"
2024
1986
  ];
2025
1987
  /** `posts` and `"app"."posts"` as `public.posts` and `app.posts`. */
2026
- function qualified(table) {
1988
+ function normalizeTableName(table) {
2027
1989
  const parts = table.split(".").map((part) => part.replaceAll("\"", ""));
2028
1990
  return parts.length === 1 ? `public.${parts[0] ?? ""}` : parts.join(".");
2029
1991
  }
@@ -2037,19 +1999,19 @@ function helpersOf(sql) {
2037
1999
  return [...found];
2038
2000
  }
2039
2001
  function expectedRls(policies, sql, indexes = []) {
2040
- const tables = [...new Set(policies.map((item) => qualified(item.table)))];
2002
+ const tables = [...new Set(policies.map((item) => normalizeTableName(item.table)))];
2041
2003
  const grants = {};
2042
2004
  for (const table of tables) {
2043
2005
  const byRole = {};
2044
2006
  for (const role of ROLES) {
2045
- const commands = new Set(policies.filter((item) => qualified(item.table) === table && item.effect === "allow" && item.roles.includes(role)).map((item) => item.command));
2007
+ const commands = new Set(policies.filter((item) => normalizeTableName(item.table) === table && item.effect === "allow" && item.roles.includes(role)).map((item) => item.command));
2046
2008
  byRole[role] = PRIVILEGES.filter((privilege) => commands.has(privilege));
2047
2009
  }
2048
2010
  grants[table] = byRole;
2049
2011
  }
2050
2012
  return {
2051
2013
  policies: policies.map((item) => ({
2052
- table: qualified(item.table),
2014
+ table: normalizeTableName(item.table),
2053
2015
  name: item.name,
2054
2016
  command: item.command,
2055
2017
  permissive: item.effect === "allow",
@@ -2060,7 +2022,7 @@ function expectedRls(policies, sql, indexes = []) {
2060
2022
  helpers: helpersOf(sql),
2061
2023
  indexes: indexes.map((target) => ({
2062
2024
  ...target,
2063
- table: qualified(target.table)
2025
+ table: normalizeTableName(target.table)
2064
2026
  }))
2065
2027
  };
2066
2028
  }
@@ -2195,6 +2157,29 @@ async function introspectRls(db, expected, connect = connectIntrospect) {
2195
2157
  await client.end();
2196
2158
  }
2197
2159
  }
2160
+ const WRITES = [
2161
+ "insert",
2162
+ "update",
2163
+ "delete"
2164
+ ];
2165
+ /**
2166
+ * Each table and write command whose permissive policies reach
2167
+ * `authenticated` (or `public`) without the read-only actor check: no
2168
+ * restrictive policy for the command calls `permdock_actor_may_write()`, and
2169
+ * some permissive one does not.
2170
+ */
2171
+ function unguardedWrites(policies, fn) {
2172
+ const out = [];
2173
+ const tables = [...new Set(policies.map((policy) => policy.table))];
2174
+ for (const table of tables) for (const command of WRITES) {
2175
+ const applying = policies.filter((policy) => policy.table === table && (policy.command === command || policy.command === "all"));
2176
+ const permissive = applying.filter((policy) => policy.restrictive !== true && (policy.roles ?? []).some((role) => role === "authenticated" || role === "public"));
2177
+ if (permissive.length === 0) continue;
2178
+ const calls = (expression) => expression.includes(fn);
2179
+ if (!(applying.some((policy) => policy.restrictive === true && calls(policy.expression)) || permissive.every((policy) => calls(policy.expression)))) out.push(`${table}: ${command} policies for authenticated do not call ${fn}(), so a read-only support or impersonation session can write (rls.readOnlyActors)`);
2180
+ }
2181
+ return out;
2182
+ }
2198
2183
  const SYSTEM_SCHEMAS = [
2199
2184
  "pg_catalog",
2200
2185
  "information_schema",
@@ -2214,7 +2199,7 @@ const SYSTEM_SCHEMAS = [
2214
2199
  "supabase_migrations"
2215
2200
  ];
2216
2201
  function inScope(table) {
2217
- const schema = table.split(".")[0] ?? "";
2202
+ const schema = beforeFirst(table, ".");
2218
2203
  return HELPER_TABLES.some((item) => item === table) || !(SYSTEM_SCHEMAS.some((item) => item === schema) || schema.startsWith("pg_"));
2219
2204
  }
2220
2205
  function seedLine(row) {
@@ -2236,13 +2221,16 @@ function diffMixed(expected, actual) {
2236
2221
  for (const key of helperCallKeys(policy.expression)) if (!permissions.has(key)) drift.push(`${policy.table}: policy ${policy.name} passes ${key}, which the policy does not declare, so it always denies`);
2237
2222
  else if (conditioned.has(key) && blind.has(key)) drift.push(`${policy.table}: policy ${policy.name} passes ${key}, whose grants carry row conditions the helpers do not check: the policy grants more than the application does`);
2238
2223
  }
2224
+ if (expected.readOnlyActors === true) drift.push(...unguardedWrites(actual.policies, ACTOR_MAY_WRITE));
2239
2225
  return {
2240
2226
  drift,
2241
2227
  info: actual.rlsTables.filter((table) => !covered.has(table)).map((table) => `${table}: no policy calls a PermDock helper`)
2242
2228
  };
2243
2229
  }
2244
2230
  const ALL_POLICIES_SQL = `select schemaname || '.' || tablename as target, policyname,
2245
- coalesce(qual, '') || ' ' || coalesce(with_check, '') as expression
2231
+ coalesce(qual, '') || ' ' || coalesce(with_check, '') as expression,
2232
+ lower(cmd) as command, permissive = 'RESTRICTIVE' as restrictive,
2233
+ array_to_string(roles, ',') as roles
2246
2234
  from pg_policies`;
2247
2235
  const RLS_TABLES_SQL = `select n.nspname || '.' || c.relname as target
2248
2236
  from pg_class c
@@ -2269,7 +2257,10 @@ async function introspectMixed(db, schema, connect = connectIntrospect) {
2269
2257
  policies: policies.rows.map((row) => ({
2270
2258
  table: String(row["target"]),
2271
2259
  name: String(row["policyname"]),
2272
- expression: String(row["expression"])
2260
+ expression: String(row["expression"]),
2261
+ command: String(row["command"]),
2262
+ restrictive: row["restrictive"] === true,
2263
+ roles: String(row["roles"] ?? "").split(",").filter((role) => role !== "")
2273
2264
  })).filter((row) => inScope(row.table)),
2274
2265
  rlsTables: tables.rows.map((row) => String(row["target"])).filter(inScope).toSorted()
2275
2266
  };
@@ -2278,26 +2269,15 @@ async function introspectMixed(db, schema, connect = connectIntrospect) {
2278
2269
  }
2279
2270
  }
2280
2271
  //#endregion
2281
- //#region src/cli/rls.ts
2272
+ //#region src/cli/rls/rls.ts
2282
2273
  const RLS_HELP = `permdock rls generate | import | verify | migrate
2283
2274
 
2284
- generate --target drizzle|sql|prisma --dialect supabase|neon|guc
2285
- [--rbac supabase] [--rbac-schema public] [--authorize database|jwt]
2286
- [--memberships <table>:tenant,user,role]
2287
- [--policy-per-role] [--policy-name '{table}_{op}'] [--tenant-type uuid] [--custom-roles]
2288
- [--capabilities] [--fields views [--revoke-columns]]
2289
- [--out <path>] [--check] [--skip-closures] [--inline-functions] [--force] [--guc-prefix app]
2290
- [--split helpers,seeds,policies,hook --out <dir>/056_permdock_{part}.sql]
2291
- [--grants-out <file>|-] [--seeds-out <file>|-]
2292
- [--helpers-only] [--shims] [--db $DATABASE_URL]
2293
- import --sql schema.sql | --db $DATABASE_URL --out src/permissions.generated.ts
2294
- [--schema zod|valibot|arktype] [--memberships <table>:tenant,user,role]
2295
- verify [--db $DATABASE_URL] [--fixtures rls.fixtures.ts] [--format pgtap|node] [--tree]
2296
- [--introspect --db $DATABASE_URL, with the generate flags]
2297
- [--advisors [--db $DATABASE_URL] [--json]]: supabase db advisors, security lints
2298
- migrate --sql <dir> [--write] [--json], with the generate flags
2299
- rewrites the rls.migrate helpers' calls in policies onto the generated helpers
2275
+ generate Compile the policy into RLS policies and helpers
2276
+ import Turn existing policies (--sql or --db-url-stdin) into a permissions module
2277
+ verify Run the fixtures, or compare a live database with --introspect
2278
+ migrate Rewrite the rls.migrate helpers' calls onto the generated helpers
2300
2279
 
2280
+ Run permdock rls <command> --help for its flags.
2301
2281
  Never emits service_role, except in the execute grant rls.trustedReaders asks for. memberOf compiles through the dialect memberships mapping.
2302
2282
  `;
2303
2283
  function asTarget(value) {
@@ -2322,6 +2302,9 @@ function generateInput(input, target, dialect) {
2322
2302
  io: input.io,
2323
2303
  ...input.out === void 0 ? {} : { out: input.out },
2324
2304
  ...input.from === void 0 ? {} : { from: input.from },
2305
+ ...input.project === void 0 ? {} : { project: input.project },
2306
+ ...input.url === void 0 ? {} : { url: input.url },
2307
+ json: input.json,
2325
2308
  ...input.memberships === void 0 ? {} : { memberships: input.memberships },
2326
2309
  ...input.gucPrefix === void 0 ? {} : { gucPrefix: input.gucPrefix },
2327
2310
  policyPerRole: input.policyPerRole,
@@ -2332,8 +2315,10 @@ function generateInput(input, target, dialect) {
2332
2315
  ...input.fields === void 0 ? {} : { fields: input.fields },
2333
2316
  revokeColumns: input.revokeColumns,
2334
2317
  ...input.split === void 0 ? {} : { split: input.split },
2318
+ ...isSupabaseSchemas(input.schemas) ? { schemas: input.schemas } : {},
2335
2319
  ...input.grantsOut === void 0 ? {} : { grantsOut: input.grantsOut },
2336
2320
  ...input.seedsOut === void 0 ? {} : { seedsOut: input.seedsOut },
2321
+ ...input.backfillOut === void 0 ? {} : { backfillOut: input.backfillOut },
2337
2322
  helpersOnly: input.helpersOnly,
2338
2323
  shims: input.shims === true
2339
2324
  };
@@ -2363,18 +2348,24 @@ async function migrate(input) {
2363
2348
  output: generated.output
2364
2349
  };
2365
2350
  await requirePeer(() => import("pgsql-parser"), "pgsql-parser", "permdock rls migrate", input.cwd);
2366
- const { migrateTarget, runRlsMigrate } = await import("./rls-migrate-CN4TlJTO.js");
2351
+ const { migrateTarget, runRlsMigrate } = await import("./migrate-BShP7Vf4.js");
2367
2352
  return runRlsMigrate({
2368
2353
  cwd: input.cwd,
2369
2354
  sql: input.sql,
2370
2355
  config,
2371
- target: migrateTarget(generated),
2356
+ target: {
2357
+ ...migrateTarget(generated),
2358
+ ...input.config.rls?.roleValues === void 0 ? {} : { roleValues: input.config.rls.roleValues }
2359
+ },
2372
2360
  write: input.write,
2373
- json: input.json
2361
+ force: input.forceRewrite === true,
2362
+ json: input.json,
2363
+ retireOut: input.retireOut,
2364
+ ...input.io.now === void 0 ? {} : { now: input.io.now() }
2374
2365
  });
2375
2366
  }
2376
2367
  /** `verify --introspect` with `--helpers-only`: helpers and seeds exactly, hand-written policies by their keys. */
2377
- async function introspectHelpersOnly(db, expected, generated, connect) {
2368
+ async function introspectHelpersOnly(db, expected, generated, connect, readOnlyActors) {
2378
2369
  const schema = generated.schema ?? "permdock";
2379
2370
  try {
2380
2371
  const actual = await introspectRls(db, expected, connect);
@@ -2384,7 +2375,8 @@ async function introspectHelpersOnly(db, expected, generated, connect) {
2384
2375
  schema,
2385
2376
  seeds: generated.seeds ?? [],
2386
2377
  permissions: generated.keys?.permissions ?? [],
2387
- rowConditions: generated.keys?.rowConditions ?? []
2378
+ rowConditions: generated.keys?.rowConditions ?? [],
2379
+ readOnlyActors
2388
2380
  }, await introspectMixed(db, schema, connect));
2389
2381
  const drift = [...helpers, ...mixed.drift];
2390
2382
  const info = mixed.info.map((line) => `info: ${line}`);
@@ -2411,7 +2403,7 @@ async function introspectHelpersOnly(db, expected, generated, connect) {
2411
2403
  async function introspect(input) {
2412
2404
  if (input.db === void 0) return {
2413
2405
  code: 2,
2414
- output: "PermDock CLI: rls verify --introspect needs --db"
2406
+ output: "PermDock CLI: rls verify --introspect needs a database: --db-url-stdin, DATABASE_URL or SUPABASE_DB_URL"
2415
2407
  };
2416
2408
  const dialect = asDialect(input.dialect ?? input.config.rls?.dialect);
2417
2409
  if (dialect === void 0) return {
@@ -2428,7 +2420,7 @@ async function introspect(input) {
2428
2420
  output: generated.output
2429
2421
  };
2430
2422
  const expected = expectedRls(generated.policies, generated.text, generated.indexes);
2431
- if (generated.helpersOnly === true) return introspectHelpersOnly(input.db, expected, generated, input.connect);
2423
+ if (generated.helpersOnly === true) return introspectHelpersOnly(input.db, expected, generated, input.connect, readOnlyActorKinds(input.config.rls?.readOnlyActors) !== void 0);
2432
2424
  try {
2433
2425
  const actual = await introspectRls(input.db, expected, input.connect);
2434
2426
  const drift = diffRls(expected, actual, { columnGrants: (input.fields ?? input.config.rls?.fields) === "views" });
@@ -2451,6 +2443,11 @@ async function runRls(input) {
2451
2443
  code: 2,
2452
2444
  output: RLS_HELP
2453
2445
  };
2446
+ const badSchemas = schemasRefusal(`rls ${action}`, input.schemas);
2447
+ if (badSchemas !== void 0) return {
2448
+ code: 2,
2449
+ output: badSchemas
2450
+ };
2454
2451
  switch (action) {
2455
2452
  case "generate": {
2456
2453
  const target = asTarget(input.target);
@@ -2471,7 +2468,7 @@ async function runRls(input) {
2471
2468
  }
2472
2469
  case "import": {
2473
2470
  await requirePeer(() => import("pgsql-parser"), "pgsql-parser", "permdock rls import", input.cwd);
2474
- const { runRlsImport } = await import("./rls-import-4lLW_pbb.js");
2471
+ const { runRlsImport } = await import("./import-DMmOG27O.js");
2475
2472
  return runRlsImport({
2476
2473
  cwd: input.cwd,
2477
2474
  config: input.config,
@@ -2489,6 +2486,7 @@ async function runRls(input) {
2489
2486
  cwd: input.cwd,
2490
2487
  json: input.json,
2491
2488
  env: input.io.env ?? process.env,
2489
+ ...input.io.signal === void 0 ? {} : { signal: input.io.signal },
2492
2490
  ...input.db === void 0 ? {} : { db: input.db },
2493
2491
  ...input.exec === void 0 ? {} : { exec: input.exec }
2494
2492
  });