permdock 0.1.0 → 0.2.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 (349) hide show
  1. package/README.md +106 -47
  2. package/dist/a2a/index.d.ts +7 -16
  3. package/dist/a2a/index.js +59 -98
  4. package/dist/adapter-context-mlWsVGGg.js +23 -0
  5. package/dist/ai-sdk/index.d.ts +37 -16
  6. package/dist/ai-sdk/index.js +30 -13
  7. package/dist/approvals/index.d.ts +3 -3
  8. package/dist/approvals/index.js +5 -4
  9. package/dist/{approvals-DaxY-Bdg.js → approvals-DbYAuNkG.js} +17 -11
  10. package/dist/approvers-DT4AVyIg.js +52 -0
  11. package/dist/{arazzo-CTeVuXr5.js → arazzo-BAgw9ap6.js} +2 -2
  12. package/dist/{arazzo-aoTkqYsJ.js → arazzo-cmvX2jDe.js} +2 -2
  13. package/dist/authzen/index.d.ts +4 -13
  14. package/dist/authzen/index.js +16 -50
  15. package/dist/better-auth/index.d.ts +20 -3
  16. package/dist/better-auth/index.js +27 -7
  17. package/dist/better-supabase/index.d.ts +206 -0
  18. package/dist/better-supabase/index.js +620 -0
  19. package/dist/bind-CVy3Espv.d.ts +25 -0
  20. package/dist/bind-VnYn2HKq.js +97 -0
  21. package/dist/boundary-BM8a_P6E.js +13 -0
  22. package/dist/boundary-BmHxVH1J.d.ts +15 -0
  23. package/dist/canonical-json-DemiK4Vp.js +26 -0
  24. package/dist/{capability-B68OFnZx.js → capability-4qPdO9gk.js} +7 -5
  25. package/dist/{capability-BDG_nkxt.d.ts → capability-Dq43tT1u.d.ts} +2 -2
  26. package/dist/catalog/index.d.ts +1 -1
  27. package/dist/catalog/index.js +1 -1
  28. package/dist/catalog-BjZSovXQ.js +2 -0
  29. package/dist/{catalog-doc-BSo_Q_-6.js → catalog-doc-Dsc9y7sd.js} +23 -12
  30. package/dist/{catalog-B0E6bggc.js → catalog-h2tlKFkq.js} +2 -2
  31. package/dist/{catalog-H-vFGwWK.js → catalog-z8q6bi_r.js} +4 -4
  32. package/dist/claude-agent/index.d.ts +5 -14
  33. package/dist/claude-agent/index.js +4 -10
  34. package/dist/clerk/index.d.ts +12 -3
  35. package/dist/clerk/index.js +26 -5
  36. package/dist/cli/bin.js +1 -1
  37. package/dist/cli/index.d.ts +3 -3
  38. package/dist/cli/index.js +2 -2
  39. package/dist/clients-C7zLFuWT.d.ts +12 -0
  40. package/dist/clients-DjU5ABMy.js +23 -0
  41. package/dist/cloud/index.d.ts +4 -5
  42. package/dist/cloud/index.js +20 -6
  43. package/dist/{cloud-BNQTrmrr.js → cloud-BGhHS-lY.js} +2 -2
  44. package/dist/{cloud-Cuxr6Czw.js → cloud-Bo3e9Ak3.js} +7 -6
  45. package/dist/{collect-O-tOtbnk.js → collect-DDDv_ESs.js} +194 -42
  46. package/dist/collect-DLlujdHC.js +83 -0
  47. package/dist/collect-rG2IQ3t0.js +2 -0
  48. package/dist/{commands-BMh_Etm2.js → commands-DWOrSQic.js} +16 -12
  49. package/dist/compile/index.d.ts +3 -0
  50. package/dist/compile/index.js +2 -0
  51. package/dist/{compile-D22iwvuV.js → compile-Ba4blROR.js} +85 -30
  52. package/dist/compile-CNnl6uIt.d.ts +94 -0
  53. package/dist/config-BJiM-xAm.js +157 -0
  54. package/dist/config-Bn00Z3ks.js +36 -0
  55. package/dist/config-report-Dw_JcUqT.js +44 -0
  56. package/dist/{connection-JvqwHODC.js → connection-CU8MQxRi.js} +1 -1
  57. package/dist/{context-CKEAabd7.js → context-Ttp0ekMT.js} +36 -27
  58. package/dist/convex/index.d.ts +9 -4
  59. package/dist/convex/index.js +14 -4
  60. package/dist/create-BWyx3jua.d.ts +245 -0
  61. package/dist/create-CPhDL6qx.js +253 -0
  62. package/dist/{credential-B_rkUNlY.js → credential-8P81ff4u.js} +26 -46
  63. package/dist/delegation-D8pUEuce.js +103 -0
  64. package/dist/{describe-Bt1YD5uS.js → describe-B_gVZEVi.js} +10 -15
  65. package/dist/{describe-DernyItf.d.ts → describe-PdoqESvg.d.ts} +9 -10
  66. package/dist/{diff-x5Il8LVa.js → diff-B8Tunc3o.js} +44 -11
  67. package/dist/{diff-BUzf5Poi.js → diff-CzqLxZ77.js} +2 -2
  68. package/dist/doctor-WpvG6p4k.js +1194 -0
  69. package/dist/doctor-collect-ByfYlNv1.js +664 -0
  70. package/dist/{doctor-Bqo6FwUa.js → doctor-rOr-4ZfS.js} +2 -2
  71. package/dist/drizzle/index.d.ts +4 -3
  72. package/dist/drizzle/index.js +3 -3
  73. package/dist/elysia/index.d.ts +6 -28
  74. package/dist/elysia/index.js +16 -60
  75. package/dist/{errors-DSMnAVNa.js → errors-CWfoNg9N.js} +8 -4
  76. package/dist/{errors-Iy71s7Oj.js → errors-Cz5zn0mW.js} +17 -71
  77. package/dist/{errors-DbZ1YOnb.d.ts → errors-DAqevpNk.d.ts} +8 -21
  78. package/dist/{errors-m3u9TmDu.js → errors-DRnGZHzF.js} +4 -1
  79. package/dist/evaluation-items-CfCCejQd.js +40 -0
  80. package/dist/{evaluations-DsAU9ppl.js → evaluations-tz6D9A2G.js} +28 -46
  81. package/dist/eve/index.d.ts +6 -15
  82. package/dist/eve/index.js +7 -12
  83. package/dist/expiry-BoBJ8Tvc.js +7 -0
  84. package/dist/express/index.d.ts +6 -29
  85. package/dist/express/index.js +28 -58
  86. package/dist/fastify/index.d.ts +6 -29
  87. package/dist/fastify/index.js +16 -65
  88. package/dist/fields-BMl1RxoX.js +26 -0
  89. package/dist/{files-B_wAGYmt.js → files-DWUN9PzI.js} +5 -1
  90. package/dist/{fixtures-Cvaw0gDG.js → fixtures-ht28Cplr.js} +5 -2
  91. package/dist/{freeze-y1_jQL4v.js → freeze-CrU8nQeX.js} +31 -2
  92. package/dist/{from-snapshot-BMGnhmgv.js → from-snapshot-C3sHpZNS.js} +65 -48
  93. package/dist/{grantee-Bv04_Vzf.js → grantee-Cv5Sogfv.js} +88 -7
  94. package/dist/graph-sql-B-dsAT6j.d.ts +29 -0
  95. package/dist/{headers-B5RRv3Xm.js → headers-BZSDieNT.js} +1 -1
  96. package/dist/{headers-C8SHQccD.d.ts → headers-Cf-zWvsO.d.ts} +1 -1
  97. package/dist/helpers-H_KARDVC.js +421 -0
  98. package/dist/hono/index.d.ts +6 -28
  99. package/dist/hono/index.js +11 -43
  100. package/dist/{hosted-BwKQ1BTA.js → hosted-DJQw46hn.js} +6 -5
  101. package/dist/{http-GgVApX1k.js → http-BIhmLfCc.js} +4 -20
  102. package/dist/http-SO7qGttf.js +44 -0
  103. package/dist/ids-DC4V4CFu.js +18 -0
  104. package/dist/{index-LF_hS-yn.d.ts → index-A0YNL8jZ.d.ts} +1 -1
  105. package/dist/index-CtE07CBF.d.ts +6 -0
  106. package/dist/{index-xIxJLwOQ.d.ts → index-D3vbkoSI.d.ts} +75 -7
  107. package/dist/{index-CltcQ4Bf.d.ts → index-D6_cSa1z.d.ts} +68 -5
  108. package/dist/index.d.ts +14 -9
  109. package/dist/index.js +22 -17
  110. package/dist/instance-options-BK_rNOSC.d.ts +11 -0
  111. package/dist/instance-options-Dhhcnm3y.js +22 -0
  112. package/dist/{parse-DPfIvFMU.js → json-schema-BsAflUGW.js} +28 -57
  113. package/dist/jwt/index.d.ts +10 -6
  114. package/dist/jwt/index.js +32 -13
  115. package/dist/kernel-GSQnMz3O.js +208 -0
  116. package/dist/kysely/index.d.ts +4 -3
  117. package/dist/kysely/index.js +3 -3
  118. package/dist/live-options-H2eG8dfJ.js +60 -0
  119. package/dist/{load-BZ24CpZP.js → load-DhWEB9P1.js} +16 -10
  120. package/dist/load-DwJUafYN.js +2 -0
  121. package/dist/manifest-iaoBXHKc.js +783 -0
  122. package/dist/manifest-uDS9y5de.d.ts +206 -0
  123. package/dist/{map-DowSO96j.js → map-AcxK9o_D.js} +4 -15
  124. package/dist/{map-claims-B_6I1Rn-.js → map-claims-Z4QZOiFF.js} +13 -4
  125. package/dist/map-error-pgUWNNtG.js +23 -0
  126. package/dist/may-use-DndQ5tQ-.js +77 -0
  127. package/dist/mcp/index.d.ts +54 -22
  128. package/dist/mcp/index.js +218 -178
  129. package/dist/nest/index.d.ts +26 -31
  130. package/dist/nest/index.js +75 -84
  131. package/dist/next/client.d.ts +7 -8
  132. package/dist/next/client.js +11 -9
  133. package/dist/next/index.d.ts +30 -47
  134. package/dist/next/index.js +45 -60
  135. package/dist/next/plugin.d.ts +1 -1
  136. package/dist/next/plugin.js +32 -1
  137. package/dist/node/index.d.ts +9 -28
  138. package/dist/node/index.js +11 -43
  139. package/dist/openai/index.d.ts +6 -15
  140. package/dist/openai/index.js +7 -20
  141. package/dist/openapi/index.d.ts +87 -4
  142. package/dist/openapi/index.js +3 -3
  143. package/dist/{openapi-CwePaF7C.js → openapi-Bbg4wGVL.js} +3 -3
  144. package/dist/{openapi-DcqM8nU7.js → openapi-BqRBvAng.js} +237 -5
  145. package/dist/{openapi-DWD0Uc_4.js → openapi-CqqEbHsL.js} +5 -5
  146. package/dist/{openapi-import-BWRUNNeW.js → openapi-import-cmwetbzw.js} +1 -1
  147. package/dist/{openapi-schema-D8j_T9tO.js → openapi-schema--ARjyfQr.js} +2 -2
  148. package/dist/orpc/index.d.ts +15 -28
  149. package/dist/orpc/index.js +74 -117
  150. package/dist/otel/index.d.ts +2 -2
  151. package/dist/parse-BuyjxhZV.js +53 -0
  152. package/dist/parse-local-manifest-BkGQNzAh.d.ts +10 -0
  153. package/dist/parse-local-manifest-BrMozJCo.js +36 -0
  154. package/dist/pdp/index.d.ts +2 -2
  155. package/dist/pdp/index.js +14 -127
  156. package/dist/{permdock-yoZ7EjxX.js → permdock-CzqXjCpU.js} +641 -126
  157. package/dist/{permissions-B8vCg_bI.d.ts → permissions-CDgvKG5L.d.ts} +2 -2
  158. package/dist/{permissions-CofRqesP.js → permissions-CpZrivYv.js} +3 -3
  159. package/dist/{permissions-DC7GROPA.js → permissions-DVoF6dMt.js} +77 -15
  160. package/dist/{permissions-B4apnahQ.d.ts → permissions-DttnqqxC.d.ts} +34 -5
  161. package/dist/{pins-CqE1jZuF.js → pins-8ILTftC9.js} +2 -2
  162. package/dist/{policy-CInmNGkM.d.ts → policy-CFZAr_Al.d.ts} +330 -37
  163. package/dist/{policy-Cq2fB8Q5.d.ts → policy-D8UFyERw.d.ts} +2 -2
  164. package/dist/{policy-ANp36PnI.js → policy-xyWg7QdR.js} +173 -22
  165. package/dist/powersync-A8x_Yyvh.js +2 -0
  166. package/dist/powersync-C7kLglqX.js +540 -0
  167. package/dist/powersync-yW1UUFzp.js +62 -0
  168. package/dist/prisma/index.d.ts +3 -2
  169. package/dist/prisma/index.js +45 -20
  170. package/dist/{problem-Cgp9-KZA.js → problem-B43ztKIm.js} +74 -109
  171. package/dist/problem-details-CkL50EEf.d.ts +7 -0
  172. package/dist/problem-details-_AMOYz-w.js +144 -0
  173. package/dist/{protected-BuPAycJT.d.ts → protected-CNsMzrVd.d.ts} +5 -4
  174. package/dist/protected-Cs6kTaHo.js +100 -0
  175. package/dist/protected-bZs6rbT1.d.ts +40 -0
  176. package/dist/{provider-wTAUda2v.d.ts → provider-Cjr62YMV.d.ts} +1 -1
  177. package/dist/provider-DLzpb2Xz.js +52 -0
  178. package/dist/provider-store-DNyC1y48.js +43 -0
  179. package/dist/react/index.d.ts +7 -5
  180. package/dist/react/index.js +5 -4
  181. package/dist/react/provider-client.d.ts +1 -1
  182. package/dist/react/provider-client.js +1 -1
  183. package/dist/react-native/index.d.ts +166 -10
  184. package/dist/react-native/index.js +607 -85
  185. package/dist/{refs-ChLSxbST.js → refs-KYb4ntaz.js} +4 -3
  186. package/dist/{relations-r4soIFZB.js → relations-cjOL01hZ.js} +110 -31
  187. package/dist/replay-BsOyTrdV.js +28 -0
  188. package/dist/replay-sqBGMnNs.d.ts +5 -0
  189. package/dist/resource-ref-CaS9yVPi.js +20 -0
  190. package/dist/revocations-Bhr0aMIC.js +27 -0
  191. package/dist/rls-BMNydpHk.js +2521 -0
  192. package/dist/{rls-BOFU3chG.js → rls-CEvs64Kj.js} +8 -3
  193. package/dist/rls-fields-BMasREhR.js +3502 -0
  194. package/dist/{rls-graph-DxQ39PJj.js → rls-graph-pc9fuXYm.js} +100 -36
  195. package/dist/{rls-import-CXV6LBlS.js → rls-import-D8F7j35Q.js} +7 -6
  196. package/dist/{rls-migrate-DrFPbf8p.js → rls-migrate-7wikfrVI.js} +6 -5
  197. package/dist/rls-ownership-B5Lkq62W.js +843 -0
  198. package/dist/rls-shared-aOQ4hGTO.js +493 -0
  199. package/dist/rls-verify-D8B7dOTh.js +745 -0
  200. package/dist/{row-check-BajTgQZ-.d.ts → row-check-Dt35czLy.d.ts} +12 -15
  201. package/dist/rpc-DOK54_CS.js +89 -0
  202. package/dist/{run-CJ6UvS3a.js → run-YXCJDNYk.js} +31 -12
  203. package/dist/runtime-CB8gQFvS.js +11 -0
  204. package/dist/{saas-BRrZzJkM.js → saas-Bj_FFkjN.js} +17 -7
  205. package/dist/{schema-P8yoma__.js → schema-D3MnDMxa.js} +8 -4
  206. package/dist/scim/index.d.ts +1 -1
  207. package/dist/scim/index.js +1 -1
  208. package/dist/{scim-9NFa0XVZ.js → scim-BFGBBAqR.js} +1 -1
  209. package/dist/{scopes-cKVp_WTw.js → scopes-CVdPrWN-.js} +35 -8
  210. package/dist/server/index.d.ts +22 -11
  211. package/dist/server/index.js +11 -8
  212. package/dist/{credentials-BgRX3WEg.js → server-DlZ28Oz5.js} +1 -1
  213. package/dist/shared-DRu8VFVM.js +123 -0
  214. package/dist/{sink-Cfr9-7YC.d.ts → sink-Gs6TTVa-.d.ts} +1 -1
  215. package/dist/skills-Biq5uup3.js +192 -0
  216. package/dist/{skills-C_lpV4Jx.js → skills-JaymNCdK.js} +10 -4
  217. package/dist/{snapshot-DE8dE_hz.js → snapshot-Enxd0n-s.js} +138 -214
  218. package/dist/snapshot-cache-D6jLelln.js +44 -0
  219. package/dist/snapshot-cache-DwQH8ufx.d.ts +45 -0
  220. package/dist/solid/index.d.ts +39 -9
  221. package/dist/solid/index.js +76 -85
  222. package/dist/sources-B0UvrJ21.js +534 -0
  223. package/dist/sources-BcJgw0lt.d.ts +192 -0
  224. package/dist/{sql-Bll1d7y0.js → sql-xdg5f4Fi.js} +6 -1
  225. package/dist/{src-CTNT-PBW.js → src-CSbUiozF.js} +149 -11
  226. package/dist/ssf/index.d.ts +3 -2
  227. package/dist/ssf/index.js +3 -29
  228. package/dist/store-DMAWwAJV.js +371 -0
  229. package/dist/{store-Bq90395i.d.ts → store-D_pjn91-.d.ts} +13 -4
  230. package/dist/store-options-yO0V6gRn.js +716 -0
  231. package/dist/{stores-CkG2HrSg.js → stores-4_sqiOA1.js} +10 -63
  232. package/dist/{stores-CvOnu_2T.d.ts → stores-DVR5NHLE.d.ts} +20 -9
  233. package/dist/{stream-Q_xlB6Ko.js → stream-DDpvynSB.js} +16 -2
  234. package/dist/{stream-BhMfZOwr.d.ts → stream-DlvG2hs-.d.ts} +2 -2
  235. package/dist/subject-settings-BwBmCdTR.js +128 -0
  236. package/dist/supabase/index.d.ts +122 -6
  237. package/dist/supabase/index.js +4 -3
  238. package/dist/supabase/middleware.d.ts +32 -27
  239. package/dist/supabase/middleware.js +56 -22
  240. package/dist/{supabase-D8UlBA7Z.js → supabase-CwafwxxF.js} +3 -3
  241. package/dist/{supabase-DTxGtYdi.js → supabase-DTBaC20O.js} +345 -367
  242. package/dist/supabase-config-Bs44dare.js +472 -0
  243. package/dist/{supabase-hook-D_OqGb_V.js → supabase-hook-BuxLQLpU.js} +994 -708
  244. package/dist/supabase-hook-D5qVd_D8.js +2 -0
  245. package/dist/supautils-DYZnzRWV.js +91 -0
  246. package/dist/svelte/PermissionBoundary.svelte +37 -0
  247. package/dist/svelte/Protected.svelte +8 -2
  248. package/dist/svelte/index.d.ts +9 -6
  249. package/dist/svelte/index.js +61 -7
  250. package/dist/svelte/runtime.d.ts +4 -3
  251. package/dist/svelte/runtime.js +4 -3
  252. package/dist/svelte/source.d.ts +9 -6
  253. package/dist/svelte/source.js +7 -4
  254. package/dist/terminal/index.d.ts +6 -14
  255. package/dist/terminal/index.js +44 -32
  256. package/dist/testing/index.d.ts +105 -16
  257. package/dist/testing/index.js +504 -40
  258. package/dist/testing/saas/index.d.ts +4 -4
  259. package/dist/testing/saas/index.js +2 -2
  260. package/dist/testing/saas/permissions.d.ts +1 -1
  261. package/dist/testing/saas/permissions.js +1 -1
  262. package/dist/token-CIWxJOCh.js +50 -0
  263. package/dist/trpc/index.d.ts +7 -26
  264. package/dist/trpc/index.js +17 -100
  265. package/dist/types-9AWO9EGt.d.ts +197 -0
  266. package/dist/{types-D6Z6-rKJ.d.ts → types-BPxgszlT.d.ts} +260 -22
  267. package/dist/{types-BvBk1wgn.d.ts → types-BVyImy5I.d.ts} +5 -3
  268. package/dist/{types-CxquUByI.d.ts → types-Bvc4oWT6.d.ts} +1 -1
  269. package/dist/types-C3s9xVJG.d.ts +48 -0
  270. package/dist/{types-BBK-exvJ.d.ts → types-Dnemwd77.d.ts} +7 -1
  271. package/dist/types-kxznc7cn.d.ts +52 -0
  272. package/dist/{types-B8w5PFtS.d.ts → types-vKyAMtcX.d.ts} +17 -1
  273. package/dist/{types-B9dmiiab.d.ts → types-vNSDF7Dq.d.ts} +1 -1
  274. package/dist/{sources-DOpW5Ila.d.ts → types-yF5LmQVD.d.ts} +86 -127
  275. package/dist/unplugin/index.d.ts +1 -1
  276. package/dist/unplugin/index.js +10 -9
  277. package/dist/{usage-1ozMubLY.js → usage-D07N9QJ3.js} +2 -2
  278. package/dist/usage-D82l1CwK.js +2 -0
  279. package/dist/{usage-CUnK6Ou9.js → usage-DG1oeCca.js} +225 -62
  280. package/dist/validation-error-CnjZ4t92.js +33 -0
  281. package/dist/validation-error-CtF23SrC.d.ts +26 -0
  282. package/dist/{validity-D0uRmJKJ.js → validity-uOp8UpYn.js} +57 -36
  283. package/dist/{verifier-B90agnhl.js → verifier-BxBo6I44.js} +10 -5
  284. package/dist/version-BvacVTZR.js +17 -0
  285. package/dist/views-Soe7bGSC.js +95 -0
  286. package/dist/{vocabulary-Dk0fs-Jk.d.ts → vocabulary-DA8a8zif.d.ts} +1 -1
  287. package/dist/vue/index.d.ts +35 -13
  288. package/dist/vue/index.js +86 -93
  289. package/dist/watch-Etoa_Mvr.js +2 -0
  290. package/dist/watch-UihESpsJ.js +128 -0
  291. package/dist/webmcp/index.d.ts +2 -2
  292. package/dist/webmcp/index.js +7 -13
  293. package/package.json +62 -36
  294. package/schemas/approval-request-v1.json +175 -0
  295. package/schemas/catalog-v1.json +29 -1
  296. package/schemas/supabase-claims-v1.json +6 -1
  297. package/schemas/supabase-manifest-v1.json +150 -6
  298. package/skills/permdock/SKILL.md +7 -7
  299. package/skills/permdock-agents/SKILL.md +5 -5
  300. package/skills/permdock-agents/references/runtimes.md +12 -10
  301. package/skills/permdock-approvals/SKILL.md +4 -4
  302. package/skills/permdock-approvals/references/stores-and-resume.md +15 -4
  303. package/skills/permdock-audit/SKILL.md +3 -3
  304. package/skills/permdock-credentials/SKILL.md +4 -4
  305. package/skills/permdock-credentials/references/tokens-keys-links.md +7 -2
  306. package/skills/permdock-data/SKILL.md +4 -4
  307. package/skills/permdock-data/references/queries-and-rls.md +14 -6
  308. package/skills/permdock-data/references/relationships.md +5 -3
  309. package/skills/permdock-tenancy/SKILL.md +9 -6
  310. package/skills/permdock-tenancy/references/ownership-and-custom-roles.md +14 -8
  311. package/skills/permdock-wire/SKILL.md +4 -4
  312. package/skills/permdock-wire/references/adapters.md +42 -30
  313. package/skills/permdock-wire/references/policy-options.md +5 -3
  314. package/dist/approvers-aH0PJkmf.js +0 -20
  315. package/dist/catalog-idsHc0vo.js +0 -2
  316. package/dist/collect-B7_TIfmg.js +0 -49
  317. package/dist/collect-fRPAQIWb.js +0 -2
  318. package/dist/compile-CQbvyIBr.d.ts +0 -30
  319. package/dist/config-BPYmjTOp.js +0 -26
  320. package/dist/context-gtAwAILx.js +0 -6
  321. package/dist/create-BKF3dgFk.d.ts +0 -162
  322. package/dist/create-CSkhDU9T.js +0 -222
  323. package/dist/doctor-BGle8Gqi.js +0 -1314
  324. package/dist/helpers-DnSOrhB9.js +0 -682
  325. package/dist/index-fG6NSXlc.d.ts +0 -9
  326. package/dist/kernel-YOGCWNLf.js +0 -211
  327. package/dist/load-QOOm1z70.js +0 -2
  328. package/dist/manifest-DDQbC6K0.d.ts +0 -118
  329. package/dist/map-error-RJ3ZsUyA.js +0 -16
  330. package/dist/may-use-CnFEjVEp.js +0 -37
  331. package/dist/pg-4GJWDUxG.js +0 -953
  332. package/dist/plugin-4gr7IxFJ.js +0 -65
  333. package/dist/protected-B4K3BKoX.js +0 -138
  334. package/dist/protected-wY7nXhY9.d.ts +0 -23
  335. package/dist/provider-_wx46HpB.js +0 -48
  336. package/dist/rls-CJVja9Ww.js +0 -2119
  337. package/dist/rls-helpers-BxRuif0l.js +0 -1346
  338. package/dist/runtime-DP9ttFh1.js +0 -27
  339. package/dist/skills-Dx3QLnMY.js +0 -110
  340. package/dist/source-DoBBYJi2.js +0 -6
  341. package/dist/sources-DVHM3Mwq.js +0 -284
  342. package/dist/store-C6hFW6ta.js +0 -508
  343. package/dist/subject-settings-xt7V845g.js +0 -82
  344. package/dist/supabase-hook-DW-xP4hU.js +0 -2
  345. package/dist/types-DbWdEfcm.d.ts +0 -87
  346. package/dist/types-bva3i4Gf.d.ts +0 -98
  347. package/dist/types-i4BAOvdb.d.ts +0 -97
  348. package/dist/usage-PyEor6GA.js +0 -2
  349. package/dist/version-CnSh33Fj.js +0 -16
