permdock 0.0.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 (344) hide show
  1. package/README.md +327 -1
  2. package/dist/a2a/index.d.ts +169 -0
  3. package/dist/a2a/index.js +232 -0
  4. package/dist/adapter-context-mlWsVGGg.js +23 -0
  5. package/dist/ai-sdk/index.d.ts +95 -0
  6. package/dist/ai-sdk/index.js +154 -0
  7. package/dist/approval-required-TzNB9oQA.js +6 -0
  8. package/dist/approvals/index.d.ts +3 -0
  9. package/dist/approvals/index.js +5 -0
  10. package/dist/approvals-DbYAuNkG.js +224 -0
  11. package/dist/approvers-DT4AVyIg.js +52 -0
  12. package/dist/arazzo-BAgw9ap6.js +70 -0
  13. package/dist/arazzo-cmvX2jDe.js +54 -0
  14. package/dist/authzen/index.d.ts +39 -0
  15. package/dist/authzen/index.js +302 -0
  16. package/dist/better-auth/index.d.ts +109 -0
  17. package/dist/better-auth/index.js +344 -0
  18. package/dist/better-supabase/index.d.ts +206 -0
  19. package/dist/better-supabase/index.js +620 -0
  20. package/dist/bind-CVy3Espv.d.ts +25 -0
  21. package/dist/bind-VnYn2HKq.js +97 -0
  22. package/dist/boundary-BM8a_P6E.js +13 -0
  23. package/dist/boundary-BmHxVH1J.d.ts +15 -0
  24. package/dist/budget-JqcRqyhK.d.ts +11 -0
  25. package/dist/budget-NbEtbJC_.js +11 -0
  26. package/dist/canonical-json-DemiK4Vp.js +26 -0
  27. package/dist/capability-4qPdO9gk.js +173 -0
  28. package/dist/capability-Dq43tT1u.d.ts +99 -0
  29. package/dist/catalog/index.d.ts +12 -0
  30. package/dist/catalog/index.js +2 -0
  31. package/dist/catalog-BjZSovXQ.js +2 -0
  32. package/dist/catalog-doc-Dsc9y7sd.js +309 -0
  33. package/dist/catalog-h2tlKFkq.js +48 -0
  34. package/dist/catalog-path-CnI0XTFK.js +12 -0
  35. package/dist/catalog-z8q6bi_r.js +55 -0
  36. package/dist/claude-agent/index.d.ts +72 -0
  37. package/dist/claude-agent/index.js +77 -0
  38. package/dist/clerk/index.d.ts +65 -0
  39. package/dist/clerk/index.js +301 -0
  40. package/dist/cli/bin.d.ts +1 -0
  41. package/dist/cli/bin.js +20 -0
  42. package/dist/cli/index.d.ts +46 -0
  43. package/dist/cli/index.js +5 -0
  44. package/dist/clients-C7zLFuWT.d.ts +12 -0
  45. package/dist/clients-DjU5ABMy.js +23 -0
  46. package/dist/cloud/index.d.ts +90 -0
  47. package/dist/cloud/index.js +404 -0
  48. package/dist/cloud-BGhHS-lY.js +50 -0
  49. package/dist/cloud-Bo3e9Ak3.js +118 -0
  50. package/dist/collect-DDDv_ESs.js +502 -0
  51. package/dist/collect-DLlujdHC.js +83 -0
  52. package/dist/collect-rG2IQ3t0.js +2 -0
  53. package/dist/commands-DWOrSQic.js +97 -0
  54. package/dist/compact-y6vfu2_f.js +19 -0
  55. package/dist/compare-pfyVnBjE.js +12 -0
  56. package/dist/compile/index.d.ts +3 -0
  57. package/dist/compile/index.js +2 -0
  58. package/dist/compile-Ba4blROR.js +613 -0
  59. package/dist/compile-CNnl6uIt.d.ts +94 -0
  60. package/dist/config-BJiM-xAm.js +157 -0
  61. package/dist/config-Bn00Z3ks.js +36 -0
  62. package/dist/config-report-Dw_JcUqT.js +44 -0
  63. package/dist/connection-CU8MQxRi.js +194 -0
  64. package/dist/context-Ttp0ekMT.js +82 -0
  65. package/dist/convex/index.d.ts +45 -0
  66. package/dist/convex/index.js +65 -0
  67. package/dist/create-BWyx3jua.d.ts +245 -0
  68. package/dist/create-CPhDL6qx.js +253 -0
  69. package/dist/credential-8P81ff4u.js +306 -0
  70. package/dist/delegation-D8pUEuce.js +103 -0
  71. package/dist/describe-B_gVZEVi.js +94 -0
  72. package/dist/describe-PdoqESvg.d.ts +36 -0
  73. package/dist/diff-B8Tunc3o.js +451 -0
  74. package/dist/diff-CzqLxZ77.js +48 -0
  75. package/dist/digest-B3kc_PgQ.d.ts +19 -0
  76. package/dist/digest-BI-6aa6d.js +27 -0
  77. package/dist/doctor-WpvG6p4k.js +1194 -0
  78. package/dist/doctor-collect-ByfYlNv1.js +664 -0
  79. package/dist/doctor-rOr-4ZfS.js +53 -0
  80. package/dist/drizzle/index.d.ts +73 -0
  81. package/dist/drizzle/index.js +160 -0
  82. package/dist/duration-xK2jGFVc.js +25 -0
  83. package/dist/elysia/index.d.ts +49 -0
  84. package/dist/elysia/index.js +59 -0
  85. package/dist/errors-CWfoNg9N.js +47 -0
  86. package/dist/errors-Cz5zn0mW.js +123 -0
  87. package/dist/errors-DAqevpNk.d.ts +125 -0
  88. package/dist/errors-DRnGZHzF.js +30 -0
  89. package/dist/evaluation-items-CfCCejQd.js +40 -0
  90. package/dist/evaluations-tz6D9A2G.js +461 -0
  91. package/dist/eve/index.d.ts +86 -0
  92. package/dist/eve/index.js +176 -0
  93. package/dist/expiry-BoBJ8Tvc.js +7 -0
  94. package/dist/express/index.d.ts +23 -0
  95. package/dist/express/index.js +78 -0
  96. package/dist/fastify/index.d.ts +33 -0
  97. package/dist/fastify/index.js +92 -0
  98. package/dist/fields-BMl1RxoX.js +26 -0
  99. package/dist/files-DWUN9PzI.js +125 -0
  100. package/dist/fixtures-ht28Cplr.js +67 -0
  101. package/dist/freeze-CrU8nQeX.js +60 -0
  102. package/dist/from-snapshot-C3sHpZNS.js +569 -0
  103. package/dist/generate-CH7ThJ14.js +173 -0
  104. package/dist/grantee-Cv5Sogfv.js +602 -0
  105. package/dist/graph-sql-B-dsAT6j.d.ts +29 -0
  106. package/dist/headers-BZSDieNT.js +6 -0
  107. package/dist/headers-Cf-zWvsO.d.ts +6 -0
  108. package/dist/helpers-H_KARDVC.js +421 -0
  109. package/dist/hono/index.d.ts +48 -0
  110. package/dist/hono/index.js +99 -0
  111. package/dist/hosted-DJQw46hn.js +266 -0
  112. package/dist/http-BIhmLfCc.js +95 -0
  113. package/dist/http-DPuDaChb.d.ts +13 -0
  114. package/dist/http-SO7qGttf.js +44 -0
  115. package/dist/ids-DC4V4CFu.js +18 -0
  116. package/dist/index-A0YNL8jZ.d.ts +177 -0
  117. package/dist/index-CtE07CBF.d.ts +6 -0
  118. package/dist/index-D3vbkoSI.d.ts +483 -0
  119. package/dist/index-D6_cSa1z.d.ts +201 -0
  120. package/dist/index.d.ts +16 -0
  121. package/dist/index.js +26 -0
  122. package/dist/instance-options-BK_rNOSC.d.ts +11 -0
  123. package/dist/instance-options-Dhhcnm3y.js +22 -0
  124. package/dist/json-schema-BsAflUGW.js +106 -0
  125. package/dist/jwt/index.d.ts +91 -0
  126. package/dist/jwt/index.js +590 -0
  127. package/dist/kernel-GSQnMz3O.js +208 -0
  128. package/dist/kysely/index.d.ts +78 -0
  129. package/dist/kysely/index.js +129 -0
  130. package/dist/live-options-H2eG8dfJ.js +60 -0
  131. package/dist/load-DhWEB9P1.js +75 -0
  132. package/dist/load-DwJUafYN.js +2 -0
  133. package/dist/load-jose-BL6LH4bL.js +11 -0
  134. package/dist/lru-8q4WZjBV.js +28 -0
  135. package/dist/manifest-iaoBXHKc.js +783 -0
  136. package/dist/manifest-uDS9y5de.d.ts +206 -0
  137. package/dist/map-AcxK9o_D.js +147 -0
  138. package/dist/map-claims-Z4QZOiFF.js +369 -0
  139. package/dist/map-error-pgUWNNtG.js +23 -0
  140. package/dist/markers-B0hYLvMD.js +60 -0
  141. package/dist/may-use-DndQ5tQ-.js +77 -0
  142. package/dist/mcp/index.d.ts +159 -0
  143. package/dist/mcp/index.js +603 -0
  144. package/dist/nest/index.d.ts +75 -0
  145. package/dist/nest/index.js +261 -0
  146. package/dist/next/client.d.ts +23 -0
  147. package/dist/next/client.js +33 -0
  148. package/dist/next/index.d.ts +93 -0
  149. package/dist/next/index.js +180 -0
  150. package/dist/next/plugin.d.ts +14 -0
  151. package/dist/next/plugin.js +33 -0
  152. package/dist/node/index.d.ts +24 -0
  153. package/dist/node/index.js +34 -0
  154. package/dist/openai/index.d.ts +62 -0
  155. package/dist/openai/index.js +92 -0
  156. package/dist/openapi/index.d.ts +176 -0
  157. package/dist/openapi/index.js +3 -0
  158. package/dist/openapi-Bbg4wGVL.js +164 -0
  159. package/dist/openapi-BqRBvAng.js +538 -0
  160. package/dist/openapi-CqqEbHsL.js +250 -0
  161. package/dist/openapi-import-cmwetbzw.js +5959 -0
  162. package/dist/openapi-schema--ARjyfQr.js +6902 -0
  163. package/dist/orpc/index.d.ts +51 -0
  164. package/dist/orpc/index.js +145 -0
  165. package/dist/otel/index.d.ts +13 -0
  166. package/dist/otel/index.js +202 -0
  167. package/dist/package-root-zGkxO3oN.js +30 -0
  168. package/dist/parse-BuyjxhZV.js +53 -0
  169. package/dist/parse-local-manifest-BkGQNzAh.d.ts +10 -0
  170. package/dist/parse-local-manifest-BrMozJCo.js +36 -0
  171. package/dist/pdp/index.d.ts +22 -0
  172. package/dist/pdp/index.js +682 -0
  173. package/dist/peer-BOr_kJE1.js +31 -0
  174. package/dist/permdock-CzqXjCpU.js +3471 -0
  175. package/dist/permissions-CDgvKG5L.d.ts +52 -0
  176. package/dist/permissions-CpZrivYv.js +142 -0
  177. package/dist/permissions-DVoF6dMt.js +662 -0
  178. package/dist/permissions-DttnqqxC.d.ts +214 -0
  179. package/dist/pins-8ILTftC9.js +12 -0
  180. package/dist/policy-CFZAr_Al.d.ts +2252 -0
  181. package/dist/policy-D8UFyERw.d.ts +20 -0
  182. package/dist/policy-xyWg7QdR.js +1122 -0
  183. package/dist/powersync-A8x_Yyvh.js +2 -0
  184. package/dist/powersync-C7kLglqX.js +540 -0
  185. package/dist/powersync-yW1UUFzp.js +62 -0
  186. package/dist/prisma/index.d.ts +136 -0
  187. package/dist/prisma/index.js +455 -0
  188. package/dist/problem-B43ztKIm.js +191 -0
  189. package/dist/problem-details-CkL50EEf.d.ts +7 -0
  190. package/dist/problem-details-_AMOYz-w.js +144 -0
  191. package/dist/prompt-CXYhLfsS.js +32 -0
  192. package/dist/protected-CNsMzrVd.d.ts +27 -0
  193. package/dist/protected-Cs6kTaHo.js +100 -0
  194. package/dist/protected-bZs6rbT1.d.ts +40 -0
  195. package/dist/provider-Cjr62YMV.d.ts +6 -0
  196. package/dist/provider-DLzpb2Xz.js +52 -0
  197. package/dist/provider-store-DNyC1y48.js +43 -0
  198. package/dist/react/index.d.ts +8 -0
  199. package/dist/react/index.js +7 -0
  200. package/dist/react/provider-client.d.ts +2 -0
  201. package/dist/react/provider-client.js +3 -0
  202. package/dist/react-native/index.d.ts +198 -0
  203. package/dist/react-native/index.js +696 -0
  204. package/dist/refs-KYb4ntaz.js +91 -0
  205. package/dist/relations-cjOL01hZ.js +505 -0
  206. package/dist/replay-BsOyTrdV.js +28 -0
  207. package/dist/replay-sqBGMnNs.d.ts +5 -0
  208. package/dist/resource-ref-CaS9yVPi.js +20 -0
  209. package/dist/revocations-Bhr0aMIC.js +27 -0
  210. package/dist/revocations-Cy06JNx2.d.ts +21 -0
  211. package/dist/rls-BMNydpHk.js +2521 -0
  212. package/dist/rls-CEvs64Kj.js +238 -0
  213. package/dist/rls-fields-BMasREhR.js +3502 -0
  214. package/dist/rls-graph-pc9fuXYm.js +435 -0
  215. package/dist/rls-import-D8F7j35Q.js +943 -0
  216. package/dist/rls-migrate-7wikfrVI.js +367 -0
  217. package/dist/rls-ownership-B5Lkq62W.js +843 -0
  218. package/dist/rls-shared-aOQ4hGTO.js +493 -0
  219. package/dist/rls-verify-D8B7dOTh.js +745 -0
  220. package/dist/row-check-Dt35czLy.d.ts +40 -0
  221. package/dist/rpc-DOK54_CS.js +89 -0
  222. package/dist/run-YXCJDNYk.js +174 -0
  223. package/dist/runtime-CB8gQFvS.js +11 -0
  224. package/dist/saas-Bj_FFkjN.js +1203 -0
  225. package/dist/schema-D3MnDMxa.js +300 -0
  226. package/dist/scim/index.d.ts +2 -0
  227. package/dist/scim/index.js +2 -0
  228. package/dist/scim-BFGBBAqR.js +1445 -0
  229. package/dist/scopes-CVdPrWN-.js +239 -0
  230. package/dist/server/index.d.ts +134 -0
  231. package/dist/server/index.js +11 -0
  232. package/dist/server-DlZ28Oz5.js +268 -0
  233. package/dist/sha256-BO0swRXL.js +150 -0
  234. package/dist/shared-DRu8VFVM.js +123 -0
  235. package/dist/sink-Bd9PSyfq.js +141 -0
  236. package/dist/sink-Gs6TTVa-.d.ts +104 -0
  237. package/dist/skills-Biq5uup3.js +192 -0
  238. package/dist/skills-JaymNCdK.js +56 -0
  239. package/dist/snapshot-Enxd0n-s.js +975 -0
  240. package/dist/snapshot-cache-D6jLelln.js +44 -0
  241. package/dist/snapshot-cache-DwQH8ufx.d.ts +45 -0
  242. package/dist/solid/index.d.ts +93 -0
  243. package/dist/solid/index.js +188 -0
  244. package/dist/sources-B0UvrJ21.js +534 -0
  245. package/dist/sources-BcJgw0lt.d.ts +192 -0
  246. package/dist/sql-xdg5f4Fi.js +27 -0
  247. package/dist/src-CSbUiozF.js +568 -0
  248. package/dist/ssf/index.d.ts +4 -0
  249. package/dist/ssf/index.js +596 -0
  250. package/dist/store-DMAWwAJV.js +371 -0
  251. package/dist/store-D_pjn91-.d.ts +36 -0
  252. package/dist/store-options-yO0V6gRn.js +716 -0
  253. package/dist/stores-4_sqiOA1.js +87 -0
  254. package/dist/stores-DVR5NHLE.d.ts +52 -0
  255. package/dist/stream-DDpvynSB.js +66 -0
  256. package/dist/stream-DlvG2hs-.d.ts +12 -0
  257. package/dist/style-CTm1AXDT.js +22 -0
  258. package/dist/subject-C6dLxqHU.js +24 -0
  259. package/dist/subject-settings-BwBmCdTR.js +128 -0
  260. package/dist/supabase/index.d.ts +183 -0
  261. package/dist/supabase/index.js +5 -0
  262. package/dist/supabase/middleware.d.ts +82 -0
  263. package/dist/supabase/middleware.js +102 -0
  264. package/dist/supabase-CwafwxxF.js +83 -0
  265. package/dist/supabase-DTBaC20O.js +774 -0
  266. package/dist/supabase-config-Bs44dare.js +472 -0
  267. package/dist/supabase-hook-BuxLQLpU.js +1814 -0
  268. package/dist/supabase-hook-D5qVd_D8.js +2 -0
  269. package/dist/supautils-DYZnzRWV.js +91 -0
  270. package/dist/svelte/PermissionBoundary.svelte +37 -0
  271. package/dist/svelte/Protected.svelte +40 -0
  272. package/dist/svelte/index.d.ts +13 -0
  273. package/dist/svelte/index.js +107 -0
  274. package/dist/svelte/runtime.d.ts +7 -0
  275. package/dist/svelte/runtime.js +4 -0
  276. package/dist/svelte/source.d.ts +13 -0
  277. package/dist/svelte/source.js +11 -0
  278. package/dist/terminal/index.d.ts +163 -0
  279. package/dist/terminal/index.js +833 -0
  280. package/dist/testing/index.d.ts +745 -0
  281. package/dist/testing/index.js +3672 -0
  282. package/dist/testing/saas/index.d.ts +194 -0
  283. package/dist/testing/saas/index.js +3 -0
  284. package/dist/testing/saas/permissions.d.ts +2 -0
  285. package/dist/testing/saas/permissions.js +2 -0
  286. package/dist/text-diff-BdkkYSbi.js +15 -0
  287. package/dist/thenable-okc2xDsj.js +10 -0
  288. package/dist/timeout-D51niuQP.js +8 -0
  289. package/dist/token-CIWxJOCh.js +50 -0
  290. package/dist/trpc/index.d.ts +61 -0
  291. package/dist/trpc/index.js +122 -0
  292. package/dist/types-9AWO9EGt.d.ts +197 -0
  293. package/dist/types-BPxgszlT.d.ts +550 -0
  294. package/dist/types-BVyImy5I.d.ts +120 -0
  295. package/dist/types-Bvc4oWT6.d.ts +9 -0
  296. package/dist/types-C3s9xVJG.d.ts +48 -0
  297. package/dist/types-Dnemwd77.d.ts +183 -0
  298. package/dist/types-kxznc7cn.d.ts +52 -0
  299. package/dist/types-vKyAMtcX.d.ts +157 -0
  300. package/dist/types-vNSDF7Dq.d.ts +66 -0
  301. package/dist/types-yF5LmQVD.d.ts +306 -0
  302. package/dist/unplugin/index.d.ts +7 -0
  303. package/dist/unplugin/index.js +31 -0
  304. package/dist/usage-D07N9QJ3.js +42 -0
  305. package/dist/usage-D82l1CwK.js +2 -0
  306. package/dist/usage-DG1oeCca.js +693 -0
  307. package/dist/validation-error-CnjZ4t92.js +33 -0
  308. package/dist/validation-error-CtF23SrC.d.ts +26 -0
  309. package/dist/validity-uOp8UpYn.js +245 -0
  310. package/dist/verifier-BxBo6I44.js +466 -0
  311. package/dist/version-BvacVTZR.js +17 -0
  312. package/dist/views-Soe7bGSC.js +95 -0
  313. package/dist/vocabulary-DA8a8zif.d.ts +56 -0
  314. package/dist/vue/index.d.ts +79 -0
  315. package/dist/vue/index.js +196 -0
  316. package/dist/watch-Etoa_Mvr.js +2 -0
  317. package/dist/watch-UihESpsJ.js +128 -0
  318. package/dist/webmcp/index.d.ts +75 -0
  319. package/dist/webmcp/index.js +307 -0
  320. package/package.json +450 -5
  321. package/rulesets/permdock.yaml +29 -0
  322. package/schemas/approval-request-v1.json +175 -0
  323. package/schemas/catalog-v1.json +535 -0
  324. package/schemas/openapi/oas-3.1.json +1298 -0
  325. package/schemas/openapi/oas-3.2.json +1516 -0
  326. package/schemas/openapi/overlay-1.1.json +79 -0
  327. package/schemas/supabase-claims-v1.json +144 -0
  328. package/schemas/supabase-manifest-v1.json +361 -0
  329. package/skills/permdock/SKILL.md +66 -0
  330. package/skills/permdock-agents/SKILL.md +77 -0
  331. package/skills/permdock-agents/references/runtimes.md +213 -0
  332. package/skills/permdock-approvals/SKILL.md +80 -0
  333. package/skills/permdock-approvals/references/stores-and-resume.md +94 -0
  334. package/skills/permdock-audit/SKILL.md +67 -0
  335. package/skills/permdock-credentials/SKILL.md +55 -0
  336. package/skills/permdock-credentials/references/tokens-keys-links.md +132 -0
  337. package/skills/permdock-data/SKILL.md +57 -0
  338. package/skills/permdock-data/references/queries-and-rls.md +62 -0
  339. package/skills/permdock-data/references/relationships.md +67 -0
  340. package/skills/permdock-tenancy/SKILL.md +80 -0
  341. package/skills/permdock-tenancy/references/ownership-and-custom-roles.md +89 -0
  342. package/skills/permdock-wire/SKILL.md +116 -0
  343. package/skills/permdock-wire/references/adapters.md +492 -0
  344. package/skills/permdock-wire/references/policy-options.md +45 -0