@@ -1,2119 +0,0 @@
1
- import { t as compact } from "./compact-y6vfu2_f.js";
2
- import { d as isFieldRelation, g as listPermissions, i as findPermission, p as isPrincipalRelation, u as isEdgeRelation, v as renamedKeys } from "./permissions-DC7GROPA.js";
3
- import { a as hasConditionOp } from "./refs-ChLSxbST.js";
4
- import { m as memoryRoleSource } from "./src-CTNT-PBW.js";
5
- import { a as resolveScope, c as scopeChain, u as scopeList } from "./scopes-cKVp_WTw.js";
6
- import { b as findRole, w as listRoles } from "./grantee-Bv04_Vzf.js";
7
- import { d as membershipsClaim, t as createPermDock, u as customRoleScope } from "./permdock-yoZ7EjxX.js";
8
- import { t as memoryRelations } from "./relations-r4soIFZB.js";
9
- import { o as jsonSchemaOf, s as policyRowConditionKeys } from "./catalog-doc-BSo_Q_-6.js";
10
- import { a as loadModule, n as asPolicy, o as pickNamed } from "./load-BZ24CpZP.js";
11
- import "./budget-NbEtbJC_.js";
12
- import { n as escapeSqlIdent } from "./sql-Bll1d7y0.js";
13
- import "./sources-DVHM3Mwq.js";
14
- import { i as SEEDS_MARKER, r as INDEXES_MARKER } from "./markers-B0hYLvMD.js";
15
- import { a as usageResult } from "./errors-DSMnAVNa.js";
16
- import { n as requirePeer } from "./peer-BOr_kJE1.js";
17
- import { n as fixtureSubject, r as loadFixtures, t as fixtureRow } from "./fixtures-Cvaw0gDG.js";
18
- import { _ as resolveAuthorize, c as supabaseHookSql, d as parseSplit, f as partPath, g as rbacScaffold, h as parseRbacAuthorize, m as writeSqlFiles, p as pgDeltaPath, r as grantsLabel, u as driftOf, y as supabaseConfig } from "./supabase-hook-D_OqGb_V.js";
19
- import { B as scopeSources, C as globalKindFilterSql, D as kindFilterSql, F as qualifiedTable$1, H as scopeTypeOf, I as quoteIdent$1, K as subjectIdSql, L as quoteLiteral, M as memberIdsHelper, N as parseMembershipsFlag, P as permittedIdsHelper, R as quoteTable, V as scopeTable, W as subjectClaimJsonSql, a as helpersSql, b as columnTypesOf, c as membershipTable, d as seedSql, f as signedIn, i as globalRoleRows, l as qualified$1, n as accessSql, o as memberColumn, s as membershipRows, t as HELPERS, u as roleRows, w as graphHelper, y as checkSuspension, z as roleKindSql } from "./rls-helpers-BxRuif0l.js";
20
- import { _ as breakGlassKey, a as columnRevokeSql, c as rowBranches, d as commandFor, f as compileGrants, g as breakGlassHolder, h as indexesSql, i as columnGrantSql, l as viewName, m as indexTargets, n as connectPgPool, o as fieldViews, p as tableFor, r as FIELD_VIEWS, s as fieldViewsSql, t as connectPg, u as assemblePolicies, v as collectGrants, y as roleNames } from "./pg-4GJWDUxG.js";
21
- import { a as HELPER_TABLE_POLICIES_SQL, c as helperCallKeys, d as rowConditionMessage, i as HELPER_TABLES, n as graphPlan, r as graphSql, s as callsHelper, t as closureDepths, u as helperTablePoliciesFromRows } from "./rls-graph-DxQ39PJj.js";
22
- import { basename, resolve } from "node:path";
23
- import { randomUUID } from "node:crypto";
24
- //#region src/cli/rls-break-glass.ts
25
- /** The audit table every generated break-glass read writes to. Part of the SQL contract. */
26
- const BREAK_GLASS_AUDIT = "permdock_break_glass_audit";
27
- const IDENT = /^[a-z][a-z0-9_]*$/u;
28
- /**
29
- * The resources a `breakGlass` grant targets, with the table each reads. A
30
- * break-glass grant is non-portable, so `compileGrants` never turns it into a
31
- * policy; the server reads restricted rows through the generated function
32
- * instead, which is why plain RLS keeps denying them.
33
- */
34
- function breakGlassEntries(policy, tables) {
35
- const byResource = /* @__PURE__ */ new Map();
36
- for (const item of collectGrants(policy)) {
37
- const { grant } = item;
38
- if (grant.breakGlass === void 0) continue;
39
- const holder = breakGlassHolder(item);
40
- const resource = grant.permission.resource;
41
- const grants = byResource.get(resource) ?? [];
42
- byResource.set(resource, grants);
43
- if (holder === void 0) {
44
- const root = policy.scopes[0];
45
- grants.push({
46
- permission: grant.permission.key,
47
- scope: "anyone",
48
- column: root?.key,
49
- ...root === void 0 ? {} : { root: root.name }
50
- });
51
- continue;
52
- }
53
- grants.push({
54
- permission: grant.permission.key,
55
- scope: holder.scope,
56
- column: holder.scope === "global" ? void 0 : policy.scopes.find((scope) => scope.name === holder.scope)?.key
57
- });
58
- }
59
- return [...byResource].map(([resource, grants]) => ({
60
- resource,
61
- table: tableFor(resource, tables),
62
- grants
63
- }));
64
- }
65
- /**
66
- * Rows the subject's roles reach for `p_permission`: a break-glass grant
67
- * lifts denies, never the tenant boundary. A grant to no role reads the rows
68
- * of the root scopes the subject is a member of, or every row when the
69
- * policy declares no scopes.
70
- */
71
- function rowsFilter(ctx, entry) {
72
- const arms = entry.grants.flatMap((grant) => {
73
- const asked = `p_permission = ${quoteLiteral(grant.permission)}`;
74
- if (grant.scope === "anyone") {
75
- if (grant.root === void 0) return [`(${asked})`];
76
- return grant.column === void 0 ? [] : [`(${asked} and ${quoteIdent$1(grant.column)} in (select ${qualified$1(ctx, memberIdsHelper(grant.root))}()))`];
77
- }
78
- if (grant.scope !== "global" && grant.column === void 0) return [];
79
- return [`(${asked} and ${accessSql(ctx, grant.scope, breakGlassKey(grant.permission), grant.column)})`];
80
- });
81
- return arms.length === 0 ? "false" : [...new Set(arms)].join("\n or ");
82
- }
83
- function functionName(resource) {
84
- if (!IDENT.test(resource)) throw new Error(`PermDock CLI: unsafe break-glass resource '${resource}'`);
85
- return `permdock_break_glass_${resource}`;
86
- }
87
- function qualifiedTable(name) {
88
- return quoteTable(name.includes(".") ? name : `public.${name}`);
89
- }
90
- function auditSql(ctx) {
91
- const audit = qualified$1(ctx, quoteIdent$1(BREAK_GLASS_AUDIT));
92
- return `-- break-glass audit: one row per session that read restricted data
93
- -- RLS never compiles a break-glass override, so restricted rows stay denied to a direct read
94
- create table if not exists ${audit} (
95
- id bigint generated always as identity primary key,
96
- at timestamptz not null default now(),
97
- subject text,
98
- permission text not null,
99
- purpose text,
100
- reason text not null
101
- );
102
- alter table ${audit} enable row level security;
103
- revoke all on table ${audit} from anon, authenticated, public;`;
104
- }
105
- function readFunctionSql(ctx, entry) {
106
- const fn = qualified$1(ctx, functionName(entry.resource));
107
- const audit = qualified$1(ctx, quoteIdent$1(BREAK_GLASS_AUDIT));
108
- const table = qualifiedTable(entry.table);
109
- const session = subjectClaimJsonSql(ctx, "break_glass");
110
- const subject = subjectIdSql(ctx);
111
- return `-- ${entry.resource}: reads restricted rows through a checked, audited break-glass session
112
- create or replace function ${fn}(p_permission text)
113
- returns setof ${table}
114
- language plpgsql
115
- security definer
116
- set search_path = ''
117
- as $$
118
- declare
119
- session jsonb := ${session};
120
- begin
121
- if session is null
122
- or jsonb_typeof(session) <> 'object'
123
- or session ->> 'v' <> '1'
124
- or session ->> 'permission' <> p_permission
125
- or coalesce(session ->> 'reason', '') = ''
126
- or jsonb_typeof(session -> 'expiresAt') <> 'number'
127
- or (session ->> 'expiresAt')::numeric <= extract(epoch from now())
128
- then
129
- raise exception 'PermDock: no valid break-glass session for %', p_permission
130
- using errcode = '42501';
131
- end if;
132
- insert into ${audit} (subject, permission, purpose, reason)
133
- values (
134
- nullif((${subject})::text, ''),
135
- p_permission,
136
- session ->> 'purpose',
137
- session ->> 'reason'
138
- );
139
- return query select * from ${table}
140
- where ${rowsFilter(ctx, entry)};
141
- end;
142
- $$;
143
- revoke execute on function ${fn}(text) from public, anon;
144
- grant execute on function ${fn}(text) to authenticated;`;
145
- }
146
- /**
147
- * The `security definer` read path for every `breakGlass` grant: an audit
148
- * table and one `permdock_break_glass_<resource>(permission)` per resource
149
- * that checks a signed break-glass session, writes an audit row and returns
150
- * the rows in the scope instances where the subject holds a role with the
151
- * break-glass grant (bypassing the table's policies as the definer). Empty
152
- * when the policy has no break-glass grant.
153
- */
154
- function breakGlassSql(ctx, entries) {
155
- if (entries.length === 0) return "";
156
- return [auditSql(ctx), ...entries.map((entry) => readFunctionSql(ctx, entry))].join("\n\n");
157
- }
158
- //#endregion
159
- //#region src/cli/rls-emit.ts
160
- const HEADER = "-- generated by permdock rls generate\n-- fail-closed: never target a bypass role\n";
161
- function assertNoServiceRole(text) {
162
- if (/\bservice_role\b/iu.test(text)) throw new Error("PermDock CLI: generated RLS must never emit service_role");
163
- }
164
- /**
165
- * Neon's Data API and `pg_session_jwt` name the anonymous role `anonymous`;
166
- * Supabase, PostgREST and the GUC pattern use `anon`. Role lists end a line
167
- * or a statement after `to` or `from`, so only those are renamed.
168
- */
169
- function dialectRoles(text, dialect) {
170
- if (dialect !== "neon") return text;
171
- return text.replaceAll(/\b(to|from) ((?:[a-z_]+, )*[a-z_]+)(?=;|\n)/gu, (_match, keyword, list) => `${keyword} ${list.split(", ").map((role) => role === "anon" ? "anonymous" : role).join(", ")}`);
172
- }
173
- function tablesOf(policies) {
174
- return [...new Set(policies.map((item) => item.table))];
175
- }
176
- /** Enable (and force) row level security, then the table grants for the commands a policy allows. */
177
- function tableSql$1(table, policies, force, views) {
178
- const closed = views.find((view) => view.table === table && view.companion !== void 0);
179
- const target = quoteTable(qualifiedTable$1(table));
180
- const lines = [`alter table ${target} enable row level security;`];
181
- if (force) lines.push(`alter table ${target} force row level security;`);
182
- lines.push(`revoke all on table ${target} from anon, authenticated;`);
183
- for (const role of ["anon", "authenticated"]) {
184
- const cmds = new Set(policies.filter((item) => item.table === table && item.effect === "allow" && item.roles.includes(role)).map((item) => item.command));
185
- const grants = [
186
- "select",
187
- "insert",
188
- "update",
189
- "delete"
190
- ].filter((cmd) => cmds.has(cmd) && (closed === void 0 || cmd !== "select"));
191
- if (grants.length > 0) lines.push(`grant ${grants.join(", ")} on table ${target} to ${role};`);
192
- if (closed !== void 0 && cmds.has("select")) lines.push(columnGrantSql(closed, role));
193
- }
194
- if (closed !== void 0) lines.push(columnRevokeSql(closed));
195
- return lines.join("\n");
196
- }
197
- function emitSql(policies, preamble, force = false, views = []) {
198
- const chunks = [HEADER];
199
- if (preamble !== "") chunks.push(preamble);
200
- for (const table of tablesOf(policies)) chunks.push(tableSql$1(table, policies, force, views), "");
201
- for (const item of policies) {
202
- const as = item.effect === "deny" ? "restrictive" : "permissive";
203
- const to = item.roles.join(", ");
204
- const lines = [
205
- `drop policy if exists ${quoteIdent$1(item.name)} on ${quoteTable(qualifiedTable$1(item.table))};`,
206
- `create policy ${quoteIdent$1(item.name)}`,
207
- ` on ${quoteTable(qualifiedTable$1(item.table))}`,
208
- ` as ${as}`,
209
- ` for ${item.command}`,
210
- ` to ${to}`
211
- ];
212
- if (item.using !== void 0) lines.push(` using (${item.using})`);
213
- if (item.check !== void 0) lines.push(` with check (${item.check})`);
214
- chunks.push(`${lines.join("\n")};\n`);
215
- }
216
- if (views.length > 0) chunks.push(fieldViewsSql(views));
217
- const text = `${chunks.join("\n").trim()}\n`;
218
- assertNoServiceRole(text);
219
- return text;
220
- }
221
- /**
222
- * The SQL a Drizzle or Prisma schema cannot declare: helpers, grants, `force` and field views.
223
- * `rls generate` writes it next to `out` for a custom migration.
224
- */
225
- function migrationSql(policies, preamble, force, views) {
226
- const parts = [
227
- preamble.trim(),
228
- ...tablesOf(policies).map((table) => tableSql$1(table, policies, force, views)),
229
- fieldViewsSql(views)
230
- ].filter((part) => part !== "");
231
- if (parts.length === 0) return "";
232
- const text = `${HEADER}\n${parts.join("\n\n")}\n`;
233
- assertNoServiceRole(text);
234
- return text;
235
- }
236
- function helpersNote(helpers, how) {
237
- return helpers === void 0 ? [] : [`// run ${helpers} ${how}`];
238
- }
239
- function camelCase(name) {
240
- return name.replace(/[_-]+([a-z0-9])/gi, (_, char) => char.toUpperCase());
241
- }
242
- function pascalCase(name) {
243
- const camel = camelCase(name);
244
- return camel.charAt(0).toUpperCase() + camel.slice(1);
245
- }
246
- const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
247
- function identifier(name, what) {
248
- if (!IDENTIFIER.test(name)) throw new Error(`PermDock CLI: ${what} ${JSON.stringify(name)} is not an identifier`);
249
- return name;
250
- }
251
- function quoted(text) {
252
- return `'${text.replace(/[\\']/g, (match) => `\\${match}`)}'`;
253
- }
254
- function templateText(text) {
255
- return text.replace(/[\\`]|\$\{/g, (match) => `\\${match}`);
256
- }
257
- const AUTH_UID = "(select auth.uid())";
258
- function drizzleRoles(list) {
259
- return list.includes("anon") ? "[anonRole, authenticatedRole]" : "authenticatedRole";
260
- }
261
- function emitDrizzle(policies, options) {
262
- const supabase = options.dialect === "supabase";
263
- const anon = policies.some((item) => item.roles.includes("anon"));
264
- const sqlOf = (text) => {
265
- const parts = text.split(AUTH_UID).map(templateText);
266
- return `sql\`${supabase ? parts.join("${authUid}") : parts.join(AUTH_UID)}\``;
267
- };
268
- const usesUid = supabase && policies.some((item) => item.using?.includes(AUTH_UID) === true || item.check?.includes(AUTH_UID) === true);
269
- const lines = [
270
- "// generated by permdock rls generate",
271
- "// fail-closed: never target a bypass role",
272
- ...helpersNote(options.helpers, "in a custom migration (drizzle-kit generate --custom)"),
273
- "import { sql } from 'drizzle-orm'"
274
- ];
275
- if (supabase) {
276
- const named = [
277
- anon ? "anonRole" : "",
278
- "authenticatedRole",
279
- usesUid ? "authUid" : ""
280
- ].filter((name) => name !== "").join(", ");
281
- lines.push("import { pgPolicy } from 'drizzle-orm/pg-core'", `import { ${named} } from 'drizzle-orm/supabase'`);
282
- } else lines.push("import { pgPolicy, pgRole } from 'drizzle-orm/pg-core'");
283
- lines.push(`import * as schema from ${quoted(options.schema ?? "./schema")}`, "");
284
- if (!supabase) {
285
- if (anon) lines.push(`export const anonRole = pgRole('${options.dialect === "neon" ? "anonymous" : "anon"}').existing()`);
286
- lines.push("export const authenticatedRole = pgRole('authenticated').existing()", "");
287
- }
288
- for (const item of policies) {
289
- const as = item.effect === "deny" ? "restrictive" : "permissive";
290
- const table = identifier(options.exports?.[item.table] ?? camelCase(item.table), "table export");
291
- const name = identifier(camelCase(item.name), "policy export");
292
- const using = item.using === void 0 ? "" : `\n using: ${sqlOf(item.using)},`;
293
- const check = item.check === void 0 ? "" : `\n withCheck: ${sqlOf(item.check)},`;
294
- lines.push(`export const ${name} = pgPolicy(${quoted(item.name)}, {`, ` as: '${as}',`, ` for: '${item.command}',`, ` to: ${drizzleRoles(item.roles)},${using}${check}`, `}).link(schema.${table})`, "");
295
- }
296
- const text = `${lines.join("\n").trimEnd()}\n`;
297
- assertNoServiceRole(text);
298
- return text;
299
- }
300
- /** Prisma 8 `policy_<command>` blocks; each target model declares `@@rls` itself. */
301
- function emitPrisma(policies, options = {}) {
302
- const modelOf = (table) => identifier(options.models?.[table] ?? pascalCase(table), "model");
303
- const models = [...new Set(policies.map((item) => modelOf(item.table)))];
304
- const lines = [
305
- "// generated by permdock rls generate (Prisma 8 policy blocks)",
306
- "// fail-closed: never target a bypass role",
307
- `// add @@rls to ${models.length === 1 ? "model" : "models"} ${models.join(", ")}; keep these blocks in their namespace`,
308
- ...helpersNote(options.helpers, "in a migration before these policies"),
309
- ""
310
- ];
311
- for (const item of policies) {
312
- if (item.effect === "deny") throw new Error(`PermDock CLI: Prisma policy blocks are permissive only, so ${item.name} (a deny) needs --target sql`);
313
- lines.push(`policy_${item.command} ${identifier(item.name, "policy name")} {`, ` target = ${modelOf(item.table)}`, ` roles = [${item.roles.join(", ")}]`);
314
- if (item.using !== void 0) lines.push(` using = ${JSON.stringify(item.using)}`);
315
- if (item.check !== void 0) lines.push(` withCheck = ${JSON.stringify(item.check)}`);
316
- lines.push("}", "");
317
- }
318
- const text = `${lines.join("\n").trimEnd()}\n`;
319
- assertNoServiceRole(text);
320
- return text;
321
- }
322
- /** The migration file written next to `out`. */
323
- function migrationOut(out) {
324
- const dot = out.lastIndexOf(".");
325
- return `${dot > Math.max(out.lastIndexOf("/"), out.lastIndexOf("\\")) ? out.slice(0, dot) : out}.migration.sql`;
326
- }
327
- function defaultOut(target) {
328
- switch (target) {
329
- case "sql": return "rls.sql";
330
- case "drizzle": return "src/db/policies.ts";
331
- case "prisma": return "prisma/policies.prisma";
332
- default: return target;
333
- }
334
- }
335
- //#endregion
336
- //#region src/cli/rls-ownership.ts
337
- /** Objects the ownership rules add next to the helpers. Names are part of the SQL contract. */
338
- const OWNERSHIP = {
339
- canAssign: "permdock_can_assign",
340
- holders: "permdock_holders",
341
- transferOnly: "permdock_transfer_only"
342
- };
343
- function roleScope(policy, scopes, name) {
344
- const on = policy.rolesByName.get(name)?.on ?? findRole(policy.vocabulary.roles, name)?.on;
345
- return typeof on === "string" ? resolveScope(scopes, on) : void 0;
346
- }
347
- /** The rules `rls generate` enforces; `undefined` when no role declares one. */
348
- function ownershipRules(policy, scopes) {
349
- const kinds = {};
350
- const assigns = [];
351
- const counted = [];
352
- for (const binding of policy.roles) {
353
- const scope = roleScope(policy, scopes, binding.name);
354
- if (binding.for !== void 0) kinds[binding.name] = binding.for;
355
- const assigner = scope ?? (binding.on === void 0 ? "global" : void 0);
356
- for (const target of binding.assigns ?? []) {
357
- const at = roleScope(policy, scopes, target);
358
- if (assigner !== void 0 && at !== void 0 && (assigner === "global" || scopeChain(scopes, at).includes(assigner))) assigns.push({
359
- assigner: binding.name,
360
- scope: assigner,
361
- role: target
362
- });
363
- }
364
- const min = binding.min ?? 0;
365
- const transferOnly = binding.transferOnly === true;
366
- if (scope !== void 0 && (min > 0 || binding.max !== void 0 || transferOnly)) counted.push(binding.max === void 0 ? {
367
- role: binding.name,
368
- scope,
369
- min,
370
- transferOnly
371
- } : {
372
- role: binding.name,
373
- scope,
374
- min,
375
- max: binding.max,
376
- transferOnly
377
- });
378
- }
379
- if (Object.keys(kinds).length === 0 && assigns.length === 0 && counted.length === 0) return;
380
- return {
381
- kinds,
382
- assigns,
383
- counted
384
- };
385
- }
386
- function objectName(prefix, scope) {
387
- return quoteIdent$1(`${prefix}_${scope}`);
388
- }
389
- /** Live holders of `roleExpr` in instance `idExpr`, with the same kind and expiry rules as the helpers. */
390
- function holdersSql(ctx, table, column, idExpr, role, into) {
391
- const roleExpr = typeof role === "string" ? quoteLiteral(role) : role.expr;
392
- const filters = [`m.${quoteIdent$1(column)} = ${idExpr}`, `m.${quoteIdent$1(table.role)}::text = ${roleExpr}`];
393
- if (table.expiresAt !== void 0) {
394
- const expires = memberColumn(table.expiresAt);
395
- filters.push(`(${expires} is null or ${expires} > now())`);
396
- }
397
- const via = table.via === void 0 ? "null::text" : `${memberColumn(table.via)}::text`;
398
- const kind = typeof role === "string" ? roleKindSql(ctx, role, via) : kindFilterSql(ctx, roleExpr, via);
399
- if (kind !== void 0) filters.push(kind);
400
- return `select count(distinct m.${quoteIdent$1(table.user)}) into ${into}
401
- from ${membershipTable(table.table)} m
402
- where ${filters.join("\n and ")}`;
403
- }
404
- /**
405
- * `min` and `max` as a deferred constraint trigger: checked at commit, so a
406
- * transfer (demote one holder, promote another) passes in one transaction. An
407
- * instance with no memberships left is being torn down and skips `min`.
408
- */
409
- function holdersTriggerSql(ctx, scope, table, column, counted) {
410
- const fn = qualified$1(ctx, `${OWNERSHIP.holders}_${scope}`);
411
- const col = quoteIdent$1(column);
412
- const checks = counted.map((rule) => {
413
- const lines = [` select count(*) into v_total from ${membershipTable(table.table)} m where m.${col} = v_key;`, ` ${holdersSql(ctx, table, column, "v_key", rule.role, "v_count")};`];
414
- if (rule.min > 0) lines.push(` if v_total > 0 and v_count < ${String(rule.min)} then
415
- raise exception using
416
- errcode = '23514',
417
- message = ${quoteLiteral(`permdock: ${scope} `)} || v_id || ${quoteLiteral(` keeps at least ${String(rule.min)} ${rule.role}`)},
418
- hint = 'last-holder';
419
- end if;`);
420
- if (rule.max !== void 0) lines.push(` if v_count > ${String(rule.max)} then
421
- raise exception using
422
- errcode = '23514',
423
- message = ${quoteLiteral(`permdock: ${scope} `)} || v_id || ${quoteLiteral(` has at most ${String(rule.max)} ${rule.role}`)},
424
- hint = 'max-holders';
425
- end if;`);
426
- return lines.join("\n");
427
- });
428
- return `-- ${scope}: holder counts (min / max), checked at commit
429
- create or replace function ${fn}()
430
- returns trigger
431
- language plpgsql
432
- security definer
433
- set search_path = ''
434
- as $$
435
- declare
436
- v_ids text[] := '{}';
437
- v_id text;
438
- v_key ${membershipTable(table.table)}.${col}%type;
439
- v_total bigint;
440
- v_count bigint;
441
- begin
442
- if tg_op in ('UPDATE', 'DELETE') then
443
- v_ids := v_ids || old.${col}::text;
444
- end if;
445
- if tg_op in ('INSERT', 'UPDATE') then
446
- v_ids := v_ids || new.${col}::text;
447
- end if;
448
- foreach v_id in array v_ids loop
449
- continue when v_id is null;
450
- v_key := v_id;
451
- ${checks.join("\n")}
452
- end loop;
453
- return null;
454
- end;
455
- $$;
456
- drop trigger if exists ${objectName(OWNERSHIP.holders, scope)} on ${membershipTable(table.table)};
457
- create constraint trigger ${objectName(OWNERSHIP.holders, scope)}
458
- after insert or update or delete on ${membershipTable(table.table)}
459
- deferrable initially deferred
460
- for each row execute function ${fn}();`;
461
- }
462
- /**
463
- * `transferOnly` as statement triggers over transition tables: a statement
464
- * that changes a role's holder count in an instance that had holders before
465
- * and still has some after is refused. Creating the first holder and removing
466
- * the last (which `min` guards) are not transfers.
467
- */
468
- function transferTriggerSql(ctx, scope, table, column, roles) {
469
- const fn = qualified$1(ctx, `${OWNERSHIP.transferOnly}_${scope}`);
470
- const col = quoteIdent$1(column);
471
- const role = quoteIdent$1(table.role);
472
- const list = `array[${roles.map(quoteLiteral).join(", ")}]::text[]`;
473
- const rows = (alias, source, delta) => `select ${alias}.${col} as id, ${alias}.${role}::text as role, ${delta} as delta from ${source} ${alias} where ${alias}.${role}::text = any(${list})`;
474
- const loop = (source) => ` for v_change in
475
- select c.id, c.role, sum(c.delta) as delta
476
- from (${source}) c
477
- group by c.id, c.role
478
- loop
479
- continue when v_change.delta = 0 or v_change.id is null;
480
- ${holdersSql(ctx, table, column, "v_change.id", { expr: "v_change.role" }, "v_after")};
481
- if v_after > 0 and v_after - v_change.delta > 0 then
482
- raise exception using
483
- errcode = '23514',
484
- message = 'permdock: ' || v_change.role || ${quoteLiteral(` in ${scope} `)} || v_change.id || ' moves only by transfer',
485
- hint = 'transfer-only';
486
- end if;
487
- end loop;`;
488
- const inserted = rows("n", "permdock_new", "1");
489
- const deleted = rows("o", "permdock_old", "-1");
490
- const target = membershipTable(table.table);
491
- const trigger = (op) => {
492
- const name = quoteIdent$1(`${OWNERSHIP.transferOnly}_${scope}_${op}`);
493
- return `drop trigger if exists ${name} on ${target};
494
- create trigger ${name}
495
- after ${op} on ${target}
496
- referencing ${op === "insert" ? "new table as permdock_new" : op === "delete" ? "old table as permdock_old" : "old table as permdock_old new table as permdock_new"}
497
- for each statement execute function ${fn}();`;
498
- };
499
- return `-- ${scope}: transfer-only roles (${roles.join(", ")}) keep their holder count per statement
500
- create or replace function ${fn}()
501
- returns trigger
502
- language plpgsql
503
- security definer
504
- set search_path = ''
505
- as $$
506
- declare
507
- v_change record;
508
- v_after bigint;
509
- begin
510
- if tg_op = 'INSERT' then
511
- ${loop(inserted)}
512
- elsif tg_op = 'DELETE' then
513
- ${loop(deleted)}
514
- else
515
- ${loop(`${inserted} union all ${deleted}`)}
516
- end if;
517
- return null;
518
- end;
519
- $$;
520
- ${trigger("insert")}
521
- ${trigger("update")}
522
- ${trigger("delete")}`;
523
- }
524
- function pairsSql(pairs) {
525
- return `values ${pairs.map((pair) => `(${quoteLiteral(pair.assigner)}, ${quoteLiteral(pair.role)})`).join(", ")}`;
526
- }
527
- /** Whether the signed-in user holds, in instance `p_scope_id`, a role whose `assigns` lists `p_role`. */
528
- function canAssignSql(ctx, own) {
529
- const fn = qualified$1(ctx, OWNERSHIP.canAssign);
530
- const parts = [];
531
- const global = own.assigns.filter((pair) => pair.scope === "global");
532
- if (global.length > 0) {
533
- if (ctx.authorize === "database") {
534
- const ur = globalRoleRows(ctx);
535
- const kind = globalKindFilterSql(ctx, ur.roleSql);
536
- parts.push(`exists (
537
- select 1 from ${ur.from}
538
- where ${ur.userSql} = ${subjectIdSql(ctx)}
539
- and (${ur.roleSql}, p_role) in (${pairsSql(global)})${kind === void 0 ? "" : `\n and ${kind}`}
540
- )`);
541
- } else {
542
- const kind = globalKindFilterSql(ctx, "r.role");
543
- parts.push(`exists (
544
- select 1 from ${roleRows(ctx)}
545
- where (r.role, p_role) in (${pairsSql(global)})${kind === void 0 ? "" : `\n and ${kind}`}
546
- )`);
547
- }
548
- }
549
- for (const { name } of ctx.scopes) {
550
- const pairs = own.assigns.filter((pair) => pair.scope === name);
551
- if (pairs.length === 0) continue;
552
- if (ctx.authorize === "database") {
553
- const mapped = scopeTable(ctx, name);
554
- if (mapped === void 0) continue;
555
- const { table, column } = mapped;
556
- const filters = [
557
- `${memberColumn(table.user)} = ${subjectIdSql(ctx)}`,
558
- `${memberColumn(column)}::text = p_scope_id`,
559
- `(${memberColumn(table.role)}::text, p_role) in (${pairsSql(pairs)})`
560
- ];
561
- if (table.expiresAt !== void 0) {
562
- const expires = memberColumn(table.expiresAt);
563
- filters.push(`(${expires} is null or ${expires} > now())`);
564
- }
565
- const kind = kindFilterSql(ctx, `${memberColumn(table.role)}::text`, table.via === void 0 ? "null::text" : `${memberColumn(table.via)}::text`);
566
- if (kind !== void 0) filters.push(kind);
567
- parts.push(`exists (
568
- select 1 from ${membershipTable(table.table)} m
569
- where ${filters.join("\n and ")}
570
- )`);
571
- } else {
572
- const kind = kindFilterSql(ctx, "r.role", "m ->> 'via'");
573
- parts.push(`exists (
574
- select 1 from ${membershipRows(ctx)}
575
- where m ->> 'scope' = ${quoteLiteral(name)}
576
- and m ->> 'id' = p_scope_id
577
- and (r.role, p_role) in (${pairsSql(pairs)})
578
- and case jsonb_typeof(m -> 'expiresAt')
579
- when 'number' then (m ->> 'expiresAt')::numeric > extract(epoch from now())
580
- else true
581
- end${kind === void 0 ? "" : `\n and ${kind}`}
582
- )`);
583
- }
584
- }
585
- const body = parts.length === 0 ? "false" : parts.join("\n or ");
586
- return `-- who may assign: a held role whose assigns lists p_role, in instance p_scope_id (its scope or an ancestor's)
587
- create or replace function ${fn}(p_role text, p_scope_id text)
588
- returns boolean
589
- language sql
590
- stable
591
- security definer
592
- set search_path = ''
593
- as $$
594
- select ${signedIn(ctx)} and (
595
- ${body}
596
- )
597
- $$;
598
- revoke execute on function ${fn}(text, text) from public, anon;
599
- grant execute on function ${fn}(text, text) to authenticated;`;
600
- }
601
- /**
602
- * The ownership objects: holder-count and transfer-only triggers on each
603
- * scope's membership table, and `permdock_can_assign` for the application's
604
- * own policies on those tables. Empty when no role declares a rule.
605
- */
606
- function ownershipSql(ctx) {
607
- const own = ctx.ownership;
608
- if (own === void 0) return "";
609
- const chunks = [];
610
- for (const { name } of ctx.scopes) {
611
- const counted = own.counted.filter((rule) => rule.scope === name);
612
- if (counted.length === 0) continue;
613
- const mapped = scopeTable(ctx, name);
614
- if (mapped === void 0) {
615
- chunks.push(`-- ${name}: no memberships table configured, so min, max and transferOnly are checked only by decideRoleChange`);
616
- continue;
617
- }
618
- chunks.push(holdersTriggerSql(ctx, name, mapped.table, mapped.column, counted));
619
- const transfer = counted.filter((rule) => rule.transferOnly).map((rule) => rule.role);
620
- if (transfer.length > 0) chunks.push(transferTriggerSql(ctx, name, mapped.table, mapped.column, transfer));
621
- }
622
- if (own.assigns.length > 0) chunks.push(canAssignSql(ctx, own));
623
- return chunks.length === 0 ? "" : `${chunks.join("\n\n")}\n`;
624
- }
625
- //#endregion
626
- //#region src/cli/rls-shims.ts
627
- const NAME = /^[a-z_][a-z0-9_]*$/u;
628
- /**
629
- * The key a legacy caller passes, mapped the way `rls migrate` maps it:
630
- * `keys` and `renamed` exactly, then the longest `prefixes` entry.
631
- */
632
- function mappedKey(config, renamed) {
633
- const exact = {
634
- ...renamed,
635
- ...config.keys
636
- };
637
- const prefixes = Object.entries(config.prefixes ?? {}).toSorted(([a], [b]) => b.length - a.length);
638
- const fallback = prefixes.length === 0 ? "p_key" : `case ${prefixes.map(([from, to]) => `when pg_catalog.starts_with(p_key, ${quoteLiteral(from)}) then ${quoteLiteral(to)} || pg_catalog.substr(p_key, ${String(from.length + 1)})`).join(" ")} else p_key end`;
639
- if (Object.keys(exact).length === 0) return fallback;
640
- return `coalesce(${quoteLiteral(JSON.stringify(exact))}::jsonb ->> p_key, ${fallback})`;
641
- }
642
- function scopeOf(ctx, scope) {
643
- if (!ctx.scopes.some((item) => item.name === scope)) throw new Error(`PermDock CLI: rls.migrate helper scope '${scope}' is not a declared scope`);
644
- return scope;
645
- }
646
- function shimFunction(name, args, argTypes, returns, sql) {
647
- return `create or replace function ${name}(${args})
648
- returns ${returns}
649
- language sql
650
- stable
651
- security invoker
652
- set search_path = ''
653
- as $$
654
- ${sql}
655
- $$;
656
- revoke execute on function ${name}(${argTypes}) from public, anon;
657
- grant execute on function ${name}(${argTypes}) to authenticated;`;
658
- }
659
- function shim(ctx, config, key, fn, helper) {
660
- switch (helper.form) {
661
- case "ids": {
662
- const scope = scopeOf(ctx, helper.scope);
663
- return shimFunction(fn, "p_key text", "text", `setof ${scopeTypeOf(ctx, scope)}`, ` select * from ${qualified$1(ctx, permittedIdsHelper(scope))}(${key})`);
664
- }
665
- case "row": {
666
- const scope = scopeOf(ctx, helper.scope);
667
- const type = scopeTypeOf(ctx, scope);
668
- return shimFunction(fn, `p_id ${type}, p_key text`, `${type}, text`, "boolean", ` select coalesce(p_id in (select ${qualified$1(ctx, permittedIdsHelper(scope))}(${key})), false)`);
669
- }
670
- case "membership": {
671
- const scope = scopeOf(ctx, helper.scope);
672
- const type = scopeTypeOf(ctx, scope);
673
- return shimFunction(fn, `p_id ${type}`, type, "boolean", ` select coalesce(p_id in (select ${qualified$1(ctx, memberIdsHelper(scope))}()), false)`);
674
- }
675
- case "global": return shimFunction(fn, "p_key text", "text", "boolean", ` select ${qualified$1(ctx, HELPERS.has)}(${key})`);
676
- case "scoped": {
677
- const literals = /* @__PURE__ */ new Map();
678
- for (const scope of ctx.scopes) literals.set(scope.name, scope.name);
679
- for (const [literal, scope] of Object.entries(config.scopes ?? {})) literals.set(literal, scopeOf(ctx, scope));
680
- const global = config.globalScopes ?? [];
681
- return shimFunction(fn, "p_scope text, p_id text, p_key text", "text, text, text", "boolean", ` select case\n${[...global.length === 0 ? [] : [` when p_scope in (${global.map(quoteLiteral).join(", ")}) then ${qualified$1(ctx, HELPERS.has)}(${key})`], ...[...literals].filter(([literal]) => !global.includes(literal)).map(([literal, scope]) => ` when p_scope = ${quoteLiteral(literal)} then coalesce(p_id in (select ids::text from ${qualified$1(ctx, permittedIdsHelper(scope))}(${key}) as ids), false)`)].join("\n")}\n else false\n end`);
682
- }
683
- default: return helper;
684
- }
685
- }
686
- /**
687
- * One wrapper per `rls.migrate.helpers` entry under its legacy name, so SQL
688
- * that `rls migrate` has not rewritten yet (function bodies, views, triggers)
689
- * answers from the PermDock helpers. Each is `security invoker`: it reads no
690
- * table, and the helpers it calls are the `security definer` ones.
691
- */
692
- function shimsSql(ctx, config, shims, renamed) {
693
- const schema = shims.schema ?? "public";
694
- const key = mappedKey(config, renamed);
695
- const chunks = [`-- permdock shims: legacy helper names over the permdock helpers; drop each once doctor reports no caller (PD056)`];
696
- for (const [name, helper] of Object.entries(config.helpers).toSorted(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
697
- if (!NAME.test(name)) throw new Error(`PermDock CLI: unsafe rls.migrate helper name '${name}'`);
698
- chunks.push(shim(ctx, config, key, `${quoteIdent$1(schema)}.${name}`, helper));
699
- }
700
- return `${chunks.join("\n\n")}\n`;
701
- }
702
- //#endregion
703
- //#region src/cli/rls-generate.ts
704
- async function loadPolicy(cwd, config, from) {
705
- const policyPath = from === "drizzle" ? config.policy : from ?? config.policy;
706
- if (policyPath === void 0) throw new Error("PermDock CLI: rls generate needs policy in permdock.config.ts or --from");
707
- return asPolicy(pickNamed(await loadModule(resolve(cwd, policyPath)), ["policy"]));
708
- }
709
- /** Declared role names, and those a tenant admin may compose into custom roles. */
710
- function customRoleNames(policy) {
711
- const declared = [...roleNames(policy)].toSorted();
712
- const assignable = declared.filter((name) => policy.rolesByName.get(name)?.assignable ?? listRoles(policy.vocabulary.roles).some((leaf) => leaf.key === name && leaf.assignable));
713
- const renamed = renamedKeys(policy.vocabulary.permissions);
714
- return renamed.size === 0 ? {
715
- declared,
716
- assignable
717
- } : {
718
- declared,
719
- assignable,
720
- renamed: Object.fromEntries(renamed)
721
- };
722
- }
723
- async function runRlsGenerate(input) {
724
- const rls = input.config.rls;
725
- const fieldsMode = input.fields ?? rls?.fields;
726
- if (fieldsMode !== void 0 && fieldsMode !== "views") return {
727
- code: 2,
728
- output: `rls generate --fields must be views (got ${fieldsMode})`,
729
- text: ""
730
- };
731
- const revokeColumns = input.revokeColumns === true || rls?.revokeColumns === true;
732
- if (revokeColumns && fieldsMode === void 0) return {
733
- code: 2,
734
- output: "rls generate --revoke-columns needs --fields views",
735
- text: ""
736
- };
737
- const policy = await loadPolicy(input.cwd, input.config, input.from);
738
- const memberships = parseMembershipsFlag(input.memberships) ?? rls?.memberships;
739
- if (input.rbac && input.dialect !== "supabase") return {
740
- code: 2,
741
- output: `rls generate --rbac supabase needs --dialect supabase (got ${input.dialect})`,
742
- text: ""
743
- };
744
- const schema = input.rbacSchema ?? rls?.schema ?? rls?.rbac?.schema ?? "permdock";
745
- const authorize = resolveAuthorize(input.config, {
746
- authorize: input.authorize,
747
- rbac: input.rbac,
748
- memberships
749
- });
750
- const memberSources = rls?.membershipSources ?? input.config.supabase?.hook?.memberships;
751
- const sources = authorize === "database" ? memberSources : void 0;
752
- const scopes = scopeList(policy.scopes);
753
- const suspension = checkSuspension(rls?.suspension, scopes);
754
- const hookRoles = input.config.supabase?.hook?.roles;
755
- const roles = rls?.roles ?? (hookRoles === false ? void 0 : hookRoles);
756
- if (roles !== void 0 && input.rbac) return {
757
- code: 2,
758
- 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",
759
- text: ""
760
- };
761
- const ownership = ownershipRules(policy, scopes);
762
- const graph = graphPlan(policy);
763
- const ctx = {
764
- dialect: input.dialect,
765
- scopes,
766
- tenantClaim: rls?.tenantClaim ?? "tenant_id",
767
- gucPrefix: input.gucPrefix ?? rls?.gucPrefix ?? "app",
768
- ...rls?.actions === void 0 ? {} : { actions: rls.actions },
769
- inlineFunctions: input.inlineFunctions || rls?.inlineFunctions === true,
770
- schema,
771
- authorize,
772
- roleClaim: rls?.roleClaim ?? "user_role",
773
- tenantType: input.tenantType ?? rls?.tenantType ?? "uuid",
774
- ...rls?.teamType === void 0 ? {} : { teamType: rls.teamType },
775
- ...rls?.scopeTypes === void 0 ? {} : { scopeTypes: rls.scopeTypes },
776
- ...memberships === void 0 ? {} : { memberships },
777
- ...sources === void 0 || sources.length === 0 ? {} : { sources },
778
- ...memberSources === void 0 || memberSources.length === 0 ? {} : { memberSources },
779
- ...suspension === void 0 ? {} : { suspension },
780
- ...roles === void 0 ? {} : { roles },
781
- ...input.customRoles === true || rls?.customRoles === true ? { customRoles: customRoleNames(policy) } : {},
782
- ...input.capabilities === true || rls?.capabilities === true ? { capabilities: true } : {},
783
- ...rls?.anonymousSignIns === "deny" ? { anonymousSignIns: "deny" } : {},
784
- ...ownership === void 0 ? {} : { ownership },
785
- ...fieldsMode === void 0 ? {} : { fields: fieldsMode },
786
- ...graph.size === 0 ? {} : { graph: {
787
- closures: closureDepths(graph),
788
- resources: policy.resources,
789
- ...rls?.tables === void 0 ? {} : { tables: rls.tables }
790
- } }
791
- };
792
- if (ctx.anonymousSignIns === "deny" && ctx.dialect !== "supabase") return {
793
- code: 2,
794
- output: `rls.anonymousSignIns needs --dialect supabase (got ${ctx.dialect})`,
795
- text: ""
796
- };
797
- const warnings = [];
798
- if (authorize === "database") {
799
- 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`);
800
- }
801
- const compiled = compileGrants(policy, ctx, rls?.tables, warnings, input.skipClosures);
802
- const views = fieldsMode === void 0 ? [] : fieldViews(policy, ctx, compiled.branches, { revokeColumns }, warnings);
803
- const force = input.force === true || rls?.force === true;
804
- 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");
805
- const policyName = input.policyName ?? rls?.policyName;
806
- const helpersOnly = input.helpersOnly === true || rls?.helpersOnly === true;
807
- if (helpersOnly && (input.target !== "sql" || fieldsMode !== void 0)) return {
808
- code: 2,
809
- output: "rls generate --helpers-only needs --target sql and no --fields",
810
- text: ""
811
- };
812
- const policies = helpersOnly ? [] : assemblePolicies(fieldsMode === void 0 ? compiled.branches : rowBranches(compiled.branches), {
813
- perRole: input.policyPerRole === true || rls?.policyPerRole === true,
814
- ...policyName === void 0 ? {} : { name: policyName }
815
- });
816
- const rootMapped = ctx.scopes[0] === void 0 ? void 0 : scopeTable(ctx, ctx.scopes[0].name);
817
- const rootTable = rootMapped === void 0 ? void 0 : {
818
- ...rootMapped.table,
819
- tenant: rootMapped.column
820
- };
821
- const rbac = input.rbac ? rbacScaffold(policy, {
822
- schema,
823
- authorize,
824
- ...rootTable === void 0 ? {} : { memberships: rootTable },
825
- ...ctx.scopes[0] === void 0 ? {} : { scope: ctx.scopes[0].name },
826
- ...ctx.customRoles === void 0 ? {} : { customRoles: { declared: ctx.customRoles.declared } },
827
- context: ctx
828
- }) : void 0;
829
- if (rbac !== void 0) warnings.push(...rbac.warnings);
830
- if (input.rbac) warnings.push("the token hook that writes user_role and memberships comes from permdock supabase hook generate");
831
- const owned = ownershipSql(ctx);
832
- const graphed = graphSql(ctx, graph, rls?.tables);
833
- const breakGlass = breakGlassSql(ctx, breakGlassEntries(policy, rls?.tables));
834
- const configured = rls?.shims;
835
- const shimsConfig = typeof configured === "object" ? configured : configured === true || input.shims === true ? {} : void 0;
836
- if (shimsConfig !== void 0 && rls?.migrate === void 0) return {
837
- code: 2,
838
- output: "PermDock CLI: rls generate --shims needs rls.migrate.helpers in permdock.config.ts",
839
- text: ""
840
- };
841
- const shims = shimsConfig === void 0 || rls?.migrate === void 0 ? void 0 : shimsSql(ctx, rls.migrate, shimsConfig, Object.fromEntries(renamedKeys(policy.vocabulary.permissions)));
842
- const preamble = [
843
- rbac?.head,
844
- helpersSql(ctx, compiled.rolePermissions, {
845
- userRoles: !input.rbac,
846
- anonExecute: views.some((view) => view.roles.includes("anon")),
847
- withoutSeeds: splitsPart(input.split, "seeds")
848
- }),
849
- owned === "" ? void 0 : owned,
850
- graphed === "" ? void 0 : graphed,
851
- breakGlass === "" ? void 0 : breakGlass,
852
- shims,
853
- rbac?.tail
854
- ].filter((part) => part !== void 0).join("\n");
855
- if (force) for (const entry of graph.values()) {
856
- const own = [...entry.relations].filter((name) => !("edge" in (entry.node.relations[name] ?? {})));
857
- 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`);
858
- }
859
- const outRel = input.out ?? rls?.out ?? defaultOut(input.target);
860
- const migration = input.target === "sql" ? "" : dialectRoles(migrationSql(policies, preamble, force, views), ctx.dialect);
861
- const migrationRel = migration === "" ? void 0 : migrationOut(outRel);
862
- const helpers = migrationRel === void 0 ? void 0 : basename(migrationRel);
863
- let text;
864
- switch (input.target) {
865
- case "sql":
866
- text = dialectRoles(emitSql(policies, preamble, force, views), ctx.dialect);
867
- break;
868
- case "drizzle":
869
- text = emitDrizzle(policies, compact({
870
- dialect: ctx.dialect,
871
- schema: rls?.drizzle?.schema,
872
- exports: rls?.drizzle?.exports,
873
- helpers
874
- }));
875
- break;
876
- case "prisma":
877
- text = emitPrisma(policies, compact({
878
- models: rls?.prisma?.models,
879
- helpers
880
- }));
881
- break;
882
- default: return input.target;
883
- }
884
- const indexes = indexTargets(ctx, compiled.rowColumns);
885
- if (!splitsPart(input.split, "indexes")) for (const target of indexes) warnings.push(`index suggestion: create index on ${target.table} (${target.columns.join(", ")}), or add indexes to --split`);
886
- const extras = migration === "" ? {} : { migration };
887
- if (input.write === false) return {
888
- code: 0,
889
- output: warnings.join("\n"),
890
- text,
891
- policies,
892
- seeds: compiled.rolePermissions,
893
- indexes,
894
- keys: {
895
- permissions: listPermissions(policy.vocabulary.permissions).map((leaf) => leaf.key),
896
- rowConditions: [...policyRowConditionKeys(policy)],
897
- renamed: Object.fromEntries(renamedKeys(policy.vocabulary.permissions))
898
- },
899
- schema,
900
- helpersOnly,
901
- ...extras
902
- };
903
- const planned = outputFiles({
904
- input,
905
- outRel,
906
- text,
907
- helpersOnly,
908
- sql: () => ({
909
- policies: dialectRoles(emitSql(policies, "", force, views), ctx.dialect),
910
- helpers: dialectRoles(emitSql([], preamble), ctx.dialect),
911
- seeds: `${SEEDS_MARKER} schema=${schema}\n${seedSql(ctx, compiled.rolePermissions)}\n`,
912
- indexes: `${INDEXES_MARKER}\n${indexesSql(indexes)}\n`
913
- }),
914
- scopes,
915
- schema
916
- });
917
- if (typeof planned === "string") return {
918
- code: 2,
919
- output: planned,
920
- text,
921
- ...extras
922
- };
923
- const files = [...planned];
924
- if (migrationRel !== void 0) files.push({
925
- part: "migration",
926
- rel: migrationRel,
927
- text: migration
928
- });
929
- if (input.check) {
930
- const drift = driftOf(input.cwd, files);
931
- return drift.length === 0 ? {
932
- code: 0,
933
- output: "rls generate up to date",
934
- text,
935
- ...extras
936
- } : {
937
- code: 1,
938
- output: drift.map((line) => `rls generate drift: ${line}`).join("\n"),
939
- text,
940
- ...extras
941
- };
942
- }
943
- const written = writeSqlFiles(input.cwd, files);
944
- const extra = warnings.length === 0 ? "" : `\n${warnings.join("\n")}`;
945
- return {
946
- code: 0,
947
- output: `wrote ${written.wrote.join(", ")}${written.printed === "" ? "" : `\n${written.printed}`}${extra}`,
948
- text,
949
- ...extras
950
- };
951
- }
952
- /** Whether `--split` names `part`, which then leaves the helpers or the warnings. */
953
- function splitsPart(raw, part) {
954
- const split = parseSplit(raw);
955
- return typeof split !== "string" && split?.includes(part) === true;
956
- }
957
- /** The files `generate` writes: one, or one per `--split` part, plus the hook's grants. */
958
- function outputFiles(plan) {
959
- const { input } = plan;
960
- const split = parseSplit(input.split);
961
- if (typeof split === "string") return split;
962
- if (split?.includes("policies") === true && plan.helpersOnly) return "rls generate --helpers-only writes no policies part; drop it from --split";
963
- if (split === void 0) {
964
- if (input.grantsOut !== void 0) return "rls generate --grants-out needs --split with the hook part: the grants are the token hook's";
965
- if (input.seedsOut !== void 0) return "rls generate --seeds-out needs --split with the seeds part";
966
- return [{
967
- part: "rls",
968
- rel: plan.outRel,
969
- text: plan.text
970
- }];
971
- }
972
- if (input.target !== "sql") return "rls generate --split needs --target sql";
973
- const pgDelta = input.out === void 0 && input.config.rls?.out === void 0 ? supabaseConfig(input.cwd).pgDelta : void 0;
974
- 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})`;
975
- 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";
976
- 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";
977
- const at = (part, schema = plan.schema) => pgDelta === void 0 ? partPath(plan.outRel, part) : pgDeltaPath(pgDelta.schemaDir, part, schema);
978
- if (input.grantsOut !== void 0 && !split.includes("hook")) return "rls generate --grants-out needs the hook part in --split";
979
- if (input.seedsOut !== void 0 && !split.includes("seeds")) return "rls generate --seeds-out needs the seeds part in --split";
980
- if (split.includes("hook") && input.config.supabase?.hook === void 0) return "rls generate --split hook needs supabase.hook in permdock.config.ts";
981
- const sql = plan.sql();
982
- const files = [];
983
- for (const part of split) switch (part) {
984
- case "helpers":
985
- files.push({
986
- part,
987
- rel: at(part),
988
- text: sql.helpers
989
- });
990
- break;
991
- case "seeds":
992
- files.push({
993
- part,
994
- rel: input.seedsOut ?? at(part),
995
- text: sql.seeds
996
- });
997
- break;
998
- case "indexes":
999
- files.push({
1000
- part,
1001
- rel: at(part),
1002
- text: sql.indexes
1003
- });
1004
- break;
1005
- case "policies":
1006
- files.push({
1007
- part,
1008
- rel: at(part),
1009
- text: sql.policies
1010
- });
1011
- break;
1012
- case "hook": {
1013
- const hook = supabaseHookSql(plan.scopes, input.config, {}, grantsLabel(input.grantsOut));
1014
- files.push({
1015
- part,
1016
- rel: at(part, hook.manifest.hook.schema),
1017
- text: hook.sql
1018
- });
1019
- if (input.grantsOut !== void 0) files.push({
1020
- part: "grants",
1021
- rel: input.grantsOut,
1022
- text: hook.grants
1023
- });
1024
- break;
1025
- }
1026
- default: return part;
1027
- }
1028
- return files;
1029
- }
1030
- //#endregion
1031
- //#region src/cli/rls-introspect.ts
1032
- const ROLES = ["anon", "authenticated"];
1033
- /** Neon's `anonymous` is the role `rls generate` writes as `anon` for the other dialects. */
1034
- function liveRole(role) {
1035
- return role === "anonymous" ? "anon" : role;
1036
- }
1037
- const PRIVILEGES = [
1038
- "select",
1039
- "insert",
1040
- "update",
1041
- "delete"
1042
- ];
1043
- /** `posts` and `"app"."posts"` as `public.posts` and `app.posts`. */
1044
- function qualified(table) {
1045
- const parts = table.split(".").map((part) => part.replaceAll("\"", ""));
1046
- return parts.length === 1 ? `public.${parts[0] ?? ""}` : parts.join(".");
1047
- }
1048
- const FUNCTION_HEADER = /create or replace function ("?)([A-Za-z_][\w$]*)\1\.("?)([A-Za-z_][\w$]*)\3\s*\(([\s\S]*?)\nas \$/gu;
1049
- function helpersOf(sql) {
1050
- const found = /* @__PURE__ */ new Set();
1051
- for (const match of sql.matchAll(FUNCTION_HEADER)) {
1052
- const [header = "", , schema = "", , name = ""] = match;
1053
- if (/\bsecurity definer\b/u.test(header)) found.add(`${schema}.${name}`);
1054
- }
1055
- return [...found];
1056
- }
1057
- function expectedRls(policies, sql, indexes = []) {
1058
- const tables = [...new Set(policies.map((item) => qualified(item.table)))];
1059
- const grants = {};
1060
- for (const table of tables) {
1061
- const byRole = {};
1062
- for (const role of ROLES) {
1063
- const commands = new Set(policies.filter((item) => qualified(item.table) === table && item.effect === "allow" && item.roles.includes(role)).map((item) => item.command));
1064
- byRole[role] = PRIVILEGES.filter((privilege) => commands.has(privilege));
1065
- }
1066
- grants[table] = byRole;
1067
- }
1068
- return {
1069
- policies: policies.map((item) => ({
1070
- table: qualified(item.table),
1071
- name: item.name,
1072
- command: item.command,
1073
- permissive: item.effect === "allow",
1074
- roles: item.roles.toSorted()
1075
- })),
1076
- tables,
1077
- grants,
1078
- helpers: helpersOf(sql),
1079
- indexes: indexes.map((target) => ({
1080
- ...target,
1081
- table: qualified(target.table)
1082
- }))
1083
- };
1084
- }
1085
- /**
1086
- * One warning per index target no index starts with. A missing index slows
1087
- * the policy down but grants nothing, so these do not fail `verify`.
1088
- */
1089
- function missingIndexes(expected, actual) {
1090
- return expected.indexes.flatMap((target) => {
1091
- const column = target.columns[0];
1092
- return column === void 0 || actual.leadingColumns[target.table]?.includes(column) === true ? [] : [`warning: ${target.table}: no index starts with ${column}, which the policies or helpers filter on; add it, or write it with rls generate --split ...,indexes`];
1093
- });
1094
- }
1095
- function sameList(left, right) {
1096
- const a = left.toSorted();
1097
- const b = right.toSorted();
1098
- return a.length === b.length && a.every((item, index) => item === b[index]);
1099
- }
1100
- /** One line per difference; policy expressions are not compared, since Postgres rewrites their text. */
1101
- function diffRls(expected, actual, options = {}) {
1102
- const out = [];
1103
- const found = new Map(actual.policies.map((item) => [`${item.table}\u0000${item.name}`, item]));
1104
- for (const want of expected.policies) {
1105
- const have = found.get(`${want.table}\u0000${want.name}`);
1106
- if (have === void 0) {
1107
- out.push(`${want.table}: policy ${want.name} is missing`);
1108
- continue;
1109
- }
1110
- if (have.command !== want.command) out.push(`${want.table}: policy ${want.name} is for ${have.command}, expected ${want.command}`);
1111
- if (have.permissive !== want.permissive) out.push(`${want.table}: policy ${want.name} is ${have.permissive ? "permissive" : "restrictive"}, expected ${want.permissive ? "permissive" : "restrictive"}`);
1112
- if (!sameList(have.roles, want.roles)) out.push(`${want.table}: policy ${want.name} applies to ${have.roles.join(", ")}, expected ${want.roles.join(", ")}`);
1113
- }
1114
- const wanted = new Set(expected.policies.map((item) => `${item.table}\u0000${item.name}`));
1115
- for (const have of actual.policies) if (expected.tables.includes(have.table) && !wanted.has(`${have.table}\u0000${have.name}`)) out.push(`${have.table}: policy ${have.name} is not generated (${have.permissive ? "permissive" : "restrictive"} ${have.command}); a permissive one widens access`);
1116
- for (const table of expected.tables) {
1117
- if (actual.rlsEnabled[table] !== true) {
1118
- out.push(actual.rlsEnabled[table] === void 0 ? `${table}: table is missing` : `${table}: row level security is disabled`);
1119
- continue;
1120
- }
1121
- for (const role of ROLES) {
1122
- const want = expected.grants[table]?.[role] ?? [];
1123
- const have = actual.grants[table]?.[role] ?? [];
1124
- for (const privilege of want) if (!have.includes(privilege) && !(privilege === "select" && options.columnGrants === true)) out.push(`${table}: ${role} lacks ${privilege}, so the policy answers 42501 instead of filtering`);
1125
- for (const privilege of have) if (!want.includes(privilege)) out.push(`${table}: ${role} holds ${privilege}, which no generated policy allows`);
1126
- }
1127
- }
1128
- for (const helper of expected.helpers) {
1129
- const have = actual.helpers[helper];
1130
- if (have === void 0) {
1131
- out.push(`${helper}: helper is missing`);
1132
- continue;
1133
- }
1134
- if (!have.securityDefiner) out.push(`${helper}: helper is not security definer`);
1135
- if (!have.emptySearchPath) out.push(`${helper}: helper does not set search_path = ''`);
1136
- }
1137
- return out;
1138
- }
1139
- const POLICIES_SQL = `select schemaname || '.' || tablename as target, policyname, cmd, permissive, roles::text[] as roles
1140
- from pg_policies
1141
- where schemaname || '.' || tablename = any($1::text[])`;
1142
- const TABLES_SQL = `select n.nspname || '.' || c.relname as target, c.relrowsecurity as enabled
1143
- from pg_class c
1144
- join pg_namespace n on n.oid = c.relnamespace
1145
- where c.relkind in ('r', 'p') and n.nspname || '.' || c.relname = any($1::text[])`;
1146
- const GRANTS_SQL = `select table_schema || '.' || table_name as target, grantee, lower(privilege_type) as privilege
1147
- from information_schema.role_table_grants
1148
- where grantee in ('anon', 'anonymous', 'authenticated')
1149
- and lower(privilege_type) in ('select', 'insert', 'update', 'delete')
1150
- and table_schema || '.' || table_name = any($1::text[])`;
1151
- const HELPERS_SQL = `select n.nspname || '.' || p.proname as target, p.prosecdef as definer,
1152
- coalesce(array_to_string(p.proconfig, ','), '') as config
1153
- from pg_proc p
1154
- join pg_namespace n on n.oid = p.pronamespace
1155
- where n.nspname || '.' || p.proname = any($1::text[])`;
1156
- const INDEXES_SQL = `select n.nspname || '.' || c.relname as target, a.attname as leading
1157
- from pg_index i
1158
- join pg_class c on c.oid = i.indrelid
1159
- join pg_namespace n on n.oid = c.relnamespace
1160
- join pg_attribute a on a.attrelid = i.indrelid and a.attnum = i.indkey[0]
1161
- where i.indpred is null and n.nspname || '.' || c.relname = any($1::text[])`;
1162
- function commandOf(cmd) {
1163
- const text = String(cmd ?? "").toLowerCase();
1164
- return text === "*" ? "all" : text;
1165
- }
1166
- function connectIntrospect(db) {
1167
- return connectPgPool(db, "permdock rls verify --introspect");
1168
- }
1169
- /** Reads the catalogs for the tables and helpers `expected` names (postgres-meta's queries, trimmed). */
1170
- async function introspectRls(db, expected, connect = connectIntrospect) {
1171
- const client = await connect(db);
1172
- try {
1173
- const tables = [...expected.tables];
1174
- const [policies, enabled, grants, helpers, indexes] = await Promise.all([
1175
- client.query(POLICIES_SQL, [tables]),
1176
- client.query(TABLES_SQL, [tables]),
1177
- client.query(GRANTS_SQL, [tables]),
1178
- client.query(HELPERS_SQL, [[...expected.helpers]]),
1179
- client.query(INDEXES_SQL, [[...new Set(expected.indexes.map((target) => target.table))]])
1180
- ]);
1181
- const leadingColumns = {};
1182
- for (const row of indexes.rows) (leadingColumns[String(row["target"])] ??= []).push(String(row["leading"]));
1183
- const byTable = {};
1184
- for (const row of grants.rows) {
1185
- const table = String(row["target"]);
1186
- const role = liveRole(String(row["grantee"]));
1187
- const entry = byTable[table] ??= {};
1188
- (entry[role] ??= []).push(String(row["privilege"]));
1189
- }
1190
- return {
1191
- policies: policies.rows.map((row) => ({
1192
- table: String(row["target"]),
1193
- name: String(row["policyname"]),
1194
- command: commandOf(row["cmd"]),
1195
- permissive: String(row["permissive"]).toUpperCase() === "PERMISSIVE",
1196
- roles: Array.isArray(row["roles"]) ? row["roles"].map((role) => liveRole(String(role))) : []
1197
- })),
1198
- rlsEnabled: Object.fromEntries(enabled.rows.map((row) => [String(row["target"]), row["enabled"] === true])),
1199
- grants: byTable,
1200
- helpers: Object.fromEntries(helpers.rows.map((row) => [String(row["target"]), {
1201
- securityDefiner: row["definer"] === true,
1202
- emptySearchPath: String(row["config"]).split(",").some((item) => item === "search_path=\"\"" || item === "search_path=''")
1203
- }])),
1204
- leadingColumns
1205
- };
1206
- } finally {
1207
- await client.end();
1208
- }
1209
- }
1210
- const SYSTEM_SCHEMAS = [
1211
- "pg_catalog",
1212
- "information_schema",
1213
- "pg_toast",
1214
- "auth",
1215
- "storage",
1216
- "realtime",
1217
- "extensions",
1218
- "graphql",
1219
- "graphql_public",
1220
- "vault",
1221
- "pgsodium",
1222
- "pgsodium_masks",
1223
- "net",
1224
- "cron",
1225
- "supabase_functions",
1226
- "supabase_migrations"
1227
- ];
1228
- function inScope(table) {
1229
- const schema = table.split(".")[0] ?? "";
1230
- return HELPER_TABLES.some((item) => item === table) || !(SYSTEM_SCHEMAS.some((item) => item === schema) || schema.startsWith("pg_"));
1231
- }
1232
- function seedLine(row) {
1233
- return `${row.role} ${row.effect} ${row.grantKey} on ${row.scope}`;
1234
- }
1235
- /** Drift is an error; `info` names RLS tables whose policies call no PermDock helper. */
1236
- function diffMixed(expected, actual) {
1237
- const drift = [];
1238
- const want = new Set(expected.seeds.map(seedLine));
1239
- const have = new Set(actual.seeds.map(seedLine));
1240
- for (const line of want) if (!have.has(line)) drift.push(`${expected.schema}.role_permissions: missing ${line}`);
1241
- for (const line of have) if (!want.has(line)) drift.push(`${expected.schema}.role_permissions: unexpected ${line}`);
1242
- const permissions = new Set(expected.permissions);
1243
- const conditioned = new Set(expected.rowConditions);
1244
- const covered = /* @__PURE__ */ new Set();
1245
- for (const policy of actual.policies) {
1246
- if (callsHelper(policy.expression)) covered.add(policy.table);
1247
- 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`);
1248
- else if (conditioned.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`);
1249
- }
1250
- return {
1251
- drift,
1252
- info: actual.rlsTables.filter((table) => !covered.has(table)).map((table) => `${table}: no policy calls a PermDock helper`)
1253
- };
1254
- }
1255
- const ALL_POLICIES_SQL = `select schemaname || '.' || tablename as target, policyname,
1256
- coalesce(qual, '') || ' ' || coalesce(with_check, '') as expression
1257
- from pg_policies`;
1258
- const RLS_TABLES_SQL = `select n.nspname || '.' || c.relname as target
1259
- from pg_class c
1260
- join pg_namespace n on n.oid = c.relnamespace
1261
- where c.relkind in ('r', 'p') and c.relrowsecurity`;
1262
- const quoteIdent = escapeSqlIdent;
1263
- /** Reads `role_permissions`, `pg_policies` and the RLS-enabled tables for {@link diffMixed}. */
1264
- async function introspectMixed(db, schema, connect = connectIntrospect) {
1265
- const client = await connect(db);
1266
- try {
1267
- const [seeds, policies, tables] = await Promise.all([
1268
- client.query(`select role, permission, grant_key, scope, effect from ${quoteIdent(schema)}.role_permissions`, []),
1269
- client.query(ALL_POLICIES_SQL, []),
1270
- client.query(RLS_TABLES_SQL, [])
1271
- ]);
1272
- return {
1273
- seeds: seeds.rows.map((row) => ({
1274
- role: String(row["role"]),
1275
- permission: String(row["permission"]),
1276
- grantKey: String(row["grant_key"]),
1277
- scope: String(row["scope"]),
1278
- effect: String(row["effect"]) === "deny" ? "deny" : "allow"
1279
- })),
1280
- policies: policies.rows.map((row) => ({
1281
- table: String(row["target"]),
1282
- name: String(row["policyname"]),
1283
- expression: String(row["expression"])
1284
- })).filter((row) => inScope(row.table)),
1285
- rlsTables: tables.rows.map((row) => String(row["target"])).filter(inScope).toSorted()
1286
- };
1287
- } finally {
1288
- await client.end();
1289
- }
1290
- }
1291
- //#endregion
1292
- //#region src/cli/rls-verify-tree.ts
1293
- const SUBJECTS = 3;
1294
- function tableSql(name) {
1295
- return quoteTable(name.includes(".") ? name : `public.${name}`);
1296
- }
1297
- function idFactory(node) {
1298
- const type = columnTypesOf(jsonSchemaOf(node))[node.id];
1299
- let next = 9e5;
1300
- return type === "numeric" ? () => {
1301
- next += 1;
1302
- return next;
1303
- } : () => randomUUID();
1304
- }
1305
- /**
1306
- * Two spines one level deeper than the walk depth, so the depth cap is
1307
- * crossed; side branches under the first levels alternate restricted, and
1308
- * the second spine is restricted two levels down.
1309
- */
1310
- function generateTree(node, depth) {
1311
- const id = idFactory(node);
1312
- const nodes = [];
1313
- for (const spine of [0, 1]) {
1314
- let parent = null;
1315
- for (let level = 0; level <= depth + 1; level += 1) {
1316
- const current = id();
1317
- nodes.push({
1318
- id: current,
1319
- parent,
1320
- restricted: node.restricted !== void 0 && spine === 1 && level === 2
1321
- });
1322
- if (spine === 0 && level >= 1 && level <= 3) {
1323
- const side = id();
1324
- nodes.push({
1325
- id: side,
1326
- parent: current,
1327
- restricted: node.restricted !== void 0 && level % 2 === 1
1328
- });
1329
- nodes.push({
1330
- id: id(),
1331
- parent: side,
1332
- restricted: false
1333
- });
1334
- }
1335
- parent = current;
1336
- }
1337
- }
1338
- return nodes;
1339
- }
1340
- function insertSql(table, rows) {
1341
- const columns = Object.keys(rows[0] ?? {});
1342
- const values = [];
1343
- const tuples = rows.map((row) => `(${columns.map((column) => {
1344
- values.push(row[column]);
1345
- return `$${String(values.length)}`;
1346
- }).join(", ")})`);
1347
- return {
1348
- sql: `insert into ${tableSql(table)} (${columns.map(quoteIdent$1).join(", ")}) values ${tuples.join(", ")} returning *`,
1349
- values
1350
- };
1351
- }
1352
- const REQUIRED_COLUMNS_SQL = `select a.attname as name,
1353
- format_type(a.atttypid, a.atttypmod) as type,
1354
- k.confrelid::regclass::text as "refTable",
1355
- r.attname as "refColumn"
1356
- from pg_attribute a
1357
- left join pg_constraint k
1358
- on k.conrelid = a.attrelid and k.contype = 'f' and k.conkey = array[a.attnum]
1359
- left join pg_attribute r
1360
- on r.attrelid = k.confrelid and r.attnum = k.confkey[1]
1361
- where a.attrelid = $1::regclass and a.attnum > 0 and not a.attisdropped
1362
- and a.attnotnull and not a.atthasdef
1363
- and a.attidentity = '' and a.attgenerated = ''`;
1364
- function placeholder(type) {
1365
- if (type === "uuid") return randomUUID();
1366
- if (type === "boolean") return false;
1367
- if (/^(smallint|integer|bigint|numeric|real|double precision)/u.test(type)) return 0;
1368
- if (type.startsWith("timestamp") || type === "date") return (/* @__PURE__ */ new Date()).toISOString();
1369
- if (type === "json" || type === "jsonb") return {};
1370
- return "permdock-tree";
1371
- }
1372
- function asRequiredColumn(row) {
1373
- const { name, type, refTable, refColumn } = row;
1374
- if (typeof name !== "string" || typeof type !== "string") return;
1375
- return {
1376
- name,
1377
- type,
1378
- refTable: typeof refTable === "string" ? refTable : null,
1379
- refColumn: typeof refColumn === "string" ? refColumn : null
1380
- };
1381
- }
1382
- /**
1383
- * Values for the required columns a generated row leaves out, such as a
1384
- * tenant column or a name: an existing value of the referenced table for a
1385
- * foreign key, a placeholder of the column's type otherwise.
1386
- */
1387
- async function requiredValues(query, table, set) {
1388
- const columns = (await query(REQUIRED_COLUMNS_SQL, [table])).rows.map(asRequiredColumn).filter((column) => column !== void 0 && !set.has(column.name));
1389
- const values = {};
1390
- for (const column of columns) if (column.refTable !== null && column.refColumn !== null) {
1391
- const existing = await query(`select ${quoteIdent$1(column.refColumn)} as value from ${column.refTable} limit 1`);
1392
- values[column.name] = existing.rows[0]?.["value"] ?? null;
1393
- } else values[column.name] = placeholder(column.type);
1394
- return values;
1395
- }
1396
- /** Inserts `rows` after filling the required columns they leave out. */
1397
- async function seedRows(query, table, rows) {
1398
- const fill = await requiredValues(query, tableSql(table), new Set(Object.keys(rows[0] ?? {})));
1399
- const inserted = insertSql(table, rows.map((row) => ({
1400
- ...fill,
1401
- ...row
1402
- })));
1403
- return (await query(inserted.sql, inserted.values)).rows;
1404
- }
1405
- /**
1406
- * Parity between `decide` and RLS on a generated object graph: for every
1407
- * self-parented resource a graph grant walks, a tree with restricted
1408
- * branches and a chain past the walk depth, child rows under it, and edges
1409
- * for a few generated principals. Everything runs in one transaction that
1410
- * is rolled back.
1411
- */
1412
- async function verifyTree(input) {
1413
- const { policy, query } = input;
1414
- const tables = input.config.rls?.tables;
1415
- const walked = [...graphPlan(policy).values()].filter((entry) => entry.closure !== void 0);
1416
- if (walked.length === 0) return {
1417
- checked: 0,
1418
- granted: 0,
1419
- mismatches: [],
1420
- notes: ["no graph grant walks a self-parented resource"]
1421
- };
1422
- const subjects = Array.from({ length: SUBJECTS }, () => randomUUID());
1423
- const rows = {};
1424
- const edges = {};
1425
- const now = Date.now();
1426
- for (const entry of walked) {
1427
- const node = entry.node;
1428
- const parent = node.parent;
1429
- if (parent === void 0) continue;
1430
- const tree = generateTree(node, entry.closure ?? 0);
1431
- const seeded = tree.map((item, index) => {
1432
- const row = {
1433
- [node.id]: item.id,
1434
- [parent.field]: item.parent
1435
- };
1436
- if (node.restricted !== void 0) row[node.restricted] = item.restricted;
1437
- for (const name of entry.relations) {
1438
- const spec = node.relations[name];
1439
- const column = isFieldRelation(spec) ? spec.memberOf === void 0 ? spec.field : void 0 : isPrincipalRelation(spec) ? spec.principal : void 0;
1440
- if (column !== void 0 && column !== parent.field) row[column] = index % 4 === 0 ? subjects[index % SUBJECTS] : randomUUID();
1441
- }
1442
- return row;
1443
- });
1444
- if ([...entry.relations].some((name) => {
1445
- const spec = node.relations[name];
1446
- return isPrincipalRelation(spec) && spec.principal === parent.field || isFieldRelation(spec) && spec.field === parent.field;
1447
- })) subjects.push(...tree.filter((_, index) => index % 3 === 0).map((item) => String(item.id)));
1448
- rows[node.name] = [...await seedRows(query, tables?.[node.name] ?? node.name, seeded)];
1449
- for (const name of entry.relations) {
1450
- const spec = node.relations[name];
1451
- if (!isEdgeRelation(spec)) continue;
1452
- const object = spec.object ?? `${node.name}_id`;
1453
- const subject = spec.subject ?? "user_id";
1454
- const list = [];
1455
- const edgeRows = [];
1456
- for (const [index, item] of tree.entries()) {
1457
- const holder = subjects[index % SUBJECTS];
1458
- if (holder === void 0 || index % 5 !== 1) continue;
1459
- const expiresAt = spec.expiresAt !== void 0 && index % 10 === 6 ? /* @__PURE__ */ new Date(now - 864e5) : void 0;
1460
- edgeRows.push({
1461
- [object]: item.id,
1462
- [subject]: holder,
1463
- ...spec.expiresAt === void 0 ? {} : { [spec.expiresAt]: expiresAt ?? null }
1464
- });
1465
- list.push({
1466
- id: String(item.id),
1467
- principal: holder,
1468
- ...expiresAt === void 0 ? {} : { expiresAt: expiresAt.getTime() / 1e3 }
1469
- });
1470
- }
1471
- if (edgeRows.length > 0) await seedRows(query, spec.edge, edgeRows);
1472
- edges[node.name] = {
1473
- ...edges[node.name],
1474
- [name]: list
1475
- };
1476
- }
1477
- for (const child of policy.resources.values()) {
1478
- if (child.name === node.name || child.parent?.resource !== node.name) continue;
1479
- const make = idFactory(child);
1480
- const childRows = tree.map((item, index) => {
1481
- const row = {
1482
- [child.id]: make(),
1483
- [child.parent?.field ?? ""]: item.id
1484
- };
1485
- if (child.restricted !== void 0) row[child.restricted] = index % 7 === 3;
1486
- return row;
1487
- });
1488
- rows[child.name] = [...await seedRows(query, tables?.[child.name] ?? child.name, childRows)];
1489
- }
1490
- }
1491
- const relations = memoryRelations(policy.permissions, {
1492
- rows,
1493
- edges
1494
- });
1495
- const mismatches = [];
1496
- let checked = 0;
1497
- let granted = 0;
1498
- const reads = listPermissions(policy.permissions).filter((leaf) => leaf.kind === "instance" && commandFor(leaf.action, input.config.rls?.actions) === "select" && rows[leaf.resource] !== void 0);
1499
- for (const subject of subjects) {
1500
- const permdock = await createPermDock(policy, {
1501
- principal: {
1502
- id: subject,
1503
- roles: []
1504
- },
1505
- context: {}
1506
- }, { relations });
1507
- await input.bind(subject);
1508
- for (const permission of reads) {
1509
- const node = policy.resources.get(permission.resource);
1510
- const list = rows[permission.resource] ?? [];
1511
- if (node === void 0 || list.length === 0) continue;
1512
- const ids = list.map((row) => String(row[node.id]));
1513
- const visible = await query(`select ${quoteIdent$1(node.id)}::text as id from ${tableSql(tables?.[node.name] ?? node.name)} where ${quoteIdent$1(node.id)} = any($1)`, [ids]);
1514
- const seen = new Set(visible.rows.map((row) => String(row["id"])));
1515
- for (const row of list) {
1516
- checked += 1;
1517
- const id = String(row[node.id]);
1518
- const allowed = permdock.can(permission, row);
1519
- if (allowed !== seen.has(id)) mismatches.push(`${permission.key} ${id}: in-process ${allowed ? "granted" : "denied"}, database ${seen.has(id) ? "allowed" : "filtered"}`);
1520
- else if (allowed) granted += 1;
1521
- }
1522
- }
1523
- }
1524
- return {
1525
- checked,
1526
- granted,
1527
- mismatches,
1528
- notes: []
1529
- };
1530
- }
1531
- //#endregion
1532
- //#region src/cli/rls-verify.ts
1533
- function isRecord(value) {
1534
- return value !== null && typeof value === "object" && !Array.isArray(value);
1535
- }
1536
- /** A custom role's scope and pinned instance, as the `custom_role_*` tables store them. */
1537
- function customRoleAt(role, scopes) {
1538
- return {
1539
- scope: customRoleScope(role, scopes),
1540
- id: role.scope === void 0 ? role.team : role.id
1541
- };
1542
- }
1543
- function canFixture(permdock, permission, row) {
1544
- if (permission.kind === "collection") return permdock.can(permission, row);
1545
- return permdock.can(permission, row);
1546
- }
1547
- function grantKind(policy, key) {
1548
- const grants = policy.grants.filter((grant) => grant.permission.key === key);
1549
- if (grants.some((grant) => hasConditionOp(grant.where, "opaque") || hasConditionOp(grant.check, "opaque"))) return "opaque";
1550
- if (grants.some((grant) => hasConditionOp(grant.where, "sqlFunction") || hasConditionOp(grant.check, "sqlFunction"))) return "sqlFunction";
1551
- return "portable";
1552
- }
1553
- function columnsOf(row) {
1554
- return isRecord(row) ? Object.entries(row).filter(([, value]) => value !== void 0 && typeof value !== "object") : [];
1555
- }
1556
- /**
1557
- * The statement a fixture runs: `create` inserts the whole row, `update` writes
1558
- * `newRow` when given. Each reads back only the key, so `--revoke-columns`
1559
- * (which keeps only unrestricted columns readable) does not reject it.
1560
- */
1561
- function statementFor(fixture, action, table) {
1562
- const quoted = quoteIdent$1(table);
1563
- const id = quoteIdent$1("id");
1564
- const key = rowId(fixture.row);
1565
- switch (action) {
1566
- case "read":
1567
- case "list":
1568
- case "get": return {
1569
- sql: `select ${id} from ${quoted} where ${id} = $1`,
1570
- values: [key]
1571
- };
1572
- case "update": {
1573
- const next = columnsOf(fixture.newRow).filter(([name]) => name !== "id");
1574
- return {
1575
- sql: `update ${quoted} set ${next.length === 0 ? `${id} = ${id}` : next.map(([name], index) => `${quoteIdent$1(name)} = $${index + 2}`).join(", ")} where ${id} = $1 returning ${id}`,
1576
- values: [key, ...next.map(([, value]) => value)]
1577
- };
1578
- }
1579
- case "create": {
1580
- const cols = columnsOf(fixture.row);
1581
- if (cols.length === 0) return {
1582
- sql: `insert into ${quoted} (${id}) values ($1) returning ${id}`,
1583
- values: [key]
1584
- };
1585
- return {
1586
- sql: `insert into ${quoted} (${cols.map(([name]) => quoteIdent$1(name)).join(", ")}) values (${cols.map((_, index) => `$${index + 1}`).join(", ")}) returning ${id}`,
1587
- values: cols.map(([, value]) => value)
1588
- };
1589
- }
1590
- case "delete": return {
1591
- sql: `delete from ${quoted} where ${id} = $1 returning ${id}`,
1592
- values: [key]
1593
- };
1594
- default: return {
1595
- sql: `select ${id} from ${quoted} where ${id} = $1`,
1596
- values: [key]
1597
- };
1598
- }
1599
- }
1600
- function rowId(row) {
1601
- if (isRecord(row) && "id" in row) return row["id"];
1602
- }
1603
- function emitPgtap(fixtures, customRoles, scopes) {
1604
- const lines = [
1605
- "begin;",
1606
- `select plan(${fixtures.length});`,
1607
- "-- fixtures carry memberships and tenant for the exists join"
1608
- ];
1609
- for (const [index, fixture] of fixtures.entries()) {
1610
- const claims = JSON.stringify({
1611
- sub: fixture.subject.id,
1612
- role: "authenticated",
1613
- tenant_id: fixture.subject.tenant ?? null,
1614
- memberships: membershipsClaim(fixture.subject.memberships ?? [], customRoles, scopes)
1615
- });
1616
- lines.push(`-- ${fixture.action}`, `select set_config('request.jwt.claims', ${JSON.stringify(claims)}, true);`, `select set_config('request.jwt.claim.sub', ${JSON.stringify(fixture.subject.id)}, true);`, `select ok(true, 'fixture ${index} ${fixture.action}');`);
1617
- }
1618
- lines.push("select * from finish();", "rollback;");
1619
- return `${lines.join("\n")}\n`;
1620
- }
1621
- /** Columns of `row` that hold a value; a masked column reads as null. */
1622
- function valuedColumns(row) {
1623
- return isRecord(row) ? Object.keys(row).filter((name) => row[name] !== null && row[name] !== void 0).toSorted() : [];
1624
- }
1625
- /**
1626
- * What a field view returns for a row: nothing for a denied row, otherwise
1627
- * `pick`'s columns with a value plus the key, which always passes through.
1628
- */
1629
- function expectedFields(granted, picked, key, row) {
1630
- if (!granted) return [];
1631
- const kept = new Set(valuedColumns(picked));
1632
- if (isRecord(row) && row[key] !== null && row[key] !== void 0) kept.add(key);
1633
- return [...kept].toSorted();
1634
- }
1635
- function fieldsMismatch(fixture, want, seen) {
1636
- const kept = want.join(", ");
1637
- if (typeof seen === "string") return [`${fixture.action}: field view read failed (${seen}), pick keeps [${kept}]`];
1638
- return seen.join(", ") === kept ? [] : [`${fixture.action} ${String(rowId(fixture.row))}: field view returns [${seen.join(", ")}], pick keeps [${kept}]`];
1639
- }
1640
- /** The field view's columns for the fixture's row: the view, or the table itself when no grant limits its fields. */
1641
- async function viewFields(query, table, key) {
1642
- const view = quoteIdent$1(viewName(table, FIELD_VIEWS.view));
1643
- const id = quoteIdent$1("id");
1644
- await query("savepoint permdock_fields");
1645
- const result = await query(`select * from ${view} where ${id} = $1`, [key]);
1646
- if (result.code === void 0) return valuedColumns(result.rows[0]);
1647
- await query("rollback to savepoint permdock_fields");
1648
- if (result.code !== "42P01") return result.code;
1649
- const base = await query(`select * from ${quoteIdent$1(table)} where ${id} = $1`, [key]);
1650
- return base.code ?? valuedColumns(base.rows[0]);
1651
- }
1652
- /** Writes the custom roles into the `database`-mode tables; the fixture transaction rolls them back. */
1653
- async function seedCustomRoles(query, schema, customRoles, scopes) {
1654
- const table = (name) => `${quoteIdent$1(schema)}.${quoteIdent$1(name)}`;
1655
- const write = async (sql, values) => {
1656
- const result = await query(sql, values);
1657
- if (result.code !== void 0) throw new Error(`PermDock CLI: rls verify --db could not seed custom roles (${result.code}); connect as a role that owns the custom_role_* tables`);
1658
- };
1659
- for (const role of customRoles) {
1660
- const at = customRoleAt(role, scopes);
1661
- if (at.scope === void 0) continue;
1662
- for (const grant of role.grants ?? []) await write(`insert into ${table("custom_role_permissions")} (tenant_id, scope, scope_id, role, permission, effect) values ($1, $2, $3, $4, $5, $6)`, [
1663
- role.tenant ?? null,
1664
- at.scope,
1665
- at.id ?? null,
1666
- role.name,
1667
- grant.permission,
1668
- grant.effect ?? "allow"
1669
- ]);
1670
- for (const name of role.includes ?? []) await write(`insert into ${table("custom_role_includes")} (tenant_id, scope, scope_id, role, include_role) values ($1, $2, $3, $4, $5)`, [
1671
- role.tenant ?? null,
1672
- at.scope,
1673
- at.id ?? null,
1674
- role.name,
1675
- name
1676
- ]);
1677
- }
1678
- }
1679
- async function bindSubject(query, fixture, dialect, gucPrefix, tenantClaim, roleClaim, customRoles, scopes) {
1680
- await query("set local role \"authenticated\"");
1681
- const roles = fixture.subject.roles ?? [];
1682
- const memberships = membershipsClaim(fixture.subject.memberships ?? [], customRoles, scopes);
1683
- if (dialect === "guc") {
1684
- const settings = [
1685
- [`${gucPrefix}.user_id`, fixture.subject.id],
1686
- [`${gucPrefix}.${roleClaim}`, roles.join(",")],
1687
- [`${gucPrefix}.memberships`, JSON.stringify(memberships)]
1688
- ];
1689
- if (fixture.subject.tenant !== void 0) settings.push([`${gucPrefix}.${tenantClaim}`, fixture.subject.tenant]);
1690
- for (const [name, value] of settings) await query("select set_config($1, $2, true)", [name, value]);
1691
- return;
1692
- }
1693
- const claims = {
1694
- sub: fixture.subject.id,
1695
- role: "authenticated",
1696
- [roleClaim]: roles.length === 1 ? roles[0] : roles,
1697
- [tenantClaim]: fixture.subject.tenant,
1698
- memberships
1699
- };
1700
- await query("select set_config($1, $2, true)", ["request.jwt.claims", JSON.stringify(claims)]);
1701
- await query("select set_config($1, $2, true)", ["request.jwt.claim.sub", fixture.subject.id]);
1702
- }
1703
- async function verifyAgainstDatabase(input) {
1704
- const client = await input.connect(input.db);
1705
- const dialect = input.config.rls?.dialect ?? "supabase";
1706
- const gucPrefix = input.config.rls?.gucPrefix ?? "app";
1707
- const tenantClaim = input.config.rls?.tenantClaim ?? "tenant_id";
1708
- const roleClaim = input.config.rls?.roleClaim ?? "user_role";
1709
- const rls = input.config.rls;
1710
- const seedsTables = rls?.customRoles === true && (rls.authorize ?? rls.rbac?.authorize ?? "jwt") === "database";
1711
- const schema = rls?.schema ?? rls?.rbac?.schema ?? "permdock";
1712
- const scopes = scopeList(input.policy.scopes);
1713
- const query = async (sql, values) => {
1714
- try {
1715
- const result = await client.query(sql, values === void 0 ? [] : [...values]);
1716
- return {
1717
- rows: result.rows,
1718
- rowCount: result.rowCount ?? result.rows.length
1719
- };
1720
- } catch (cause) {
1721
- const code = cause !== null && typeof cause === "object" && "code" in cause && typeof cause.code === "string" ? cause.code : void 0;
1722
- return {
1723
- rows: [],
1724
- rowCount: 0,
1725
- ...code === void 0 ? {} : { code }
1726
- };
1727
- }
1728
- };
1729
- const mismatches = [];
1730
- const notes = [];
1731
- try {
1732
- const conditioned = policyRowConditionKeys(input.policy);
1733
- for (const found of helperTablePoliciesFromRows((await query(HELPER_TABLE_POLICIES_SQL)).rows)) {
1734
- const keys = found.keys.filter((key) => conditioned.has(key));
1735
- if (keys.length > 0) mismatches.push(`PD037 ${rowConditionMessage(found, keys)}`);
1736
- }
1737
- for (const [index, fixture] of input.fixtures.entries()) {
1738
- const permission = findPermission(input.policy.permissions, fixture.action);
1739
- const status = input.inProcess[index];
1740
- if (permission === void 0 || status === void 0) continue;
1741
- if (status.kind === "opaque") {
1742
- notes.push(`${fixture.action}: opaque grant untestable app-side`);
1743
- continue;
1744
- }
1745
- const table = input.config.rls?.tables?.[permission.resource] ?? permission.resource;
1746
- await query("begin");
1747
- try {
1748
- if (seedsTables) await seedCustomRoles(query, schema, input.customRoles, scopes);
1749
- await bindSubject(query, fixture, dialect, gucPrefix, tenantClaim, roleClaim, input.customRoles, scopes);
1750
- const statement = statementFor(fixture, permission.action, table);
1751
- const result = await query(statement.sql, statement.values);
1752
- const count = result.rowCount ?? result.rows.length;
1753
- const database = result.code === "42501" ? "rejected" : count > 0 ? "allowed" : "filtered";
1754
- if (!(status.granted ? database === "allowed" : database === "filtered" || database === "rejected")) mismatches.push(`${fixture.action}: in-process ${status.granted ? "granted" : "denied"}, database ${database}`);
1755
- else if (status.kind === "sqlFunction") notes.push(`${fixture.action}: verified through twin`);
1756
- mismatches.push(...status.fields === void 0 ? [] : fieldsMismatch(fixture, status.fields, await viewFields(query, table, rowId(fixture.row))));
1757
- } finally {
1758
- await query("rollback");
1759
- }
1760
- }
1761
- } finally {
1762
- await client.end();
1763
- }
1764
- return {
1765
- mismatches,
1766
- notes
1767
- };
1768
- }
1769
- /** `--tree`: seeds a generated object graph in a rolled-back transaction and compares `can()` with RLS. */
1770
- async function verifyTreeAgainstDatabase(db, policy, config, connect) {
1771
- const client = await connect(db);
1772
- const scopes = scopeList(policy.scopes);
1773
- const query = async (sql, values) => {
1774
- return { rows: (await client.query(sql, values === void 0 ? [] : [...values])).rows };
1775
- };
1776
- const loose = async (sql, values) => {
1777
- await query(sql, values);
1778
- return { rows: [] };
1779
- };
1780
- try {
1781
- await query("begin");
1782
- const result = await verifyTree({
1783
- policy,
1784
- config,
1785
- query,
1786
- bind: (subject) => bindSubject(loose, {
1787
- subject: { id: subject },
1788
- row: {},
1789
- action: "read"
1790
- }, config.rls?.dialect ?? "supabase", config.rls?.gucPrefix ?? "app", config.rls?.tenantClaim ?? "tenant_id", config.rls?.roleClaim ?? "user_role", [], scopes)
1791
- });
1792
- if (result.mismatches.length > 0) return {
1793
- code: 1,
1794
- output: [...result.mismatches, ...result.notes].join("\n")
1795
- };
1796
- const verified = `verified ${String(result.checked)} tree check(s) against the database (${String(result.granted)} granted)`;
1797
- return {
1798
- code: 0,
1799
- output: result.notes.length === 0 ? verified : `${verified}\n${result.notes.join("\n")}`
1800
- };
1801
- } catch (cause) {
1802
- return {
1803
- code: 2,
1804
- output: `PermDock CLI: rls verify --tree could not seed the tree (${cause instanceof Error ? cause.message : String(cause)}); connect as a role that owns the tables and is a member of authenticated`
1805
- };
1806
- } finally {
1807
- await client.query("rollback", []).catch(() => void 0);
1808
- await client.end();
1809
- }
1810
- }
1811
- async function runRlsVerify(input) {
1812
- const connect = input.connect ?? ((db) => connectPg(db, "permdock rls verify --db"));
1813
- const policyPath = input.from ?? input.config.policy;
1814
- if (policyPath === void 0) return {
1815
- code: 2,
1816
- output: "PermDock CLI: rls verify needs policy in the config or --from"
1817
- };
1818
- const policy = asPolicy(pickNamed(await loadModule(resolve(input.cwd, policyPath)), ["policy"]));
1819
- if (input.tree === true) {
1820
- if (input.db === void 0) return {
1821
- code: 2,
1822
- output: "PermDock CLI: rls verify --tree needs --db"
1823
- };
1824
- try {
1825
- return await verifyTreeAgainstDatabase(input.db, policy, input.config, connect);
1826
- } catch (cause) {
1827
- return usageResult(cause);
1828
- }
1829
- }
1830
- const fixturesPath = input.fixtures ?? input.config.rls?.fixtures ?? "rls.fixtures.json";
1831
- const { fixtures, customRoles } = await loadFixtures(input.cwd, fixturesPath);
1832
- if (input.format === "pgtap") return {
1833
- code: 0,
1834
- output: emitPgtap(fixtures, customRoles, scopeList(policy.scopes))
1835
- };
1836
- const mismatches = [];
1837
- const notes = [];
1838
- const inProcess = [];
1839
- const fieldsMode = input.config.rls?.fields === "views";
1840
- for (const fixture of fixtures) {
1841
- const permission = findPermission(policy.permissions, fixture.action);
1842
- if (permission === void 0) {
1843
- mismatches.push(`${fixture.action}: unknown permission`);
1844
- inProcess.push({
1845
- action: fixture.action,
1846
- granted: false,
1847
- kind: "portable"
1848
- });
1849
- continue;
1850
- }
1851
- const kind = grantKind(policy, fixture.action);
1852
- const permdock = await createPermDock(policy, fixtureSubject(fixture.subject), { customRoles: memoryRoleSource(customRoles) });
1853
- const granted = canFixture(permdock, permission, fixtureRow(fixture, permission.kind));
1854
- const outcome = granted ? "granted" : "denied";
1855
- if (fixture.expected !== void 0 && fixture.expected !== outcome) mismatches.push(`${fixture.action}: in-process ${outcome}, expected ${fixture.expected}`);
1856
- if (kind === "opaque") notes.push(`${fixture.action}: opaque grant untestable app-side`);
1857
- const fields = fieldsMode && permission.kind === "instance" && commandFor(permission.action, input.config.rls?.actions) === "select" ? expectedFields(granted, permdock.pick(permission, fixture.row), policy.resources.get(permission.resource)?.id ?? "id", fixture.row) : void 0;
1858
- inProcess.push({
1859
- action: fixture.action,
1860
- granted,
1861
- kind,
1862
- ...fields === void 0 ? {} : { fields }
1863
- });
1864
- }
1865
- if (input.db !== void 0) try {
1866
- const database = await verifyAgainstDatabase({
1867
- db: input.db,
1868
- policy,
1869
- fixtures,
1870
- customRoles,
1871
- config: input.config,
1872
- inProcess,
1873
- connect
1874
- });
1875
- mismatches.push(...database.mismatches);
1876
- notes.push(...database.notes);
1877
- } catch (cause) {
1878
- return usageResult(cause);
1879
- }
1880
- if (mismatches.length > 0) return {
1881
- code: 1,
1882
- output: [...mismatches, ...notes].join("\n")
1883
- };
1884
- const verified = `verified ${fixtures.length} fixture(s) in-process`;
1885
- return {
1886
- code: 0,
1887
- output: notes.length === 0 ? verified : `${verified}\n${notes.join("\n")}`
1888
- };
1889
- }
1890
- //#endregion
1891
- //#region src/cli/rls.ts
1892
- const RLS_HELP = `permdock rls generate | import | verify | migrate
1893
-
1894
- generate --target drizzle|sql|prisma --dialect supabase|neon|guc
1895
- [--rbac supabase] [--rbac-schema public] [--authorize database|jwt]
1896
- [--memberships <table>:tenant,user,role]
1897
- [--policy-per-role] [--policy-name '{table}_{op}'] [--tenant-type uuid] [--custom-roles]
1898
- [--capabilities] [--fields views [--revoke-columns]]
1899
- [--out <path>] [--check] [--skip-closures] [--inline-functions] [--force] [--guc-prefix app]
1900
- [--split helpers,seeds,policies,hook --out <dir>/056_permdock_{part}.sql]
1901
- [--grants-out <file>|-] [--seeds-out <file>|-]
1902
- [--helpers-only] [--shims]
1903
- import --sql schema.sql | --db $DATABASE_URL --out src/permissions.generated.ts
1904
- [--schema zod|valibot|arktype] [--memberships <table>:tenant,user,role]
1905
- verify [--db $DATABASE_URL] [--fixtures rls.fixtures.ts] [--format pgtap|node] [--tree]
1906
- [--introspect --db $DATABASE_URL, with the generate flags]
1907
- migrate --sql <dir> [--write] [--json], with the generate flags
1908
- rewrites the rls.migrate helpers' calls in policies onto the generated helpers
1909
-
1910
- Never emits service_role. memberOf compiles through the dialect memberships mapping.
1911
- `;
1912
- function asTarget(value) {
1913
- if (value === void 0 || value === "sql" || value === "drizzle" || value === "prisma") return value ?? "sql";
1914
- }
1915
- function asDialect(value) {
1916
- if (value === void 0 || value === "supabase" || value === "neon" || value === "guc") return value ?? "supabase";
1917
- }
1918
- function generateInput(input, target, dialect) {
1919
- return {
1920
- cwd: input.cwd,
1921
- config: input.config,
1922
- target,
1923
- dialect,
1924
- rbac: input.rbac,
1925
- ...input.rbacSchema === void 0 ? {} : { rbacSchema: input.rbacSchema },
1926
- ...input.authorize === void 0 ? {} : { authorize: parseRbacAuthorize(input.authorize) ?? "database" },
1927
- check: input.check,
1928
- skipClosures: input.skipClosures,
1929
- inlineFunctions: input.inlineFunctions,
1930
- force: input.force,
1931
- io: input.io,
1932
- ...input.out === void 0 ? {} : { out: input.out },
1933
- ...input.from === void 0 ? {} : { from: input.from },
1934
- ...input.memberships === void 0 ? {} : { memberships: input.memberships },
1935
- ...input.gucPrefix === void 0 ? {} : { gucPrefix: input.gucPrefix },
1936
- policyPerRole: input.policyPerRole,
1937
- ...input.policyName === void 0 ? {} : { policyName: input.policyName },
1938
- ...input.tenantType === void 0 ? {} : { tenantType: input.tenantType },
1939
- customRoles: input.customRoles,
1940
- capabilities: input.capabilities,
1941
- ...input.fields === void 0 ? {} : { fields: input.fields },
1942
- revokeColumns: input.revokeColumns,
1943
- ...input.split === void 0 ? {} : { split: input.split },
1944
- ...input.grantsOut === void 0 ? {} : { grantsOut: input.grantsOut },
1945
- ...input.seedsOut === void 0 ? {} : { seedsOut: input.seedsOut },
1946
- helpersOnly: input.helpersOnly,
1947
- shims: input.shims === true
1948
- };
1949
- }
1950
- async function migrate(input) {
1951
- const config = input.config.rls?.migrate;
1952
- if (config === void 0) return {
1953
- code: 2,
1954
- output: "rls migrate needs rls.migrate.helpers in permdock.config.ts"
1955
- };
1956
- if (input.sql === void 0) return {
1957
- code: 2,
1958
- output: "rls migrate needs --sql <dir>"
1959
- };
1960
- const dialect = asDialect(input.dialect ?? input.config.rls?.dialect);
1961
- if (dialect === void 0) return {
1962
- code: 2,
1963
- output: "rls migrate --dialect (or rls.dialect) must be supabase, neon or guc"
1964
- };
1965
- const generated = await runRlsGenerate({
1966
- ...generateInput(input, "sql", dialect),
1967
- check: false,
1968
- write: false
1969
- });
1970
- if (generated.code !== 0) return {
1971
- code: 2,
1972
- output: generated.output
1973
- };
1974
- await requirePeer(() => import("pgsql-parser"), "pgsql-parser", "permdock rls migrate", input.cwd);
1975
- const { migrateTarget, runRlsMigrate } = await import("./rls-migrate-DrFPbf8p.js");
1976
- return runRlsMigrate({
1977
- cwd: input.cwd,
1978
- sql: input.sql,
1979
- config,
1980
- target: migrateTarget(generated),
1981
- write: input.write,
1982
- json: input.json
1983
- });
1984
- }
1985
- /** `verify --introspect` with `--helpers-only`: helpers and seeds exactly, hand-written policies by their keys. */
1986
- async function introspectHelpersOnly(db, expected, generated, connect) {
1987
- const schema = generated.schema ?? "permdock";
1988
- try {
1989
- const actual = await introspectRls(db, expected, connect);
1990
- const helpers = diffRls(expected, actual);
1991
- const warnings = missingIndexes(expected, actual);
1992
- const mixed = diffMixed({
1993
- schema,
1994
- seeds: generated.seeds ?? [],
1995
- permissions: generated.keys?.permissions ?? [],
1996
- rowConditions: generated.keys?.rowConditions ?? []
1997
- }, await introspectMixed(db, schema, connect));
1998
- const drift = [...helpers, ...mixed.drift];
1999
- const info = mixed.info.map((line) => `info: ${line}`);
2000
- return drift.length > 0 ? {
2001
- code: 1,
2002
- output: [
2003
- ...drift,
2004
- ...warnings,
2005
- ...info
2006
- ].join("\n")
2007
- } : {
2008
- code: 0,
2009
- output: [
2010
- ...warnings,
2011
- ...info,
2012
- `introspected ${String(expected.helpers.length)} helper(s) and ${String(generated.seeds?.length ?? 0)} seeded row(s), helpers only: no drift`
2013
- ].join("\n")
2014
- };
2015
- } catch (cause) {
2016
- return usageResult(cause);
2017
- }
2018
- }
2019
- /** `verify --introspect`: the catalogs against what `generate` would write with the same flags. */
2020
- async function introspect(input) {
2021
- if (input.db === void 0) return {
2022
- code: 2,
2023
- output: "PermDock CLI: rls verify --introspect needs --db"
2024
- };
2025
- const dialect = asDialect(input.dialect ?? input.config.rls?.dialect);
2026
- if (dialect === void 0) return {
2027
- code: 2,
2028
- output: "rls verify --dialect (or rls.dialect) must be supabase, neon or guc"
2029
- };
2030
- const generated = await runRlsGenerate({
2031
- ...generateInput(input, "sql", dialect),
2032
- check: false,
2033
- write: false
2034
- });
2035
- if (generated.code !== 0 || generated.policies === void 0) return {
2036
- code: 2,
2037
- output: generated.output
2038
- };
2039
- const expected = expectedRls(generated.policies, generated.text, generated.indexes);
2040
- if (generated.helpersOnly === true) return introspectHelpersOnly(input.db, expected, generated, input.connect);
2041
- try {
2042
- const actual = await introspectRls(input.db, expected, input.connect);
2043
- const drift = diffRls(expected, actual, { columnGrants: (input.fields ?? input.config.rls?.fields) === "views" });
2044
- const warnings = missingIndexes(expected, actual);
2045
- if (drift.length > 0) return {
2046
- code: 1,
2047
- output: [...drift, ...warnings].join("\n")
2048
- };
2049
- return {
2050
- code: 0,
2051
- output: `${warnings.map((line) => `${line}\n`).join("")}introspected ${String(expected.policies.length)} policies on ${String(expected.tables.length)} table(s) and ${String(expected.helpers.length)} helper(s): no drift`
2052
- };
2053
- } catch (cause) {
2054
- return usageResult(cause);
2055
- }
2056
- }
2057
- async function runRls(input) {
2058
- const action = input.rest[0];
2059
- if (action === void 0 || action === "help") return {
2060
- code: 2,
2061
- output: RLS_HELP
2062
- };
2063
- switch (action) {
2064
- case "generate": {
2065
- const target = asTarget(input.target);
2066
- const dialect = asDialect(input.dialect ?? input.config.rls?.dialect);
2067
- if (target === void 0) return {
2068
- code: 2,
2069
- output: "rls generate --target must be drizzle, sql or prisma"
2070
- };
2071
- if (dialect === void 0) return {
2072
- code: 2,
2073
- output: "rls generate --dialect (or rls.dialect) must be supabase, neon or guc"
2074
- };
2075
- return runRlsGenerate(generateInput(input, target, dialect));
2076
- }
2077
- case "import": {
2078
- await requirePeer(() => import("pgsql-parser"), "pgsql-parser", "permdock rls import", input.cwd);
2079
- const { runRlsImport } = await import("./rls-import-CXV6LBlS.js");
2080
- return runRlsImport({
2081
- cwd: input.cwd,
2082
- config: input.config,
2083
- schema: input.schema ?? "zod",
2084
- io: input.io,
2085
- ...input.sql === void 0 ? {} : { sql: input.sql },
2086
- ...input.db === void 0 ? {} : { db: input.db },
2087
- ...input.out === void 0 ? {} : { out: input.out },
2088
- ...input.memberships === void 0 ? {} : { memberships: input.memberships },
2089
- ...input.connect === void 0 ? {} : { connect: input.connect }
2090
- });
2091
- }
2092
- case "verify": {
2093
- if (input.introspect) return introspect(input);
2094
- const format = input.format ?? "node";
2095
- if (format !== "node" && format !== "pgtap") return {
2096
- code: 2,
2097
- output: "rls verify --format must be pgtap or node"
2098
- };
2099
- return await runRlsVerify({
2100
- cwd: input.cwd,
2101
- config: input.config,
2102
- format,
2103
- tree: input.tree,
2104
- io: input.io,
2105
- ...input.fixtures === void 0 ? {} : { fixtures: input.fixtures },
2106
- ...input.db === void 0 ? {} : { db: input.db },
2107
- ...input.from === void 0 ? {} : { from: input.from },
2108
- ...input.connect === void 0 ? {} : { connect: input.connect }
2109
- });
2110
- }
2111
- case "migrate": return migrate(input);
2112
- default: return {
2113
- code: 2,
2114
- output: RLS_HELP
2115
- };
2116
- }
2117
- }
2118
- //#endregion
2119
- export { runRls };