@@ -0,0 +1,2521 @@
1
+ import { t as compact } from "./compact-y6vfu2_f.js";
2
+ import { _ as listPermissions, y as renamedKeys } from "./permissions-DVoF6dMt.js";
3
+ import { d as scopeList, o as resolveScope } from "./scopes-CVdPrWN-.js";
4
+ import { E as listRoles, s as flattenGrantee } from "./grantee-Cv5Sogfv.js";
5
+ import { h as scopeField } from "./validity-uOp8UpYn.js";
6
+ import { i as describeError, o as usageResult } from "./errors-CWfoNg9N.js";
7
+ import { s as policyRowConditionKeys } from "./catalog-doc-Dsc9y7sd.js";
8
+ import { a as loadModule, n as asPolicy, o as pickNamed } from "./load-DhWEB9P1.js";
9
+ import "./budget-NbEtbJC_.js";
10
+ import { n as escapeSqlIdent } from "./sql-xdg5f4Fi.js";
11
+ import "./sources-B0UvrJ21.js";
12
+ import { i as SEEDS_MARKER, r as INDEXES_MARKER, t as GRANTS_MARKER } from "./markers-B0hYLvMD.js";
13
+ import { n as requirePeer } from "./peer-BOr_kJE1.js";
14
+ import { f as splitUndiffed, n as supabaseConfig } from "./supabase-config-Bs44dare.js";
15
+ import { B as parseMembershipsFlag, C as graphHelper, G as quoteLiteral, H as permittedIdsHelper, K as quoteTable, L as memberIdsHelper, Q as sessionLiveHelperSql, U as qualifiedTable$1, W as quoteIdent$1, X as scopeTable, Y as scopeSources, Z as scopeTypeOf, a as qualified$1, d as SESSION_LIVE_HELPER, et as subjectClaimJsonSql, n as HELPERS, nt as subjectIdSql, v as checkSuspension, w as graphSqlName } from "./rls-shared-aOQ4hGTO.js";
16
+ import { A as helpersSql, B as breakGlassHolder, C as readTableIndexFacts, E as accessSql, F as seedSql, H as collectGrants, S as pruneIndexTargets, T as connectPgPool, U as roleNames, V as breakGlassKey, a as fieldViewsSql, b as indexTargets, c as assemblePolicies, d as readOnlyActorPolicies, f as branchClauses, g as permissionHelpersSql, h as tableFor, i as fieldViews, l as orSql, m as compileGrants, n as columnGrantSql, o as rowBranches, q as apiKeysPlan, r as columnRevokeSql, u as readOnlyActorKinds, v as permittedByPermissionHelper, x as indexesSql, y as COLUMNS_SQL } from "./rls-fields-BMasREhR.js";
17
+ import { c as helperCallKeys, i as HELPER_TABLES, n as graphPlan, r as graphSql, s as callsHelper, t as closureDepths } from "./rls-graph-pc9fuXYm.js";
18
+ import { a as ownershipRules, o as ownershipSql, r as assignmentSql } from "./rls-ownership-B5Lkq62W.js";
19
+ import { _ as seedsMigration, d as resolveAuthorize, f as driftOf, g as pgDeltaPath, h as partPath, l as parseRbacAuthorize, m as parseSplit, o as supabaseHookSql, p as isSeedsDirectory, t as grantsLabel, u as rbacScaffold, v as writeSqlFiles } from "./supabase-hook-BuxLQLpU.js";
20
+ import { n as runRlsVerify } from "./rls-verify-D8B7dOTh.js";
21
+ import { t as supautilsRejections } from "./supautils-DYZnzRWV.js";
22
+ import { basename, delimiter, join, resolve } from "node:path";
23
+ import { spawnSync } from "node:child_process";
24
+ //#region src/cli/rls-advisors.ts
25
+ /** The PermDock doctor check that reports the same problem in migrations. */
26
+ const ADVISOR_CODES = {
27
+ function_search_path_mutable: "PD048",
28
+ auth_rls_initplan: "PD051",
29
+ rls_disabled_in_public: "PD050",
30
+ security_definer_view: "PD022",
31
+ anon_security_definer_function_executable: "PD049",
32
+ authenticated_security_definer_function_executable: "PD046"
33
+ };
34
+ const defaultExec = (command, args, options) => {
35
+ const result = spawnSync(command, [...args], {
36
+ cwd: options.cwd,
37
+ env: options.env,
38
+ encoding: "utf8"
39
+ });
40
+ return {
41
+ status: result.status,
42
+ stdout: result.stdout ?? "",
43
+ stderr: result.stderr ?? "",
44
+ ...result.error === void 0 ? {} : { error: result.error }
45
+ };
46
+ };
47
+ function text(value, key) {
48
+ const field = typeof value === "object" && value !== null ? Reflect.get(value, key) : void 0;
49
+ return typeof field === "string" ? field : "";
50
+ }
51
+ function findingsOf(value) {
52
+ const results = typeof value === "object" && value !== null ? Reflect.get(value, "results") : void 0;
53
+ if (!Array.isArray(results)) return;
54
+ return results.map((row) => {
55
+ const name = text(row, "name");
56
+ const finding = {
57
+ name,
58
+ level: text(row, "level"),
59
+ detail: text(row, "detail"),
60
+ remediation: text(row, "remediation"),
61
+ cacheKey: text(row, "cacheKey")
62
+ };
63
+ const code = ADVISOR_CODES[name];
64
+ return code === void 0 ? finding : Object.assign(finding, { code });
65
+ });
66
+ }
67
+ const INSTALL = "rls verify --advisors runs the Supabase CLI: install it with npm install --save-dev supabase (or brew install supabase/tap/supabase)";
68
+ /**
69
+ * `supabase db advisors --type security` against `--db` or the local stack.
70
+ * Exit 1 on a WARN or ERROR row, 2 when the CLI is missing or fails.
71
+ */
72
+ function runRlsAdvisors(input) {
73
+ const exec = input.exec ?? defaultExec;
74
+ const bin = join(input.cwd, "node_modules", ".bin");
75
+ const path = input.env["PATH"];
76
+ const result = exec("supabase", [
77
+ "db",
78
+ "advisors",
79
+ ...input.db === void 0 ? ["--local"] : ["--db-url", input.db],
80
+ "--type",
81
+ "security",
82
+ "--output-format",
83
+ "json"
84
+ ], {
85
+ cwd: input.cwd,
86
+ env: {
87
+ ...input.env,
88
+ PATH: path === void 0 ? bin : `${bin}${delimiter}${path}`
89
+ }
90
+ });
91
+ if (result.error !== void 0) return {
92
+ code: 2,
93
+ output: INSTALL
94
+ };
95
+ let parsed;
96
+ try {
97
+ parsed = JSON.parse(result.stdout);
98
+ } catch {
99
+ parsed = void 0;
100
+ }
101
+ const findings = findingsOf(parsed);
102
+ if (findings === void 0) {
103
+ const error = typeof parsed === "object" && parsed !== null ? Reflect.get(parsed, "error") : void 0;
104
+ const message = [text(error, "message"), text(error, "suggestion")].filter((part) => part !== "").join(" ");
105
+ return {
106
+ code: 2,
107
+ output: `supabase db advisors failed: ${message === "" ? result.stderr.trim() || `exit ${String(result.status)}` : message}`
108
+ };
109
+ }
110
+ const failing = findings.filter((finding) => finding.level === "WARN" || finding.level === "ERROR");
111
+ const code = failing.length > 0 ? 1 : 0;
112
+ if (input.json) return {
113
+ code,
114
+ output: JSON.stringify({
115
+ findings,
116
+ errors: findings.filter((finding) => finding.level === "ERROR").length,
117
+ warnings: findings.filter((finding) => finding.level === "WARN").length
118
+ }, null, 2)
119
+ };
120
+ if (findings.length === 0) return {
121
+ code,
122
+ output: "supabase db advisors: no security findings"
123
+ };
124
+ return {
125
+ code,
126
+ output: `${findings.map((finding) => `${finding.level} ${finding.name}${finding.code === void 0 ? "" : ` (${finding.code})`}: ${finding.detail}\n ${finding.remediation}`).join("\n")}\n\n${String(failing.length)} finding(s) at WARN or above`
127
+ };
128
+ }
129
+ //#endregion
130
+ //#region src/cli/approval-schema.ts
131
+ /** An RFC 3339 instant as `Temporal.Instant` prints it; pg_jsonschema asserts no `format`. */
132
+ const INSTANT = String.raw`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$`;
133
+ const STRINGS = {
134
+ type: "array",
135
+ items: { type: "string" }
136
+ };
137
+ /**
138
+ * `schemas/approval-request-v1.json`: the `ApprovalRequest` an approval store
139
+ * holds. Self-contained (`$defs`, no remote `$ref`) so `rls.jsonSchema` can
140
+ * inline it into a pg_jsonschema check constraint.
141
+ */
142
+ const APPROVAL_REQUEST_SCHEMA = {
143
+ $schema: "https://json-schema.org/draft/2020-12/schema",
144
+ $id: "https://permdock.com/schemas/approval-request-v1.json",
145
+ title: "PermDock approval request",
146
+ description: "An ApprovalRequest as an ApprovalStore holds it, wire format v1. Unknown properties are allowed so a v1 minor release can add one.",
147
+ type: "object",
148
+ required: [
149
+ "v",
150
+ "token",
151
+ "permission",
152
+ "scope",
153
+ "resource",
154
+ "subject",
155
+ "detail",
156
+ "createdAt",
157
+ "expiresAt",
158
+ "status"
159
+ ],
160
+ properties: {
161
+ v: { const: 1 },
162
+ token: {
163
+ type: "string",
164
+ minLength: 1
165
+ },
166
+ permission: {
167
+ type: "string",
168
+ minLength: 1
169
+ },
170
+ scope: { type: "string" },
171
+ resource: {
172
+ type: "object",
173
+ required: ["type"],
174
+ properties: {
175
+ type: { type: "string" },
176
+ id: { type: "string" }
177
+ }
178
+ },
179
+ subject: { $ref: "#/$defs/subject" },
180
+ membership: {
181
+ type: "object",
182
+ required: ["roles"],
183
+ properties: { roles: STRINGS }
184
+ },
185
+ approvers: { type: "object" },
186
+ detail: { type: "string" },
187
+ adapter: { type: "string" },
188
+ createdAt: { $ref: "#/$defs/instant" },
189
+ expiresAt: { $ref: "#/$defs/instant" },
190
+ status: { enum: [
191
+ "pending",
192
+ "approved",
193
+ "rejected",
194
+ "expired"
195
+ ] },
196
+ approvals: {
197
+ type: "array",
198
+ items: { $ref: "#/$defs/signature" }
199
+ },
200
+ resolvedAt: { $ref: "#/$defs/instant" },
201
+ resolvedBy: { type: "string" },
202
+ vouched: { type: "string" },
203
+ note: { type: "string" },
204
+ consumedAt: { $ref: "#/$defs/instant" }
205
+ },
206
+ $defs: {
207
+ instant: {
208
+ type: "string",
209
+ pattern: INSTANT
210
+ },
211
+ subject: {
212
+ type: "object",
213
+ required: ["principal"],
214
+ properties: {
215
+ principal: { anyOf: [{ type: "null" }, {
216
+ type: "object",
217
+ required: ["id", "roles"],
218
+ properties: {
219
+ id: { type: "string" },
220
+ roles: STRINGS,
221
+ tenant: { type: "string" }
222
+ }
223
+ }] },
224
+ actor: {
225
+ type: "object",
226
+ required: ["id", "kind"],
227
+ properties: {
228
+ id: { type: "string" },
229
+ kind: { type: "string" }
230
+ }
231
+ },
232
+ session: { type: "string" },
233
+ delegation: { type: "object" }
234
+ }
235
+ },
236
+ signature: {
237
+ type: "object",
238
+ required: ["by", "at"],
239
+ properties: {
240
+ by: { type: "string" },
241
+ at: { $ref: "#/$defs/instant" },
242
+ stage: {
243
+ type: "integer",
244
+ minimum: 0
245
+ },
246
+ vouched: { type: "string" }
247
+ }
248
+ }
249
+ }
250
+ };
251
+ //#endregion
252
+ //#region src/cli/rls-approvals.ts
253
+ /** The objects `rls.approvals` adds. Names are part of the SQL contract `supabaseApprovalStore` calls. */
254
+ const APPROVAL_STORE = {
255
+ table: "approval_requests",
256
+ open: "permdock_approval_open",
257
+ get: "permdock_approval_get",
258
+ resolve: "permdock_approval_resolve",
259
+ consume: "permdock_approval_consume",
260
+ list: "permdock_approval_list",
261
+ expire: "permdock_approval_expire",
262
+ cancel: "permdock_approval_cancel"
263
+ };
264
+ /** `body` as the row's filter columns hold it. */
265
+ const COLUMNS = `p_request ->> 'token',
266
+ p_request,
267
+ p_request ->> 'status',
268
+ p_request #>> '{subject,principal,tenant}',
269
+ p_request #>> '{subject,principal,id}',
270
+ p_request #>> '{subject,actor,id}',
271
+ p_request #>> '{subject,session}',
272
+ coalesce(jsonb_array_length(p_request -> 'approvals'), 0),
273
+ p_request ->> 'createdAt',
274
+ (p_request ->> 'expiresAt')::timestamptz,
275
+ (p_request ->> 'consumedAt')::timestamptz`;
276
+ /** The `ApprovalListFilter` in `p_filter`, over the row aliased `a`. */
277
+ const FILTER = `(p_filter ->> 'status' is null or a.status = p_filter ->> 'status')
278
+ and (p_filter ->> 'tenant' is null or a.tenant = p_filter ->> 'tenant')
279
+ and (p_filter ->> 'principalId' is null or a.principal_id = p_filter ->> 'principalId')
280
+ and (p_filter ->> 'actorId' is null or a.actor_id = p_filter ->> 'actorId')
281
+ and (p_filter ->> 'session' is null or a.session = p_filter ->> 'session')`;
282
+ function serverOnly(fn, args) {
283
+ return `revoke execute on function ${fn}(${args}) from public, anon, authenticated;`;
284
+ }
285
+ const BODY_CHECK = `${APPROVAL_STORE.table}_body_schema`;
286
+ /**
287
+ * `rls.jsonSchema`: a pg_jsonschema check that `body` matches
288
+ * `approval-request-v1.json`, added `not valid` and then validated. `auto`
289
+ * adds it only where the extension is available.
290
+ */
291
+ function bodySchemaSql(table, mode, body = "body", check = BODY_CHECK) {
292
+ const statements = [
293
+ "create schema if not exists extensions;",
294
+ "create extension if not exists pg_jsonschema with schema extensions;",
295
+ `alter table ${table} drop constraint if exists "${check}";`,
296
+ `alter table ${table} add constraint "${check}" check (extensions.jsonb_matches_schema(${quoteLiteral(JSON.stringify(APPROVAL_REQUEST_SCHEMA))}::json, ${body})) not valid;`,
297
+ `alter table ${table} validate constraint "${check}";`
298
+ ];
299
+ const comment = "-- approval store: body matches schemas/approval-request-v1.json through pg_jsonschema";
300
+ if (mode === true) return `\n${comment}\n${statements.join("\n")}\n`;
301
+ return `\n${comment}, where the extension is available
302
+ do $permdock$
303
+ begin
304
+ if exists (select 1 from pg_available_extensions where name = 'pg_jsonschema') then
305
+ ${statements.map((statement) => ` ${statement}`).join("\n")}
306
+ end if;
307
+ end
308
+ $permdock$;\n`;
309
+ }
310
+ /**
311
+ * The approval store (`rls.approvals`): one table of `ApprovalRequest`
312
+ * bodies with the columns a list filters on, and one `security definer`
313
+ * function per `ApprovalStore` method, each one atomic statement. No client
314
+ * role may read the table or execute the functions: approvals are opened and
315
+ * resolved by the server, which grants its own role.
316
+ */
317
+ function approvalStoreSql(ctx, jsonSchema = false) {
318
+ const table = qualified$1(ctx, APPROVAL_STORE.table);
319
+ const fn = (name) => qualified$1(ctx, name);
320
+ const index = (name) => `"${APPROVAL_STORE.table}_${name}"`;
321
+ return `-- approval store: ApprovalRequest bodies, opened, resolved and consumed through the functions below; supabaseApprovalStore calls them
322
+ create table if not exists ${table} (
323
+ token text primary key,
324
+ body jsonb not null,
325
+ status text not null check (status in ('pending', 'approved', 'rejected', 'expired')),
326
+ tenant text,
327
+ principal_id text,
328
+ actor_id text,
329
+ session text,
330
+ approvals integer not null default 0,
331
+ created_at text not null,
332
+ expires_at timestamptz not null,
333
+ consumed_at timestamptz
334
+ );
335
+ create index if not exists ${index("page")} on ${table} (created_at collate "C", token collate "C");
336
+ create index if not exists ${index("pending")} on ${table} (status, expires_at);
337
+ alter table ${table} enable row level security;
338
+ revoke all on table ${table} from anon, authenticated, public;
339
+ ${jsonSchema === false ? "" : bodySchemaSql(table, jsonSchema)}
340
+ -- open a request; a repeated ask keeps the open record and replaces only an expired one
341
+ create or replace function ${fn(APPROVAL_STORE.open)}(p_request jsonb)
342
+ returns void
343
+ language sql
344
+ volatile
345
+ security definer
346
+ set search_path = ''
347
+ as $$
348
+ insert into ${table} as a (token, body, status, tenant, principal_id, actor_id, session, approvals, created_at, expires_at, consumed_at)
349
+ values (
350
+ ${COLUMNS}
351
+ )
352
+ on conflict (token) do update
353
+ set body = excluded.body, status = excluded.status, tenant = excluded.tenant,
354
+ principal_id = excluded.principal_id, actor_id = excluded.actor_id, session = excluded.session,
355
+ approvals = excluded.approvals, created_at = excluded.created_at,
356
+ expires_at = excluded.expires_at, consumed_at = excluded.consumed_at
357
+ where a.status = 'expired' or a.expires_at <= now()
358
+ $$;
359
+ ${serverOnly(fn(APPROVAL_STORE.open), "jsonb")}
360
+
361
+ create or replace function ${fn(APPROVAL_STORE.get)}(p_token text)
362
+ returns jsonb
363
+ language sql
364
+ stable
365
+ security definer
366
+ set search_path = ''
367
+ as $$
368
+ select a.body from ${table} a where a.token = p_token
369
+ $$;
370
+ ${serverOnly(fn(APPROVAL_STORE.get), "text")}
371
+
372
+ -- write a verdict over the record it was computed from: still pending, unexpired, with p_seen approvals
373
+ create or replace function ${fn(APPROVAL_STORE.resolve)}(p_token text, p_next jsonb, p_seen integer)
374
+ returns jsonb
375
+ language sql
376
+ volatile
377
+ security definer
378
+ set search_path = ''
379
+ as $$
380
+ update ${table} a
381
+ set body = p_next,
382
+ status = p_next ->> 'status',
383
+ approvals = coalesce(jsonb_array_length(p_next -> 'approvals'), 0)
384
+ where a.token = p_token
385
+ and a.status = 'pending'
386
+ and a.expires_at > now()
387
+ and a.approvals = p_seen
388
+ returning a.body
389
+ $$;
390
+ ${serverOnly(fn(APPROVAL_STORE.resolve), "text, jsonb, integer")}
391
+
392
+ -- consume an approved, unexpired, unconsumed request once
393
+ create or replace function ${fn(APPROVAL_STORE.consume)}(p_token text, p_now text)
394
+ returns jsonb
395
+ language sql
396
+ volatile
397
+ security definer
398
+ set search_path = ''
399
+ as $$
400
+ update ${table} a
401
+ set consumed_at = p_now::timestamptz,
402
+ body = a.body || jsonb_build_object('consumedAt', p_now)
403
+ where a.token = p_token
404
+ and a.status = 'approved'
405
+ and a.consumed_at is null
406
+ and a.expires_at > p_now::timestamptz
407
+ returning a.body
408
+ $$;
409
+ ${serverOnly(fn(APPROVAL_STORE.consume), "text, text")}
410
+
411
+ -- one page in (createdAt, token) order after the cursor position
412
+ create or replace function ${fn(APPROVAL_STORE.list)}(p_filter jsonb, p_after_created text, p_after_token text, p_limit integer)
413
+ returns setof jsonb
414
+ language sql
415
+ stable
416
+ security definer
417
+ set search_path = ''
418
+ as $$
419
+ select a.body
420
+ from ${table} a
421
+ where ${FILTER}
422
+ and (p_after_created is null
423
+ or (a.created_at collate "C", a.token collate "C") > (p_after_created collate "C", p_after_token collate "C"))
424
+ order by a.created_at collate "C", a.token collate "C"
425
+ limit p_limit
426
+ $$;
427
+ ${serverOnly(fn(APPROVAL_STORE.list), "jsonb, text, text, integer")}
428
+
429
+ create or replace function ${fn(APPROVAL_STORE.expire)}(p_now text)
430
+ returns integer
431
+ language sql
432
+ volatile
433
+ security definer
434
+ set search_path = ''
435
+ as $$
436
+ with expired as (
437
+ update ${table} a
438
+ set status = 'expired', body = a.body || '{"status":"expired"}'::jsonb
439
+ where a.status = 'pending' and a.expires_at <= p_now::timestamptz
440
+ returning 1
441
+ )
442
+ select count(*)::integer from expired
443
+ $$;
444
+ ${serverOnly(fn(APPROVAL_STORE.expire), "text")}
445
+
446
+ -- reject every pending request the filter matches, as system:<by>
447
+ create or replace function ${fn(APPROVAL_STORE.cancel)}(p_filter jsonb, p_by text, p_note text, p_now text)
448
+ returns integer
449
+ language sql
450
+ volatile
451
+ security definer
452
+ set search_path = ''
453
+ as $$
454
+ with cancelled as (
455
+ update ${table} a
456
+ set status = 'rejected',
457
+ body = (a.body - 'note') || jsonb_strip_nulls(jsonb_build_object(
458
+ 'status', 'rejected', 'resolvedAt', p_now, 'resolvedBy', 'system:' || p_by, 'note', p_note
459
+ ))
460
+ where a.status = 'pending'
461
+ and ${FILTER.replaceAll("\n ", "\n ")}
462
+ returning 1
463
+ )
464
+ select count(*)::integer from cancelled
465
+ $$;
466
+ ${serverOnly(fn(APPROVAL_STORE.cancel), "jsonb, text, text, text")}
467
+ `;
468
+ }
469
+ /** The request fields an adopted table may mirror into its own columns, as SQL over a body. */
470
+ const MIRRORS = {
471
+ status: (body) => `${body} ->> 'status'`,
472
+ permission: (body) => `${body} ->> 'permission'`,
473
+ tenant: (body) => `${body} #>> '{subject,principal,tenant}'`,
474
+ principalId: (body) => `${body} #>> '{subject,principal,id}'`,
475
+ actorId: (body) => `${body} #>> '{subject,actor,id}'`,
476
+ session: (body) => `${body} #>> '{subject,session}'`,
477
+ approvals: (body) => `coalesce(jsonb_array_length(${body} -> 'approvals'), 0)`,
478
+ createdAt: (body) => `${body} ->> 'createdAt'`,
479
+ expiresAt: (body) => `${body} ->> 'expiresAt'`,
480
+ resolvedAt: (body) => `${body} ->> 'resolvedAt'`,
481
+ resolvedBy: (body) => `${body} ->> 'resolvedBy'`,
482
+ consumedAt: (body) => `${body} ->> 'consumedAt'`
483
+ };
484
+ function mirrorsOf(adopt) {
485
+ const out = [];
486
+ for (const [field, column] of Object.entries(adopt.mirror ?? {})) {
487
+ if (!Object.hasOwn(MIRRORS, field)) throw new Error(`PermDock CLI: rls.approvals.mirror.${field} is not a request field; use ${Object.keys(MIRRORS).join(", ")}`);
488
+ if (typeof column !== "string") throw new TypeError(`PermDock CLI: rls.approvals.mirror.${field} must be a column name`);
489
+ out.push([column, field]);
490
+ }
491
+ return out;
492
+ }
493
+ /** One row of `table` from `values` (column, SQL), each converted to the column's own type. */
494
+ function populate(table, values) {
495
+ return `pg_catalog.jsonb_populate_record(null::${table}, pg_catalog.jsonb_build_object(${values.map(([column, sql]) => `${quoteLiteral(column)}, ${sql}`).join(", ")}))`;
496
+ }
497
+ /** The mirrored columns' values for the request `body`. */
498
+ function mirrorRow(table, mirror, body) {
499
+ return populate(table, mirror.map(([column, field]) => [column, MIRRORS[field](body)]));
500
+ }
501
+ /** `, (<mirrored columns>) = (select …)` for an update that writes `body`; empty without mirrors. */
502
+ function mirrorSet(table, mirror, body) {
503
+ if (mirror.length === 0) return "";
504
+ const columns = mirror.map(([column]) => quoteIdent$1(column));
505
+ const picked = columns.map((column) => `r.${column}`).join(", ");
506
+ return `,\n ${columns.length === 1 ? columns[0] ?? "" : `(${columns.join(", ")})`} = (select ${picked} from ${mirrorRow(table, mirror, body)} r)`;
507
+ }
508
+ /**
509
+ * The approval store over a table the app already has (`rls.approvals:
510
+ * { table, token?, body?, mirror? }`): the same `permdock_approval_*`
511
+ * functions, reading every field from the request body in `body`, keyed by
512
+ * the text column `token`, and copying the fields `mirror` names into the
513
+ * app's own columns, converted to their types, on every write. The table's
514
+ * grants and policies stay the app's.
515
+ */
516
+ function adoptedApprovalStoreSql(ctx, adopt, jsonSchema = false) {
517
+ const table = quoteTable(qualifiedTable$1(adopt.table));
518
+ const base = adopt.table.split(".").at(-1) ?? adopt.table;
519
+ const token = quoteIdent$1(adopt.token ?? "token");
520
+ const body = quoteIdent$1(adopt.body ?? "body");
521
+ const mirror = mirrorsOf(adopt);
522
+ for (const [column] of mirror) if (column === (adopt.token ?? "token") || column === (adopt.body ?? "body")) throw new Error(`PermDock CLI: rls.approvals.mirror names the ${column} column, which already holds the token or the body`);
523
+ const open = adopt.open ?? "insert";
524
+ if (open !== "insert" && open !== "attach") throw new Error(`PermDock CLI: rls.approvals.open must be 'insert' or 'attach', got '${String(open)}'`);
525
+ const schema = adopt.schema;
526
+ if (schema !== void 0 && (typeof schema !== "string" || schema === "")) throw new Error("PermDock CLI: rls.approvals.schema must be a schema name");
527
+ const fn = (name) => schema === void 0 ? qualified$1(ctx, name) : `${quoteIdent$1(schema)}.${name}`;
528
+ const index = (name) => quoteIdent$1(`${base}_permdock_${name}`);
529
+ const status = `a.${body} ->> 'status'`;
530
+ const expires = `(a.${body} ->> 'expiresAt')::timestamptz`;
531
+ const created = `(a.${body} ->> 'createdAt')`;
532
+ const filter = `(p_filter ->> 'status' is null or ${status} = p_filter ->> 'status')
533
+ and (p_filter ->> 'tenant' is null or a.${body} #>> '{subject,principal,tenant}' = p_filter ->> 'tenant')
534
+ and (p_filter ->> 'principalId' is null or a.${body} #>> '{subject,principal,id}' = p_filter ->> 'principalId')
535
+ and (p_filter ->> 'actorId' is null or a.${body} #>> '{subject,actor,id}' = p_filter ->> 'actorId')
536
+ and (p_filter ->> 'session' is null or a.${body} #>> '{subject,session}' = p_filter ->> 'session')`;
537
+ const columns = [
538
+ token,
539
+ body,
540
+ ...mirror.map(([column]) => quoteIdent$1(column))
541
+ ];
542
+ const inserted = populate(table, [
543
+ [adopt.token ?? "token", "p_request ->> 'token'"],
544
+ [adopt.body ?? "body", "p_request"],
545
+ ...mirror.map(([column, field]) => [column, MIRRORS[field]("p_request")])
546
+ ]);
547
+ const insertSql = `-- open a request; a repeated ask keeps the open record and replaces only an expired one
548
+ create or replace function ${fn(APPROVAL_STORE.open)}(p_request jsonb)
549
+ returns void
550
+ language sql
551
+ volatile
552
+ security definer
553
+ set search_path = ''
554
+ as $$
555
+ insert into ${table} as a (${columns.join(", ")})
556
+ select ${columns.map((column) => `r.${column}`).join(", ")}
557
+ from ${inserted} r
558
+ on conflict (${token}) do update
559
+ set ${columns.filter((column) => column !== token).map((column) => `${column} = excluded.${column}`).join(", ")}
560
+ where ${status} = 'expired' or ${expires} <= now()
561
+ $$;`;
562
+ const attachSql = `create or replace function ${fn(APPROVAL_STORE.open)}(p_request jsonb)
563
+ returns void
564
+ language plpgsql
565
+ volatile
566
+ security definer
567
+ set search_path = ''
568
+ as $$
569
+ begin
570
+ update ${table} a
571
+ set ${body} = p_request${mirrorSet(table, mirror, "p_request")}
572
+ where a.${token} = p_request ->> 'token'
573
+ and (a.${body} is null or ${status} = 'expired' or ${expires} <= now());
574
+ if not found and not exists (select 1 from ${table} a where a.${token} = p_request ->> 'token') then
575
+ raise exception using
576
+ errcode = 'P0002',
577
+ message = 'permdock: no row of ${adopt.table.replaceAll("'", "''")} holds approval token ' || (p_request ->> 'token'),
578
+ hint = 'insert the application row with this token before opening the request';
579
+ end if;
580
+ end;
581
+ $$;`;
582
+ return `-- approval store adopted onto ${adopt.table}: ApprovalRequest bodies in ${adopt.body ?? "body"}, keyed by ${adopt.token ?? "token"}; supabaseApprovalStore calls the functions below
583
+ alter table ${table} add column if not exists ${token} text;
584
+ alter table ${table} add column if not exists ${body} jsonb;
585
+ create unique index if not exists ${index("token")} on ${table} (${token});
586
+ create index if not exists ${index("page")} on ${table} ((${body} ->> 'createdAt') collate "C", ${token} collate "C");
587
+ create index if not exists ${index("status")} on ${table} ((${body} ->> 'status'));
588
+ ${jsonSchema === false ? "" : bodySchemaSql(table, jsonSchema, body, `${base}_permdock_body_schema`)}
589
+ ${open === "attach" ? attachSql : insertSql}
590
+ ${serverOnly(fn(APPROVAL_STORE.open), "jsonb")}
591
+
592
+ create or replace function ${fn(APPROVAL_STORE.get)}(p_token text)
593
+ returns jsonb
594
+ language sql
595
+ stable
596
+ security definer
597
+ set search_path = ''
598
+ as $$
599
+ select a.${body} from ${table} a where a.${token} = p_token
600
+ $$;
601
+ ${serverOnly(fn(APPROVAL_STORE.get), "text")}
602
+
603
+ -- write a verdict over the record it was computed from: still pending, unexpired, with p_seen approvals
604
+ create or replace function ${fn(APPROVAL_STORE.resolve)}(p_token text, p_next jsonb, p_seen integer)
605
+ returns jsonb
606
+ language sql
607
+ volatile
608
+ security definer
609
+ set search_path = ''
610
+ as $$
611
+ update ${table} a
612
+ set ${body} = p_next${mirrorSet(table, mirror, "p_next")}
613
+ where a.${token} = p_token
614
+ and ${status} = 'pending'
615
+ and ${expires} > now()
616
+ and coalesce(jsonb_array_length(a.${body} -> 'approvals'), 0) = p_seen
617
+ returning a.${body}
618
+ $$;
619
+ ${serverOnly(fn(APPROVAL_STORE.resolve), "text, jsonb, integer")}
620
+
621
+ -- consume an approved, unexpired, unconsumed request once
622
+ create or replace function ${fn(APPROVAL_STORE.consume)}(p_token text, p_now text)
623
+ returns jsonb
624
+ language sql
625
+ volatile
626
+ security definer
627
+ set search_path = ''
628
+ as $$
629
+ update ${table} a
630
+ set ${body} = a.${body} || jsonb_build_object('consumedAt', p_now)${mirrorSet(table, mirror, `(a.${body} || jsonb_build_object('consumedAt', p_now))`)}
631
+ where a.${token} = p_token
632
+ and ${status} = 'approved'
633
+ and a.${body} ->> 'consumedAt' is null
634
+ and ${expires} > p_now::timestamptz
635
+ returning a.${body}
636
+ $$;
637
+ ${serverOnly(fn(APPROVAL_STORE.consume), "text, text")}
638
+
639
+ -- one page in (createdAt, token) order after the cursor position
640
+ create or replace function ${fn(APPROVAL_STORE.list)}(p_filter jsonb, p_after_created text, p_after_token text, p_limit integer)
641
+ returns setof jsonb
642
+ language sql
643
+ stable
644
+ security definer
645
+ set search_path = ''
646
+ as $$
647
+ select a.${body}
648
+ from ${table} a
649
+ where a.${body} is not null
650
+ and ${filter}
651
+ and (p_after_created is null
652
+ or (${created} collate "C", a.${token} collate "C") > (p_after_created collate "C", p_after_token collate "C"))
653
+ order by ${created} collate "C", a.${token} collate "C"
654
+ limit p_limit
655
+ $$;
656
+ ${serverOnly(fn(APPROVAL_STORE.list), "jsonb, text, text, integer")}
657
+
658
+ create or replace function ${fn(APPROVAL_STORE.expire)}(p_now text)
659
+ returns integer
660
+ language sql
661
+ volatile
662
+ security definer
663
+ set search_path = ''
664
+ as $$
665
+ with expired as (
666
+ update ${table} a
667
+ set ${body} = a.${body} || '{"status":"expired"}'::jsonb${mirrorSet(table, mirror, `(a.${body} || '{"status":"expired"}'::jsonb)`)}
668
+ where ${status} = 'pending' and ${expires} <= p_now::timestamptz
669
+ returning 1
670
+ )
671
+ select count(*)::integer from expired
672
+ $$;
673
+ ${serverOnly(fn(APPROVAL_STORE.expire), "text")}
674
+
675
+ -- reject every pending request the filter matches, as system:<by>
676
+ create or replace function ${fn(APPROVAL_STORE.cancel)}(p_filter jsonb, p_by text, p_note text, p_now text)
677
+ returns integer
678
+ language sql
679
+ volatile
680
+ security definer
681
+ set search_path = ''
682
+ as $$
683
+ with cancelled as (
684
+ update ${table} a
685
+ set ${body} = (a.${body} - 'note') || jsonb_strip_nulls(jsonb_build_object(
686
+ 'status', 'rejected', 'resolvedAt', p_now, 'resolvedBy', 'system:' || p_by, 'note', p_note
687
+ ))${mirrorSet(table, mirror, `((a.${body} - 'note') || jsonb_strip_nulls(jsonb_build_object('status', 'rejected', 'resolvedAt', p_now, 'resolvedBy', 'system:' || p_by, 'note', p_note)))`)}
688
+ where ${status} = 'pending'
689
+ and ${filter.replaceAll("\n ", "\n ")}
690
+ returning 1
691
+ )
692
+ select count(*)::integer from cancelled
693
+ $$;
694
+ ${serverOnly(fn(APPROVAL_STORE.cancel), "jsonb, text, text, text")}
695
+ `;
696
+ }
697
+ //#endregion
698
+ //#region src/cli/rls-break-glass.ts
699
+ /** The audit table every generated break-glass read writes to. Part of the SQL contract. */
700
+ const BREAK_GLASS_AUDIT = "permdock_break_glass_audit";
701
+ const IDENT = /^[a-z][a-z0-9_]*$/u;
702
+ /**
703
+ * The resources a `breakGlass` grant targets, with the table each reads. A
704
+ * break-glass grant is non-portable, so `compileGrants` never turns it into a
705
+ * policy; the server reads restricted rows through the generated function
706
+ * instead, which is why plain RLS keeps denying them.
707
+ */
708
+ function breakGlassEntries(policy, tables) {
709
+ const byResource = /* @__PURE__ */ new Map();
710
+ for (const item of collectGrants(policy)) {
711
+ const { grant } = item;
712
+ if (grant.breakGlass === void 0) continue;
713
+ const holder = breakGlassHolder(item);
714
+ const resource = grant.permission.resource;
715
+ const grants = byResource.get(resource) ?? [];
716
+ byResource.set(resource, grants);
717
+ if (holder === void 0) {
718
+ const root = policy.scopes[0];
719
+ grants.push({
720
+ permission: grant.permission.key,
721
+ scope: "anyone",
722
+ column: root === void 0 ? void 0 : scopeField(policy.resources.get(resource), root.name, policy.scopes),
723
+ ...root === void 0 ? {} : { root: root.name }
724
+ });
725
+ continue;
726
+ }
727
+ grants.push({
728
+ permission: grant.permission.key,
729
+ scope: holder.scope,
730
+ column: holder.scope === "global" ? void 0 : scopeField(policy.resources.get(resource), holder.scope, policy.scopes)
731
+ });
732
+ }
733
+ return [...byResource].map(([resource, grants]) => ({
734
+ resource,
735
+ table: tableFor(resource, tables),
736
+ grants
737
+ }));
738
+ }
739
+ /**
740
+ * Rows the subject's roles reach for `p_permission`: a break-glass grant
741
+ * lifts denies, never the tenant boundary. A grant to no role reads the rows
742
+ * of the root scopes the subject is a member of, or every row when the
743
+ * policy declares no scopes.
744
+ */
745
+ function rowsFilter(ctx, entry) {
746
+ const arms = entry.grants.flatMap((grant) => {
747
+ const asked = `p_permission = ${quoteLiteral(grant.permission)}`;
748
+ if (grant.scope === "anyone") {
749
+ if (grant.root === void 0) return [`(${asked})`];
750
+ return grant.column === void 0 ? [] : [`(${asked} and ${quoteIdent$1(grant.column)} in (select ${qualified$1(ctx, memberIdsHelper(grant.root))}()))`];
751
+ }
752
+ if (grant.scope !== "global" && grant.column === void 0) return [];
753
+ return [`(${asked} and ${accessSql(ctx, grant.scope, breakGlassKey(grant.permission), grant.column)})`];
754
+ });
755
+ return arms.length === 0 ? "false" : [...new Set(arms)].join("\n or ");
756
+ }
757
+ function functionName(resource) {
758
+ if (!IDENT.test(resource)) throw new Error(`PermDock CLI: unsafe break-glass resource '${resource}'`);
759
+ return `permdock_break_glass_${resource}`;
760
+ }
761
+ function qualifiedTable(name) {
762
+ return quoteTable(name.includes(".") ? name : `public.${name}`);
763
+ }
764
+ function auditSql(ctx) {
765
+ const audit = qualified$1(ctx, quoteIdent$1(BREAK_GLASS_AUDIT));
766
+ return `-- break-glass audit: one row per session that read restricted data
767
+ -- RLS never compiles a break-glass override, so restricted rows stay denied to a direct read
768
+ create table if not exists ${audit} (
769
+ id bigint generated always as identity primary key,
770
+ at timestamptz not null default now(),
771
+ subject text,
772
+ permission text not null,
773
+ purpose text,
774
+ reason text not null
775
+ );
776
+ alter table ${audit} enable row level security;
777
+ revoke all on table ${audit} from anon, authenticated, public;`;
778
+ }
779
+ function readFunctionSql(ctx, entry) {
780
+ const fn = qualified$1(ctx, functionName(entry.resource));
781
+ const audit = qualified$1(ctx, quoteIdent$1(BREAK_GLASS_AUDIT));
782
+ const table = qualifiedTable(entry.table);
783
+ const session = subjectClaimJsonSql(ctx, "break_glass");
784
+ const subject = subjectIdSql(ctx);
785
+ return `-- ${entry.resource}: reads restricted rows through a checked, audited break-glass session
786
+ create or replace function ${fn}(p_permission text)
787
+ returns setof ${table}
788
+ language plpgsql
789
+ security definer
790
+ set search_path = ''
791
+ as $$
792
+ declare
793
+ session jsonb := ${session};
794
+ begin
795
+ if session is null
796
+ or jsonb_typeof(session) <> 'object'
797
+ or session ->> 'v' <> '1'
798
+ or session ->> 'permission' <> p_permission
799
+ or coalesce(session ->> 'reason', '') = ''
800
+ or jsonb_typeof(session -> 'expiresAt') <> 'number'
801
+ or (session ->> 'expiresAt')::numeric <= extract(epoch from now())
802
+ then
803
+ raise exception 'PermDock: no valid break-glass session for %', p_permission
804
+ using errcode = '42501';
805
+ end if;
806
+ insert into ${audit} (subject, permission, purpose, reason)
807
+ values (
808
+ nullif((${subject})::text, ''),
809
+ p_permission,
810
+ session ->> 'purpose',
811
+ session ->> 'reason'
812
+ );
813
+ return query select * from ${table}
814
+ where ${rowsFilter(ctx, entry)};
815
+ end;
816
+ $$;
817
+ revoke execute on function ${fn}(text) from public, anon;
818
+ grant execute on function ${fn}(text) to authenticated;`;
819
+ }
820
+ /**
821
+ * The `security definer` read path for every `breakGlass` grant: an audit
822
+ * table and one `permdock_break_glass_<resource>(permission)` per resource
823
+ * that checks a signed break-glass session, writes an audit row and returns
824
+ * the rows in the scope instances where the subject holds a role with the
825
+ * break-glass grant (bypassing the table's policies as the definer). Empty
826
+ * when the policy has no break-glass grant.
827
+ */
828
+ function breakGlassSql(ctx, entries) {
829
+ if (entries.length === 0) return "";
830
+ return [auditSql(ctx), ...entries.map((entry) => readFunctionSql(ctx, entry))].join("\n\n");
831
+ }
832
+ //#endregion
833
+ //#region src/cli/rls-emit.ts
834
+ const HEADER = "-- generated by permdock rls generate\n-- fail-closed: never target a bypass role\n";
835
+ const TRUSTED_READER_GRANT = /^grant execute on function [^\n]+\.permdock_trusted_role_permissions\([^)\n]*\) to [^\n;]+;$/gmu;
836
+ function assertNoServiceRole(text) {
837
+ if (/\bservice_role\b/iu.test(text.replace(TRUSTED_READER_GRANT, ""))) throw new Error("PermDock CLI: generated RLS must never emit service_role");
838
+ }
839
+ function assertSupautilsSafe(text) {
840
+ const [first] = supautilsRejections(text);
841
+ if (first !== void 0) throw new Error(`PermDock CLI: generated SQL line ${String(first.line)} ${first.reason}, which supautils rejects on Supabase`);
842
+ }
843
+ /**
844
+ * Neon's Data API and `pg_session_jwt` name the anonymous role `anonymous`;
845
+ * Supabase, PostgREST and the GUC pattern use `anon`. Role lists end a line
846
+ * or a statement after `to` or `from`, so only those are renamed.
847
+ */
848
+ function dialectRoles(text, dialect) {
849
+ if (dialect !== "neon") return text;
850
+ 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(", ")}`);
851
+ }
852
+ function tablesOf(policies) {
853
+ return [...new Set(policies.map((item) => item.table))];
854
+ }
855
+ /** Enable (and force) row level security, then the table grants for the commands a policy allows. */
856
+ function tableSql(table, policies, force, views) {
857
+ const closed = views.find((view) => view.table === table && view.companion !== void 0);
858
+ const target = quoteTable(qualifiedTable$1(table));
859
+ const lines = [`alter table ${target} enable row level security;`];
860
+ if (force) lines.push(`alter table ${target} force row level security;`);
861
+ lines.push(`revoke all on table ${target} from anon, authenticated;`);
862
+ for (const role of ["anon", "authenticated"]) {
863
+ const cmds = new Set(policies.filter((item) => item.table === table && item.effect === "allow" && item.roles.includes(role)).map((item) => item.command));
864
+ const grants = [
865
+ "select",
866
+ "insert",
867
+ "update",
868
+ "delete"
869
+ ].filter((cmd) => cmds.has(cmd) && (closed === void 0 || cmd !== "select"));
870
+ if (grants.length > 0) lines.push(`grant ${grants.join(", ")} on table ${target} to ${role};`);
871
+ if (closed !== void 0 && cmds.has("select")) lines.push(columnGrantSql(closed, role));
872
+ }
873
+ if (closed !== void 0) lines.push(columnRevokeSql(closed));
874
+ return lines.join("\n");
875
+ }
876
+ /** `drop policy if exists` and `create policy` for one policy, without the table's grants. */
877
+ function policySql(item) {
878
+ const target = quoteTable(qualifiedTable$1(item.table));
879
+ const lines = [
880
+ `drop policy if exists ${quoteIdent$1(item.name)} on ${target};`,
881
+ `create policy ${quoteIdent$1(item.name)}`,
882
+ ` on ${target}`,
883
+ ` as ${item.effect === "deny" ? "restrictive" : "permissive"}`,
884
+ ` for ${item.command}`,
885
+ ` to ${item.roles.join(", ")}`
886
+ ];
887
+ if (item.using !== void 0) lines.push(` using (${item.using})`);
888
+ if (item.check !== void 0) lines.push(` with check (${item.check})`);
889
+ return `${lines.join("\n")};\n`;
890
+ }
891
+ function emitSql(policies, preamble, force = false, views = []) {
892
+ const chunks = [HEADER];
893
+ if (preamble !== "") chunks.push(preamble);
894
+ for (const table of tablesOf(policies)) chunks.push(tableSql(table, policies, force, views), "");
895
+ for (const item of policies) chunks.push(policySql(item));
896
+ if (views.length > 0) chunks.push(fieldViewsSql(views));
897
+ const text = `${chunks.join("\n").trim()}\n`;
898
+ assertNoServiceRole(text);
899
+ assertSupautilsSafe(text);
900
+ return text;
901
+ }
902
+ /**
903
+ * The SQL a Drizzle or Prisma schema cannot declare: helpers, grants, `force` and field views.
904
+ * `rls generate` writes it next to `out` for a custom migration.
905
+ */
906
+ function migrationSql(policies, preamble, force, views) {
907
+ const parts = [
908
+ preamble.trim(),
909
+ ...tablesOf(policies).map((table) => tableSql(table, policies, force, views)),
910
+ fieldViewsSql(views)
911
+ ].filter((part) => part !== "");
912
+ if (parts.length === 0) return "";
913
+ const text = `${HEADER}\n${parts.join("\n\n")}\n`;
914
+ assertNoServiceRole(text);
915
+ assertSupautilsSafe(text);
916
+ return text;
917
+ }
918
+ function helpersNote(helpers, how) {
919
+ return helpers === void 0 ? [] : [`// run ${helpers} ${how}`];
920
+ }
921
+ function camelCase(name) {
922
+ return name.replace(/[_-]+([a-z0-9])/gi, (_, char) => char.toUpperCase());
923
+ }
924
+ function tableName(table) {
925
+ return table.slice(table.lastIndexOf(".") + 1);
926
+ }
927
+ function pascalCase(name) {
928
+ const camel = camelCase(name);
929
+ return camel.charAt(0).toUpperCase() + camel.slice(1);
930
+ }
931
+ const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
932
+ function identifier(name, what) {
933
+ if (!IDENTIFIER.test(name)) throw new Error(`PermDock CLI: ${what} ${JSON.stringify(name)} is not an identifier`);
934
+ return name;
935
+ }
936
+ function quoted(text) {
937
+ return `'${text.replace(/[\\']/g, (match) => `\\${match}`)}'`;
938
+ }
939
+ function templateText(text) {
940
+ return text.replace(/[\\`]|\$\{/g, (match) => `\\${match}`);
941
+ }
942
+ const AUTH_UID = "(select auth.uid())";
943
+ function drizzleRoles(list) {
944
+ return list.includes("anon") ? "[anonRole, authenticatedRole]" : "authenticatedRole";
945
+ }
946
+ function emitDrizzle(policies, options) {
947
+ const supabase = options.dialect === "supabase";
948
+ const anon = policies.some((item) => item.roles.includes("anon"));
949
+ const sqlOf = (text) => {
950
+ const parts = text.split(AUTH_UID).map(templateText);
951
+ return `sql\`${supabase ? parts.join("${authUid}") : parts.join(AUTH_UID)}\``;
952
+ };
953
+ const usesUid = supabase && policies.some((item) => item.using?.includes(AUTH_UID) === true || item.check?.includes(AUTH_UID) === true);
954
+ const lines = [
955
+ "// generated by permdock rls generate",
956
+ "// fail-closed: never target a bypass role",
957
+ ...helpersNote(options.helpers, "in a custom migration (drizzle-kit generate --custom)"),
958
+ "import { sql } from 'drizzle-orm'"
959
+ ];
960
+ if (supabase) {
961
+ const named = [
962
+ anon ? "anonRole" : "",
963
+ "authenticatedRole",
964
+ usesUid ? "authUid" : ""
965
+ ].filter((name) => name !== "").join(", ");
966
+ lines.push("import { pgPolicy } from 'drizzle-orm/pg-core'", `import { ${named} } from 'drizzle-orm/supabase'`);
967
+ } else lines.push("import { pgPolicy, pgRole } from 'drizzle-orm/pg-core'");
968
+ lines.push(`import * as schema from ${quoted(options.schema ?? "./schema")}`, "");
969
+ if (!supabase) {
970
+ if (anon) lines.push(`export const anonRole = pgRole('${options.dialect === "neon" ? "anonymous" : "anon"}').existing()`);
971
+ lines.push("export const authenticatedRole = pgRole('authenticated').existing()", "");
972
+ }
973
+ for (const item of policies) {
974
+ const as = item.effect === "deny" ? "restrictive" : "permissive";
975
+ const table = identifier(options.exports?.[item.table] ?? camelCase(tableName(item.table)), "table export");
976
+ const name = identifier(camelCase(item.name), "policy export");
977
+ const using = item.using === void 0 ? "" : `\n using: ${sqlOf(item.using)},`;
978
+ const check = item.check === void 0 ? "" : `\n withCheck: ${sqlOf(item.check)},`;
979
+ lines.push(`export const ${name} = pgPolicy(${quoted(item.name)}, {`, ` as: '${as}',`, ` for: '${item.command}',`, ` to: ${drizzleRoles(item.roles)},${using}${check}`, `}).link(schema.${table})`, "");
980
+ }
981
+ const text = `${lines.join("\n").trimEnd()}\n`;
982
+ assertNoServiceRole(text);
983
+ return text;
984
+ }
985
+ /** Prisma 8 `policy_<command>` blocks; each target model declares `@@rls` itself. */
986
+ function emitPrisma(policies, options = {}) {
987
+ const modelOf = (table) => identifier(options.models?.[table] ?? pascalCase(tableName(table)), "model");
988
+ const models = [...new Set(policies.map((item) => modelOf(item.table)))];
989
+ const lines = [
990
+ "// generated by permdock rls generate (Prisma 8 policy blocks)",
991
+ "// fail-closed: never target a bypass role",
992
+ `// add @@rls to ${models.length === 1 ? "model" : "models"} ${models.join(", ")}; keep these blocks in their namespace`,
993
+ ...helpersNote(options.helpers, "in a migration before these policies"),
994
+ ""
995
+ ];
996
+ for (const item of policies) {
997
+ if (item.effect === "deny") throw new Error(`PermDock CLI: Prisma policy blocks are permissive only, so ${item.name} (a deny) needs --target sql`);
998
+ lines.push(`policy_${item.command} ${identifier(item.name, "policy name")} {`, ` target = ${modelOf(item.table)}`, ` roles = [${item.roles.join(", ")}]`);
999
+ if (item.using !== void 0) lines.push(` using = ${JSON.stringify(item.using)}`);
1000
+ if (item.check !== void 0) lines.push(` withCheck = ${JSON.stringify(item.check)}`);
1001
+ lines.push("}", "");
1002
+ }
1003
+ const text = `${lines.join("\n").trimEnd()}\n`;
1004
+ assertNoServiceRole(text);
1005
+ return text;
1006
+ }
1007
+ /** The migration file written next to `out`. */
1008
+ function migrationOut(out) {
1009
+ const dot = out.lastIndexOf(".");
1010
+ return `${dot > Math.max(out.lastIndexOf("/"), out.lastIndexOf("\\")) ? out.slice(0, dot) : out}.migration.sql`;
1011
+ }
1012
+ function defaultOut(target) {
1013
+ switch (target) {
1014
+ case "sql": return "rls.sql";
1015
+ case "drizzle": return "src/db/policies.ts";
1016
+ case "prisma": return "prisma/policies.prisma";
1017
+ default: return target;
1018
+ }
1019
+ }
1020
+ //#endregion
1021
+ //#region src/cli/rls-realtime-storage.ts
1022
+ const SCOPE_SEGMENT = /^\{([a-z][a-z0-9_]*)\}$/u;
1023
+ const LITERAL_SEGMENT = /^[^{}:\s]+$/u;
1024
+ const TOPIC = "(select realtime.topic())";
1025
+ function policyName(prefix, name, command) {
1026
+ const full = `permdock_${prefix}_${name.replaceAll(/[^A-Za-z0-9]+/gu, "_").replaceAll(/^_|_$/gu, "")}_${command}`;
1027
+ if (new TextEncoder().encode(full).length > 63) throw new Error(`PermDock CLI: the ${prefix} policy name ${full} is longer than Postgres's 63 bytes; shorten '${name}'`);
1028
+ return full;
1029
+ }
1030
+ function grantKey(facts, ref, where, warnings) {
1031
+ if (ref === void 0) return;
1032
+ const key = typeof ref === "object" && ref !== null ? Reflect.get(ref, "key") : ref;
1033
+ if (typeof key !== "string" || !facts.keys.has(key)) throw new Error(`PermDock CLI: ${where} must be a permission reference from the definitions (got ${JSON.stringify(key)})`);
1034
+ if (facts.rowConditions.has(key)) warnings.push(`${where} is ${key}, whose grants carry row conditions (a relation, a where or another condition): the policy admits only the instances its role allows without a condition reach, through permitted_<scope>_ids_by_permission`);
1035
+ return key;
1036
+ }
1037
+ function declaredScope(facts, scope, where) {
1038
+ if (!facts.scopes.has(scope)) throw new Error(`PermDock CLI: ${where} names the scope '${scope}', which definePolicy({ scopes }) does not declare`);
1039
+ return scope;
1040
+ }
1041
+ /**
1042
+ * The ids of `scope` where the subject holds `key` through an allow without
1043
+ * a row condition, minus a deny of it, as text: `<value> in (…)`. A
1044
+ * relationship or a `where` on the same permission never widens it.
1045
+ */
1046
+ function permitted(ctx, value, scope, key) {
1047
+ return `${value} in (select x::text from ${qualified$1(ctx, permittedByPermissionHelper(scope))}(${quoteLiteral(key)}) x)`;
1048
+ }
1049
+ function escapeRegex(text) {
1050
+ return text.replaceAll(/[.*+?^${}()|[\]\\]/gu, String.raw`\$&`);
1051
+ }
1052
+ function realtimePolicies(ctx, facts, realtime, warnings) {
1053
+ return Object.entries(realtime.topics).flatMap(([pattern, topic]) => {
1054
+ const where = `rls.realtime.topics['${pattern}']`;
1055
+ const segments = pattern.split(":");
1056
+ const placeholders = segments.flatMap((segment, index) => {
1057
+ const match = SCOPE_SEGMENT.exec(segment);
1058
+ return match === null ? [] : [{
1059
+ index,
1060
+ scope: match[1] ?? ""
1061
+ }];
1062
+ });
1063
+ const [placeholder] = placeholders;
1064
+ if (placeholder === void 0 || placeholders.length > 1 || segments.some((segment, index) => index !== placeholder.index && !LITERAL_SEGMENT.test(segment))) throw new Error(`PermDock CLI: ${where} must be ':'-separated segments with exactly one '{<scope>}' segment, such as 'org:{organization}:chat'`);
1065
+ const scope = declaredScope(facts, placeholder.scope, where);
1066
+ const regex = `^${segments.map((segment, index) => index === placeholder.index ? "[^:]+" : escapeRegex(segment)).join(":")}$`;
1067
+ const id = `split_part(${TOPIC}, ':', ${String(placeholder.index + 1)})`;
1068
+ const condition = (key) => `extension in ('broadcast', 'presence')
1069
+ and ${TOPIC} ~ ${quoteLiteral(regex)}
1070
+ and ${permitted(ctx, id, scope, key)}`;
1071
+ const read = grantKey(facts, topic.read, `${where}.read`, warnings);
1072
+ const write = grantKey(facts, topic.write, `${where}.write`, warnings);
1073
+ if (read === void 0) throw new Error(`PermDock CLI: ${where}.read is required`);
1074
+ const policies = [{
1075
+ name: policyName("realtime", pattern, "select"),
1076
+ table: "realtime.messages",
1077
+ command: "select",
1078
+ effect: "allow",
1079
+ roles: ["authenticated"],
1080
+ using: condition(read)
1081
+ }];
1082
+ if (write !== void 0) policies.push({
1083
+ name: policyName("realtime", pattern, "insert"),
1084
+ table: "realtime.messages",
1085
+ command: "insert",
1086
+ effect: "allow",
1087
+ roles: ["authenticated"],
1088
+ check: condition(write)
1089
+ });
1090
+ return policies;
1091
+ });
1092
+ }
1093
+ function folderOf(bucket, where) {
1094
+ const folder = bucket.folder ?? 1;
1095
+ if (!Number.isInteger(folder) || folder < 1) throw new Error(`PermDock CLI: ${where}.folder must be a positive integer (got ${String(folder)})`);
1096
+ return folder;
1097
+ }
1098
+ function storagePolicies(ctx, facts, storage, warnings) {
1099
+ return Object.entries(storage.buckets).flatMap(([id, bucket]) => {
1100
+ const where = `rls.storage.buckets['${id}']`;
1101
+ const scope = declaredScope(facts, bucket.scope, `${where}.scope`);
1102
+ const folder = folderOf(bucket, where);
1103
+ const condition = (key) => `bucket_id = ${quoteLiteral(id)}
1104
+ and ${permitted(ctx, `(storage.foldername(name))[${String(folder)}]`, scope, key)}`;
1105
+ const policies = [
1106
+ [
1107
+ "select",
1108
+ bucket.read,
1109
+ "read"
1110
+ ],
1111
+ [
1112
+ "insert",
1113
+ bucket.write,
1114
+ "write"
1115
+ ],
1116
+ [
1117
+ "update",
1118
+ bucket.write,
1119
+ "write"
1120
+ ],
1121
+ [
1122
+ "delete",
1123
+ bucket.delete,
1124
+ "delete"
1125
+ ]
1126
+ ].flatMap(([command, ref, field]) => {
1127
+ const key = grantKey(facts, ref, `${where}.${field}`, warnings);
1128
+ if (key === void 0) return [];
1129
+ const expression = condition(key);
1130
+ return [{
1131
+ name: policyName("storage", id, command),
1132
+ table: "storage.objects",
1133
+ command,
1134
+ effect: "allow",
1135
+ roles: ["authenticated"],
1136
+ ...command === "insert" ? {} : { using: expression },
1137
+ ...command === "select" || command === "delete" ? {} : { check: expression }
1138
+ }];
1139
+ });
1140
+ if (policies.length === 0) throw new Error(`PermDock CLI: ${where} needs read, write or delete`);
1141
+ return policies;
1142
+ });
1143
+ }
1144
+ /**
1145
+ * The `realtime.messages` and `storage.objects` policies `rls.realtime` and
1146
+ * `rls.storage` describe. Each one admits a row only where the subject holds
1147
+ * the permission in the scope instance its topic segment or folder names,
1148
+ * through an allow without a row condition, so the helpers' API-key ceiling
1149
+ * and suspension rules apply and a relationship grant on the same permission
1150
+ * never reaches a topic or a folder.
1151
+ */
1152
+ function realtimeStoragePolicies(ctx, facts, config, warnings = []) {
1153
+ if (config.realtime === void 0 && config.storage === void 0) return [];
1154
+ if (ctx.dialect !== "supabase") throw new Error(`PermDock CLI: rls.realtime and rls.storage need --dialect supabase (got ${ctx.dialect})`);
1155
+ const policies = [...config.realtime === void 0 ? [] : realtimePolicies(ctx, facts, config.realtime, warnings), ...config.storage === void 0 ? [] : storagePolicies(ctx, facts, config.storage, warnings)];
1156
+ const seen = /* @__PURE__ */ new Set();
1157
+ for (const { name } of policies) {
1158
+ if (seen.has(name)) throw new Error(`PermDock CLI: two rls.realtime or rls.storage entries both name the policy ${name}; rename one`);
1159
+ seen.add(name);
1160
+ }
1161
+ return policies;
1162
+ }
1163
+ //#endregion
1164
+ //#region src/cli/rls-rows.ts
1165
+ /** `permitted_<resource>_rows(p_permission)`: the resource's row ids the subject may act on with one permission. */
1166
+ function rowsHelper(resource) {
1167
+ const name = graphSqlName(resource);
1168
+ const helper = `permitted_${name ?? resource}_rows`;
1169
+ if (name === void 0 || new TextEncoder().encode(`${helper}_for`).length > 63) throw new Error(`PermDock CLI: rls.rowHelpers cannot name a helper for resource '${resource}'; use a SQL name of at most 45 bytes`);
1170
+ return helper;
1171
+ }
1172
+ /** The branches a row check of a permission reads: its allows and denies on rows that exist, for signed-in callers. */
1173
+ function rowBranchesOf(branches, resource) {
1174
+ const out = /* @__PURE__ */ new Map();
1175
+ for (const branch of branches) {
1176
+ if (branch.resource !== resource || branch.command === "insert" || branch.coverage === true || !branch.roles.includes("authenticated")) continue;
1177
+ out.set(branch.permissionKey, [...out.get(branch.permissionKey) ?? [], branch]);
1178
+ }
1179
+ return out;
1180
+ }
1181
+ function usingOf(branches) {
1182
+ const parts = branches.map((branch) => branchClauses(branch).using ?? "true");
1183
+ return parts.length === 0 ? void 0 : orSql(parts);
1184
+ }
1185
+ function permissionCase(key, branches) {
1186
+ const allow = usingOf(branches.filter((branch) => branch.effect === "allow"));
1187
+ if (allow === void 0) return;
1188
+ const deny = usingOf(branches.filter((branch) => branch.effect === "deny"));
1189
+ const granted = `coalesce((${allow}), false)`;
1190
+ return ` when ${quoteLiteral(key)} then ${deny === void 0 ? granted : `${granted} and (${deny}) is not true`}`;
1191
+ }
1192
+ function forUserSql(ctx, fn, helper) {
1193
+ const user = ctx.dialect === "supabase" ? "uuid" : "text";
1194
+ const signature = `${user}, text, jsonb`;
1195
+ const head = `create or replace function ${fn}(p_user ${user}, p_permission text, p_claims jsonb default '{}')
1196
+ returns setof text
1197
+ language plpgsql
1198
+ volatile
1199
+ security definer
1200
+ set search_path = ''
1201
+ as $$`;
1202
+ const tail = `revoke execute on function ${fn}(${signature}) from public, anon, authenticated;`;
1203
+ if (ctx.dialect === "supabase") return `${head}
1204
+ declare
1205
+ v_claims text := pg_catalog.current_setting('request.jwt.claims', true);
1206
+ begin
1207
+ perform pg_catalog.set_config(
1208
+ 'request.jwt.claims',
1209
+ (coalesce(p_claims, '{}'::jsonb) || pg_catalog.jsonb_build_object('sub', p_user::text, 'role', 'authenticated'))::text,
1210
+ true
1211
+ );
1212
+ return query select r.id from ${helper}(p_permission) r(id);
1213
+ perform pg_catalog.set_config('request.jwt.claims', coalesce(v_claims, ''), true);
1214
+ end
1215
+ $$;
1216
+ ${tail}`;
1217
+ const prefix = ctx.gucPrefix;
1218
+ return `${head}
1219
+ declare
1220
+ v_saved jsonb := '{}'::jsonb;
1221
+ v_key text;
1222
+ v_value text;
1223
+ begin
1224
+ for v_key, v_value in
1225
+ select 'user_id', p_user
1226
+ union all
1227
+ select c.key, c.value from pg_catalog.jsonb_each_text(coalesce(p_claims, '{}'::jsonb)) c where c.key <> 'user_id'
1228
+ loop
1229
+ v_saved := v_saved || pg_catalog.jsonb_build_object(v_key, coalesce(pg_catalog.current_setting(${quoteLiteral(`${prefix}.`)} || v_key, true), ''));
1230
+ perform pg_catalog.set_config(${quoteLiteral(`${prefix}.`)} || v_key, v_value, true);
1231
+ end loop;
1232
+ return query select r.id from ${helper}(p_permission) r(id);
1233
+ for v_key, v_value in select s.key, s.value from pg_catalog.jsonb_each_text(v_saved) s loop
1234
+ perform pg_catalog.set_config(${quoteLiteral(`${prefix}.`)} || v_key, v_value, true);
1235
+ end loop;
1236
+ end
1237
+ $$;
1238
+ ${tail}`;
1239
+ }
1240
+ function inheritTargets(policy) {
1241
+ const out = /* @__PURE__ */ new Set();
1242
+ for (const grant of policy.grants) for (const item of flattenGrantee(grant.to)) if (item.kind === "inherit") out.add(item.resource);
1243
+ return [...out].toSorted();
1244
+ }
1245
+ function stubSql(ctx, resource) {
1246
+ return `create or replace function ${qualified$1(ctx, rowsHelper(resource))}(p_permission text)
1247
+ returns setof text
1248
+ language sql
1249
+ stable
1250
+ security definer
1251
+ set search_path = ''
1252
+ as $$
1253
+ select null::text where false
1254
+ $$;`;
1255
+ }
1256
+ function rowSql(ctx, rows, table, where) {
1257
+ const fn = qualified$1(ctx, rows.replace(/_rows$/u, "_row"));
1258
+ return `create or replace function ${fn}(p_row ${table}, p_permission text)
1259
+ returns boolean
1260
+ language sql
1261
+ stable
1262
+ security definer
1263
+ set search_path = ''
1264
+ as $$
1265
+ select exists (select 1 from (select (p_row).*) r where ${where})
1266
+ $$;
1267
+ revoke execute on function ${fn}(${table}, text) from public, anon;
1268
+ grant execute on function ${fn}(${table}, text) to authenticated;`;
1269
+ }
1270
+ function resourceSql(ctx, node, branches, tables) {
1271
+ const name = rowsHelper(node.name);
1272
+ const fn = qualified$1(ctx, name);
1273
+ const table = quoteTable(qualifiedTable$1(tableFor(node.name, tables)));
1274
+ const cases = [...branches.keys()].toSorted().flatMap((key) => {
1275
+ const sql = permissionCase(key, branches.get(key) ?? []);
1276
+ return sql === void 0 ? [] : [sql];
1277
+ });
1278
+ const where = cases.length === 0 ? "false" : `case p_permission\n${cases.join("\n")}\n else false\n end`;
1279
+ const rows = `-- ${node.name}: ids of the rows the caller may act on with p_permission, as the table's policies decide it
1280
+ create or replace function ${fn}(p_permission text)
1281
+ returns setof text
1282
+ language sql
1283
+ stable
1284
+ security definer
1285
+ set search_path = ''
1286
+ as $$
1287
+ select ${quoteIdent$1(node.id)}::text from ${table}
1288
+ where ${where}
1289
+ $$;
1290
+ revoke execute on function ${fn}(text) from public, anon;
1291
+ grant execute on function ${fn}(text) to authenticated;
1292
+ ${rowSql(ctx, name, table, where)}`;
1293
+ if (ctx.dialect === "neon") return rows;
1294
+ return `${rows}
1295
+ -- the same rows for a user the caller names, with claims it supplies; no client role may execute it
1296
+ ${forUserSql(ctx, qualified$1(ctx, `${name}_for`), fn)}`;
1297
+ }
1298
+ /**
1299
+ * `permitted_<resource>_rows(p_permission)` for each resource `select`
1300
+ * names (or every resource a grant reaches with `true`): the ids of its rows
1301
+ * the caller may read, update or delete with that permission, from the same
1302
+ * compiled allows and denies the table policies use, so closure walks, link
1303
+ * hops, the restricted stop, `includes`, groups and `requires` all apply. A
1304
+ * `security definer` helper, so hand-written policies and functions call it
1305
+ * without restating the graph. Outside the `neon` dialect a
1306
+ * `permitted_<resource>_rows_for(p_user, p_permission, p_claims)` form runs it
1307
+ * as the named user, with the claims the caller supplies, for trusted SQL.
1308
+ */
1309
+ function rowHelpersSql(ctx, policy, branches, select, tables, warnings) {
1310
+ const names = select === true ? [...new Set(branches.flatMap((branch) => branch.resource === void 0 ? [] : [branch.resource]))].toSorted() : [...new Set(select)];
1311
+ const called = inheritTargets(policy);
1312
+ for (const name of called) if (!names.includes(name)) names.push(name);
1313
+ const chunks = called.map((name) => stubSql(ctx, name));
1314
+ for (const name of names) {
1315
+ const node = policy.resources.get(name);
1316
+ if (node === void 0) throw new Error(`PermDock CLI: rls.rowHelpers names '${name}', which the policy does not declare`);
1317
+ chunks.push(resourceSql(ctx, node, rowBranchesOf(branches, name), tables));
1318
+ }
1319
+ if (chunks.length > 0 && ctx.dialect === "neon") warnings.push("rls.rowHelpers: the neon dialect reads the subject from auth.session(), which SQL cannot set, so no permitted_<resource>_rows_for is written");
1320
+ return chunks.length === 0 ? "" : `${chunks.join("\n\n")}\n`;
1321
+ }
1322
+ //#endregion
1323
+ //#region src/cli/rls-shims.ts
1324
+ const NAME = /^[a-z_][a-z0-9_]*$/u;
1325
+ /**
1326
+ * The key a legacy caller passes, mapped the way `rls migrate` maps it:
1327
+ * `keys` and `renamed` exactly, then the longest `prefixes` entry.
1328
+ */
1329
+ function mappedKey(config, renamed) {
1330
+ const exact = {
1331
+ ...renamed,
1332
+ ...config.keys
1333
+ };
1334
+ const prefixes = Object.entries(config.prefixes ?? {}).toSorted(([a], [b]) => b.length - a.length);
1335
+ const fallback = prefixes.length === 0 ? "p_key" : `case ${prefixes.map(([from, to]) => `when pg_catalog.starts_with(p_key, ${quoteLiteral(from)}) then ${quoteLiteral(to)} || pg_catalog.substr(p_key, ${String(from.length + 1)})`).join(" ")} else p_key end`;
1336
+ if (Object.keys(exact).length === 0) return fallback;
1337
+ return `coalesce(${quoteLiteral(JSON.stringify(exact))}::jsonb ->> p_key, ${fallback})`;
1338
+ }
1339
+ function scopeOf(ctx, scope) {
1340
+ if (!ctx.scopes.some((item) => item.name === scope)) throw new Error(`PermDock CLI: rls.migrate helper scope '${scope}' is not a declared scope`);
1341
+ return scope;
1342
+ }
1343
+ function addKey(list, key) {
1344
+ if (!list.includes(key)) list.push(key);
1345
+ }
1346
+ /**
1347
+ * The grant keys behind each permission, per scope: the keys of its
1348
+ * unconditional allows, and every deny key. A shim cannot apply a row
1349
+ * condition or a validity window, so a conditional allow answers nothing and
1350
+ * a conditional deny always subtracts; the conditioned keys are listed apart.
1351
+ * A break-glass key answers only the break-glass read, so it is left out too.
1352
+ * Rows seeded under a former key are left out; the shim maps a former key to
1353
+ * the current one first.
1354
+ */
1355
+ function shimGrants(rows, conditioned, renamed) {
1356
+ const byScope = /* @__PURE__ */ new Map();
1357
+ for (const row of rows.toSorted((a, b) => a.grantKey < b.grantKey ? -1 : a.grantKey > b.grantKey ? 1 : 0)) {
1358
+ if (Object.hasOwn(renamed, row.permission) || row.effect === "allow" && row.grantKey === breakGlassKey(row.permission)) continue;
1359
+ const scope = byScope.get(row.scope) ?? {};
1360
+ byScope.set(row.scope, scope);
1361
+ const keys = (Object.hasOwn(scope, row.permission) ? scope[row.permission] : void 0) ?? {
1362
+ allow: [],
1363
+ deny: []
1364
+ };
1365
+ scope[row.permission] = keys;
1366
+ const isConditioned = conditioned.has(row.grantKey);
1367
+ if (row.effect === "allow") {
1368
+ if (isConditioned) {
1369
+ keys["conditioned-allow"] ??= [];
1370
+ addKey(keys["conditioned-allow"], row.grantKey);
1371
+ } else addKey(keys.allow, row.grantKey);
1372
+ continue;
1373
+ }
1374
+ addKey(keys.deny, row.grantKey);
1375
+ if (isConditioned) {
1376
+ keys["conditioned-deny"] ??= [];
1377
+ addKey(keys["conditioned-deny"], row.grantKey);
1378
+ }
1379
+ }
1380
+ return byScope;
1381
+ }
1382
+ function shimMap(grants, scope) {
1383
+ const entries = Object.entries(grants.get(scope) ?? {}).flatMap(([permission, keys]) => keys.allow.length === 0 && keys.deny.length === 0 ? [] : [[permission, {
1384
+ allow: keys.allow,
1385
+ deny: keys.deny
1386
+ }]]);
1387
+ return JSON.stringify(Object.fromEntries(entries));
1388
+ }
1389
+ function shimFunction(name, args, argTypes, returns, sql) {
1390
+ return `create or replace function ${name}(${args})
1391
+ returns ${returns}
1392
+ language sql
1393
+ stable
1394
+ security definer
1395
+ set search_path = ''
1396
+ as $$
1397
+ ${sql}
1398
+ $$;
1399
+ revoke execute on function ${name}(${argTypes}) from public, anon;
1400
+ grant execute on function ${name}(${argTypes}) to authenticated;`;
1401
+ }
1402
+ function grantKeys(keys, scope, effect) {
1403
+ return `pg_catalog.jsonb_array_elements_text(coalesce(${keys.map(scope)} -> (${keys.key}) -> ${quoteLiteral(effect)}, '[]'::jsonb)) g(grant_key)`;
1404
+ }
1405
+ /** The instances of `scope` the caller holds an unconditional allow of the key in, minus those it holds a deny in. */
1406
+ function idsSql(ctx, keys, scope) {
1407
+ const helper = qualified$1(ctx, permittedIdsHelper(scope));
1408
+ return ` select a.id
1409
+ from ${grantKeys(keys, scope, "allow")}
1410
+ cross join lateral ${helper}(g.grant_key) a(id)
1411
+ except
1412
+ select d.id
1413
+ from ${grantKeys(keys, scope, "deny")}
1414
+ cross join lateral ${helper}(g.grant_key) d(id)`;
1415
+ }
1416
+ function hasSql(ctx, keys) {
1417
+ const has = qualified$1(ctx, HELPERS.has);
1418
+ return `exists (select 1 from ${grantKeys(keys, "global", "allow")} where ${has}(g.grant_key))
1419
+ and not exists (select 1 from ${grantKeys(keys, "global", "deny")} where ${has}(g.grant_key))`;
1420
+ }
1421
+ function shim(ctx, config, keys, fn, helper) {
1422
+ switch (helper.form) {
1423
+ case "ids": {
1424
+ const scope = scopeOf(ctx, helper.scope);
1425
+ return shimFunction(fn, "p_key text", "text", `setof ${scopeTypeOf(ctx, scope)}`, idsSql(ctx, keys, scope));
1426
+ }
1427
+ case "row": {
1428
+ const scope = scopeOf(ctx, helper.scope);
1429
+ const type = scopeTypeOf(ctx, scope);
1430
+ return shimFunction(fn, `p_id ${type}, p_key text`, `${type}, text`, "boolean", ` select coalesce(p_id in (\n${idsSql(ctx, keys, scope)}\n ), false)`);
1431
+ }
1432
+ case "membership": {
1433
+ const scope = scopeOf(ctx, helper.scope);
1434
+ const type = scopeTypeOf(ctx, scope);
1435
+ return shimFunction(fn, `p_id ${type}`, type, "boolean", ` select coalesce(p_id in (select ${qualified$1(ctx, memberIdsHelper(scope))}()), false)`);
1436
+ }
1437
+ case "global": return shimFunction(fn, "p_key text", "text", "boolean", ` select ${hasSql(ctx, keys)}`);
1438
+ case "scoped": {
1439
+ const literals = /* @__PURE__ */ new Map();
1440
+ for (const scope of ctx.scopes) literals.set(scope.name, scope.name);
1441
+ for (const [literal, scope] of Object.entries(config.scopes ?? {})) literals.set(literal, scopeOf(ctx, scope));
1442
+ const global = config.globalScopes ?? [];
1443
+ 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 ${hasSql(ctx, keys)}`], ...[...literals].filter(([literal]) => !global.includes(literal)).map(([literal, scope]) => ` when p_scope = ${quoteLiteral(literal)} then coalesce(p_id in (select s.id::text from (\n${idsSql(ctx, keys, scope)}\n ) s(id)), false)`)].join("\n")}\n else false\n end`);
1444
+ }
1445
+ default: return helper;
1446
+ }
1447
+ }
1448
+ /**
1449
+ * One wrapper per `rls.migrate.helpers` entry under its legacy name, so SQL
1450
+ * that `rls migrate` has not rewritten yet (function bodies, views, triggers)
1451
+ * answers from the PermDock helpers. A wrapper maps the legacy key to the
1452
+ * permission key, then to the grant keys `grants` lists for the helper's
1453
+ * scope, so a permission whose grants are split by condition still answers.
1454
+ * Each is `security definer` with an empty `search_path`, so a caller needs
1455
+ * `execute` on the wrapper only, not usage on the helper schema.
1456
+ */
1457
+ function shimsSql(ctx, config, shims, renamed, grants) {
1458
+ const schema = shims.schema ?? "public";
1459
+ const keys = {
1460
+ key: mappedKey(config, renamed),
1461
+ map: (scope) => `${quoteLiteral(shimMap(grants, scope))}::jsonb`
1462
+ };
1463
+ const chunks = [`-- permdock shims: legacy helper names over the permdock helpers; drop each once doctor reports no caller (PD056)`];
1464
+ for (const [name, helper] of Object.entries(config.helpers).toSorted(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
1465
+ if (!NAME.test(name)) throw new Error(`PermDock CLI: unsafe rls.migrate helper name '${name}'`);
1466
+ chunks.push(shim(ctx, config, keys, `${quoteIdent$1(schema)}.${name}`, helper));
1467
+ }
1468
+ return `${chunks.join("\n\n")}\n`;
1469
+ }
1470
+ //#endregion
1471
+ //#region src/cli/rls-generate.ts
1472
+ async function loadPolicy(cwd, config, from) {
1473
+ const policyPath = from === "drizzle" ? config.policy : from ?? config.policy;
1474
+ if (policyPath === void 0) throw new Error("PermDock CLI: rls generate needs policy in permdock.config.ts or --from");
1475
+ return asPolicy(pickNamed(await loadModule(resolve(cwd, policyPath)), ["policy"]));
1476
+ }
1477
+ /** `rls.customRoleWrites.requires` as permission keys, or why it names none. */
1478
+ function writeRequirement(policy, requires) {
1479
+ if (requires === void 0) return {};
1480
+ const leaves = listPermissions(policy.permissions);
1481
+ const keys = requires === "manageRoles" ? leaves.filter((leaf) => leaf.meta.manageRoles === true).map((leaf) => leaf.key) : [...requires];
1482
+ if (keys.length === 0) return { error: requires === "manageRoles" ? "rls.customRoleWrites.requires is 'manageRoles', but no permission declares meta.manageRoles" : "rls.customRoleWrites.requires names no permission" };
1483
+ const known = new Set(leaves.map((leaf) => leaf.key));
1484
+ const unknown = keys.filter((key) => !known.has(key));
1485
+ if (unknown.length > 0) return { error: `rls.customRoleWrites.requires names undeclared permissions: ${unknown.join(", ")}` };
1486
+ return { keys: [...new Set(keys)].toSorted() };
1487
+ }
1488
+ /** Declared role names, and those a tenant admin may compose into custom roles. */
1489
+ function customRoleNames(policy, requires) {
1490
+ const declared = [...roleNames(policy)].toSorted();
1491
+ const assignable = declared.filter((name) => policy.rolesByName.get(name)?.assignable ?? listRoles(policy.vocabulary.roles).some((leaf) => leaf.key === name && leaf.assignable));
1492
+ const renamed = renamedKeys(policy.vocabulary.permissions);
1493
+ const leaves = listPermissions(policy.permissions);
1494
+ const manage = leaves.filter((leaf) => leaf.meta.manageRoles === true).map((leaf) => leaf.key);
1495
+ return {
1496
+ declared,
1497
+ assignable,
1498
+ ...renamed.size === 0 ? {} : { renamed: Object.fromEntries(renamed) },
1499
+ permissions: leaves.map((leaf) => leaf.key).toSorted(),
1500
+ ...manage.length === 0 ? {} : { manage: manage.toSorted() },
1501
+ ...requires === void 0 ? {} : { requires },
1502
+ ...policy.levels === void 0 ? {} : { levels: true }
1503
+ };
1504
+ }
1505
+ function moveUndiffed(sql, label, part) {
1506
+ const { kept, moved } = splitUndiffed(sql);
1507
+ const [first = "", ...rest] = kept.split("\n");
1508
+ return {
1509
+ sql: [
1510
+ first,
1511
+ `-- what supabase db diff drops from this part is in ${label}`,
1512
+ ...rest
1513
+ ].join("\n"),
1514
+ grants: `-- the privileges and view options supabase db diff drops from the ${part} part\n${moved.join("\n")}`
1515
+ };
1516
+ }
1517
+ /**
1518
+ * The row columns that belong to a table. With `rls.tables` set, a resource
1519
+ * it does not name is a table only when generated policies are written for
1520
+ * it; with `--helpers-only` it has none, so it gets no index.
1521
+ */
1522
+ function tableColumns(rowColumns, tables, helpersOnly, warnings) {
1523
+ if (tables === void 0) return rowColumns;
1524
+ const unmapped = [...new Set(rowColumns.map((item) => item.resource).filter((resource) => !Object.hasOwn(tables, resource)))].toSorted();
1525
+ if (unmapped.length === 0) return rowColumns;
1526
+ if (helpersOnly) {
1527
+ warnings.push(`no index is suggested for resources without an rls.tables entry: ${unmapped.join(", ")}; map each one that is a table`);
1528
+ return rowColumns.filter((item) => Object.hasOwn(tables, item.resource));
1529
+ }
1530
+ warnings.push(`policies for resources without an rls.tables entry target a table named after the resource: ${unmapped.map((resource) => qualifiedTable$1(resource)).join(", ")}; map each one to its table`);
1531
+ return rowColumns;
1532
+ }
1533
+ async function runRlsGenerate(input) {
1534
+ const rls = input.config.rls;
1535
+ const fieldsMode = input.fields ?? rls?.fields;
1536
+ if (fieldsMode !== void 0 && fieldsMode !== "views") return {
1537
+ code: 2,
1538
+ output: `rls generate --fields must be views (got ${fieldsMode})`,
1539
+ text: ""
1540
+ };
1541
+ const revokeColumns = input.revokeColumns === true || rls?.revokeColumns === true;
1542
+ if (revokeColumns && fieldsMode === void 0) return {
1543
+ code: 2,
1544
+ output: "rls generate --revoke-columns needs --fields views",
1545
+ text: ""
1546
+ };
1547
+ const policy = await loadPolicy(input.cwd, input.config, input.from);
1548
+ const memberships = parseMembershipsFlag(input.memberships) ?? rls?.memberships;
1549
+ if (input.rbac && input.dialect !== "supabase") return {
1550
+ code: 2,
1551
+ output: `rls generate --rbac supabase needs --dialect supabase (got ${input.dialect})`,
1552
+ text: ""
1553
+ };
1554
+ const schema = input.rbacSchema ?? rls?.schema ?? rls?.rbac?.schema ?? "permdock";
1555
+ const authorize = resolveAuthorize(input.config, {
1556
+ authorize: input.authorize,
1557
+ rbac: input.rbac,
1558
+ memberships
1559
+ });
1560
+ const memberSources = rls?.membershipSources ?? input.config.supabase?.hook?.memberships;
1561
+ const sources = authorize === "database" ? memberSources : void 0;
1562
+ const scopes = scopeList(policy.scopes);
1563
+ const suspension = checkSuspension(rls?.suspension, scopes);
1564
+ const hookRoles = input.config.supabase?.hook?.roles;
1565
+ const roles = rls?.roles ?? (hookRoles === false ? void 0 : hookRoles);
1566
+ if (roles !== void 0 && input.rbac) return {
1567
+ code: 2,
1568
+ output: "rls generate --rbac supabase creates its own user_roles (user_id, role app_role); drop rls.roles (or supabase.hook.roles) or the --rbac flag",
1569
+ text: ""
1570
+ };
1571
+ const requires = writeRequirement(policy, rls?.customRoleWrites?.requires);
1572
+ if (requires.error !== void 0) return {
1573
+ code: 2,
1574
+ output: requires.error,
1575
+ text: ""
1576
+ };
1577
+ const readers = rls?.trustedReaders;
1578
+ if (readers !== void 0 && (!Array.isArray(readers) || !readers.every((name) => typeof name === "string" && name !== ""))) return {
1579
+ code: 2,
1580
+ output: "rls.trustedReaders must list Postgres role names, such as ['service_role']",
1581
+ text: ""
1582
+ };
1583
+ const ownership = ownershipRules(policy, scopes);
1584
+ const graph = graphPlan(policy);
1585
+ const apiKeys = apiKeysPlan(rls?.apiKeys, new Set(roleNames(policy)), Object.fromEntries(renamedKeys(policy.vocabulary.permissions)));
1586
+ const ctx = {
1587
+ dialect: input.dialect,
1588
+ scopes,
1589
+ tenantClaim: rls?.tenantClaim ?? "tenant_id",
1590
+ ...rls?.tenants === "all" ? { tenants: "all" } : {},
1591
+ gucPrefix: input.gucPrefix ?? rls?.gucPrefix ?? "app",
1592
+ ...rls?.actions === void 0 ? {} : { actions: rls.actions },
1593
+ inlineFunctions: input.inlineFunctions || rls?.inlineFunctions === true,
1594
+ schema,
1595
+ authorize,
1596
+ roleClaim: rls?.roleClaim ?? "user_role",
1597
+ tenantType: input.tenantType ?? rls?.tenantType ?? "uuid",
1598
+ ...rls?.teamType === void 0 ? {} : { teamType: rls.teamType },
1599
+ ...rls?.scopeTypes === void 0 ? {} : { scopeTypes: rls.scopeTypes },
1600
+ ...memberships === void 0 ? {} : { memberships },
1601
+ ...sources === void 0 || sources.length === 0 ? {} : { sources },
1602
+ ...memberSources === void 0 || memberSources.length === 0 ? {} : { memberSources },
1603
+ ...suspension === void 0 ? {} : { suspension },
1604
+ ...roles === void 0 ? {} : { roles },
1605
+ ...input.customRoles === true || rls?.customRoles === true ? { customRoles: {
1606
+ ...customRoleNames(policy, requires.keys),
1607
+ ...rls?.customRoleWrites?.roles === void 0 ? {} : { table: rls.customRoleWrites.roles }
1608
+ } } : {},
1609
+ ...input.capabilities === true || rls?.capabilities === true ? { capabilities: true } : {},
1610
+ ...apiKeys === void 0 ? {} : { apiKeys },
1611
+ ...rls?.anonymousSignIns === "deny" ? { anonymousSignIns: "deny" } : {},
1612
+ ...ownership === void 0 ? {} : { ownership },
1613
+ ...rls?.ownershipTriggers === void 0 ? {} : { skipOwnershipTriggers: rls.ownershipTriggers === false ? "all" : Object.keys(rls.ownershipTriggers).map((key) => {
1614
+ const name = resolveScope(scopes, key);
1615
+ if (name === void 0) throw new Error(`PermDock CLI: rls.ownershipTriggers.${key} names a scope the policy does not declare`);
1616
+ return name;
1617
+ }) },
1618
+ ...rls?.assignments === void 0 ? {} : { assignments: rls.assignments === true ? { tables: [] } : {
1619
+ tables: rls.assignments.tables ?? [],
1620
+ ...rls.assignments.ownRole === void 0 ? {} : { ownRole: rls.assignments.ownRole }
1621
+ } },
1622
+ ...fieldsMode === void 0 ? {} : { fields: fieldsMode },
1623
+ ...graph.size === 0 ? {} : { graph: {
1624
+ closures: closureDepths(graph),
1625
+ resources: policy.resources,
1626
+ ...rls?.tables === void 0 ? {} : { tables: rls.tables }
1627
+ } }
1628
+ };
1629
+ if (ctx.anonymousSignIns === "deny" && ctx.dialect !== "supabase") return {
1630
+ code: 2,
1631
+ output: `rls.anonymousSignIns needs --dialect supabase (got ${ctx.dialect})`,
1632
+ text: ""
1633
+ };
1634
+ const jsonSchema = rls?.jsonSchema;
1635
+ if (jsonSchema !== void 0 && jsonSchema !== "auto" && typeof jsonSchema !== "boolean") return {
1636
+ code: 2,
1637
+ output: `rls.jsonSchema must be 'auto', true or false (got ${JSON.stringify(jsonSchema)})`,
1638
+ text: ""
1639
+ };
1640
+ const warnings = [];
1641
+ if (jsonSchema !== void 0 && jsonSchema !== false && (rls?.approvals === void 0 || rls.approvals === false)) warnings.push("rls.jsonSchema constrains the approval store's body, which needs rls.approvals: true; nothing else is jsonb");
1642
+ if (authorize === "database") {
1643
+ for (const { name } of ctx.scopes) if (policy.grants.some((grant) => grant.scope === name) && scopeTable(ctx, name) === void 0 && scopeSources(ctx, name).length === 0) warnings.push(`${name}-scoped grants read the ${name} memberships table in database mode: set rls.memberships.scopes.${name}, rls.membershipSources or supabase.hook.memberships (or --memberships for the first scope), otherwise they deny`);
1644
+ }
1645
+ const compiled = compileGrants(policy, ctx, rls?.tables, warnings, input.skipClosures, rls?.rowHelpers !== void 0 || inheritTargets(policy).length > 0);
1646
+ const helpersOnly = input.helpersOnly === true || rls?.helpersOnly === true;
1647
+ if (helpersOnly && (input.target !== "sql" || revokeColumns)) return {
1648
+ code: 2,
1649
+ output: "rls generate --helpers-only needs --target sql and no --revoke-columns: the table grants are hand-written, so revoke the restricted columns there",
1650
+ text: ""
1651
+ };
1652
+ const views = fieldsMode === void 0 ? [] : fieldViews(policy, ctx, compiled.branches, {
1653
+ revokeColumns,
1654
+ helpersOnly
1655
+ }, warnings);
1656
+ const force = input.force === true || rls?.force === true;
1657
+ if (force && views.some((view) => view.companion !== void 0)) warnings.push("--force with --revoke-columns: each <table>_visible_fields companion reads as its owner, so its owner needs BYPASSRLS or the restricted columns read as null");
1658
+ const policyName = input.policyName ?? rls?.policyName;
1659
+ const assembled = helpersOnly ? [] : assemblePolicies(fieldsMode === void 0 ? compiled.branches : rowBranches(compiled.branches), {
1660
+ perRole: input.policyPerRole === true || rls?.policyPerRole === true,
1661
+ ...policyName === void 0 ? {} : { name: policyName }
1662
+ });
1663
+ const actorKinds = readOnlyActorKinds(rls?.readOnlyActors);
1664
+ if (actorKinds !== void 0 && helpersOnly) warnings.push("rls.readOnlyActors adds policies, and --helpers-only writes none: add the restrictive read-only policies to the hand-written ones");
1665
+ const policies = actorKinds === void 0 ? assembled : [...assembled, ...readOnlyActorPolicies(assembled, subjectClaimJsonSql(ctx, "act"), actorKinds)];
1666
+ let tablePolicies;
1667
+ try {
1668
+ tablePolicies = realtimeStoragePolicies(ctx, {
1669
+ scopes: new Set(ctx.scopes.map((scope) => scope.name)),
1670
+ keys: new Set(listPermissions(policy.vocabulary.permissions).map((leaf) => leaf.key)),
1671
+ rowConditions: new Set(policyRowConditionKeys(policy))
1672
+ }, compact({
1673
+ realtime: rls?.realtime,
1674
+ storage: rls?.storage
1675
+ }), warnings);
1676
+ } catch (cause) {
1677
+ return {
1678
+ code: 2,
1679
+ output: describeError(cause),
1680
+ text: ""
1681
+ };
1682
+ }
1683
+ if (tablePolicies.length > 0 && helpersOnly) warnings.push("rls.realtime and rls.storage add policies, and --helpers-only writes none: no realtime.messages or storage.objects policy is written");
1684
+ const servicePolicies = tablePolicies.length === 0 || helpersOnly ? [] : [...tablePolicies, ...actorKinds === void 0 ? [] : readOnlyActorPolicies(tablePolicies, subjectClaimJsonSql(ctx, "act"), actorKinds)];
1685
+ const helperTables = servicePolicies.length === 0 ? void 0 : [SERVICE_TABLES_HEADER, ...servicePolicies.map(policySql)].join("\n");
1686
+ const guardedTables = servicePolicies.length === 0 ? void 0 : [`${SERVICE_TABLES_HEADER} A stack without the service has no table, so each policy is skipped there.`, ...servicePolicies.map(guardedPolicySql)].join("\n");
1687
+ const rootMapped = ctx.scopes[0] === void 0 ? void 0 : scopeTable(ctx, ctx.scopes[0].name);
1688
+ const rootTable = rootMapped === void 0 ? void 0 : {
1689
+ ...rootMapped.table,
1690
+ tenant: rootMapped.column
1691
+ };
1692
+ const rbac = input.rbac ? rbacScaffold(policy, {
1693
+ schema,
1694
+ authorize,
1695
+ ...rootTable === void 0 ? {} : { memberships: rootTable },
1696
+ ...ctx.scopes[0] === void 0 ? {} : { scope: ctx.scopes[0].name },
1697
+ ...ctx.customRoles === void 0 ? {} : { customRoles: {
1698
+ declared: ctx.customRoles.declared,
1699
+ ...ctx.customRoles.levels === true ? { levels: true } : {}
1700
+ } },
1701
+ context: ctx
1702
+ }) : void 0;
1703
+ if (rbac !== void 0) warnings.push(...rbac.warnings);
1704
+ if (input.rbac) warnings.push("the token hook that writes user_role and memberships comes from permdock supabase hook generate");
1705
+ const owned = [ownershipSql(ctx), assignmentSql(ctx)].filter((part) => part !== "").join("\n");
1706
+ if (ctx.assignments !== void 0 && (ownership === void 0 || ownership.assigns.length === 0)) warnings.push("rls.assignments needs a role that declares assigns: no assignment trigger is written");
1707
+ let approvals;
1708
+ try {
1709
+ approvals = rls?.approvals === true ? approvalStoreSql(ctx, jsonSchema ?? false) : typeof rls?.approvals === "object" ? adoptedApprovalStoreSql(ctx, rls.approvals, jsonSchema ?? false) : void 0;
1710
+ } catch (cause) {
1711
+ return {
1712
+ code: 2,
1713
+ output: describeError(cause),
1714
+ text: ""
1715
+ };
1716
+ }
1717
+ const graphed = graphSql(ctx, graph, rls?.tables);
1718
+ let rowHelpers = "";
1719
+ if (rls?.rowHelpers !== void 0 || inheritTargets(policy).length > 0) try {
1720
+ rowHelpers = rowHelpersSql(ctx, policy, [...fieldsMode === void 0 ? compiled.branches : rowBranches(compiled.branches), ...compiled.actionBranches], rls?.rowHelpers ?? [], rls?.tables, warnings);
1721
+ } catch (cause) {
1722
+ return {
1723
+ code: 2,
1724
+ output: describeError(cause),
1725
+ text: ""
1726
+ };
1727
+ }
1728
+ const breakGlass = breakGlassSql(ctx, breakGlassEntries(policy, rls?.tables));
1729
+ const configured = rls?.shims;
1730
+ const shimsConfig = typeof configured === "object" ? configured : configured === true || input.shims === true ? {} : void 0;
1731
+ if (shimsConfig !== void 0 && rls?.migrate === void 0) return {
1732
+ code: 2,
1733
+ output: "PermDock CLI: rls generate --shims needs rls.migrate.helpers in permdock.config.ts",
1734
+ text: ""
1735
+ };
1736
+ const renamed = Object.fromEntries(renamedKeys(policy.vocabulary.permissions));
1737
+ const grants = shimGrants(compiled.rolePermissions, compiled.conditionedKeys, renamed);
1738
+ const anonExecute = rls?.anonExecute === true || views.some((view) => view.roles.includes("anon")) || compiled.branches.some((branch) => branch.roles.includes("anon") && [
1739
+ branch.access,
1740
+ branch.using,
1741
+ branch.check
1742
+ ].some((sql) => sql?.includes("permdock_user_id") === true));
1743
+ const usesLiveSession = compiled.branches.some((branch) => [
1744
+ branch.access,
1745
+ branch.using,
1746
+ branch.check
1747
+ ].some((sql) => sql?.includes(SESSION_LIVE_HELPER) === true));
1748
+ const shims = shimsConfig === void 0 || rls?.migrate === void 0 ? void 0 : shimsSql(ctx, rls.migrate, shimsConfig, renamed, grants);
1749
+ const preamble = [
1750
+ rbac?.head,
1751
+ helpersSql(ctx, compiled.rolePermissions, {
1752
+ levelReach: compiled.levelReach,
1753
+ permissions: listPermissions(policy.permissions).map((leaf) => leaf.key).toSorted(),
1754
+ userRoles: !input.rbac,
1755
+ anonExecute,
1756
+ withoutSeeds: splitsPart(input.split, "seeds"),
1757
+ ...rls?.trustedReaders === void 0 ? {} : { trustedReaders: rls.trustedReaders }
1758
+ }),
1759
+ permissionHelpersSql(ctx, grants, renamed, anonExecute),
1760
+ usesLiveSession ? sessionLiveHelperSql(ctx) : void 0,
1761
+ approvals,
1762
+ owned === "" ? void 0 : owned,
1763
+ graphed === "" ? void 0 : graphed,
1764
+ breakGlass === "" ? void 0 : breakGlass,
1765
+ rowHelpers === "" ? void 0 : rowHelpers,
1766
+ shims,
1767
+ rbac?.tail
1768
+ ].filter((part) => part !== void 0).join("\n");
1769
+ if (force) for (const entry of graph.values()) {
1770
+ const own = [...entry.relations].filter((name) => !("edge" in (entry.node.relations[name] ?? {})));
1771
+ if (own.length > 0) warnings.push(`--force: ${entry.node.name} relations ${own.join(", ")} are read from the ${entry.node.name} table itself, so ${graphHelper(entry.node.name)} would recurse through its own policy; keep ${entry.node.name} unforced or move them to edge tables`);
1772
+ }
1773
+ const outRel = input.out ?? rls?.out ?? defaultOut(input.target);
1774
+ const migration = input.target === "sql" ? "" : dialectRoles(migrationSql(policies, withHelperTables(preamble, helperTables), force, views), ctx.dialect);
1775
+ const migrationRel = migration === "" ? void 0 : migrationOut(outRel);
1776
+ const helpers = migrationRel === void 0 ? void 0 : basename(migrationRel);
1777
+ let text;
1778
+ switch (input.target) {
1779
+ case "sql":
1780
+ text = dialectRoles(withHelperTables(emitSql(policies, preamble, force, views), helperTables), ctx.dialect);
1781
+ break;
1782
+ case "drizzle":
1783
+ text = emitDrizzle(policies, compact({
1784
+ dialect: ctx.dialect,
1785
+ schema: rls?.drizzle?.schema,
1786
+ exports: rls?.drizzle?.exports,
1787
+ helpers
1788
+ }));
1789
+ break;
1790
+ case "prisma":
1791
+ text = emitPrisma(policies, compact({
1792
+ models: rls?.prisma?.models,
1793
+ helpers
1794
+ }));
1795
+ break;
1796
+ default: return input.target;
1797
+ }
1798
+ let indexes = indexTargets(ctx, tableColumns(compiled.rowColumns, rls?.tables, helpersOnly, warnings));
1799
+ if (input.db !== void 0) try {
1800
+ const pruned = pruneIndexTargets(indexes, await readTableIndexFacts(input.db, [...new Set(indexes.map((target) => target.table))], input.connect));
1801
+ indexes = pruned.targets;
1802
+ warnings.push(...pruned.warnings);
1803
+ } catch (cause) {
1804
+ return {
1805
+ code: 2,
1806
+ output: describeError(cause),
1807
+ text: ""
1808
+ };
1809
+ }
1810
+ 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`);
1811
+ const extras = migration === "" ? {} : { migration };
1812
+ if (input.write === false) return {
1813
+ code: 0,
1814
+ output: warnings.join("\n"),
1815
+ text,
1816
+ policies,
1817
+ seeds: compiled.rolePermissions,
1818
+ indexes,
1819
+ keys: {
1820
+ permissions: listPermissions(policy.vocabulary.permissions).map((leaf) => leaf.key),
1821
+ rowConditions: [...policyRowConditionKeys(policy)],
1822
+ renamed: Object.fromEntries(renamedKeys(policy.vocabulary.permissions))
1823
+ },
1824
+ schema,
1825
+ helpersOnly,
1826
+ ...extras
1827
+ };
1828
+ const planned = outputFiles({
1829
+ input,
1830
+ outRel,
1831
+ text,
1832
+ helpersOnly,
1833
+ sql: () => ({
1834
+ policies: dialectRoles(withHelperTables(emitSql(policies, "", force, views), helperTables), ctx.dialect),
1835
+ declarativePolicies: dialectRoles(emitSql(policies, "", force, views), ctx.dialect),
1836
+ ...guardedTables === void 0 ? {} : { servicePolicies: dialectRoles(guardedTables, ctx.dialect) },
1837
+ helpers: dialectRoles(emitSql([], preamble, false, helpersOnly ? views : []), ctx.dialect),
1838
+ seeds: `${SEEDS_MARKER} schema=${schema}\n${seedSql(ctx, compiled.rolePermissions)}\n`,
1839
+ indexes: `${INDEXES_MARKER}\n${indexesSql(indexes)}\n`
1840
+ }),
1841
+ scopes,
1842
+ schema
1843
+ });
1844
+ if (typeof planned === "string") return {
1845
+ code: 2,
1846
+ output: planned,
1847
+ text,
1848
+ ...extras
1849
+ };
1850
+ const files = [...planned];
1851
+ if (migrationRel !== void 0) files.push({
1852
+ part: "migration",
1853
+ rel: migrationRel,
1854
+ text: migration
1855
+ });
1856
+ if (input.check) {
1857
+ const drift = driftOf(input.cwd, files);
1858
+ return drift.length === 0 ? {
1859
+ code: 0,
1860
+ output: "rls generate up to date",
1861
+ text,
1862
+ ...extras
1863
+ } : {
1864
+ code: 1,
1865
+ output: drift.map((line) => `rls generate drift: ${line}`).join("\n"),
1866
+ text,
1867
+ ...extras
1868
+ };
1869
+ }
1870
+ const written = writeSqlFiles(input.cwd, files);
1871
+ const extra = warnings.length === 0 ? "" : `\n${warnings.join("\n")}`;
1872
+ return {
1873
+ code: 0,
1874
+ output: `wrote ${written.wrote.join(", ")}${written.printed === "" ? "" : `\n${written.printed}`}${extra}`,
1875
+ text,
1876
+ ...extras
1877
+ };
1878
+ }
1879
+ /** Whether `--split` names `part`, which then leaves the helpers or the warnings. */
1880
+ const SERVICE_TABLES_HEADER = "-- Supabase Realtime and Storage: rls.realtime and rls.storage. RLS on both tables is Supabase's.";
1881
+ /**
1882
+ * A `realtime.messages` or `storage.objects` policy that applies only where
1883
+ * the table exists. The Realtime and Storage services create those tables, so
1884
+ * a local stack with the service off, and pg-delta's shadow database, have
1885
+ * none.
1886
+ */
1887
+ function guardedPolicySql(item) {
1888
+ const body = policySql(item).trimEnd().split("\n").map((line) => ` ${line}`).join("\n");
1889
+ return `do $permdock$
1890
+ begin
1891
+ if to_regclass(${quoteLiteral(qualifiedTable$1(item.table))}) is not null then
1892
+ ${body}
1893
+ end if;
1894
+ end
1895
+ $permdock$;
1896
+ `;
1897
+ }
1898
+ /** Appends the `realtime.messages` and `storage.objects` policies, which never get `tableSql`. */
1899
+ function withHelperTables(sql, helperTables) {
1900
+ if (helperTables === void 0) return sql;
1901
+ const tables = `${helperTables.trimEnd()}\n`;
1902
+ return sql.trim() === "" ? tables : `${sql.trimEnd()}\n\n${tables}`;
1903
+ }
1904
+ function splitsPart(raw, part) {
1905
+ const split = parseSplit(raw);
1906
+ return typeof split !== "string" && split?.includes(part) === true;
1907
+ }
1908
+ /** The files `generate` writes: one, or one per `--split` part, plus the hook's grants. */
1909
+ function outputFiles(plan) {
1910
+ const { input } = plan;
1911
+ const split = parseSplit(input.split);
1912
+ if (typeof split === "string") return split;
1913
+ if (split?.includes("policies") === true && plan.helpersOnly) return "rls generate --helpers-only writes no policies part; drop it from --split";
1914
+ if (split === void 0) {
1915
+ if (input.grantsOut !== void 0) return "rls generate --grants-out needs --split with the helpers or hook part";
1916
+ if (input.seedsOut !== void 0) return "rls generate --seeds-out needs --split with the seeds part";
1917
+ return [{
1918
+ part: "rls",
1919
+ rel: plan.outRel,
1920
+ text: plan.text
1921
+ }];
1922
+ }
1923
+ if (input.target !== "sql") return "rls generate --split needs --target sql";
1924
+ const pgDelta = input.out === void 0 && input.config.rls?.out === void 0 ? supabaseConfig(input.cwd).pgDelta : void 0;
1925
+ if (pgDelta === void 0 && !plan.outRel.includes("{part}")) return `rls generate --split needs {part} in --out, for example supabase/schemas/identity/056_permdock_{part}.sql (got ${plan.outRel})`;
1926
+ if (pgDelta !== void 0 && split.includes("helpers") && !split.includes("seeds")) return "rls generate --split helpers under pg-delta needs the seeds part with --seeds-out: pg-delta rejects the role_permissions rows in a declarative file";
1927
+ if (pgDelta !== void 0 && split.includes("seeds") && input.seedsOut === void 0) return "rls generate --split seeds under pg-delta needs --seeds-out: pg-delta does not diff the role_permissions rows, so they go in a migration";
1928
+ const at = (part, schema = plan.schema) => pgDelta === void 0 ? partPath(plan.outRel, part) : pgDeltaPath(pgDelta.schemaDir, part, schema);
1929
+ if (input.grantsOut !== void 0 && !split.includes("hook") && !split.includes("helpers")) return "rls generate --grants-out needs the helpers or hook part in --split";
1930
+ if (input.seedsOut !== void 0 && !split.includes("seeds")) return "rls generate --seeds-out needs the seeds part in --split";
1931
+ if (split.includes("hook") && input.config.supabase?.hook === void 0) return "rls generate --split hook needs supabase.hook in permdock.config.ts";
1932
+ const sql = plan.sql();
1933
+ const relocated = pgDelta === void 0 ? void 0 : sql.servicePolicies;
1934
+ if (relocated !== void 0 && split.includes("policies") && !split.includes("seeds")) return "rls generate --split policies under pg-delta with rls.realtime or rls.storage needs the seeds part with --seeds-out: the shadow database has no realtime.messages or storage.objects, so their policies go in that migration";
1935
+ const seeds = relocated === void 0 || !split.includes("policies") ? sql.seeds : `${sql.seeds}\n${relocated}`;
1936
+ const files = [];
1937
+ const label = grantsLabel(input.grantsOut);
1938
+ let grantsMarker = `${GRANTS_MARKER} schema=${plan.schema}`;
1939
+ const grants = [];
1940
+ for (const part of split) switch (part) {
1941
+ case "helpers": {
1942
+ if (label === void 0) {
1943
+ files.push({
1944
+ part,
1945
+ rel: at(part),
1946
+ text: sql.helpers
1947
+ });
1948
+ break;
1949
+ }
1950
+ const moved = moveUndiffed(sql.helpers, label, part);
1951
+ files.push({
1952
+ part,
1953
+ rel: at(part),
1954
+ text: moved.sql
1955
+ });
1956
+ grants.push(moved.grants);
1957
+ break;
1958
+ }
1959
+ case "seeds": {
1960
+ const out = input.seedsOut;
1961
+ if (out !== void 0 && isSeedsDirectory(input.cwd, out)) {
1962
+ const planned = seedsMigration(input.cwd, out, SEEDS_MARKER, seeds, input.io.now?.() ?? /* @__PURE__ */ new Date());
1963
+ if (!planned.current || input.check) files.push({
1964
+ part,
1965
+ rel: planned.rel,
1966
+ text: seeds
1967
+ });
1968
+ break;
1969
+ }
1970
+ files.push({
1971
+ part,
1972
+ rel: out ?? at(part),
1973
+ text: seeds
1974
+ });
1975
+ break;
1976
+ }
1977
+ case "indexes":
1978
+ files.push({
1979
+ part,
1980
+ rel: at(part),
1981
+ text: sql.indexes
1982
+ });
1983
+ break;
1984
+ case "policies":
1985
+ files.push({
1986
+ part,
1987
+ rel: at(part),
1988
+ text: pgDelta === void 0 ? sql.policies : sql.declarativePolicies
1989
+ });
1990
+ break;
1991
+ case "hook": {
1992
+ const hook = supabaseHookSql(plan.scopes, input.config, {}, label);
1993
+ files.push({
1994
+ part,
1995
+ rel: at(part, hook.manifest.hook.schema),
1996
+ text: hook.sql
1997
+ });
1998
+ const [marker = grantsMarker, ...rest] = hook.grants.split("\n");
1999
+ grantsMarker = marker;
2000
+ grants.push(rest.join("\n").trimEnd());
2001
+ break;
2002
+ }
2003
+ default: return part;
2004
+ }
2005
+ if (input.grantsOut !== void 0) files.push({
2006
+ part: "grants",
2007
+ rel: input.grantsOut,
2008
+ text: `${[grantsMarker, ...grants].join("\n")}\n`
2009
+ });
2010
+ return files;
2011
+ }
2012
+ //#endregion
2013
+ //#region src/cli/rls-introspect.ts
2014
+ const ROLES = ["anon", "authenticated"];
2015
+ /** Neon's `anonymous` is the role `rls generate` writes as `anon` for the other dialects. */
2016
+ function liveRole(role) {
2017
+ return role === "anonymous" ? "anon" : role;
2018
+ }
2019
+ const PRIVILEGES = [
2020
+ "select",
2021
+ "insert",
2022
+ "update",
2023
+ "delete"
2024
+ ];
2025
+ /** `posts` and `"app"."posts"` as `public.posts` and `app.posts`. */
2026
+ function qualified(table) {
2027
+ const parts = table.split(".").map((part) => part.replaceAll("\"", ""));
2028
+ return parts.length === 1 ? `public.${parts[0] ?? ""}` : parts.join(".");
2029
+ }
2030
+ const FUNCTION_HEADER = /create or replace function ("?)([A-Za-z_][\w$]*)\1\.("?)([A-Za-z_][\w$]*)\3\s*\(([\s\S]*?)\nas \$/gu;
2031
+ function helpersOf(sql) {
2032
+ const found = /* @__PURE__ */ new Set();
2033
+ for (const match of sql.matchAll(FUNCTION_HEADER)) {
2034
+ const [header = "", , schema = "", , name = ""] = match;
2035
+ if (/\bsecurity definer\b/u.test(header)) found.add(`${schema}.${name}`);
2036
+ }
2037
+ return [...found];
2038
+ }
2039
+ function expectedRls(policies, sql, indexes = []) {
2040
+ const tables = [...new Set(policies.map((item) => qualified(item.table)))];
2041
+ const grants = {};
2042
+ for (const table of tables) {
2043
+ const byRole = {};
2044
+ for (const role of ROLES) {
2045
+ const commands = new Set(policies.filter((item) => qualified(item.table) === table && item.effect === "allow" && item.roles.includes(role)).map((item) => item.command));
2046
+ byRole[role] = PRIVILEGES.filter((privilege) => commands.has(privilege));
2047
+ }
2048
+ grants[table] = byRole;
2049
+ }
2050
+ return {
2051
+ policies: policies.map((item) => ({
2052
+ table: qualified(item.table),
2053
+ name: item.name,
2054
+ command: item.command,
2055
+ permissive: item.effect === "allow",
2056
+ roles: item.roles.toSorted()
2057
+ })),
2058
+ tables,
2059
+ grants,
2060
+ helpers: helpersOf(sql),
2061
+ indexes: indexes.map((target) => ({
2062
+ ...target,
2063
+ table: qualified(target.table)
2064
+ }))
2065
+ };
2066
+ }
2067
+ /**
2068
+ * One warning per index target no index starts with. A missing index slows
2069
+ * the policy down but grants nothing, so these do not fail `verify`.
2070
+ */
2071
+ function missingIndexes(expected, actual) {
2072
+ return expected.indexes.flatMap((target) => {
2073
+ const column = target.columns[0];
2074
+ const columns = actual.columns?.[target.table];
2075
+ return column === void 0 || columns !== void 0 && !columns.includes(column) || actual.leadingColumns[target.table]?.includes(column) === true ? [] : [`warning: ${target.table}: no index starts with ${column}, which the policies or helpers filter on; add it, or write it with rls generate --split ...,indexes`];
2076
+ });
2077
+ }
2078
+ function sameList(left, right) {
2079
+ const a = left.toSorted();
2080
+ const b = right.toSorted();
2081
+ return a.length === b.length && a.every((item, index) => item === b[index]);
2082
+ }
2083
+ /** One line per difference; policy expressions are not compared, since Postgres rewrites their text. */
2084
+ function diffRls(expected, actual, options = {}) {
2085
+ const out = [];
2086
+ const found = new Map(actual.policies.map((item) => [`${item.table}\u0000${item.name}`, item]));
2087
+ for (const want of expected.policies) {
2088
+ const have = found.get(`${want.table}\u0000${want.name}`);
2089
+ if (have === void 0) {
2090
+ out.push(`${want.table}: policy ${want.name} is missing`);
2091
+ continue;
2092
+ }
2093
+ if (have.command !== want.command) out.push(`${want.table}: policy ${want.name} is for ${have.command}, expected ${want.command}`);
2094
+ if (have.permissive !== want.permissive) out.push(`${want.table}: policy ${want.name} is ${have.permissive ? "permissive" : "restrictive"}, expected ${want.permissive ? "permissive" : "restrictive"}`);
2095
+ if (!sameList(have.roles, want.roles)) out.push(`${want.table}: policy ${want.name} applies to ${have.roles.join(", ")}, expected ${want.roles.join(", ")}`);
2096
+ }
2097
+ const wanted = new Set(expected.policies.map((item) => `${item.table}\u0000${item.name}`));
2098
+ 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`);
2099
+ for (const table of expected.tables) {
2100
+ if (actual.rlsEnabled[table] !== true) {
2101
+ out.push(actual.rlsEnabled[table] === void 0 ? `${table}: table is missing` : `${table}: row level security is disabled`);
2102
+ continue;
2103
+ }
2104
+ for (const role of ROLES) {
2105
+ const want = expected.grants[table]?.[role] ?? [];
2106
+ const have = actual.grants[table]?.[role] ?? [];
2107
+ 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`);
2108
+ for (const privilege of have) if (!want.includes(privilege)) out.push(`${table}: ${role} holds ${privilege}, which no generated policy allows`);
2109
+ }
2110
+ }
2111
+ for (const helper of expected.helpers) {
2112
+ const have = actual.helpers[helper];
2113
+ if (have === void 0) {
2114
+ out.push(`${helper}: helper is missing`);
2115
+ continue;
2116
+ }
2117
+ if (!have.securityDefiner) out.push(`${helper}: helper is not security definer`);
2118
+ if (!have.emptySearchPath) out.push(`${helper}: helper does not set search_path = ''`);
2119
+ }
2120
+ return out;
2121
+ }
2122
+ const POLICIES_SQL = `select schemaname || '.' || tablename as target, policyname, cmd, permissive, roles::text[] as roles
2123
+ from pg_policies
2124
+ where schemaname || '.' || tablename = any($1::text[])`;
2125
+ const TABLES_SQL = `select n.nspname || '.' || c.relname as target, c.relrowsecurity as enabled
2126
+ from pg_class c
2127
+ join pg_namespace n on n.oid = c.relnamespace
2128
+ where c.relkind in ('r', 'p') and n.nspname || '.' || c.relname = any($1::text[])`;
2129
+ const GRANTS_SQL = `select table_schema || '.' || table_name as target, grantee, lower(privilege_type) as privilege
2130
+ from information_schema.role_table_grants
2131
+ where grantee in ('anon', 'anonymous', 'authenticated')
2132
+ and lower(privilege_type) in ('select', 'insert', 'update', 'delete')
2133
+ and table_schema || '.' || table_name = any($1::text[])`;
2134
+ const HELPERS_SQL = `select n.nspname || '.' || p.proname as target, p.prosecdef as definer,
2135
+ coalesce(array_to_string(p.proconfig, ','), '') as config
2136
+ from pg_proc p
2137
+ join pg_namespace n on n.oid = p.pronamespace
2138
+ where n.nspname || '.' || p.proname = any($1::text[])`;
2139
+ const INDEXES_SQL = `select n.nspname || '.' || c.relname as target, a.attname as leading
2140
+ from pg_index i
2141
+ join pg_class c on c.oid = i.indrelid
2142
+ join pg_namespace n on n.oid = c.relnamespace
2143
+ join pg_attribute a on a.attrelid = i.indrelid and a.attnum = i.indkey[0]
2144
+ where i.indpred is null and n.nspname || '.' || c.relname = any($1::text[])`;
2145
+ function commandOf(cmd) {
2146
+ const text = String(cmd ?? "").toLowerCase();
2147
+ return text === "*" ? "all" : text;
2148
+ }
2149
+ function connectIntrospect(db) {
2150
+ return connectPgPool(db, "permdock rls verify --introspect");
2151
+ }
2152
+ /** Reads the catalogs for the tables and helpers `expected` names (postgres-meta's queries, trimmed). */
2153
+ async function introspectRls(db, expected, connect = connectIntrospect) {
2154
+ const client = await connect(db);
2155
+ try {
2156
+ const tables = [...expected.tables];
2157
+ const indexed = [...new Set(expected.indexes.map((target) => target.table))];
2158
+ const [policies, enabled, grants, helpers, indexes, columns] = await Promise.all([
2159
+ client.query(POLICIES_SQL, [tables]),
2160
+ client.query(TABLES_SQL, [tables]),
2161
+ client.query(GRANTS_SQL, [tables]),
2162
+ client.query(HELPERS_SQL, [[...expected.helpers]]),
2163
+ client.query(INDEXES_SQL, [indexed]),
2164
+ client.query(COLUMNS_SQL, [indexed])
2165
+ ]);
2166
+ const columnsOf = {};
2167
+ for (const row of columns.rows) (columnsOf[String(row["target"])] ??= []).push(String(row["name"]));
2168
+ const leadingColumns = {};
2169
+ for (const row of indexes.rows) (leadingColumns[String(row["target"])] ??= []).push(String(row["leading"]));
2170
+ const byTable = {};
2171
+ for (const row of grants.rows) {
2172
+ const table = String(row["target"]);
2173
+ const role = liveRole(String(row["grantee"]));
2174
+ const entry = byTable[table] ??= {};
2175
+ (entry[role] ??= []).push(String(row["privilege"]));
2176
+ }
2177
+ return {
2178
+ policies: policies.rows.map((row) => ({
2179
+ table: String(row["target"]),
2180
+ name: String(row["policyname"]),
2181
+ command: commandOf(row["cmd"]),
2182
+ permissive: String(row["permissive"]).toUpperCase() === "PERMISSIVE",
2183
+ roles: Array.isArray(row["roles"]) ? row["roles"].map((role) => liveRole(String(role))) : []
2184
+ })),
2185
+ rlsEnabled: Object.fromEntries(enabled.rows.map((row) => [String(row["target"]), row["enabled"] === true])),
2186
+ grants: byTable,
2187
+ helpers: Object.fromEntries(helpers.rows.map((row) => [String(row["target"]), {
2188
+ securityDefiner: row["definer"] === true,
2189
+ emptySearchPath: String(row["config"]).split(",").some((item) => item === "search_path=\"\"" || item === "search_path=''")
2190
+ }])),
2191
+ leadingColumns,
2192
+ columns: columnsOf
2193
+ };
2194
+ } finally {
2195
+ await client.end();
2196
+ }
2197
+ }
2198
+ const SYSTEM_SCHEMAS = [
2199
+ "pg_catalog",
2200
+ "information_schema",
2201
+ "pg_toast",
2202
+ "auth",
2203
+ "storage",
2204
+ "realtime",
2205
+ "extensions",
2206
+ "graphql",
2207
+ "graphql_public",
2208
+ "vault",
2209
+ "pgsodium",
2210
+ "pgsodium_masks",
2211
+ "net",
2212
+ "cron",
2213
+ "supabase_functions",
2214
+ "supabase_migrations"
2215
+ ];
2216
+ function inScope(table) {
2217
+ const schema = table.split(".")[0] ?? "";
2218
+ return HELPER_TABLES.some((item) => item === table) || !(SYSTEM_SCHEMAS.some((item) => item === schema) || schema.startsWith("pg_"));
2219
+ }
2220
+ function seedLine(row) {
2221
+ return `${row.role} ${row.effect} ${row.grantKey} on ${row.scope}`;
2222
+ }
2223
+ /** Drift is an error; `info` names RLS tables whose policies call no PermDock helper. */
2224
+ function diffMixed(expected, actual) {
2225
+ const drift = [];
2226
+ const want = new Set(expected.seeds.map(seedLine));
2227
+ const have = new Set(actual.seeds.map(seedLine));
2228
+ for (const line of want) if (!have.has(line)) drift.push(`${expected.schema}.role_permissions: missing ${line}`);
2229
+ for (const line of have) if (!want.has(line)) drift.push(`${expected.schema}.role_permissions: unexpected ${line}`);
2230
+ const permissions = new Set(expected.permissions);
2231
+ const conditioned = new Set(expected.rowConditions);
2232
+ const covered = /* @__PURE__ */ new Set();
2233
+ for (const policy of actual.policies) {
2234
+ if (callsHelper(policy.expression)) covered.add(policy.table);
2235
+ const blind = new Set(helperCallKeys(policy.expression, true));
2236
+ for (const key of helperCallKeys(policy.expression)) if (!permissions.has(key)) drift.push(`${policy.table}: policy ${policy.name} passes ${key}, which the policy does not declare, so it always denies`);
2237
+ else if (conditioned.has(key) && blind.has(key)) drift.push(`${policy.table}: policy ${policy.name} passes ${key}, whose grants carry row conditions the helpers do not check: the policy grants more than the application does`);
2238
+ }
2239
+ return {
2240
+ drift,
2241
+ info: actual.rlsTables.filter((table) => !covered.has(table)).map((table) => `${table}: no policy calls a PermDock helper`)
2242
+ };
2243
+ }
2244
+ const ALL_POLICIES_SQL = `select schemaname || '.' || tablename as target, policyname,
2245
+ coalesce(qual, '') || ' ' || coalesce(with_check, '') as expression
2246
+ from pg_policies`;
2247
+ const RLS_TABLES_SQL = `select n.nspname || '.' || c.relname as target
2248
+ from pg_class c
2249
+ join pg_namespace n on n.oid = c.relnamespace
2250
+ where c.relkind in ('r', 'p') and c.relrowsecurity`;
2251
+ const quoteIdent = escapeSqlIdent;
2252
+ /** Reads `role_permissions`, `pg_policies` and the RLS-enabled tables for {@link diffMixed}. */
2253
+ async function introspectMixed(db, schema, connect = connectIntrospect) {
2254
+ const client = await connect(db);
2255
+ try {
2256
+ const [seeds, policies, tables] = await Promise.all([
2257
+ client.query(`select role, permission, grant_key, scope, effect from ${quoteIdent(schema)}.role_permissions`, []),
2258
+ client.query(ALL_POLICIES_SQL, []),
2259
+ client.query(RLS_TABLES_SQL, [])
2260
+ ]);
2261
+ return {
2262
+ seeds: seeds.rows.map((row) => ({
2263
+ role: String(row["role"]),
2264
+ permission: String(row["permission"]),
2265
+ grantKey: String(row["grant_key"]),
2266
+ scope: String(row["scope"]),
2267
+ effect: String(row["effect"]) === "deny" ? "deny" : "allow"
2268
+ })),
2269
+ policies: policies.rows.map((row) => ({
2270
+ table: String(row["target"]),
2271
+ name: String(row["policyname"]),
2272
+ expression: String(row["expression"])
2273
+ })).filter((row) => inScope(row.table)),
2274
+ rlsTables: tables.rows.map((row) => String(row["target"])).filter(inScope).toSorted()
2275
+ };
2276
+ } finally {
2277
+ await client.end();
2278
+ }
2279
+ }
2280
+ //#endregion
2281
+ //#region src/cli/rls.ts
2282
+ const RLS_HELP = `permdock rls generate | import | verify | migrate
2283
+
2284
+ generate --target drizzle|sql|prisma --dialect supabase|neon|guc
2285
+ [--rbac supabase] [--rbac-schema public] [--authorize database|jwt]
2286
+ [--memberships <table>:tenant,user,role]
2287
+ [--policy-per-role] [--policy-name '{table}_{op}'] [--tenant-type uuid] [--custom-roles]
2288
+ [--capabilities] [--fields views [--revoke-columns]]
2289
+ [--out <path>] [--check] [--skip-closures] [--inline-functions] [--force] [--guc-prefix app]
2290
+ [--split helpers,seeds,policies,hook --out <dir>/056_permdock_{part}.sql]
2291
+ [--grants-out <file>|-] [--seeds-out <file>|-]
2292
+ [--helpers-only] [--shims] [--db $DATABASE_URL]
2293
+ import --sql schema.sql | --db $DATABASE_URL --out src/permissions.generated.ts
2294
+ [--schema zod|valibot|arktype] [--memberships <table>:tenant,user,role]
2295
+ verify [--db $DATABASE_URL] [--fixtures rls.fixtures.ts] [--format pgtap|node] [--tree]
2296
+ [--introspect --db $DATABASE_URL, with the generate flags]
2297
+ [--advisors [--db $DATABASE_URL] [--json]]: supabase db advisors, security lints
2298
+ migrate --sql <dir> [--write] [--json], with the generate flags
2299
+ rewrites the rls.migrate helpers' calls in policies onto the generated helpers
2300
+
2301
+ Never emits service_role, except in the execute grant rls.trustedReaders asks for. memberOf compiles through the dialect memberships mapping.
2302
+ `;
2303
+ function asTarget(value) {
2304
+ if (value === void 0 || value === "sql" || value === "drizzle" || value === "prisma") return value ?? "sql";
2305
+ }
2306
+ function asDialect(value) {
2307
+ if (value === void 0 || value === "supabase" || value === "neon" || value === "guc") return value ?? "supabase";
2308
+ }
2309
+ function generateInput(input, target, dialect) {
2310
+ return {
2311
+ cwd: input.cwd,
2312
+ config: input.config,
2313
+ target,
2314
+ dialect,
2315
+ rbac: input.rbac,
2316
+ ...input.rbacSchema === void 0 ? {} : { rbacSchema: input.rbacSchema },
2317
+ ...input.authorize === void 0 ? {} : { authorize: parseRbacAuthorize(input.authorize) ?? "database" },
2318
+ check: input.check,
2319
+ skipClosures: input.skipClosures,
2320
+ inlineFunctions: input.inlineFunctions,
2321
+ force: input.force,
2322
+ io: input.io,
2323
+ ...input.out === void 0 ? {} : { out: input.out },
2324
+ ...input.from === void 0 ? {} : { from: input.from },
2325
+ ...input.memberships === void 0 ? {} : { memberships: input.memberships },
2326
+ ...input.gucPrefix === void 0 ? {} : { gucPrefix: input.gucPrefix },
2327
+ policyPerRole: input.policyPerRole,
2328
+ ...input.policyName === void 0 ? {} : { policyName: input.policyName },
2329
+ ...input.tenantType === void 0 ? {} : { tenantType: input.tenantType },
2330
+ customRoles: input.customRoles,
2331
+ capabilities: input.capabilities,
2332
+ ...input.fields === void 0 ? {} : { fields: input.fields },
2333
+ revokeColumns: input.revokeColumns,
2334
+ ...input.split === void 0 ? {} : { split: input.split },
2335
+ ...input.grantsOut === void 0 ? {} : { grantsOut: input.grantsOut },
2336
+ ...input.seedsOut === void 0 ? {} : { seedsOut: input.seedsOut },
2337
+ helpersOnly: input.helpersOnly,
2338
+ shims: input.shims === true
2339
+ };
2340
+ }
2341
+ async function migrate(input) {
2342
+ const config = input.config.rls?.migrate;
2343
+ if (config === void 0) return {
2344
+ code: 2,
2345
+ output: "rls migrate needs rls.migrate.helpers in permdock.config.ts"
2346
+ };
2347
+ if (input.sql === void 0) return {
2348
+ code: 2,
2349
+ output: "rls migrate needs --sql <dir>"
2350
+ };
2351
+ const dialect = asDialect(input.dialect ?? input.config.rls?.dialect);
2352
+ if (dialect === void 0) return {
2353
+ code: 2,
2354
+ output: "rls migrate --dialect (or rls.dialect) must be supabase, neon or guc"
2355
+ };
2356
+ const generated = await runRlsGenerate({
2357
+ ...generateInput(input, "sql", dialect),
2358
+ check: false,
2359
+ write: false
2360
+ });
2361
+ if (generated.code !== 0) return {
2362
+ code: 2,
2363
+ output: generated.output
2364
+ };
2365
+ await requirePeer(() => import("pgsql-parser"), "pgsql-parser", "permdock rls migrate", input.cwd);
2366
+ const { migrateTarget, runRlsMigrate } = await import("./rls-migrate-7wikfrVI.js");
2367
+ return runRlsMigrate({
2368
+ cwd: input.cwd,
2369
+ sql: input.sql,
2370
+ config,
2371
+ target: migrateTarget(generated),
2372
+ write: input.write,
2373
+ json: input.json
2374
+ });
2375
+ }
2376
+ /** `verify --introspect` with `--helpers-only`: helpers and seeds exactly, hand-written policies by their keys. */
2377
+ async function introspectHelpersOnly(db, expected, generated, connect) {
2378
+ const schema = generated.schema ?? "permdock";
2379
+ try {
2380
+ const actual = await introspectRls(db, expected, connect);
2381
+ const helpers = diffRls(expected, actual);
2382
+ const warnings = missingIndexes(expected, actual);
2383
+ const mixed = diffMixed({
2384
+ schema,
2385
+ seeds: generated.seeds ?? [],
2386
+ permissions: generated.keys?.permissions ?? [],
2387
+ rowConditions: generated.keys?.rowConditions ?? []
2388
+ }, await introspectMixed(db, schema, connect));
2389
+ const drift = [...helpers, ...mixed.drift];
2390
+ const info = mixed.info.map((line) => `info: ${line}`);
2391
+ return drift.length > 0 ? {
2392
+ code: 1,
2393
+ output: [
2394
+ ...drift,
2395
+ ...warnings,
2396
+ ...info
2397
+ ].join("\n")
2398
+ } : {
2399
+ code: 0,
2400
+ output: [
2401
+ ...warnings,
2402
+ ...info,
2403
+ `introspected ${String(expected.helpers.length)} helper(s) and ${String(generated.seeds?.length ?? 0)} seeded row(s), helpers only: no drift`
2404
+ ].join("\n")
2405
+ };
2406
+ } catch (cause) {
2407
+ return usageResult(cause);
2408
+ }
2409
+ }
2410
+ /** `verify --introspect`: the catalogs against what `generate` would write with the same flags. */
2411
+ async function introspect(input) {
2412
+ if (input.db === void 0) return {
2413
+ code: 2,
2414
+ output: "PermDock CLI: rls verify --introspect needs --db"
2415
+ };
2416
+ const dialect = asDialect(input.dialect ?? input.config.rls?.dialect);
2417
+ if (dialect === void 0) return {
2418
+ code: 2,
2419
+ output: "rls verify --dialect (or rls.dialect) must be supabase, neon or guc"
2420
+ };
2421
+ const generated = await runRlsGenerate({
2422
+ ...generateInput(input, "sql", dialect),
2423
+ check: false,
2424
+ write: false
2425
+ });
2426
+ if (generated.code !== 0 || generated.policies === void 0) return {
2427
+ code: 2,
2428
+ output: generated.output
2429
+ };
2430
+ const expected = expectedRls(generated.policies, generated.text, generated.indexes);
2431
+ if (generated.helpersOnly === true) return introspectHelpersOnly(input.db, expected, generated, input.connect);
2432
+ try {
2433
+ const actual = await introspectRls(input.db, expected, input.connect);
2434
+ const drift = diffRls(expected, actual, { columnGrants: (input.fields ?? input.config.rls?.fields) === "views" });
2435
+ const warnings = missingIndexes(expected, actual);
2436
+ if (drift.length > 0) return {
2437
+ code: 1,
2438
+ output: [...drift, ...warnings].join("\n")
2439
+ };
2440
+ return {
2441
+ code: 0,
2442
+ 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`
2443
+ };
2444
+ } catch (cause) {
2445
+ return usageResult(cause);
2446
+ }
2447
+ }
2448
+ async function runRls(input) {
2449
+ const action = input.rest[0];
2450
+ if (action === void 0 || action === "help") return {
2451
+ code: 2,
2452
+ output: RLS_HELP
2453
+ };
2454
+ switch (action) {
2455
+ case "generate": {
2456
+ const target = asTarget(input.target);
2457
+ const dialect = asDialect(input.dialect ?? input.config.rls?.dialect);
2458
+ if (target === void 0) return {
2459
+ code: 2,
2460
+ output: "rls generate --target must be drizzle, sql or prisma"
2461
+ };
2462
+ if (dialect === void 0) return {
2463
+ code: 2,
2464
+ output: "rls generate --dialect (or rls.dialect) must be supabase, neon or guc"
2465
+ };
2466
+ return runRlsGenerate({
2467
+ ...generateInput(input, target, dialect),
2468
+ ...input.db === void 0 ? {} : { db: input.db },
2469
+ ...input.connect === void 0 ? {} : { connect: input.connect }
2470
+ });
2471
+ }
2472
+ case "import": {
2473
+ await requirePeer(() => import("pgsql-parser"), "pgsql-parser", "permdock rls import", input.cwd);
2474
+ const { runRlsImport } = await import("./rls-import-D8F7j35Q.js");
2475
+ return runRlsImport({
2476
+ cwd: input.cwd,
2477
+ config: input.config,
2478
+ schema: input.schema ?? "zod",
2479
+ io: input.io,
2480
+ ...input.sql === void 0 ? {} : { sql: input.sql },
2481
+ ...input.db === void 0 ? {} : { db: input.db },
2482
+ ...input.out === void 0 ? {} : { out: input.out },
2483
+ ...input.memberships === void 0 ? {} : { memberships: input.memberships },
2484
+ ...input.connect === void 0 ? {} : { connect: input.connect }
2485
+ });
2486
+ }
2487
+ case "verify": {
2488
+ if (input.advisors === true) return runRlsAdvisors({
2489
+ cwd: input.cwd,
2490
+ json: input.json,
2491
+ env: input.io.env ?? process.env,
2492
+ ...input.db === void 0 ? {} : { db: input.db },
2493
+ ...input.exec === void 0 ? {} : { exec: input.exec }
2494
+ });
2495
+ if (input.introspect) return introspect(input);
2496
+ const format = input.format ?? "node";
2497
+ if (format !== "node" && format !== "pgtap") return {
2498
+ code: 2,
2499
+ output: "rls verify --format must be pgtap or node"
2500
+ };
2501
+ return await runRlsVerify({
2502
+ cwd: input.cwd,
2503
+ config: input.config,
2504
+ format,
2505
+ tree: input.tree,
2506
+ io: input.io,
2507
+ ...input.fixtures === void 0 ? {} : { fixtures: input.fixtures },
2508
+ ...input.db === void 0 ? {} : { db: input.db },
2509
+ ...input.from === void 0 ? {} : { from: input.from },
2510
+ ...input.connect === void 0 ? {} : { connect: input.connect }
2511
+ });
2512
+ }
2513
+ case "migrate": return migrate(input);
2514
+ default: return {
2515
+ code: 2,
2516
+ output: RLS_HELP
2517
+ };
2518
+ }
2519
+ }
2520
+ //#endregion
2521
+ export { runRls };