permdock 0.4.0 → 0.6.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 (515) hide show
  1. package/README.md +37 -35
  2. package/dist/{schema-Bol6gqXA.js → 2020-C9VN-x8c.js} +5 -130
  3. package/dist/a2a/index.d.ts +6 -6
  4. package/dist/a2a/index.js +14 -17
  5. package/dist/{adapter-context-mlWsVGGg.js → adapter-context-CRhqyzCk.js} +1 -1
  6. package/dist/adoption-scan-DCYe60Pi.js +107 -0
  7. package/dist/agent/index.d.ts +3 -0
  8. package/dist/agent/index.js +1 -0
  9. package/dist/ai-sdk/index.d.ts +18 -35
  10. package/dist/ai-sdk/index.js +37 -23
  11. package/dist/{app-data-CV8pqqMi.js → app-data-MuyzVqX8.js} +2 -1
  12. package/dist/approval-header-CJACtHkv.js +23 -0
  13. package/dist/approval-prompt-Dg5LeAcT.js +62 -0
  14. package/dist/approval-prompt-DvSSPQJX.d.ts +57 -0
  15. package/dist/approvals/index.d.ts +5 -3
  16. package/dist/approvals/index.js +8 -5
  17. package/dist/approvals-p1PGoVjb.js +995 -0
  18. package/dist/arazzo-41WuzHou.js +36 -0
  19. package/dist/{arazzo-C4BcskbX.js → arazzo-CaUVBn7D.js} +20 -14
  20. package/dist/args-IIolxxKA.js +197 -0
  21. package/dist/astro/index.d.ts +43 -0
  22. package/dist/astro/index.js +27 -0
  23. package/dist/audience-DabClFLQ.js +55 -0
  24. package/dist/auth-DRLEizbL.js +178 -0
  25. package/dist/authzen/index.d.ts +26 -6
  26. package/dist/authzen/index.js +94 -79
  27. package/dist/automation-BNM3TtDo.d.ts +114 -0
  28. package/dist/automation-DVjOckjV.js +157 -0
  29. package/dist/better-auth/index.d.ts +8 -6
  30. package/dist/better-auth/index.js +9 -8
  31. package/dist/better-supabase/index.d.ts +357 -30
  32. package/dist/better-supabase/index.js +936 -194
  33. package/dist/{bind-kk-uyCwH.js → bind-CAvgUuKs.js} +14 -10
  34. package/dist/{bind-BzqG8SMb.d.ts → bind-DOyrv6zi.d.ts} +12 -10
  35. package/dist/body-CH5of8Ne.js +10 -0
  36. package/dist/body-text-QcXok_SI.js +52 -0
  37. package/dist/{build-instance-DIIV9DbC.js → build-instance-Bu_eDqYw.js} +4 -3
  38. package/dist/{canonical-json-CFKyFOke.js → canonical-json-BC8_nMTw.js} +5 -3
  39. package/dist/{capability-CksyCWDH.d.ts → capability-Drg-1YhL.d.ts} +44 -25
  40. package/dist/{capability-txATtKV4.js → capability-N1K9IpqS.js} +68 -42
  41. package/dist/casbin-Cr7RLtA3.js +30 -0
  42. package/dist/casbin-Dyu2gnUD.js +234 -0
  43. package/dist/casl-64gpT-8x.js +24 -0
  44. package/dist/casl-DjEDhTCP.js +299 -0
  45. package/dist/catalog/index.d.ts +74 -2
  46. package/dist/catalog/index.js +51 -2
  47. package/dist/catalog-BoWglDPI.js +1 -0
  48. package/dist/catalog-DZVbrF-z.js +52 -0
  49. package/dist/{catalog-tZUyOfOv.js → catalog-HD2gf5mY.js} +51 -6
  50. package/dist/{catalog-doc-DWtg0gGR.js → catalog-doc-Cdn7-0pQ.js} +45 -23
  51. package/dist/catalog-fingerprint-CCP0-yQS.js +153 -0
  52. package/dist/chat/index.d.ts +2 -0
  53. package/dist/chat/index.js +224 -0
  54. package/dist/chat-sdk/index.d.ts +87 -0
  55. package/dist/chat-sdk/index.js +197 -0
  56. package/dist/claims-map-7oQ3ue4X.js +13 -0
  57. package/dist/claims-map-Ca-QX8C6.d.ts +32 -0
  58. package/dist/claude-agent/index.d.ts +7 -14
  59. package/dist/claude-agent/index.js +24 -27
  60. package/dist/clerk/index.d.ts +9 -8
  61. package/dist/clerk/index.js +9 -14
  62. package/dist/cli/bin.js +36 -17
  63. package/dist/cli/index.d.ts +19 -5
  64. package/dist/cli/index.js +164 -4
  65. package/dist/cloud/index.d.ts +102 -63
  66. package/dist/cloud/index.js +239 -340
  67. package/dist/cloud-ytQ3uviA.js +87 -0
  68. package/dist/codemod-5mpSpnUY.js +37 -0
  69. package/dist/codemod-ua2hHy7w.js +2084 -0
  70. package/dist/collect-Ci6akVwH.js +55 -0
  71. package/dist/collect-D7eQDffE.js +1 -0
  72. package/dist/{collect-2SrB7lfk.js → collect-mLTY4nwb.js} +75 -11
  73. package/dist/command-L8EstFNm.js +6 -0
  74. package/dist/compile/index.d.ts +1 -1
  75. package/dist/compile/index.js +1 -2
  76. package/dist/{compile-CtTP7eur.d.ts → compile-1bsAezL0.d.ts} +5 -3
  77. package/dist/{compile-CcEu2ZQ1.js → compile-CMqMx_IN.js} +11 -8
  78. package/dist/completion-BTFrHZhX.js +143 -0
  79. package/dist/config-Bj2Ixq3d.js +26 -0
  80. package/dist/{config-BIjREpyE.js → config-bs_kZeYq.js} +12 -7
  81. package/dist/{config-report--Xb9APJS.js → config-report-DSpsLpCE.js} +15 -5
  82. package/dist/{connection-DUsczpB5.js → connection-DjhIJSs2.js} +6 -5
  83. package/dist/{context-DieZVJXu.js → context-B_u3_bDJ.js} +2 -2
  84. package/dist/convex/index.d.ts +11 -10
  85. package/dist/convex/index.js +6 -4
  86. package/dist/create-B30OMUOJ.d.ts +107 -0
  87. package/dist/create-CBMmFie-.d.ts +138 -0
  88. package/dist/create-CGm28gyI.js +130 -0
  89. package/dist/create-CQWIpUG-.js +403 -0
  90. package/dist/{create-BBO24vj6.js → create-DCF2SvoT.js} +31 -32
  91. package/dist/create-D_NmPOdH.d.ts +110 -0
  92. package/dist/{credential-kue0qpbm.js → credential-BcoiejpY.js} +152 -31
  93. package/dist/credentials-DPyHmYz5.js +74 -0
  94. package/dist/delegation-j6-rc37o.js +286 -0
  95. package/dist/delivery/index.d.ts +5 -0
  96. package/dist/delivery/index.js +5 -0
  97. package/dist/delivery-DCAgNyCn.js +499 -0
  98. package/dist/delivery-event-xX4obB8F.js +40 -0
  99. package/dist/{denied-text-DfRk15uw.d.ts → denied-text-CiFQNuMV.d.ts} +5 -5
  100. package/dist/{describe-C3ldA9os.d.ts → describe-B5qgDvEW.d.ts} +2 -2
  101. package/dist/{describe-Cgkp7eP0.js → describe-BybMRY7A.js} +6 -2
  102. package/dist/device-BARQ8eYG.js +193 -0
  103. package/dist/{diff-rpqoe7Ag.js → diff-3jbx6Rm4.js} +60 -54
  104. package/dist/diff-CmuSD3vc.js +37 -0
  105. package/dist/doctor-3ZqNHX_G.js +44 -0
  106. package/dist/{doctor-owlv6ddh.js → doctor-BdZw53-k.js} +245 -50
  107. package/dist/drizzle/index.d.ts +5 -3
  108. package/dist/drizzle/index.js +2 -2
  109. package/dist/elysia/index.d.ts +16 -13
  110. package/dist/elysia/index.js +13 -12
  111. package/dist/env-DJZrPjRi.js +8 -0
  112. package/dist/errors-BDUwg2qC.js +27 -0
  113. package/dist/{errors-drnfxYtm.js → errors-BKy0EfhN.js} +5 -9
  114. package/dist/errors-BWXBMaKK.d.ts +11 -0
  115. package/dist/errors-CWTY6uih.js +143 -0
  116. package/dist/{errors-OD_4c__H.d.ts → errors-DH5QEGXh.d.ts} +2 -2
  117. package/dist/evaluate-CNClBvcj.js +1785 -0
  118. package/dist/evaluations-C_1vdSZc.d.ts +32 -0
  119. package/dist/{evaluations-C9YJsv3H.js → evaluations-beAooV0n.js} +145 -34
  120. package/dist/eve/index.d.ts +12 -16
  121. package/dist/eve/index.js +22 -28
  122. package/dist/event-time-CsegxXxB.js +23 -0
  123. package/dist/expiry-D4_YKRKD.js +52 -0
  124. package/dist/express/index.d.ts +13 -10
  125. package/dist/express/index.js +12 -11
  126. package/dist/fastify/index.d.ts +25 -22
  127. package/dist/fastify/index.js +13 -12
  128. package/dist/fetch-adapter-DD_gfmrC.js +120 -0
  129. package/dist/fetch-adapter-Dw0MTS3D.d.ts +31 -0
  130. package/dist/fields-OYG_JR78.js +203 -0
  131. package/dist/files-C5ZI27eV.js +124 -0
  132. package/dist/{fixtures-h2eG2OZV.js → fixtures-B7XrLeY7.js} +8 -7
  133. package/dist/flags-C3DktBDY.js +86 -0
  134. package/dist/{from-snapshot-7REQZe0H.js → from-snapshot-CTCMsFZk.js} +55 -52
  135. package/dist/{grantee-conditions-MQs5NIuX.js → grantee-conditions-CcXf8hMM.js} +4 -3
  136. package/dist/{grantee-CXY-0C87.js → grantee-hxrUsbpM.js} +16 -6
  137. package/dist/{grantee-match-fxYp9Nmt.js → grantee-match-DbENBHLB.js} +5 -4
  138. package/dist/h3/index.d.ts +37 -0
  139. package/dist/h3/index.js +35 -0
  140. package/dist/headers-BIjfMaoC.js +8 -0
  141. package/dist/headers-CIbDTUTK.d.ts +7 -0
  142. package/dist/{helpers-C9YmyBIa.js → helpers-CBYVMjwh.js} +45 -38
  143. package/dist/hono/index.d.ts +12 -9
  144. package/dist/hono/index.js +13 -12
  145. package/dist/hosted-B46-5LYE.js +315 -0
  146. package/dist/hosted-BPk51-kX.d.ts +78 -0
  147. package/dist/{http-DPuDaChb.d.ts → http-B_LphE5Y.d.ts} +1 -2
  148. package/dist/http-DXJ37RDA.js +46 -0
  149. package/dist/{http-BEbdMZ05.js → http-ujKm0JXE.js} +1 -2
  150. package/dist/{import-UHk1N7vH.js → import-CNV7I2Dw.js} +25 -20
  151. package/dist/{import-BFIlhetD.js → import-wyqXUyjm.js} +190 -53
  152. package/dist/index-B8YjkEZ4.d.ts +210 -0
  153. package/dist/index-C-6Letxg.d.ts +431 -0
  154. package/dist/index-CaxrMD1A.d.ts +1 -0
  155. package/dist/{index-BwMe94Xq.d.ts → index-Ch8LpNDp.d.ts} +34 -16
  156. package/dist/index-Cp_ilKi1.d.ts +91 -0
  157. package/dist/index-CueFnxQc.d.ts +110 -0
  158. package/dist/index-DuWxc5dt.d.ts +909 -0
  159. package/dist/index.d.ts +19 -15
  160. package/dist/index.js +31 -21
  161. package/dist/init-DeMYsnYJ.js +96 -0
  162. package/dist/init-GG6Jh8HD.js +95 -0
  163. package/dist/{instance-options-plkrsAog.d.ts → instance-options-BsVpyGIk.d.ts} +2 -2
  164. package/dist/instance-snapshot-BOEhcqIF.js +601 -0
  165. package/dist/instant-BluqDvyS.js +19 -0
  166. package/dist/internals-Dk8A8fX3.js +17 -0
  167. package/dist/{json-schema-DBkdUuCO.js → json-schema-B-vAwlde.js} +45 -6
  168. package/dist/json-schema-CN_iLFH2.d.ts +40 -0
  169. package/dist/jwt/index.d.ts +18 -11
  170. package/dist/jwt/index.js +156 -135
  171. package/dist/{kernel-CB3LrIJM.js → kernel-D0NVb1cK.js} +52 -24
  172. package/dist/kysely/index.d.ts +5 -3
  173. package/dist/kysely/index.js +2 -2
  174. package/dist/load-BxEtwlBS.js +1 -0
  175. package/dist/{load-C3_oTHyO.js → load-DFS_ry0W.js} +22 -6
  176. package/dist/{load-jose-BL6LH4bL.js → load-jose-DZ4dnjri.js} +1 -4
  177. package/dist/login-Dfe5RlYk.js +68 -0
  178. package/dist/login-imhuwvn3.js +254 -0
  179. package/dist/manifest-C8IzW3Do.js +42 -0
  180. package/dist/manifest-dvL2XEe1.js +370 -0
  181. package/dist/manifest-json-CAGzls-C.js +9 -0
  182. package/dist/{manifest-uDS9y5de.d.ts → manifest-mx5ghSFM.d.ts} +9 -2
  183. package/dist/manifest-schema-C0kK_8V0.js +399 -0
  184. package/dist/{map-claims-AVlBaJoa.js → map-claims-D3oRmTk0.js} +13 -10
  185. package/dist/{map-error-BE27PYkE.js → map-error-Dr70oA_3.js} +3 -3
  186. package/dist/{markers-CiwAOZtO.js → markers-C6joDI3o.js} +2 -1
  187. package/dist/mcp/index.d.ts +22 -12
  188. package/dist/mcp/index.js +57 -39
  189. package/dist/{migrate-BwlB5-Wk.js → migrate-CIq4-E9F.js} +53 -14
  190. package/dist/nest/index.d.ts +12 -9
  191. package/dist/nest/index.js +15 -14
  192. package/dist/next/index.d.ts +18 -21
  193. package/dist/next/index.js +13 -11
  194. package/dist/next/plugin.d.ts +1 -1
  195. package/dist/next/plugin.js +2 -2
  196. package/dist/node/index.d.ts +10 -10
  197. package/dist/node/index.js +7 -9
  198. package/dist/{ocsf-uYFXGVw2.js → ocsf-BFp-PZZA.js} +22 -11
  199. package/dist/openai/index.d.ts +9 -16
  200. package/dist/openai/index.js +17 -24
  201. package/dist/openapi/index.d.ts +3 -3
  202. package/dist/openapi/index.js +1 -1
  203. package/dist/{openapi-DBzHQbov.js → openapi-B7HZ0f-r.js} +8 -8
  204. package/dist/openapi-C4dDTVEd.js +138 -0
  205. package/dist/{openapi-BJdBmYDG.js → openapi-DD1SV-5-.js} +10 -9
  206. package/dist/operation-findings-DgATDpvh.js +36 -0
  207. package/dist/operations-X0VM7F3m.d.ts +59 -0
  208. package/dist/orpc/index.d.ts +13 -10
  209. package/dist/orpc/index.js +20 -15
  210. package/dist/otel/index.d.ts +3 -2
  211. package/dist/otel/index.js +30 -7
  212. package/dist/output-1im__-08.js +88 -0
  213. package/dist/output-files-B3IZJksC.js +1 -0
  214. package/dist/{output-files-CgIsvdSa.js → output-files-DhRBBE4n.js} +24 -26
  215. package/dist/{permission-keys-B26jeyUv.js → ownership-DqaJNJPn.js} +1045 -153
  216. package/dist/{package-root-DtNnpmUd.js → package-root-Dk_ETdkp.js} +1 -1
  217. package/dist/{parse-CVFQHk6c.js → parse-Cxq0WxrA.js} +2 -2
  218. package/dist/{parse-local-manifest-DvYUR0QL.d.ts → parse-local-manifest-5ODwqu8b.d.ts} +1 -1
  219. package/dist/{parse-local-manifest-C-q02Pal.js → parse-local-manifest-CVSs6J9I.js} +1 -1
  220. package/dist/parse-request-D_tUXG6M.js +26 -0
  221. package/dist/paths-DXSHAMIa.js +42 -0
  222. package/dist/pdp/index.d.ts +7 -3
  223. package/dist/pdp/index.js +45 -26
  224. package/dist/{peer-BOr_kJE1.js → peer-C8swJ2GH.js} +23 -4
  225. package/dist/peer-CRvC7Hg1.js +1 -0
  226. package/dist/permdock-BrAscV28.js +868 -0
  227. package/dist/permdock-SV8ZINYT.d.ts +502 -0
  228. package/dist/{permission-registry-BsblpB-z.js → permission-registry-Br4jSNVi.js} +3 -3
  229. package/dist/permissions-B-wCP5ZC.js +30 -0
  230. package/dist/{permissions-CCJ6lmxB.js → permissions-BPxlwVhN.js} +4 -3
  231. package/dist/{permissions-COH-KRgT.d.ts → permissions-u_KHg-sN.d.ts} +1 -2
  232. package/dist/policies-DP2nqRoF.js +295 -0
  233. package/dist/{policy-ur0BBK_e.d.ts → policy-77FSAq_V.d.ts} +2 -2
  234. package/dist/{hosted-CBHgkzDl.js → policy-DbM5YYtV.js} +17 -273
  235. package/dist/policy-from-catalog-C2gMa2Dj.js +450 -0
  236. package/dist/policy-module-CTlwMvLI.js +89 -0
  237. package/dist/{policy-types-sp9P1iIV.d.ts → policy-types-n0uRTsW8.d.ts} +460 -632
  238. package/dist/postgrest-CEgRbefE.d.ts +100 -0
  239. package/dist/postgrest-DAjUkF9w.js +575 -0
  240. package/dist/{powersync-cZe4kzhL.js → powersync-BkWWCS1K.js} +23 -24
  241. package/dist/powersync-DISt6SUe.js +62 -0
  242. package/dist/powersync-MMJ2mI2q.js +1 -0
  243. package/dist/preset/index.d.ts +146 -0
  244. package/dist/preset/index.js +1 -0
  245. package/dist/preset-DZdU0tne.js +1768 -0
  246. package/dist/principal-source-pHneukrx.js +103 -0
  247. package/dist/prisma/index.d.ts +6 -3
  248. package/dist/prisma/index.js +2 -2
  249. package/dist/{problem-CnWraVhC.js → problem-CTviyH5L.js} +13 -23
  250. package/dist/{problem-details-rIIo8B2C.d.ts → problem-details-B45V5d2b.d.ts} +1 -1
  251. package/dist/{problem-details-DECUnXAn.js → problem-details-cn5X30gZ.js} +43 -40
  252. package/dist/problem-types-CJ2325SZ.js +5 -0
  253. package/dist/{prompt-CXYhLfsS.js → prompt-DfZLJLIn.js} +2 -2
  254. package/dist/{protected-BN7BUdY1.js → protected-BlF0F6wL.js} +21 -4
  255. package/dist/{protected-B3_P7xuw.d.ts → protected-RFGpakN9.d.ts} +16 -8
  256. package/dist/{provider-Bi-0HNL3.js → provider-B1al-AZz.js} +2 -2
  257. package/dist/{provider-kwHC9CZD.d.ts → provider-DILL9oPK.d.ts} +1 -1
  258. package/dist/{provider-store-Ctt-81Ni.js → provider-store-DM6DNcta.js} +2 -2
  259. package/dist/pull-CvpINcMK.js +1 -0
  260. package/dist/pull-jEwbf80J.js +256 -0
  261. package/dist/push-BD4f15co.js +116 -0
  262. package/dist/pushed-policy-CqtxkV4G.js +238 -0
  263. package/dist/pushed-policy-DCW91KQU.d.ts +57 -0
  264. package/dist/react/index.d.ts +8 -8
  265. package/dist/react/index.js +6 -6
  266. package/dist/react/provider-client.d.ts +1 -1
  267. package/dist/react/provider-client.js +1 -2
  268. package/dist/react-native/index.d.ts +24 -72
  269. package/dist/react-native/index.js +29 -25
  270. package/dist/{refs-f3imMjNW.js → refs-K6eWUJ0H.js} +1 -1
  271. package/dist/{relation-walk-BXnlLtg2.js → relation-walk-3OyDBF0S.js} +2 -2
  272. package/dist/{relations-CvmSpG67.js → relations-BnCWnJGI.js} +3 -3
  273. package/dist/renames-CGj7YwcQ.js +175 -0
  274. package/dist/{replay-COpmZUfx.d.ts → replay-Bkm-Ne4l.d.ts} +1 -1
  275. package/dist/{replay-BsOyTrdV.js → replay-Du1tJjG7.js} +2 -1
  276. package/dist/{request-context-BVM7aWNs.js → request-context-C81Pip9n.js} +3 -3
  277. package/dist/request-schema-CMreVuFM.js +148 -0
  278. package/dist/resend/index.d.ts +126 -0
  279. package/dist/resend/index.js +379 -0
  280. package/dist/{resource-relations-D1Un3pcz.js → resource-relations-D_jw0BFx.js} +1 -1
  281. package/dist/{retire-BFcxL6un.js → retire-CALjIQ7e.js} +2 -2
  282. package/dist/{revocations-Cy06JNx2.d.ts → revocations-R07lFBnX.d.ts} +2 -0
  283. package/dist/rls-BAqP4q66.js +336 -0
  284. package/dist/{rls-BEktUaqg.js → rls-DTvYd-lI.js} +288 -690
  285. package/dist/{role-builder-B4GbKM-i.js → role-builder-uWQZ4v84.js} +14 -207
  286. package/dist/{rpc-nYC3cR_u.js → rpc-VSl5ylaQ.js} +7 -7
  287. package/dist/run-CUKuz0A8.js +723 -0
  288. package/dist/run-DDwUaJDU.js +1 -0
  289. package/dist/run-ticket-DocwaTKn.d.ts +124 -0
  290. package/dist/run-ticket-sxFI6LAk.js +337 -0
  291. package/dist/{runtime-D_tMXnsq.js → runtime-D3VTAiCA.js} +1 -1
  292. package/dist/{saas-DtaA4JuW.js → saas-BCXRuXca.js} +16 -14
  293. package/dist/scan-qvOe0hS3.js +29 -0
  294. package/dist/{schema-DjQBcAP9.js → schema-B4v0Qrac.js} +102 -8
  295. package/dist/schema-yLuBWWsh.js +130 -0
  296. package/dist/scim/index.d.ts +2 -2
  297. package/dist/scim/index.js +1 -2
  298. package/dist/{scim-vFR1O5oG.js → scim-ClHtpyV3.js} +208 -76
  299. package/dist/{scopes-tvpa34ep.js → scopes-CUPSjo9q.js} +12 -7
  300. package/dist/server/index.d.ts +12 -143
  301. package/dist/server/index.js +12 -9
  302. package/dist/server-C3WXAHZj.js +453 -0
  303. package/dist/{server-types-DdqBwZN_.d.ts → server-types-CKQiCnON.d.ts} +51 -68
  304. package/dist/{sha256-BO0swRXL.js → sha256-D9ihqmvd.js} +8 -1
  305. package/dist/{shared-1uhI-e-b.js → shared-BAK8t4Uo.js} +20 -9
  306. package/dist/{shared-JhMeOsDF.js → shared-DvvFzYXe.js} +8 -9
  307. package/dist/signals-D3NcTKUS.js +32 -0
  308. package/dist/sink-4-eQIRWf.d.ts +225 -0
  309. package/dist/sink-BEZSlMS_.js +957 -0
  310. package/dist/{skills-BZdRQkxs.js → skills-B6lVdD_W.js} +13 -6
  311. package/dist/skills-CTYwkl44.js +51 -0
  312. package/dist/snapshot-B7DkYg6g.js +269 -0
  313. package/dist/{snapshot-cache-Cod40D-X.js → snapshot-cache-DZHQ0KF6.js} +11 -5
  314. package/dist/{snapshot-cache-J5T7t_vD.d.ts → snapshot-cache-PXI-vGpY.d.ts} +7 -5
  315. package/dist/solid/index.d.ts +11 -12
  316. package/dist/solid/index.js +7 -7
  317. package/dist/solid-start/index.d.ts +52 -0
  318. package/dist/solid-start/index.js +49 -0
  319. package/dist/{sources-Dljy06zL.d.ts → sources-Xc4zkszI.d.ts} +7 -13
  320. package/dist/{sources-B1-b-R0t.js → sources-vs5dGOsS.js} +125 -119
  321. package/dist/{sql-t8teWOfo.js → sql-B5T1IL83.js} +13 -1
  322. package/dist/src-Cu2m75uo.js +1860 -0
  323. package/dist/ssf/index.d.ts +4 -4
  324. package/dist/ssf/index.js +384 -47
  325. package/dist/standard-webhooks-DgFFD-Ga.js +228 -0
  326. package/dist/standard-webhooks-rbD7LS6h.d.ts +88 -0
  327. package/dist/storage-C2bWdvJH.js +132 -0
  328. package/dist/{store-ILkLUj_o.js → store-BDutJkDL.js} +40 -53
  329. package/dist/{store-options-CbUHDoSe.js → store-options-u0ycEe8s.js} +26 -21
  330. package/dist/{stores-Cxz_80g-.d.ts → stores-BUGNbJTt.d.ts} +7 -8
  331. package/dist/{stores-BHXdSbw9.js → stores-CGhM8BN_.js} +2 -2
  332. package/dist/{stream-Dwsg6WwE.d.ts → stream-CeCMJmBB.d.ts} +2 -2
  333. package/dist/{stream-BMui98KK.js → stream-CvdUS9V1.js} +9 -2
  334. package/dist/style-DiKWnAvn.js +1 -0
  335. package/dist/{style-CTm1AXDT.js → style-Uao76wO7.js} +3 -2
  336. package/dist/subject-BGoM5o8B.js +17 -0
  337. package/dist/supabase/index.d.ts +23 -106
  338. package/dist/supabase/index.js +5 -4
  339. package/dist/supabase/middleware.d.ts +7 -6
  340. package/dist/supabase/middleware.js +13 -11
  341. package/dist/supabase-By_frk3U.js +92 -0
  342. package/dist/{supabase-hook-Cva3ikXz.js → supabase-hook-BMoEhZir.js} +960 -64
  343. package/dist/supabase-hook-CCw5m35C.js +1 -0
  344. package/dist/{supabase-C1TWlnYt.js → supabase-x5JtR2tc.js} +99 -206
  345. package/dist/support-session-C6xJQYut.js +158 -0
  346. package/dist/svelte/Protected.svelte +4 -4
  347. package/dist/svelte/index.d.ts +7 -7
  348. package/dist/svelte/index.js +10 -10
  349. package/dist/svelte/runtime.d.ts +3 -3
  350. package/dist/svelte/runtime.js +3 -3
  351. package/dist/svelte/source.d.ts +7 -7
  352. package/dist/svelte/source.js +4 -4
  353. package/dist/sveltekit/index.d.ts +51 -0
  354. package/dist/sveltekit/index.js +27 -0
  355. package/dist/tanstack-start/index.d.ts +51 -0
  356. package/dist/tanstack-start/index.js +40 -0
  357. package/dist/target-Bw37TFNA.js +51 -0
  358. package/dist/{tenancy-DshjqY0n.js → tenancy-DN2QAx9z.js} +5 -5
  359. package/dist/terminal/index.d.ts +11 -9
  360. package/dist/terminal/index.js +23 -287
  361. package/dist/testing/index.d.ts +311 -38
  362. package/dist/testing/index.js +3475 -201
  363. package/dist/testing/saas/index.d.ts +9 -8
  364. package/dist/testing/saas/index.js +2 -2
  365. package/dist/testing/saas/permissions.d.ts +1 -1
  366. package/dist/testing/saas/permissions.js +1 -2
  367. package/dist/text-BMFRfBqc.js +8 -0
  368. package/dist/{text-diff-BdkkYSbi.js → text-diff-BQcUapOb.js} +1 -1
  369. package/dist/thenable-yD7SabqG.js +31 -0
  370. package/dist/time-DkWjdL98.js +71 -0
  371. package/dist/{token-Bx9qy30p.js → token-BLZcjQgt.js} +15 -6
  372. package/dist/{tool-refusal-CQ7OEia9.js → tool-refusal-CMLN5xhn.js} +2 -2
  373. package/dist/trpc/index.d.ts +12 -9
  374. package/dist/trpc/index.js +16 -14
  375. package/dist/types-8RnwUAvE.d.ts +107 -0
  376. package/dist/{store-9KO4BlkB.d.ts → types-B3_ooZgZ.d.ts} +100 -75
  377. package/dist/{types-Br9IXUBG.d.ts → types-BdtSDbBO.d.ts} +77 -17
  378. package/dist/types-BtHtcKMr.d.ts +157 -0
  379. package/dist/{types-CqrjB9o6.d.ts → types-BzzaHxt3.d.ts} +23 -11
  380. package/dist/types-C791HFu9.d.ts +332 -0
  381. package/dist/types-C863OQiR.d.ts +57 -0
  382. package/dist/{types-ByUo56un.d.ts → types-CVZSNocA.d.ts} +23 -22
  383. package/dist/{types-DHrmBJLH.d.ts → types-CX7AvRqP.d.ts} +7 -7
  384. package/dist/types-C_IiEs64.d.ts +65 -0
  385. package/dist/{types-7rJVLWe5.d.ts → types-Dfv0aTER.d.ts} +63 -11
  386. package/dist/types-DqzsnlSA.js +67 -0
  387. package/dist/{types-DpWfQdSh.d.ts → types-Druu9wLM.d.ts} +46 -11
  388. package/dist/{types-CIlTojhq.d.ts → types-_WhUP7ud.d.ts} +9 -6
  389. package/dist/{types-CUcFZfpU.d.ts → types-lUlG3TVd.d.ts} +2 -1
  390. package/dist/unplugin/index.d.ts +1 -1
  391. package/dist/unplugin/index.js +2 -2
  392. package/dist/usage-CRVl9V3t.js +32 -0
  393. package/dist/usage-CZpSQP6L.js +1 -0
  394. package/dist/{usage-CG9nOYZw.js → usage-SveTRTyr.js} +54 -11
  395. package/dist/{validation-error-BSSw0l_8.d.ts → validation-error-DHUrAkku.d.ts} +1 -1
  396. package/dist/{validity-dVrUEPJt.js → validity-CZ7-y0Cj.js} +32 -15
  397. package/dist/{verifier-DMr_Zmb_.js → verifier-DHuh-okk.js} +18 -7
  398. package/dist/{verify-BztUB3GX.js → verify-BaAKx7t6.js} +645 -50
  399. package/dist/{version-BEogC-rB.js → version-CEbQ5UeM.js} +4 -3
  400. package/dist/version-DXkYUISx.js +1 -0
  401. package/dist/{views-CJsrBS1f.js → views-DTIIO-oQ.js} +22 -5
  402. package/dist/{permission-types-Dg39z9Kv.d.ts → vocabulary-CN8H_9UF.d.ts} +67 -2
  403. package/dist/vue/index.d.ts +9 -10
  404. package/dist/vue/index.js +8 -8
  405. package/dist/waiter-B986w1oK.d.ts +36 -0
  406. package/dist/waiter-BGUtGYoU.js +67 -0
  407. package/dist/watch--ovlPOc0.js +1 -0
  408. package/dist/{watch-zeucB7eF.js → watch-vea54PRw.js} +44 -5
  409. package/dist/webmcp/index.d.ts +2 -2
  410. package/dist/webmcp/index.js +3 -3
  411. package/dist/where-scope-C9vfEphl.js +306 -0
  412. package/dist/who-can-DWBAcaCQ.js +364 -0
  413. package/dist/wire-delegation-Cv3W2eaM.js +36 -0
  414. package/dist/wire-denial-C7PaVYEM.js +27 -0
  415. package/dist/workflow/index.d.ts +5 -0
  416. package/dist/workflow/index.js +223 -0
  417. package/dist/workflow-sdk/index.d.ts +56 -0
  418. package/dist/workflow-sdk/index.js +78 -0
  419. package/dist/{wrap-Cr93qqdP.d.ts → wrap-C5jC6sKI.d.ts} +2 -1
  420. package/dist/{wrap-DWsU_5Bd.js → wrap-gX61EtpB.js} +7 -6
  421. package/package.json +136 -37
  422. package/schemas/access-event-v1.json +118 -0
  423. package/schemas/approval-request-v1.json +34 -1
  424. package/schemas/approval-signature-v1.json +94 -0
  425. package/schemas/automation-event-v1.json +113 -0
  426. package/schemas/capability-v1.json +84 -0
  427. package/schemas/catalog-event-v1.json +87 -0
  428. package/schemas/catalog-v1.json +123 -12
  429. package/schemas/cli-commands-v1.json +164 -0
  430. package/schemas/cli-result-v1.json +96 -0
  431. package/schemas/cloud-api-v1.json +5407 -0
  432. package/schemas/connector-v1.json +408 -0
  433. package/schemas/credential-event-v1.json +81 -0
  434. package/schemas/credential-v1.json +134 -0
  435. package/schemas/decision-event-v1.json +288 -0
  436. package/schemas/delivery-event-v1.json +117 -0
  437. package/schemas/directory-event-v1.json +85 -0
  438. package/schemas/evidence-export-v1.json +54 -0
  439. package/schemas/membership-event-v1.json +93 -0
  440. package/schemas/policy-v1.json +68 -0
  441. package/schemas/pushed-policy-v1.json +236 -0
  442. package/schemas/run-v1.json +162 -0
  443. package/schemas/snapshot-v1.json +138 -0
  444. package/schemas/supabase-claims-v1.json +4 -4
  445. package/schemas/supabase-manifest-v1.json +5 -1
  446. package/schemas/support-session-v1.json +111 -0
  447. package/skills/permdock/SKILL.md +16 -12
  448. package/skills/permdock-agents/SKILL.md +40 -23
  449. package/skills/permdock-agents/references/operations-automations-chat.md +221 -0
  450. package/skills/permdock-agents/references/runtimes.md +22 -18
  451. package/skills/permdock-approvals/SKILL.md +29 -13
  452. package/skills/permdock-approvals/references/delivery-and-waiters.md +172 -0
  453. package/skills/permdock-approvals/references/stores-and-resume.md +21 -7
  454. package/skills/permdock-audit/SKILL.md +3 -3
  455. package/skills/permdock-credentials/SKILL.md +5 -5
  456. package/skills/permdock-credentials/references/tokens-keys-links.md +14 -10
  457. package/skills/permdock-data/SKILL.md +6 -6
  458. package/skills/permdock-data/references/queries-and-rls.md +10 -9
  459. package/skills/permdock-data/references/relationships.md +3 -3
  460. package/skills/permdock-tenancy/SKILL.md +20 -7
  461. package/skills/permdock-tenancy/references/contacts-and-support-sessions.md +122 -0
  462. package/skills/permdock-tenancy/references/ownership-and-custom-roles.md +6 -4
  463. package/skills/permdock-wire/SKILL.md +17 -11
  464. package/skills/permdock-wire/references/adapters.md +139 -51
  465. package/skills/permdock-wire/references/policy-options.md +5 -5
  466. package/skills/permdock-wire/references/presets-and-recipes.md +58 -0
  467. package/dist/approvals-DZL76lKW.js +0 -240
  468. package/dist/arazzo-CD8d0WLb.js +0 -54
  469. package/dist/catalog-6yWAX6Ps.js +0 -2
  470. package/dist/catalog-WHG4kBq7.js +0 -48
  471. package/dist/cloud-C7bSDgX4.js +0 -50
  472. package/dist/cloud-PeBN3IEn.js +0 -118
  473. package/dist/collect-Bw2Jdq1q.js +0 -2
  474. package/dist/collect-CQx4hX4N.js +0 -83
  475. package/dist/commands-Cpip4ULx.js +0 -97
  476. package/dist/config-DLt6BvV7.js +0 -36
  477. package/dist/diff-Cnghh_Ip.js +0 -48
  478. package/dist/doctor-CuVJvhV_.js +0 -53
  479. package/dist/duration-xK2jGFVc.js +0 -25
  480. package/dist/errors-CWfoNg9N.js +0 -47
  481. package/dist/errors-DRnGZHzF.js +0 -30
  482. package/dist/expiry-BoBJ8Tvc.js +0 -7
  483. package/dist/fields-fwfXN01n.js +0 -1016
  484. package/dist/headers-BZSDieNT.js +0 -6
  485. package/dist/headers-Cf-zWvsO.d.ts +0 -6
  486. package/dist/ids-DC4V4CFu.js +0 -18
  487. package/dist/index-DgM7w96K.d.ts +0 -6
  488. package/dist/index-Dz7didpC.d.ts +0 -201
  489. package/dist/index-UdVQgq48.d.ts +0 -550
  490. package/dist/load-B_shHrmd.js +0 -2
  491. package/dist/manifest-BngWKryL.js +0 -770
  492. package/dist/may-use-BOn_dR_w.js +0 -78
  493. package/dist/openapi-DcQ1Kg40.js +0 -164
  494. package/dist/ownership-BgnbOJFT.js +0 -851
  495. package/dist/permdock-L26udHSk.js +0 -3646
  496. package/dist/powersync-BFSbSElQ.js +0 -62
  497. package/dist/powersync-DCDgZgfZ.js +0 -2
  498. package/dist/rls-55DQTuf5.js +0 -250
  499. package/dist/run-DMcNW_8T.js +0 -174
  500. package/dist/server-ZYEHtNHS.js +0 -268
  501. package/dist/sink-BTtmfPnE.js +0 -141
  502. package/dist/sink-CPN4zLfs.d.ts +0 -104
  503. package/dist/skills-Dle4ZoLF.js +0 -56
  504. package/dist/snapshot-Dc9CHPME.js +0 -1078
  505. package/dist/src-Bb7no-hS.js +0 -639
  506. package/dist/subject-C6dLxqHU.js +0 -24
  507. package/dist/supabase-Dbqr141s.js +0 -83
  508. package/dist/supabase-hook-BULg7Ut_.js +0 -2
  509. package/dist/thenable-QSmvt-vZ.js +0 -65
  510. package/dist/types-C0dee0Qf.d.ts +0 -197
  511. package/dist/types-DvupZE0w.d.ts +0 -9
  512. package/dist/usage-4G6t643F.js +0 -42
  513. package/dist/usage-iFLJ0-rr.js +0 -2
  514. package/dist/vocabulary-hhEFLFoL.d.ts +0 -65
  515. package/dist/watch-C8TpsAl4.js +0 -2
@@ -1,79 +1,17 @@
1
1
  import { a as sole, n as isReadonlyArray, r as isRecord } from "./compact-CAf557ZB.js";
2
- import { o as isForbiddenKey } from "./thenable-QSmvt-vZ.js";
3
- import { l as isSqlFunctionField } from "./refs-f3imMjNW.js";
4
- import { c as rootScope, d as scopeList, l as scopeChain, o as resolveScope } from "./scopes-tvpa34ep.js";
5
- import { v as listRoles } from "./grantee-CXY-0C87.js";
6
- import { r as relationCondition, t as inheritCondition } from "./grantee-conditions-MQs5NIuX.js";
7
- import { l as partitionsOf, p as scopeField } from "./tenancy-DshjqY0n.js";
8
- import { t as CliError } from "./errors-CWfoNg9N.js";
9
- import { i as scopeMembershipTable, o as relatedSql, r as scopeColumn } from "./compile-CcEu2ZQ1.js";
10
- import { l as sqlTextArray, r as qualifySqlTable, t as SQL_IDENT } from "./sql-t8teWOfo.js";
11
- import { d as keptKeys, l as roleColumn, u as roleManifest } from "./sources-B1-b-R0t.js";
12
- import { n as requirePeer } from "./peer-BOr_kJE1.js";
13
- import { D as sqlType, E as scopeTypeOf, F as quoteQualified, I as quoteTable, M as qualified, N as quoteIdent, O as tenantTypeOf, P as quoteLiteral, S as CLAIM, _ as subjectClaimJsonSql, b as tenantClaimSql, d as keyTenantSql, f as kindFilterSql, h as roleKindSql, i as allowEntry, j as helperSchema, k as CLI, m as memberVia, n as GLOBAL_ROLES, o as rootName, p as liveSessionSql, r as HELPERS, s as signedIn, t as CUSTOM_ROLES, u as globalKindFilterSql, v as subjectClaimSql, x as userIdHelperSql, y as subjectIdSql } from "./shared-1uhI-e-b.js";
14
- //#region src/cli/pg.ts
15
- const CONNECT_TIMEOUT_MS = 1e4;
16
- const STATEMENT_TIMEOUT_MS = 6e4;
17
- function loadPg(command) {
18
- return requirePeer(() => import("pg"), "pg", command);
19
- }
20
- function options(db) {
21
- return {
22
- connectionString: db,
23
- connectionTimeoutMillis: CONNECT_TIMEOUT_MS,
24
- statement_timeout: STATEMENT_TIMEOUT_MS
25
- };
26
- }
27
- /** Ctrl-C ends the connection, so a query that hangs cannot keep the process alive. */
28
- function endOnInterrupt(end) {
29
- const interrupt = () => {
30
- end().catch(() => void 0);
31
- process.exitCode = 130;
32
- };
33
- process.once("SIGINT", interrupt);
34
- return async () => {
35
- process.off("SIGINT", interrupt);
36
- await end();
37
- };
38
- }
39
- function unreachable(command, cause) {
40
- return new CliError("unavailable", `PermDock CLI: ${command.replace(/^permdock /u, "")} could not connect`, { cause });
41
- }
42
- /** The default `connect` of a command: one connection, failing with a message that names `command`. */
43
- function pgConnector(command) {
44
- return (db) => connectPg(db, command);
45
- }
46
- /** One connection, for commands that run a transaction. */
47
- async function connectPg(db, command) {
48
- const client = new (await (loadPg(command))).Client(options(db));
49
- try {
50
- await client.connect();
51
- } catch (cause) {
52
- throw unreachable(command, cause);
53
- }
54
- return {
55
- query: (sql, values) => client.query(sql, values),
56
- end: endOnInterrupt(() => client.end())
57
- };
58
- }
59
- /** A small pool, for independent catalog reads that can run in parallel. */
60
- async function connectPgPool(db, command) {
61
- const pool = new (await (loadPg(command))).Pool({
62
- ...options(db),
63
- max: 4
64
- });
65
- try {
66
- (await pool.connect()).release();
67
- } catch (cause) {
68
- await pool.end();
69
- throw unreachable(command, cause);
70
- }
71
- return {
72
- query: (sql, values) => pool.query(sql, values),
73
- end: endOnInterrupt(() => pool.end())
74
- };
75
- }
76
- //#endregion
2
+ import { n as isForbiddenKey } from "./paths-DXSHAMIa.js";
3
+ import { l as isSqlFunctionField } from "./refs-K6eWUJ0H.js";
4
+ import { u as isoAt } from "./time-DkWjdL98.js";
5
+ import { c as rootScope, d as scopeList, l as scopeChain, o as resolveScope } from "./scopes-CUPSjo9q.js";
6
+ import { t as isoInstant } from "./instant-BluqDvyS.js";
7
+ import { m as findRole, v as listRoles } from "./grantee-hxrUsbpM.js";
8
+ import { r as relationCondition, t as inheritCondition } from "./grantee-conditions-CcXf8hMM.js";
9
+ import { l as partitionsOf, p as scopeField } from "./tenancy-DN2QAx9z.js";
10
+ import { i as configError } from "./errors-CWTY6uih.js";
11
+ import { i as scopeMembershipTable, o as relatedSql, r as scopeColumn } from "./compile-CMqMx_IN.js";
12
+ import { i as qualifySqlTable, t as SQL_IDENT, u as sqlTextArray } from "./sql-B5T1IL83.js";
13
+ import { d as keptKeys, l as roleColumn, u as roleManifest } from "./sources-vs5dGOsS.js";
14
+ import { A as declaredArray, D as sqlType, E as scopeTypeOf, F as quoteQualified, I as quoteTable, L as textArrayLiteral, M as qualified, N as quoteIdent, O as tenantTypeOf, P as quoteLiteral, S as CLAIM, _ as subjectClaimJsonSql, b as tenantClaimSql, d as keyTenantSql, f as kindFilterSql, h as roleKindSql, i as allowEntry, j as helperSchema, k as CLI, m as memberVia, n as GLOBAL_ROLES, o as rootName, p as liveSessionSql, r as HELPERS, s as signedIn, t as CUSTOM_ROLES, u as globalKindFilterSql, v as subjectClaimSql, x as userIdHelperSql, y as subjectIdSql } from "./shared-BAK8t4Uo.js";
77
15
  //#region src/cli/sql/active.ts
78
16
  function checkActiveRow(label, row) {
79
17
  quoteTable(row.table);
@@ -81,9 +19,9 @@ function checkActiveRow(label, row) {
81
19
  if (row.disabledAt !== void 0) quoteIdent(row.disabledAt);
82
20
  if (row.status !== void 0) {
83
21
  quoteIdent(row.status);
84
- if (row.active === void 0 || row.active.length === 0) throw new Error(`PermDock CLI: ${label}.status needs the active values in ${label}.active`);
22
+ if (row.active === void 0 || row.active.length === 0) throw configError(`PermDock CLI: ${label}.status needs the active values in ${label}.active`);
85
23
  }
86
- if (row.disabledAt === void 0 && row.status === void 0) throw new Error(`PermDock CLI: ${label} needs disabledAt or status to tell an active row`);
24
+ if (row.disabledAt === void 0 && row.status === void 0) throw configError(`PermDock CLI: ${label} needs disabledAt or status to tell an active row`);
87
25
  }
88
26
  /** Validates `rls.suspension` and keys its scopes by declared name. */
89
27
  function checkSuspension(suspension, scopes) {
@@ -92,9 +30,9 @@ function checkSuspension(suspension, scopes) {
92
30
  const byName = {};
93
31
  for (const [key, row] of Object.entries(suspension.scopes ?? {})) {
94
32
  const name = resolveScope(scopes, key);
95
- if (name === void 0) throw new Error(`PermDock CLI: rls.suspension.scopes.${key} names a scope the policy does not declare`);
33
+ if (name === void 0) throw configError(`PermDock CLI: rls.suspension.scopes.${key} names a scope the policy does not declare`);
96
34
  checkActiveRow(`rls.suspension.scopes.${key}`, row);
97
- if (row.keep !== void 0 && !Array.isArray(row.keep)) throw new Error(`PermDock CLI: rls.suspension.scopes.${key}.keep is a list of permissions or permission keys`);
35
+ if (row.keep !== void 0 && !Array.isArray(row.keep)) throw configError(`PermDock CLI: rls.suspension.scopes.${key}.keep is a list of permissions or permission keys`);
98
36
  const keep = keptKeys(row);
99
37
  const { keep: _given, ...active } = row;
100
38
  byName[name] = keep.length === 0 ? active : {
@@ -103,7 +41,7 @@ function checkSuspension(suspension, scopes) {
103
41
  };
104
42
  }
105
43
  const memberships = suspension.memberships;
106
- if (memberships?.keep !== void 0 && !Array.isArray(memberships.keep)) throw new Error("PermDock CLI: rls.suspension.memberships.keep is a list of permissions or permission keys");
44
+ if (memberships?.keep !== void 0 && !Array.isArray(memberships.keep)) throw configError("PermDock CLI: rls.suspension.memberships.keep is a list of permissions or permission keys");
107
45
  const keep = keptKeys(memberships);
108
46
  return {
109
47
  ...suspension.users === void 0 ? {} : { users: suspension.users },
@@ -126,18 +64,20 @@ function keptRowSql(keep, permission) {
126
64
  return `case ${kind} when 'null' then true when 'array' then ${keep} @> jsonb_build_array(${"key" in permission ? quoteLiteral(permission.key) : permission.sql}) else false end`;
127
65
  }
128
66
  /**
129
- * `exists` over an active row whose id column equals `id`: a missing row is
130
- * suspended, so the check fails closed.
67
+ * Whether `id` is among the active rows' ids: a missing row is suspended, so
68
+ * the check fails closed, and so does a null `id`. `id` stays outside the
69
+ * subquery so a column of the same name on `row.table` cannot capture it.
131
70
  */
132
71
  function activeRowSql(row, id) {
133
72
  const table = quoteTable(qualifySqlTable(row.table));
134
- const parts = [`s.${quoteIdent(row.id)} = ${id}`];
73
+ const parts = [];
135
74
  if (row.disabledAt !== void 0) parts.push(`s.${quoteIdent(row.disabledAt)} is null`);
136
75
  if (row.status !== void 0) {
137
76
  const values = (row.active ?? []).map(quoteLiteral).join(", ");
138
77
  parts.push(`s.${quoteIdent(row.status)}::text = any(array[${values}]::text[])`);
139
78
  }
140
- return `exists (select 1 from ${table} s where ${parts.join(" and ")})`;
79
+ const where = parts.length === 0 ? "" : ` where ${parts.join(" and ")}`;
80
+ return `coalesce(${id} in (select s.${quoteIdent(row.id)} from ${table} s${where}), false)`;
141
81
  }
142
82
  /** The active-user check for `user`; empty without `rls.suspension.users`. */
143
83
  function activeUserSql(ctx, user = subjectIdSql(ctx)) {
@@ -161,7 +101,7 @@ function activeInstancesSql(ctx, scope, idOf, permission = ctx.permission === vo
161
101
  const row = ctx.suspension?.scopes?.[name];
162
102
  if (row === void 0) continue;
163
103
  const id = idOf(name);
164
- if (id === void 0) throw new Error(`PermDock CLI: rls.suspension.scopes.${name} needs the ${name} id on ${scope} memberships: add columns.${name} to the ${scope} memberships table`);
104
+ if (id === void 0) throw configError(`PermDock CLI: rls.suspension.scopes.${name} needs the ${name} id on ${scope} memberships: add columns.${name} to the ${scope} memberships table`);
165
105
  const keep = keptKeys(row);
166
106
  if (permission !== void 0 && "key" in permission && keep.includes(permission.key)) continue;
167
107
  const active = activeRowSql(row, `(${id})::${scopeTypeOf(ctx, name)}`);
@@ -184,7 +124,7 @@ function graphSqlName(name) {
184
124
  }
185
125
  function sqlNameOf(name, label) {
186
126
  const sql = graphSqlName(name);
187
- if (sql === void 0) throw new Error(`PermDock CLI: ${label} is not a SQL name (letters, digits and underscores, starting with a letter), so rls generate cannot name its helper`);
127
+ if (sql === void 0) throw configError(`PermDock CLI: ${label} is not a SQL name (letters, digits and underscores, starting with a letter), so rls generate cannot name its helper`);
188
128
  return sql;
189
129
  }
190
130
  /** The helper returning the ids of `resource` the subject holds a relation on: `permitted_<snake_case resource>_ids`. */
@@ -225,7 +165,7 @@ function memberRoleOf(table, alias = "m", indent) {
225
165
  /** The schema-qualified name of a table under `search_path = ''`. */
226
166
  /** The helper returning the ids of scope `name` a grant key reaches. */
227
167
  function permittedIdsHelper(name) {
228
- if (!/^[a-z][a-z0-9_]*$/u.test(name)) throw new Error(`PermDock CLI: unsafe scope name '${name}'`);
168
+ if (!/^[a-z][a-z0-9_]*$/u.test(name)) throw configError(`PermDock CLI: unsafe scope name '${name}'`);
229
169
  return `permitted_${name}_ids`;
230
170
  }
231
171
  /** `permitted_<scope>_ids_for(p_user, p_grant)`: the same ids for a user the caller names. */
@@ -234,7 +174,7 @@ function permittedForHelper(name) {
234
174
  }
235
175
  /** The helper returning the ids of scope `name` the subject holds any live membership of. */
236
176
  function memberIdsHelper(name) {
237
- if (!/^[a-z][a-z0-9_]*$/u.test(name)) throw new Error(`PermDock CLI: unsafe scope name '${name}'`);
177
+ if (!/^[a-z][a-z0-9_]*$/u.test(name)) throw configError(`PermDock CLI: unsafe scope name '${name}'`);
238
178
  return `member_${name}_ids`;
239
179
  }
240
180
  /** `member_<scope>_ids_for(p_user uuid)`: the same rows for a user the caller names; the token hook's helper. */
@@ -250,6 +190,15 @@ function hasMemberFor(input, name) {
250
190
  if (input.dialect !== "supabase") return false;
251
191
  return memberForTable(input, name) !== void 0 || memberForSources(input, name).length > 0;
252
192
  }
193
+ /**
194
+ * Whether `permitted_<scope>_users` can list the members of one instance of
195
+ * `name`: a mapped membership table, or sources that all read a table.
196
+ */
197
+ function hasPermittedUsers(input, name) {
198
+ if (memberForTable(input, name) !== void 0) return true;
199
+ const sources = memberForSources(input, name);
200
+ return sources.length > 0 && sources.every((source) => source.sql.holders !== void 0);
201
+ }
253
202
  /** The table `member_<scope>_ids_for` reads for scope `name`, when `memberships` maps one with its id column. */
254
203
  function memberForTable(input, name) {
255
204
  const table = scopeMembershipTable(input.memberships, input.scopes, name);
@@ -262,7 +211,7 @@ function memberForSources(input, name) {
262
211
  }
263
212
  /**
264
213
  * The membership sources that can hold scope `name` when `memberships` maps
265
- * no table for it: every `fromTable` source and the `fromJunction` sources of
214
+ * no table for it: every `tableMembershipSource` source and the `junctionMembershipSource` sources of
266
215
  * that scope. Empty outside `database` mode.
267
216
  */
268
217
  function scopeSources(ctx, name) {
@@ -297,8 +246,8 @@ function claimPath(ref) {
297
246
  const prefix = ["principal.claim.", "principal.claims."].find((item) => ref.startsWith(item));
298
247
  if (prefix === void 0) return;
299
248
  const segments = ref.slice(prefix.length).split(".");
300
- if (segments.length > MAX_CLAIM_DEPTH) throw new Error(`PermDock CLI: claim path '${ref}' is deeper than ${MAX_CLAIM_DEPTH}`);
301
- for (const segment of segments) if (!CLAIM.test(segment) || isForbiddenKey(segment)) throw new Error(`PermDock CLI: unsafe claim name '${segment}'`);
249
+ if (segments.length > MAX_CLAIM_DEPTH) throw configError(`PermDock CLI: claim path '${ref}' is deeper than ${MAX_CLAIM_DEPTH}`);
250
+ for (const segment of segments) if (!CLAIM.test(segment) || isForbiddenKey(segment)) throw configError(`PermDock CLI: unsafe claim name '${segment}'`);
302
251
  return segments;
303
252
  }
304
253
  function jsonPath(base, keys, last) {
@@ -312,7 +261,7 @@ function jsonPath(base, keys, last) {
312
261
  */
313
262
  function claimRoot(ctx, path) {
314
263
  const [head, ...rest] = path;
315
- if (head === void 0) throw new Error("PermDock CLI: empty claim path");
264
+ if (head === void 0) throw configError("PermDock CLI: empty claim path");
316
265
  switch (ctx.dialect) {
317
266
  case "supabase": return {
318
267
  root: "(select auth.jwt())",
@@ -372,13 +321,16 @@ function claimArraySql(ctx, path, type) {
372
321
  }
373
322
  function sqlValue(value, ctx, field) {
374
323
  if (value !== null && typeof value === "object" && "ref" in value) return compileRef(value.ref, ctx, field);
375
- if (value !== null && typeof value === "object" && "date" in value) return quoteLiteral(value.date);
324
+ if (value !== null && typeof value === "object" && "date" in value) {
325
+ const instant = isoInstant(value.date);
326
+ return instant === void 0 ? "null" : quoteLiteral(isoAt(instant));
327
+ }
376
328
  if (isReadonlyArray(value)) return `array[${value.map((item) => sqlValue(item, ctx)).join(", ")}]`;
377
329
  if (value === null) return "null";
378
330
  if (typeof value === "boolean") return value ? "true" : "false";
379
331
  if (typeof value === "number") return Number.isFinite(value) ? String(value) : "null";
380
332
  if (typeof value === "string") return quoteLiteral(value);
381
- throw new Error("PermDock CLI: non-portable condition value");
333
+ throw configError("PermDock CLI: non-portable condition value");
382
334
  }
383
335
  function compileRef(ref, ctx, field) {
384
336
  if (ref === "principal.id") return subjectIdSql(ctx);
@@ -386,8 +338,8 @@ function compileRef(ref, ctx, field) {
386
338
  const path = claimPath(ref);
387
339
  if (ref === "principal.tenant" || path?.length === 1 && path[0] === ctx.tenantClaim) return tenantClaimSql(ctx);
388
340
  if (path !== void 0) return typedClaimSql(ctx, path, field === void 0 ? void 0 : ctx.columnTypes?.[field]);
389
- if (ref === "context" || ref.startsWith("context.")) throw new Error(`PermDock CLI: '${ref}' is request context, which is not in the token; RLS cannot read it (permdock doctor PD027)`);
390
- throw new Error(`PermDock CLI: non-portable subject ref '${ref}'`);
341
+ if (ref === "context" || ref.startsWith("context.")) throw configError(`PermDock CLI: '${ref}' is request context, which is not in the token; RLS cannot read it (permdock doctor PD027)`);
342
+ throw configError(`PermDock CLI: non-portable subject ref '${ref}'`);
391
343
  }
392
344
  /** `value` with `\`, `%` and `_` escaped for `like ... escape '\'`, as `escapeLike` does in process. */
393
345
  function likeLiteralSql(value) {
@@ -406,13 +358,18 @@ function compareSql(op, field, value) {
406
358
  default: return op;
407
359
  }
408
360
  }
409
- function existsSql(table, rowColumn, rowField, roles, ctx, tenantColumn, scope) {
410
- const parts = [`m.${quoteIdent(rowColumn)} = ${quoteIdent(rowField)}`, `m.${quoteIdent(table.user)} = ${subjectIdSql(ctx)}`];
361
+ /**
362
+ * Whether the row's `rowField` is among the ids in `table.rowColumn` the
363
+ * subject holds a matching membership on. The row field stays outside the
364
+ * subquery: inside it, an unqualified name resolves to the membership table
365
+ * first, so `m.org_id = "org_id"` would compare the membership with itself.
366
+ */
367
+ function membershipSql(table, rowColumn, rowField, roles, ctx, tenantColumn, scope) {
368
+ const parts = [`m.${quoteIdent(table.user)} = ${subjectIdSql(ctx)}`];
411
369
  const role = memberRoleOf(table);
412
370
  if (roles.length > 0) {
413
- const roleList = roles.map((name) => name.replaceAll("'", "''")).join(",");
414
371
  const held = role.through === void 0 && !role.lateral ? `m.${quoteIdent(role.column)}` : role.lookup;
415
- parts.push(`${held} = any('{${roleList}}')`);
372
+ parts.push(`${held} = any(${textArrayLiteral(roles)})`);
416
373
  const via = memberVia(table);
417
374
  const single = sole(roles);
418
375
  const kind = single === void 0 ? kindFilterSql(ctx, role.lookup, via) : roleKindSql(ctx, single, via);
@@ -432,23 +389,23 @@ function existsSql(table, rowColumn, rowField, roles, ctx, tenantColumn, scope)
432
389
  }));
433
390
  }
434
391
  const expand = roles.length > 0 && role.lateral ? role.join : "";
435
- return `exists (select 1 from ${quoteTable(table.table)} m${expand} where ${parts.join(" and ")})`;
392
+ return `${quoteIdent(rowField)} in (select m.${quoteIdent(rowColumn)} from ${quoteTable(table.table)} m${expand} where ${parts.join(" and ")})`;
436
393
  }
437
394
  function compileMemberOf(condition, ctx) {
438
395
  const scope = condition.scope === "resource" ? void 0 : resolveScope(ctx.scopes, condition.scope);
439
- if (condition.scope !== "resource" && scope === void 0) throw new Error(`PermDock CLI: memberOf ${condition.scope} names a scope the policy does not declare`);
396
+ if (condition.scope !== "resource" && scope === void 0) throw configError(`PermDock CLI: memberOf ${condition.scope} names a scope the policy does not declare`);
440
397
  const mapping = scope === void 0 ? condition.resource === void 0 ? void 0 : ctx.memberships?.resource?.[condition.resource] : scopeMembershipTable(ctx.memberships, ctx.scopes, scope);
441
398
  if (mapping !== void 0) {
442
399
  const named = scope === void 0 ? void 0 : scopeTable(ctx, scope);
443
400
  const rowColumn = scope === void 0 ? mapping.id : named?.column;
444
- if (rowColumn === void 0) throw new Error(`PermDock CLI: memberships mapping for ${condition.scope} is missing the row column`);
401
+ if (rowColumn === void 0) throw configError(`PermDock CLI: memberships mapping for ${condition.scope} is missing the row column`);
445
402
  const tenantColumn = scope === void 0 || scope === rootScope(ctx.scopes) ? void 0 : named?.tenantColumn;
446
- const primary = existsSql(mapping, rowColumn, condition.field, condition.roles, ctx, tenantColumn, scope);
403
+ const primary = membershipSql(mapping, rowColumn, condition.field, condition.roles, ctx, tenantColumn, scope);
447
404
  if (condition.scope !== "resource" || condition.parents === void 0) return primary;
448
405
  return `(${[primary, ...condition.parents.flatMap((parent) => {
449
- if (typeof parent === "string") return [existsSql(mapping, rowColumn, parent, condition.roles, ctx, tenantColumn)];
406
+ if (typeof parent === "string") return [membershipSql(mapping, rowColumn, parent, condition.roles, ctx, tenantColumn)];
450
407
  const hopTable = ctx.memberships?.resource?.[parent.resource];
451
- return hopTable?.id === void 0 ? [] : [existsSql(hopTable, hopTable.id, parent.field, condition.roles, ctx)];
408
+ return hopTable?.id === void 0 ? [] : [membershipSql(hopTable, hopTable.id, parent.field, condition.roles, ctx)];
452
409
  })].join(" or ")})`;
453
410
  }
454
411
  if (scope !== void 0 && condition.roles.length === 0 && scopeSources(ctx, scope).length > 0) return `${quoteIdent(condition.field)} in (select ${qualified(ctx, memberIdsHelper(scope))}())`;
@@ -461,7 +418,7 @@ function compileMemberOf(condition, ctx) {
461
418
  ];
462
419
  return parts.length === 1 ? parts.join("") : `(${parts.join(" and ")})`;
463
420
  }
464
- throw new Error(`PermDock CLI: memberOf ${condition.scope} needs a memberships table mapping`);
421
+ throw configError(`PermDock CLI: memberOf ${condition.scope} needs a memberships table mapping`);
465
422
  }
466
423
  function compileConditionSql(condition, ctx) {
467
424
  switch (condition.op) {
@@ -489,13 +446,14 @@ function compileConditionSql(condition, ctx) {
489
446
  const path = claimPath(condition.value.ref);
490
447
  if (path === void 0) {
491
448
  compileRef(condition.value.ref, ctx);
492
- throw new Error(`PermDock CLI: non-portable ${condition.op} against '${condition.value.ref}'`);
449
+ throw configError(`PermDock CLI: non-portable ${condition.op} against '${condition.value.ref}'`);
493
450
  }
494
451
  const column = quoteIdent(condition.field);
495
452
  const list = claimArraySql(ctx, path, ctx.columnTypes?.[condition.field]);
496
453
  return condition.op === "in" ? `${column} = any (${list})` : `(${column} is not null and not (${column} = any (${list})))`;
497
454
  }
498
455
  const values = condition.value.map((item) => sqlValue(item, ctx));
456
+ if (values.length === 0) return condition.op === "in" ? "false" : `${quoteIdent(condition.field)} is not null`;
499
457
  return `${quoteIdent(condition.field)} ${keyword} (${values.join(", ")})`;
500
458
  }
501
459
  case "isNull": return `${quoteIdent(condition.field)} is ${condition.value ? "" : "not "}null`;
@@ -609,7 +567,7 @@ function compileRelatedSql(condition, ctx) {
609
567
  */
610
568
  function compileHoppedSql(condition, ctx) {
611
569
  const resources = ctx.graph?.resources;
612
- if (resources === void 0) throw new Error("PermDock CLI: a related condition with link hops needs the policy resources in the RLS context");
570
+ if (resources === void 0) throw configError("PermDock CLI: a related condition with link hops needs the policy resources in the RLS context");
613
571
  const schema = quoteIdent(helperSchema(ctx));
614
572
  return graphSqlText(relatedSql(condition, {
615
573
  resources,
@@ -661,17 +619,17 @@ function relationWhere(policy, grant, grantee) {
661
619
  resources: policy.resources,
662
620
  now: { ref: "now" }
663
621
  });
664
- if (where === void 0) throw new Error(`PermDock CLI: grant ${grant.permission.key} names relation '${grantee.relation}', which ${grant.permission.resource} does not declare`);
622
+ if (where === void 0) throw configError(`PermDock CLI: grant ${grant.permission.key} names relation '${grantee.relation}', which ${grant.permission.resource} does not declare`);
665
623
  return where;
666
624
  }
667
625
  function inheritWhere(policy, grant, grantee) {
668
626
  const where = inheritCondition(grantee, policy.resources.get(grant.permission.resource), policy.resources);
669
- if (where === void 0) throw new Error(`PermDock CLI: grant ${grant.permission.key} inherits ${grantee.permission}, which ${grant.permission.resource} cannot reach`);
627
+ if (where === void 0) throw configError(`PermDock CLI: grant ${grant.permission.key} inherits ${grantee.permission}, which ${grant.permission.resource} cannot reach`);
670
628
  return where;
671
629
  }
672
630
  /** Only a deny for `oauth-client` actors compiles: Postgres sees `client_id` and `act`, not other actor kinds. */
673
631
  function actorAccess(grant, grantee, others) {
674
- if (grant.effect !== "deny" || grantee.actor !== "oauth-client" || others > 0) throw new Error(`PermDock CLI: grant ${grant.permission.key} is limited to an actor grantee; RLS compiles only deny(…, { to: actor('oauth-client') }) on its own, so enforce it in the application`);
632
+ if (grant.effect !== "deny" || grantee.actor !== "oauth-client" || others > 0) throw configError(`PermDock CLI: grant ${grant.permission.key} is limited to an actor grantee; RLS compiles only deny(…, { to: actor('oauth-client') }) on its own, so enforce it in the application`);
675
633
  return {
676
634
  kind: "actor",
677
635
  actor: "oauth-client"
@@ -679,7 +637,7 @@ function actorAccess(grant, grantee, others) {
679
637
  }
680
638
  function accessOf(grant, roles, anyone) {
681
639
  const [held, ...more] = roles;
682
- if (more.length > 0) throw new Error(`PermDock CLI: grant ${grant.permission.key} requires several roles at once (${roles.map((item) => item.role).join(", ")}); RLS compiles one role per grant`);
640
+ if (more.length > 0) throw configError(`PermDock CLI: grant ${grant.permission.key} requires several roles at once (${roles.map((item) => item.role).join(", ")}); RLS compiles one role per grant`);
683
641
  if (held === void 0) return anyone ? { kind: "anyone" } : { kind: "authenticated" };
684
642
  if (typeof held.scope === "object") return {
685
643
  kind: "resource",
@@ -724,7 +682,7 @@ function collectGrants(policy) {
724
682
  delegated = actorAccess(grant, item, items.length - 1);
725
683
  break;
726
684
  case "plan":
727
- case "assurance": throw new Error(`PermDock CLI: grant ${grant.permission.key} is limited to a ${item.kind} grantee, which RLS cannot compile; enforce it in the application`);
685
+ case "assurance": throw configError(`PermDock CLI: grant ${grant.permission.key} is limited to a ${item.kind} grantee, which RLS cannot compile; enforce it in the application`);
728
686
  default: return item;
729
687
  }
730
688
  const access = delegated ?? accessOf(grant, roles, anyone);
@@ -759,7 +717,7 @@ const DEFAULTS = {
759
717
  };
760
718
  function field(settings, name) {
761
719
  const value = settings[name] ?? DEFAULTS[name];
762
- if (typeof value !== "string" || !SQL_IDENT.test(value)) throw new Error(`PermDock CLI: rls.apiKeys.${name} must be a claim field name matching ${SQL_IDENT.source}`);
720
+ if (typeof value !== "string" || !SQL_IDENT.test(value)) throw configError(`PermDock CLI: rls.apiKeys.${name} must be a claim field name matching ${SQL_IDENT.source}`);
763
721
  return value;
764
722
  }
765
723
  function apiKeyFields(setting) {
@@ -776,7 +734,7 @@ function apiKeysPlan(setting, declared, renamed) {
776
734
  if (setting === void 0) return;
777
735
  const fields = apiKeyFields(setting);
778
736
  const unknown = fields.serviceRoles.filter((role) => !declared.has(role));
779
- if (unknown.length > 0) throw new Error(`PermDock CLI: rls.apiKeys.serviceRoles names roles the policy does not declare: ${unknown.join(", ")}`);
737
+ if (unknown.length > 0) throw configError(`PermDock CLI: rls.apiKeys.serviceRoles names roles the policy does not declare: ${unknown.join(", ")}`);
780
738
  return {
781
739
  ...fields,
782
740
  ...Object.keys(renamed).length === 0 ? {} : { renamed }
@@ -873,6 +831,15 @@ ${body}
873
831
  ), false)`;
874
832
  }
875
833
  //#endregion
834
+ //#region src/cli/shared/compare.ts
835
+ /** UTF-16 code-unit order, never `localeCompare`: generated SQL must match byte for byte on every machine. */
836
+ function byCodeUnit(a, b) {
837
+ return a < b ? -1 : a > b ? 1 : 0;
838
+ }
839
+ function byKey([a], [b]) {
840
+ return byCodeUnit(a, b);
841
+ }
842
+ //#endregion
876
843
  //#region src/cli/rls/custom-roles.ts
877
844
  /** The readers every helpers file ends with. Names are part of the SQL contract. */
878
845
  const READERS = {
@@ -916,9 +883,14 @@ create unique index if not exists ${quoteIdent(`${name}_level_key`)}` : `create
916
883
  scope text not null default ${quoteLiteral(rootName(ctx))},
917
884
  scope_id text,
918
885
  role text not null,
919
- ${column} text not null${check},
920
- check ((scope = 'global') = (tenant_id is null)),
921
- check (scope <> 'global' or scope_id is null)
886
+ ${column} text not null${check}
887
+ );
888
+ -- global: no tenant or instance; a tenant role: a tenant; a shared role: no tenant or instance
889
+ alter table ${table} drop constraint if exists ${quoteIdent(`${name}_check`)};
890
+ alter table ${table} drop constraint if exists ${quoteIdent(`${name}_shape`)};
891
+ alter table ${table} add constraint ${quoteIdent(`${name}_shape`)} check (
892
+ case when scope = 'global' then tenant_id is null and scope_id is null
893
+ else tenant_id is not null or scope_id is null end
922
894
  );
923
895
  ${index}
924
896
  on ${table} (coalesce(tenant_id::text, ''), scope, coalesce(scope_id, ''), role, ${unique});
@@ -941,9 +913,11 @@ where rp.role = any(${sqlTextArray(custom.assignable)})
941
913
  and a.effect = 'allow'
942
914
  );
943
915
  revoke all on table ${ceiling} from anon, authenticated, public;`);
944
- const renames = Object.entries(custom.renamed ?? {}).toSorted(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
916
+ const renames = Object.entries(custom.renamed ?? {}).toSorted(byKey);
917
+ const shared = sharedRoleSql(ctx);
945
918
  if (custom.levels === true) {
946
919
  chunks.push(leveledKeysSql(ctx, renames));
920
+ if (shared !== "") chunks.push(shared);
947
921
  return chunks.join("\n\n");
948
922
  }
949
923
  const rename = renamesSql(renames);
@@ -1001,8 +975,30 @@ as $$
1001
975
  where c.permission in (select a.permission from allowed a)
1002
976
  $$;
1003
977
  revoke execute on function ${keys}(text[], text[], text[], text) from public, anon, authenticated;`);
978
+ if (shared !== "") chunks.push(shared);
1004
979
  return chunks.join("\n\n");
1005
980
  }
981
+ /** `permdock_is_shared_role(p_scope, p_role)` (`database` mode): whether a shared custom role of that name exists at the scope. */
982
+ function sharedRoleSql(ctx) {
983
+ if (ctx.authorize !== "database") return "";
984
+ const perms = qualified(ctx, CUSTOM_ROLES.permissions);
985
+ const includes = qualified(ctx, CUSTOM_ROLES.includes);
986
+ const shared = qualified(ctx, CUSTOM_ROLES.shared);
987
+ return `-- a shared custom role: rows with no tenant at a scope below global; its name wins over a tenant role's in every tenant
988
+ create or replace function ${shared}(p_scope text, p_role text)
989
+ returns boolean
990
+ language sql
991
+ stable
992
+ security definer
993
+ set search_path = ''
994
+ as $$
995
+ select p_scope <> 'global' and (
996
+ exists (select 1 from ${perms} c where c.tenant_id is null and c.scope = p_scope and c.role = p_role)
997
+ or exists (select 1 from ${includes} c where c.tenant_id is null and c.scope = p_scope and c.role = p_role)
998
+ )
999
+ $$;
1000
+ revoke execute on function ${shared}(text, text) from public, anon, authenticated;`;
1001
+ }
1006
1002
  /**
1007
1003
  * `permdock_custom_keys` when resources declare levels. An allow entry is
1008
1004
  * `key` or `key@level`. A level reaches only the ceiling's `<grant key>@<level>`
@@ -1139,7 +1135,8 @@ function renamesSql(renames) {
1139
1135
  * for `authenticated`. A declared role answers from `role_permissions`, which
1140
1136
  * is the catalog. A custom role (`database` mode) answers from the tenant's
1141
1137
  * own rows through `permdock_custom_keys`, only to a member of the tenant or
1142
- * a holder of a `meta.manageRoles` permission for a platform role. The
1138
+ * a holder of a `meta.manageRoles` permission for a platform role; a shared
1139
+ * role (no tenant below global) to any signed-in caller. The
1143
1140
  * effect is `deny` for a deny the role carries after the ceiling, as
1144
1141
  * `resolveCustomRole` returns it, never for a deny that merely removes an allow.
1145
1142
  */
@@ -1230,6 +1227,7 @@ end;`;
1230
1227
  body = checked(`
1231
1228
  if not (${signedIn(ctx)} and (
1232
1229
  ${platform}
1230
+ or (p_scope <> 'global' and p_tenant is null and p_scope_id is null)
1233
1231
  or (p_scope <> 'global' and (${memberOf}))
1234
1232
  )) then
1235
1233
  ${raiseSql(" ", "42501", `'permdock: the caller is not a member of ' || coalesce(p_tenant::text, 'the platform')`, "not-member")}
@@ -1263,6 +1261,27 @@ $$;
1263
1261
  revoke execute on function ${trusted}(${signature}) from public, anon, authenticated;${grants}`);
1264
1262
  return chunks.join("\n\n");
1265
1263
  }
1264
+ /** The stored definition of `p_role` at `p_scope` for `p_tenant`: the shared rows when a shared role has the name. */
1265
+ function storedTenant(ctx) {
1266
+ return `case when ${qualified(ctx, CUSTOM_ROLES.shared)}(p_scope, p_role) then c.tenant_id is null else c.tenant_id is not distinct from p_tenant end`;
1267
+ }
1268
+ /**
1269
+ * The tenant half of a custom-role row match for a membership: the
1270
+ * membership's tenant, or no tenant when a shared role of that name exists at
1271
+ * the scope, so a shared role shadows a tenant role of the same name.
1272
+ */
1273
+ function customTenantMatch(ctx, tenant, scope, role) {
1274
+ return `case when ${qualified(ctx, CUSTOM_ROLES.shared)}(${quoteLiteral(scope)}, ${role}) then c.tenant_id is null else c.tenant_id::text = ${tenant} end`;
1275
+ }
1276
+ /**
1277
+ * The scope of a row of the application's roles table: its scope column (or
1278
+ * the first scope) with a tenant; with no tenant, the same scope when the
1279
+ * `shared` column is true, else `'global'`.
1280
+ */
1281
+ function rolesTableScope(table, tenant, root, column) {
1282
+ const own = table.scope === void 0 ? quoteLiteral(root) : `${column(table.scope)}::text`;
1283
+ return `case when ${tenant} is null then ${table.shared === void 0 ? "'global'" : `case when coalesce(${column(table.shared)}, false) then ${own} else 'global' end`} else ${own} end`;
1284
+ }
1266
1285
  /** `expr` read as its current key: a former key maps to the key it was renamed to. */
1267
1286
  function currentKeyExpr(renames, expr) {
1268
1287
  if (renames.length === 0) return expr;
@@ -1301,7 +1320,8 @@ function globalRoleSource(source, schema, alias, roles) {
1301
1320
  indent: " "
1302
1321
  });
1303
1322
  const tenant = role.lateral ? void 0 : throughTenant(role.through, roles);
1304
- const global = tenant === void 0 ? "" : ` and ${alias}k.${quoteIdent(tenant)} is null`;
1323
+ const shared = tenant === void 0 ? void 0 : roles?.shared;
1324
+ const global = tenant === void 0 ? "" : ` and ${alias}k.${quoteIdent(tenant)} is null${shared === void 0 ? "" : ` and ${alias}k.${quoteIdent(shared)} is not true`}`;
1305
1325
  return {
1306
1326
  table,
1307
1327
  user,
@@ -1310,7 +1330,8 @@ function globalRoleSource(source, schema, alias, roles) {
1310
1330
  roleSql: role.sql,
1311
1331
  role: roleManifest(role),
1312
1332
  ...role.through === void 0 ? {} : { through: role.through },
1313
- ...tenant === void 0 ? {} : { tenant }
1333
+ ...tenant === void 0 ? {} : { tenant },
1334
+ ...shared === void 0 ? {} : { shared }
1314
1335
  };
1315
1336
  }
1316
1337
  //#endregion
@@ -1544,7 +1565,7 @@ function tableBody(ctx, scope, type) {
1544
1565
  const custom = ctx.customRoles;
1545
1566
  if (custom === void 0) return lines.join("\n");
1546
1567
  const match = [
1547
- `c.tenant_id::text = ${memberColumn(tenantColumn ?? column)}::text`,
1568
+ customTenantMatch(ctx, `${memberColumn(tenantColumn ?? column)}::text`, scope, role.sql),
1548
1569
  `c.scope = ${quoteLiteral(scope)}`,
1549
1570
  `(c.scope_id is null or c.scope_id = ${memberColumn(column)}::text)`,
1550
1571
  `c.role = ${role.sql}`
@@ -1585,8 +1606,8 @@ function claimBody(ctx, scope, type) {
1585
1606
  and (${activeTenant(ctx)} is null or ${claimTenant(ctx, scope)} = ${activeTenant(ctx)})` : "";
1586
1607
  const filtersFor = (grant) => ` and m ->> 'scope' = ${quoteLiteral(scope)}
1587
1608
  and m ->> 'id' is not null${narrow}${keyTenantLine(ctx, scope, claimTenant(ctx, scope))}
1588
- and case jsonb_typeof(m -> 'expiresAt')
1589
- when 'number' then (m ->> 'expiresAt')::numeric > extract(epoch from now())
1609
+ and case jsonb_typeof(m -> 'expires_at')
1610
+ when 'number' then (m ->> 'expires_at')::numeric > extract(epoch from now())
1590
1611
  else true
1591
1612
  end${[
1592
1613
  ...claimKeptLines(ctx, " ", grantPermissionSql(grant)),
@@ -1707,7 +1728,7 @@ ${rows}
1707
1728
  const custom = ctx.customRoles;
1708
1729
  if (custom === void 0) return sourcesBodyOf(declared, sources, subjectIdSql(ctx));
1709
1730
  const match = [
1710
- `c.tenant_id::text = (${tenant})::text`,
1731
+ customTenantMatch(ctx, `(${tenant})::text`, scope, "r.role"),
1711
1732
  `c.scope = ${quoteLiteral(scope)}`,
1712
1733
  "(c.scope_id is null or c.scope_id = (ms.id)::text)",
1713
1734
  "c.role = r.role"
@@ -1771,8 +1792,8 @@ as $$
1771
1792
  and jsonb_typeof(c -> 'roles') = 'array'
1772
1793
  and c -> 'roles' @> jsonb_build_array(p_role)
1773
1794
  and (c -> 'permissions' is null or c -> 'permissions' @> jsonb_build_array(p_permission))
1774
- and jsonb_typeof(c -> 'expiresAt') = 'number'
1775
- and (c ->> 'expiresAt')::numeric > extract(epoch from now())
1795
+ and jsonb_typeof(c -> 'expires_at') = 'number'
1796
+ and (c ->> 'expires_at')::numeric > extract(epoch from now())
1776
1797
  $$;
1777
1798
  revoke execute on function ${fn}(text, text, text) from public;
1778
1799
  grant execute on function ${fn}(text, text, text) to anon, authenticated;`;
@@ -1808,7 +1829,7 @@ function customRoleCascadeSql(ctx) {
1808
1829
  const fn = qualified(ctx, CUSTOM_ROLES.cascade);
1809
1830
  const target = quoteTable(qualifySqlTable(table.table));
1810
1831
  const tenant = (row) => table.tenant === void 0 ? `null::${tenantType}` : `${rowColumn(row, table.tenant)}::${tenantType}`;
1811
- const scope = (row) => table.scope === void 0 ? `case when ${tenant(row)} is null then 'global' else ${quoteLiteral(root)} end` : `case when ${tenant(row)} is null then 'global' else ${rowColumn(row, table.scope)}::text end`;
1832
+ const scope = (row) => rolesTableScope(table, tenant(row), root, (name) => rowColumn(row, name));
1812
1833
  const id = (row) => table.id === void 0 ? "null::text" : `${rowColumn(row, table.id)}::text`;
1813
1834
  const skipped = (row) => table.skip === void 0 ? "false" : `coalesce(${rowColumn(row, table.skip)}, false)`;
1814
1835
  const guard = qualified(ctx, CUSTOM_ROLES.guard);
@@ -1942,7 +1963,7 @@ ${raiseSql(" ", "42501", `'permdock: the caller may not manage roles in ' ||
1942
1963
  )` : "";
1943
1964
  const reachCte = leveled && reachRows !== "" ? `,
1944
1965
  reach (grant_key, level) as (values ${reachRows})` : "";
1945
- const match = `c.tenant_id is not distinct from p_tenant and c.scope = p_scope and c.scope_id is not distinct from p_scope_id`;
1966
+ const match = `${storedTenant(ctx)} and c.scope = p_scope and c.scope_id is not distinct from p_scope_id`;
1946
1967
  const memberOf = `p_tenant::text in ${members(root)}
1947
1968
  or ${perScope(ctx, (scope) => scope === root ? "false" : `p_scope_id in ${members(scope)}`)}`;
1948
1969
  return {
@@ -1994,7 +2015,7 @@ begin
1994
2015
  perform ${shape}(p_tenant, p_scope, p_scope_id, p_role);
1995
2016
  if not (${signedIn(ctx, user)} and (
1996
2017
  ${platform}
1997
- or (p_scope <> 'global' and (${memberOf}))
2018
+ or (p_scope <> 'global' and coalesce(${memberOf}, false))
1998
2019
  )) then
1999
2020
  ${raiseSql(" ", "42501", `'permdock: the caller is not a member of ' || coalesce(p_tenant::text, 'the platform')`, "not-member")}
2000
2021
  end if;${requires}
@@ -2022,8 +2043,9 @@ revoke execute on function ${guard}(${userArg}${tenantType}, text, text, text) f
2022
2043
  * \`assignablePermissions\`: entries inside the ceiling of the role's scope,
2023
2044
  * includes naming declared roles, and only permissions and levels the caller
2024
2045
  * may hand out, for the new definition and the stored one alike. A platform
2025
- * custom role (\`p_scope = 'global'\`, no tenant) is written by a caller who
2026
- * holds a \`meta.manageRoles\` permission through a global role. The
2046
+ * custom role (\`p_scope = 'global'\`, no tenant) and a shared custom role (no
2047
+ * tenant at a lower scope) are written by a caller who holds a
2048
+ * \`meta.manageRoles\` permission through a global role. The
2027
2049
  * \`permdock_trusted_*\` variants skip the caller checks and keep the
2028
2050
  * definition checks; no client role may execute them.
2029
2051
  */
@@ -2041,7 +2063,7 @@ function customRoleWritesSql(ctx, levelReach) {
2041
2063
  const guard = qualified(ctx, CUSTOM_ROLES.guard);
2042
2064
  const shape = qualified(ctx, CUSTOM_ROLES.shape);
2043
2065
  const entries = qualified(ctx, CUSTOM_ROLES.entries);
2044
- const renames = Object.entries(custom.renamed ?? {}).toSorted(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
2066
+ const renames = Object.entries(custom.renamed ?? {}).toSorted(byKey);
2045
2067
  const current = (expr) => currentKeyExpr(renames, expr);
2046
2068
  const known = sqlTextArray([...custom.permissions ?? [], ...renames.map(([former]) => former)]);
2047
2069
  const caller = callerFunctionsSql(ctx, custom, root, levelReach, false);
@@ -2049,9 +2071,15 @@ function customRoleWritesSql(ctx, levelReach) {
2049
2071
  const scopes = sqlTextArray(ctx.scopes.map((scope) => scope.name));
2050
2072
  const roleName = (param) => `coalesce(${param}, '') = '' or ${param} = any(${sqlTextArray(custom.declared)})`;
2051
2073
  const levelOf = leveled ? ", nullif(split_part(e, '@', 2), '')" : "";
2052
- const write = ` delete from ${perms} c where ${match} and c.role = p_role;
2074
+ const levelColumn = leveled ? ", level" : "";
2075
+ const sharedRole = qualified(ctx, CUSTOM_ROLES.shared);
2076
+ const taken = (param) => ` if p_tenant is not null and ${sharedRole}(p_scope, ${param}) then
2077
+ ${raiseSql(" ", "22023", `'permdock: ' || ${param} || ' is the name of a shared custom role'`, "name-taken")}
2078
+ end if;
2079
+ `;
2080
+ const write = `${taken("p_role")} delete from ${perms} c where ${match} and c.role = p_role;
2053
2081
  delete from ${includes} c where ${match} and c.role = p_role;
2054
- insert into ${perms} (tenant_id, scope, scope_id, role, permission, effect${leveled ? ", level" : ""})
2082
+ insert into ${perms} (tenant_id, scope, scope_id, role, permission, effect${levelColumn})
2055
2083
  select distinct p_tenant, p_scope, p_scope_id, p_role, ${current(leveled ? "split_part(e, '@', 1)" : "e")}, 'allow'${levelOf}
2056
2084
  from unnest(coalesce(p_allow, '{}'::text[])) e
2057
2085
  union
@@ -2060,7 +2088,7 @@ function customRoleWritesSql(ctx, levelReach) {
2060
2088
  insert into ${includes} (tenant_id, scope, scope_id, role, include_role)
2061
2089
  select distinct p_tenant, p_scope, p_scope_id, p_role, e
2062
2090
  from unnest(coalesce(p_include, '{}'::text[])) e;`;
2063
- const move = ` if ${roleName("p_to")} then
2091
+ const move = `${taken("p_to")} if ${roleName("p_to")} then
2064
2092
  ${raiseSql(" ", "22023", `'permdock: ' || coalesce(p_to, 'null') || ' is not a custom role name'`, "declared-role")}
2065
2093
  end if;
2066
2094
  if exists (select 1 from ${perms} c where ${match} and c.role = p_to)
@@ -2091,7 +2119,7 @@ grant execute on function ${qualified(ctx, name)}(${args}) to authenticated;` :
2091
2119
  const removeParams = `p_tenant ${tenantType}, p_scope text, p_scope_id text, p_role text`;
2092
2120
  return `${caller.beyond}
2093
2121
 
2094
- -- where a custom role may live: a scope of a tenant, or the platform (scope global, no tenant or instance), under an undeclared name
2122
+ -- where a custom role may live: a scope of a tenant, a scope of every tenant (shared: no tenant or instance), or the platform (scope global, no tenant or instance), under an undeclared name
2095
2123
  create or replace function ${shape}(p_tenant ${tenantType}, p_scope text, p_scope_id text, p_role text)
2096
2124
  returns void
2097
2125
  language plpgsql
@@ -2103,8 +2131,10 @@ begin
2103
2131
  if p_tenant is not null or p_scope_id is not null then
2104
2132
  ${raiseSql(" ", "22023", `'permdock: a platform custom role has no tenant or instance'`, "unknown-scope")}
2105
2133
  end if;
2106
- elsif p_tenant is null or not (p_scope = any(${scopes})) then
2134
+ elsif not (p_scope = any(${scopes})) then
2107
2135
  ${raiseSql(" ", "22023", `'permdock: custom roles are held at a scope of a tenant or at global, not ' || coalesce(p_scope, 'null')`, "unknown-scope")}
2136
+ elsif p_tenant is null and p_scope_id is not null then
2137
+ ${raiseSql(" ", "22023", `'permdock: a shared custom role has no instance'`, "unknown-scope")}
2108
2138
  end if;
2109
2139
  if ${roleName("p_role")} then
2110
2140
  ${raiseSql(" ", "22023", `'permdock: ' || coalesce(p_role, 'null') || ' is not a custom role name'`, "declared-role")}
@@ -2250,7 +2280,7 @@ function globalRolesSql(ctx) {
2250
2280
  const id = quoteIdent(through.id);
2251
2281
  const key = quoteIdent(through.key);
2252
2282
  const ref = quoteIdent(through.ref);
2253
- const global = source.tenant === void 0 ? "" : ` and k.${quoteIdent(source.tenant)} is null`;
2283
+ const global = source.tenant === void 0 ? "" : ` and k.${quoteIdent(source.tenant)} is null${source.shared === void 0 ? "" : ` and k.${quoteIdent(source.shared)} is not true`}`;
2254
2284
  body = ` select string_agg(w.r, ', ' order by w.r) into v_unknown
2255
2285
  from (${wanted}) w(r)
2256
2286
  where not exists (select 1 from ${keys} k where k.${key}::text = w.r${global});
@@ -2298,7 +2328,7 @@ grant execute on function ${fn}(${args}) to authenticated;`;
2298
2328
  * a live membership of, with any role. Not narrowed to the active tenant, and
2299
2329
  * no cascade: a membership of a child scope is not one of its parent.
2300
2330
  */
2301
- function memberBody(ctx, scope, type) {
2331
+ function memberBody(ctx, scope, type, roles) {
2302
2332
  if (ctx.authorize !== "database") return sqlBody(` select distinct (m ->> 'id')::${type}
2303
2333
  from jsonb_array_elements(
2304
2334
  case jsonb_typeof(${membershipClaim(ctx)}) when 'array' then ${membershipClaim(ctx)} else '[]'::jsonb end
@@ -2307,22 +2337,22 @@ function memberBody(ctx, scope, type) {
2307
2337
  and m ->> 'scope' = ${quoteLiteral(scope)}
2308
2338
  and m ->> 'id' is not null${keyTenantLine(ctx, scope, claimTenant(ctx, scope))}
2309
2339
  and jsonb_typeof(m -> 'roles') = 'array'
2310
- and jsonb_array_length(m -> 'roles') > 0
2311
- and case jsonb_typeof(m -> 'expiresAt')
2312
- when 'number' then (m ->> 'expiresAt')::numeric > extract(epoch from now())
2340
+ and jsonb_array_length(m -> 'roles') > 0${roles === void 0 ? "" : `\n and (m -> 'roles') ?| ${roles}`}
2341
+ and case jsonb_typeof(m -> 'expires_at')
2342
+ when 'number' then (m ->> 'expires_at')::numeric > extract(epoch from now())
2313
2343
  else true
2314
2344
  end${[
2315
2345
  ...claimKeptLines(ctx, " "),
2316
2346
  ...userActive(ctx, " "),
2317
2347
  ...instancesActive(ctx, scope, (name) => name === scope ? `m ->> 'id'` : `m -> 'within' ->> ${quoteLiteral(name)}`, " ")
2318
2348
  ].map((line) => `\n${line}`).join("")}`);
2319
- return memberRowsBody(ctx, scope, type, subjectIdSql(ctx), scopeSources(ctx, scope), true);
2349
+ return memberRowsBody(ctx, scope, type, subjectIdSql(ctx), scopeSources(ctx, scope), true, roles);
2320
2350
  }
2321
2351
  /**
2322
2352
  * The membership rows of `user` for `scope`, from the sources or else the
2323
2353
  * mapped table, with the same expiry and suspension filters either way.
2324
2354
  */
2325
- function memberRowsBody(ctx, scope, type, user, sources, keyed = false) {
2355
+ function memberRowsBody(ctx, scope, type, user, sources, keyed = false, roles) {
2326
2356
  if (sources.length > 0) {
2327
2357
  const narrow = keyed ? keyTenantLine(ctx, scope, sourceIdOf(scope)(rootName(ctx))) : "";
2328
2358
  return sourcesBodyOf(` select distinct (ms.id)::${type}
@@ -2332,7 +2362,7 @@ ${sourceRows(sources)}
2332
2362
  where ${signedIn(ctx, user)}
2333
2363
  and ms.scope = ${quoteLiteral(scope)}
2334
2364
  and jsonb_typeof(ms.roles) = 'array'
2335
- and jsonb_array_length(ms.roles) > 0${narrow}${sourceFilters(ctx, scope, user, void 0, sourcesKeep(sources))}`, sources, user);
2365
+ and jsonb_array_length(ms.roles) > 0${roles === void 0 ? "" : `\n and ms.roles ?| ${roles}`}${narrow}${sourceFilters(ctx, scope, user, void 0, sourcesKeep(sources))}`, sources, user);
2336
2366
  }
2337
2367
  const mapped = scopeTable(ctx, scope);
2338
2368
  if (mapped === void 0) return sqlBody(` select null::${type} where false -- no ${scope} memberships table configured`);
@@ -2344,6 +2374,7 @@ ${sourceRows(sources)}
2344
2374
  ` where ${memberColumn(table.user)} = ${user}`,
2345
2375
  ` and ${role.through === void 0 && !role.lateral ? memberColumn(role.column) : role.sql} is not null`
2346
2376
  ];
2377
+ if (roles !== void 0) lines.push(` and (${role.through === void 0 && !role.lateral ? memberColumn(role.column) : role.sql})::text = any(${roles})`);
2347
2378
  const narrow = keyed ? keyTenantSql(ctx, tenantColumn === void 0 ? void 0 : memberColumn(tenantColumn)) : void 0;
2348
2379
  if (narrow !== void 0) lines.push(` and ${narrow}`);
2349
2380
  if (table.expiresAt !== void 0) {
@@ -2377,6 +2408,22 @@ ${functionBody(keyed)}
2377
2408
  ${grants}`;
2378
2409
  }
2379
2410
  /**
2411
+ * `member_<scope>_ids(p_roles text[])`: `member_<scope>_ids()` narrowed to
2412
+ * memberships holding one of `p_roles` by name, for `rls migrate`'s `role`
2413
+ * helpers. Generated only for a scope such a helper names.
2414
+ */
2415
+ function memberRolesFunction(ctx, scope, type, anonExecute) {
2416
+ const fn = qualified(ctx, memberIdsHelper(scope));
2417
+ const body = memberBody(ctx, scope, type, "p_roles");
2418
+ const grants = anonExecute ? `revoke execute on function ${fn}(text[]) from public;
2419
+ grant execute on function ${fn}(text[]) to anon, authenticated;` : `revoke execute on function ${fn}(text[]) from public, anon;
2420
+ grant execute on function ${fn}(text[]) to authenticated;`;
2421
+ return `create or replace function ${fn}(p_roles text[])
2422
+ returns setof ${type}
2423
+ ${functionBody(body)}
2424
+ ${grants}`;
2425
+ }
2426
+ /**
2380
2427
  * `member_<scope>_ids_for(p_user uuid)`: `member_<scope>_ids()` for a user the
2381
2428
  * caller names, read from the membership sources (or the mapped table) in
2382
2429
  * either mode, so a token hook can call it before any claim exists. Naming
@@ -2400,7 +2447,7 @@ revoke execute on function ${fn}(uuid) from public, anon, authenticated;`;
2400
2447
  function accessSql(ctx, scope, grantKey, column) {
2401
2448
  const key = quoteLiteral(grantKey);
2402
2449
  if (scope === "global") return `(select ${qualified(ctx, HELPERS.has)}(${key}))`;
2403
- if (column === void 0) throw new Error(`PermDock CLI: ${scope}-scoped grant needs definePolicy({ scopes.${scope} })`);
2450
+ if (column === void 0) throw configError(`PermDock CLI: ${scope}-scoped grant needs definePolicy({ scopes.${scope} })`);
2404
2451
  return `${quoteIdent(column)} in (select ${qualified(ctx, permittedIdsHelper(scope))}(${key}))`;
2405
2452
  }
2406
2453
  function userIdSql(ctx, anonExecute) {
@@ -2534,6 +2581,7 @@ revoke all on table ${ur} from anon, authenticated, public;`);
2534
2581
  const type = scopeTypeOf(ctx, scope.name);
2535
2582
  chunks.push(helperFunction(ctx, permittedIdsHelper(scope.name), `setof ${type}`, bodies.ids(scope.name), anon));
2536
2583
  chunks.push(memberFunction(ctx, scope.name, type, anon));
2584
+ if (ctx.roleMemberScopes?.includes(scope.name) === true) chunks.push(memberRolesFunction(ctx, scope.name, type, anon));
2537
2585
  if (hasMemberFor(ctx, scope.name)) chunks.push(memberForFunction(ctx, scope.name, type));
2538
2586
  }
2539
2587
  const forUser = forUserSql(ctx);
@@ -2620,12 +2668,12 @@ function requiresSql(ctx, key, resource) {
2620
2668
  }
2621
2669
  /** Postgres truncates an identifier past 63 bytes, which would merge two helpers. */
2622
2670
  function checkName(name) {
2623
- if (new TextEncoder().encode(name).length > 63) throw new Error(`PermDock CLI: the helper name ${name} is longer than 63 bytes; use a shorter scope name`);
2671
+ if (new TextEncoder().encode(name).length > 63) throw configError(`PermDock CLI: the helper name ${name} is longer than 63 bytes; use a shorter scope name`);
2624
2672
  return name;
2625
2673
  }
2626
2674
  function grantsFunction(ctx, grants, renamed, anonExecute) {
2627
2675
  const fn = qualified(ctx, PERMISSION_HELPERS.grantKeys);
2628
- const map = Object.fromEntries([...grants].toSorted(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
2676
+ const map = Object.fromEntries([...grants].toSorted(byKey));
2629
2677
  const current = Object.keys(renamed).length === 0 ? "p_permission" : `coalesce(${quoteLiteral(JSON.stringify(renamed))}::jsonb ->> p_permission, p_permission)`;
2630
2678
  return `-- the grant keys a permission key reaches on one scope: its unconditional allows, with p_effect 'deny' every deny, and with 'conditioned-allow' or 'conditioned-deny' the allows or denies that carry a row condition
2631
2679
  create or replace function ${fn}(p_permission text, p_scope text, p_effect text default 'allow')
@@ -2712,7 +2760,7 @@ function permissionHelpersSql(ctx, grants, renamed, anonExecute) {
2712
2760
  ) g
2713
2761
  cross join lateral ${helper}(${user}g.grant_key) d(id)`;
2714
2762
  const keysBody = (scope, bodies, filtered) => {
2715
- const rows = ["global", scope].flatMap((at) => Object.entries(grants.get(at) ?? {}).toSorted(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).flatMap(([key, effects]) => ["allow", "deny"].flatMap((effect) => effects[effect].map((grant) => `(${[
2763
+ const rows = ["global", scope].flatMap((at) => Object.entries(grants.get(at) ?? {}).toSorted(byKey).flatMap(([key, effects]) => ["allow", "deny"].flatMap((effect) => effects[effect].map((grant) => `(${[
2716
2764
  key,
2717
2765
  at,
2718
2766
  effect,
@@ -2802,4 +2850,848 @@ ${keysFunction(keysFor, `p_user ${user}, p_id ${type}, p_keys text[]`, keysBody(
2802
2850
  return `${chunks.join("\n\n")}\n`;
2803
2851
  }
2804
2852
  //#endregion
2805
- export { sqlFunctionNames as A, memberIdsHelper as B, apiKeysPlan as C, roleNames as D, collectGrants as E, hasMemberFor as F, scopeSources as G, permittedForHelper as H, linkHelper as I, activeUserSql as J, scopeTable as K, memberForHelper as L, graphHelper as M, graphSqlName as N, compileConditionSql as O, graphSqlText as P, memberForSources as R, apiKeyFields as S, breakGlassKey as T, permittedIdsHelper as U, memberRoleOf as V, restrictedHelper as W, connectPgPool as X, checkSuspension as Y, pgConnector as Z, globalRoleSource as _, accessSql as a, API_KEY_ALLOWS as b, capabilityAccessSql as c, memberColumn as d, sourceFilters as f, roleRows as g, membershipRows as h, requiresSql as i, CLOSURE as j, contextRefs as k, claimKeptLines as l, globalRoleRows as m, permissionKeysForHelper as n, helpersSql as o, sourcesKeep as p, activeRowSql as q, permittedByPermissionHelper as r, seedSql as s, permissionHelpersSql as t, disabledColumn as u, currentKeyExpr as v, breakGlassHolder as w, apiKeyAllowsCall as x, hasCustomRoleChecksFor as y, memberForTable as z };
2853
+ //#region src/cli/rls/ownership-names.ts
2854
+ /** Objects the ownership rules add next to the helpers. Names are part of the SQL contract. */
2855
+ const OWNERSHIP = {
2856
+ canAssign: "permdock_can_assign",
2857
+ canAssignFor: "permdock_can_assign_for",
2858
+ canAssignAny: "permdock_can_assign_any",
2859
+ canAssignAnyFor: "permdock_can_assign_any_for",
2860
+ holders: "permdock_holders",
2861
+ transferOnly: "permdock_transfer_only"
2862
+ };
2863
+ /** Objects `rls.assignments` adds. Names are part of the SQL contract. */
2864
+ const ASSIGNMENTS = {
2865
+ custom: "permdock_can_assign_custom_role",
2866
+ customFor: "permdock_can_assign_custom_role_for",
2867
+ trigger: "permdock_assignment"
2868
+ };
2869
+ //#endregion
2870
+ //#region src/cli/rls/assignments.ts
2871
+ /** The client roles whose writes the assignment triggers check; any other role is a trusted path. */
2872
+ const CLIENT_ROLES = [
2873
+ "anon",
2874
+ "anonymous",
2875
+ "authenticated"
2876
+ ];
2877
+ /** The mapped membership tables, `rls.assignments.tables` and the global-roles table `rls.roles`. */
2878
+ function assignedTables(ctx) {
2879
+ const out = [];
2880
+ for (const { name } of ctx.scopes) {
2881
+ const mapped = scopeTable(ctx, name);
2882
+ if (mapped === void 0) continue;
2883
+ out.push({
2884
+ table: mapped.table.table,
2885
+ scope: name,
2886
+ id: mapped.column,
2887
+ tenant: mapped.tenantColumn ?? mapped.column,
2888
+ role: mapped.table.role,
2889
+ user: mapped.table.user
2890
+ });
2891
+ }
2892
+ if (ctx.roles !== void 0) out.push({
2893
+ table: ctx.roles.table,
2894
+ scope: "global",
2895
+ role: ctx.roles.role ?? "role",
2896
+ user: ctx.roles.user ?? "user_id"
2897
+ });
2898
+ for (const extra of ctx.assignments?.tables ?? []) {
2899
+ const scope = resolveScope(ctx.scopes, extra.scope);
2900
+ if (scope === void 0) throw configError(`PermDock CLI: rls.assignments.tables names scope '${extra.scope}', which the policy does not declare`);
2901
+ if (scope !== ctx.scopes[0]?.name && extra.tenant === void 0) throw configError(`PermDock CLI: rls.assignments.tables entry ${extra.table} assigns at ${scope}, below the first scope: name its tenant column`);
2902
+ out.push({
2903
+ table: extra.table,
2904
+ scope,
2905
+ id: extra.id,
2906
+ tenant: extra.tenant ?? extra.id,
2907
+ role: extra.role,
2908
+ ...extra.user === void 0 ? {} : { user: extra.user }
2909
+ });
2910
+ }
2911
+ return out;
2912
+ }
2913
+ /** The tables `rls.assignments.ownRole` refuses a caller's own rows on, by table key. */
2914
+ function ownRoleTables(ctx, tables) {
2915
+ const own = ctx.assignments?.ownRole;
2916
+ if (own === void 0) return /* @__PURE__ */ new Set();
2917
+ if (own === "refuse") return new Set(tables.filter((entry) => entry.user !== void 0).map((entry) => qualifySqlTable(entry.table)));
2918
+ const out = /* @__PURE__ */ new Set();
2919
+ for (const [name, value] of Object.entries(own)) {
2920
+ const entry = tables.find((table) => qualifySqlTable(table.table) === qualifySqlTable(name));
2921
+ if (entry === void 0) throw configError(`PermDock CLI: rls.assignments.ownRole names ${name}, which no assignment trigger guards`);
2922
+ if (value !== "refuse") throw configError(`PermDock CLI: rls.assignments.ownRole.${name} must be 'refuse'`);
2923
+ if (entry.user === void 0) throw configError(`PermDock CLI: rls.assignments.ownRole names ${name}, which has no user column: set its user in rls.assignments.tables`);
2924
+ out.add(qualifySqlTable(entry.table));
2925
+ }
2926
+ return out;
2927
+ }
2928
+ /**
2929
+ * The tables `rls.assignments` guards, schema-qualified: each scope's mapped
2930
+ * membership table, the global-roles table (`rls.roles`, else
2931
+ * `supabase.hook.roles`) and `rls.assignments.tables`. Empty without
2932
+ * `rls.assignments`.
2933
+ */
2934
+ function guardedTables(config, scopes) {
2935
+ const rls = config.rls;
2936
+ if (rls?.assignments === void 0) return [];
2937
+ const hook = config.supabase?.hook?.roles;
2938
+ const roles = rls.roles ?? (hook === false ? void 0 : hook);
2939
+ const tables = [
2940
+ ...scopes.flatMap((scope) => {
2941
+ const mapped = memberForTable({
2942
+ scopes,
2943
+ ...rls.memberships === void 0 ? {} : { memberships: rls.memberships }
2944
+ }, scope.name);
2945
+ return mapped === void 0 ? [] : [mapped.table];
2946
+ }),
2947
+ ...roles === void 0 ? [] : [roles.table],
2948
+ ...(rls.assignments === true ? [] : rls.assignments.tables ?? []).map((entry) => entry.table)
2949
+ ];
2950
+ return [...new Set(tables.map(qualifySqlTable))];
2951
+ }
2952
+ function isRoleList(role) {
2953
+ return Array.isArray(role);
2954
+ }
2955
+ /** The role keys a row variable holds, one scalar per role source. */
2956
+ function rowRoles(entry, row) {
2957
+ return (isRoleList(entry.role) ? entry.role : [entry.role]).map((part) => roleColumn(part, qualifySqlTable(entry.table), row, {
2958
+ label: `rls.assignments ${entry.table} role`,
2959
+ prefix: "PermDock CLI"
2960
+ }).lookup);
2961
+ }
2962
+ /**
2963
+ * `permdock_can_assign_custom_role`: whether the signed-in caller may assign a
2964
+ * custom role of the tenant at the instance, by the checks the custom-role
2965
+ * write functions run on its stored definition.
2966
+ */
2967
+ function canAssignCustomSql(ctx, forUser = false) {
2968
+ const tenantType = tenantTypeOf(ctx);
2969
+ const fn = qualified(ctx, forUser ? ASSIGNMENTS.customFor : ASSIGNMENTS.custom);
2970
+ const perms = qualified(ctx, CUSTOM_ROLES.permissions);
2971
+ const includes = qualified(ctx, CUSTOM_ROLES.includes);
2972
+ const guard = qualified(ctx, forUser ? CUSTOM_ROLES.guardFor : CUSTOM_ROLES.guard);
2973
+ const userParam = forUser ? "p_user uuid, " : "";
2974
+ const userArg = forUser ? "uuid, " : "";
2975
+ const stored = `${storedTenant(ctx)} and c.scope = p_scope and c.role = p_role and (c.scope_id is null or c.scope_id = p_scope_id)`;
2976
+ const grants = forUser ? `revoke execute on function ${fn}(${userArg}${tenantType}, text, text, text) from public, anon, authenticated;` : `revoke execute on function ${fn}(${tenantType}, text, text, text) from public, anon;
2977
+ grant execute on function ${fn}(${tenantType}, text, text, text) to authenticated;`;
2978
+ return `-- whether ${forUser ? "user p_user" : "the caller"} may assign custom role p_role at instance p_scope_id (a platform role: null tenant, scope global): the custom-role write checks on its stored definition${forUser ? "; no client role may execute it" : ""}
2979
+ create or replace function ${fn}(${userParam}p_tenant ${tenantType}, p_scope text, p_scope_id text, p_role text)
2980
+ returns boolean
2981
+ language plpgsql
2982
+ stable
2983
+ security definer
2984
+ set search_path = ''
2985
+ as $$
2986
+ declare
2987
+ v_scope_id text;
2988
+ v_found boolean := false;
2989
+ begin
2990
+ select true, c.scope_id into v_found, v_scope_id
2991
+ from (
2992
+ select c.scope_id from ${perms} c where ${stored}
2993
+ union all
2994
+ select c.scope_id from ${includes} c where ${stored}
2995
+ ) c
2996
+ order by c.scope_id nulls first
2997
+ limit 1;
2998
+ if not coalesce(v_found, false) then
2999
+ return false;
3000
+ end if;
3001
+ perform ${guard}(${forUser ? "p_user, " : ""}p_tenant, p_scope, v_scope_id, p_role);
3002
+ return true;
3003
+ exception
3004
+ when insufficient_privilege or invalid_parameter_value then
3005
+ return false;
3006
+ end;
3007
+ $$;
3008
+ ${grants}`;
3009
+ }
3010
+ /**
3011
+ * The assignment triggers (\`rls.assignments\`): before a client role inserts,
3012
+ * updates or deletes a row that assigns a role, every role the old and new
3013
+ * row hold must be one the caller may assign at that instance, by
3014
+ * \`permdock_can_assign\` for a declared role and
3015
+ * \`permdock_can_assign_custom_role\` for a custom one. The functions run as
3016
+ * the invoker, so \`current_user\` tells a client write from a trusted one.
3017
+ */
3018
+ function assignmentSql(ctx) {
3019
+ const own = ctx.ownership;
3020
+ if (ctx.assignments === void 0 || own === void 0 || own.assigns.length === 0) return "";
3021
+ const canAssign = qualified(ctx, OWNERSHIP.canAssign);
3022
+ const custom = ctx.customRoles !== void 0 && ctx.authorize === "database";
3023
+ const declared = ctx.customRoles?.declared;
3024
+ const tenantType = tenantTypeOf(ctx);
3025
+ const chunks = [];
3026
+ const clients = `current_user::text = any(array[${CLIENT_ROLES.map(quoteLiteral).join(", ")}])`;
3027
+ const tables = assignedTables(ctx);
3028
+ const ownRole = ownRoleTables(ctx, tables);
3029
+ for (const entry of tables) {
3030
+ const table = quoteQualified(entry.table);
3031
+ const suffix = entry.table.replaceAll(/[^a-z0-9_]/gu, "_");
3032
+ const fn = qualified(ctx, quoteIdent(`${ASSIGNMENTS.trigger}_${suffix}`));
3033
+ const ownUser = entry.user !== void 0 && ownRole.has(qualifySqlTable(entry.table)) ? entry.user : void 0;
3034
+ const ownCheck = (row, reason) => ownUser === void 0 ? "" : ` if ${row}.${quoteIdent(ownUser)}::text = (${subjectIdSql(ctx)})::text then
3035
+ raise exception using
3036
+ errcode = '42501',
3037
+ message = 'permdock: the caller may not change their own roles',
3038
+ hint = '${reason}';
3039
+ end if;
3040
+ `;
3041
+ const check = (row) => {
3042
+ const id = entry.id === void 0 ? "null::text" : `${row}.${quoteIdent(entry.id)}::text`;
3043
+ const customFn = qualified(ctx, ASSIGNMENTS.custom);
3044
+ let allowed = `${canAssign}(v_role, ${id})`;
3045
+ if (custom && declared !== void 0) {
3046
+ const platform = `${customFn}(null, 'global', null, v_role)`;
3047
+ allowed = entry.id === void 0 || entry.tenant === void 0 ? `case when v_role = any(${declaredArray(declared)}) then ${canAssign}(v_role, ${id}) else ${platform} end` : `case when v_role = any(${declaredArray(declared)}) then ${canAssign}(v_role, ${id}) when ${id} is null then ${platform} else ${customFn}(${row}.${quoteIdent(entry.tenant)}::${tenantType}, ${quoteLiteral(entry.scope)}, ${id}, v_role) end`;
3048
+ }
3049
+ return ` foreach v_role in array array[${rowRoles(entry, row).join(", ")}]::text[] loop
3050
+ if v_role is not null and not coalesce(${allowed}, false) then
3051
+ raise exception using
3052
+ errcode = '42501',
3053
+ message = 'permdock: the caller may not assign ' || v_role || ' in ' || coalesce(${id}, 'null'),
3054
+ hint = 'not-assignable-by';
3055
+ end if;
3056
+ end loop;`;
3057
+ };
3058
+ const what = entry.id === void 0 ? "global roles" : `${entry.scope} roles`;
3059
+ chunks.push(`-- ${entry.table}: a client role may write only the ${what} it may assign${ownUser === void 0 ? "" : ", and none on its own rows"}
3060
+ create or replace function ${fn}()
3061
+ returns trigger
3062
+ language plpgsql
3063
+ volatile
3064
+ set search_path = ''
3065
+ as $$
3066
+ declare
3067
+ v_role text;
3068
+ begin
3069
+ if not (${clients}) then
3070
+ return coalesce(new, old);
3071
+ end if;
3072
+ if tg_op in ('UPDATE', 'DELETE') then
3073
+ ${ownCheck("old", "self-demotion")}${check("old")}
3074
+ end if;
3075
+ if tg_op in ('INSERT', 'UPDATE') then
3076
+ ${ownCheck("new", "not-assignable-by")}${check("new")}
3077
+ end if;
3078
+ return coalesce(new, old);
3079
+ end;
3080
+ $$;
3081
+ revoke execute on function ${fn}() from public, anon, authenticated;
3082
+ drop trigger if exists "permdock_assignment" on ${table};
3083
+ create trigger "permdock_assignment"
3084
+ before insert or update or delete on ${table}
3085
+ for each row execute function ${fn}();`);
3086
+ }
3087
+ return `${chunks.join("\n\n")}\n`;
3088
+ }
3089
+ //#endregion
3090
+ //#region src/cli/rls/ownership-triggers.ts
3091
+ function objectName(prefix, scope) {
3092
+ return quoteIdent(`${prefix}_${scope}`);
3093
+ }
3094
+ /** Live holders of `roleExpr` in instance `idExpr`, with the same kind and expiry rules as the helpers. */
3095
+ function holdersSql(ctx, table, column, idExpr, role, into) {
3096
+ const roleExpr = typeof role === "string" ? quoteLiteral(role) : role.expr;
3097
+ const held = memberRoleOf(table, "m", " ");
3098
+ const filters = [`m.${quoteIdent(column)} = ${idExpr}`, `${held.sql} = ${roleExpr}`];
3099
+ if (table.expiresAt !== void 0) {
3100
+ const expires = memberColumn(table.expiresAt);
3101
+ filters.push(`(${expires} is null or ${expires} > now())`);
3102
+ }
3103
+ const disabled = disabledColumn(table);
3104
+ if (disabled !== void 0) filters.push(`${disabled} is null`);
3105
+ const via = memberVia(table);
3106
+ const kind = typeof role === "string" ? roleKindSql(ctx, role, via) : kindFilterSql(ctx, roleExpr, via);
3107
+ if (kind !== void 0) filters.push(kind);
3108
+ return `select count(distinct m.${quoteIdent(table.user)}) into ${into}
3109
+ from ${quoteQualified(table.table)} m${held.join}
3110
+ where ${filters.join("\n and ")}`;
3111
+ }
3112
+ /**
3113
+ * `min` and `max` as a deferred constraint trigger: checked at commit, so a
3114
+ * transfer (demote one holder, promote another) passes in one transaction. An
3115
+ * instance with no memberships left is being torn down and skips `min`.
3116
+ */
3117
+ function holdersTriggerSql(ctx, scope, table, column, counted) {
3118
+ const fn = qualified(ctx, `${OWNERSHIP.holders}_${scope}`);
3119
+ const col = quoteIdent(column);
3120
+ const checks = counted.map((rule) => {
3121
+ const lines = [` select count(*) into v_total from ${quoteQualified(table.table)} m where m.${col} = v_key;`, ` ${holdersSql(ctx, table, column, "v_key", rule.role, "v_count")};`];
3122
+ if (rule.min > 0) lines.push(` if v_total > 0 and v_count < ${String(rule.min)} then
3123
+ raise exception using
3124
+ errcode = '23514',
3125
+ message = ${quoteLiteral(`permdock: ${scope} `)} || v_id || ${quoteLiteral(` keeps at least ${String(rule.min)} ${rule.role}`)},
3126
+ hint = 'last-holder';
3127
+ end if;`);
3128
+ if (rule.max !== void 0) lines.push(` if v_count > ${String(rule.max)} then
3129
+ raise exception using
3130
+ errcode = '23514',
3131
+ message = ${quoteLiteral(`permdock: ${scope} `)} || v_id || ${quoteLiteral(` has at most ${String(rule.max)} ${rule.role}`)},
3132
+ hint = 'max-holders';
3133
+ end if;`);
3134
+ return lines.join("\n");
3135
+ });
3136
+ return `-- ${scope}: holder counts (min / max), checked at commit
3137
+ create or replace function ${fn}()
3138
+ returns trigger
3139
+ language plpgsql
3140
+ security definer
3141
+ set search_path = ''
3142
+ as $$
3143
+ declare
3144
+ v_ids text[] := '{}';
3145
+ v_id text;
3146
+ v_key ${quoteQualified(table.table)}.${col}%type;
3147
+ v_total bigint;
3148
+ v_count bigint;
3149
+ begin
3150
+ if tg_op in ('UPDATE', 'DELETE') then
3151
+ v_ids := v_ids || old.${col}::text;
3152
+ end if;
3153
+ if tg_op in ('INSERT', 'UPDATE') then
3154
+ v_ids := v_ids || new.${col}::text;
3155
+ end if;
3156
+ foreach v_id in array v_ids loop
3157
+ continue when v_id is null;
3158
+ v_key := v_id;
3159
+ ${checks.join("\n")}
3160
+ end loop;
3161
+ return null;
3162
+ end;
3163
+ $$;
3164
+ drop trigger if exists ${objectName(OWNERSHIP.holders, scope)} on ${quoteQualified(table.table)};
3165
+ create constraint trigger ${objectName(OWNERSHIP.holders, scope)}
3166
+ after insert or update or delete on ${quoteQualified(table.table)}
3167
+ deferrable initially deferred
3168
+ for each row execute function ${fn}();`;
3169
+ }
3170
+ /**
3171
+ * `transferOnly` as statement triggers over transition tables: a statement
3172
+ * that changes a role's holder count in an instance that had holders before
3173
+ * and still has some after is refused. Creating the first holder and removing
3174
+ * the last (which `min` guards) are not transfers.
3175
+ */
3176
+ function transferTriggerSql(ctx, scope, table, column, roles) {
3177
+ const fn = qualified(ctx, `${OWNERSHIP.transferOnly}_${scope}`);
3178
+ const col = quoteIdent(column);
3179
+ const list = `array[${roles.map(quoteLiteral).join(", ")}]::text[]`;
3180
+ const rows = (alias, source, delta) => {
3181
+ const role = memberRoleOf(table, alias);
3182
+ return `select ${alias}.${col} as id, ${role.sql} as role, ${delta} as delta from ${source} ${alias}${role.join} where ${role.sql} = any(${list})`;
3183
+ };
3184
+ const loop = (source) => ` for v_change in
3185
+ select c.id, c.role, sum(c.delta) as delta
3186
+ from (${source}) c
3187
+ group by c.id, c.role
3188
+ loop
3189
+ continue when v_change.delta = 0 or v_change.id is null;
3190
+ ${holdersSql(ctx, table, column, "v_change.id", { expr: "v_change.role" }, "v_after")};
3191
+ if v_after > 0 and v_after - v_change.delta > 0 then
3192
+ raise exception using
3193
+ errcode = '23514',
3194
+ message = 'permdock: ' || v_change.role || ${quoteLiteral(` in ${scope} `)} || v_change.id || ' moves only by transfer',
3195
+ hint = 'transfer-only';
3196
+ end if;
3197
+ end loop;`;
3198
+ const inserted = rows("n", "permdock_new", "1");
3199
+ const deleted = rows("o", "permdock_old", "-1");
3200
+ const target = quoteQualified(table.table);
3201
+ const trigger = (op) => {
3202
+ const name = quoteIdent(`${OWNERSHIP.transferOnly}_${scope}_${op}`);
3203
+ return `drop trigger if exists ${name} on ${target};
3204
+ create trigger ${name}
3205
+ after ${op} on ${target}
3206
+ referencing ${op === "insert" ? "new table as permdock_new" : op === "delete" ? "old table as permdock_old" : "old table as permdock_old new table as permdock_new"}
3207
+ for each statement execute function ${fn}();`;
3208
+ };
3209
+ return `-- ${scope}: transfer-only roles (${roles.join(", ")}) keep their holder count per statement
3210
+ create or replace function ${fn}()
3211
+ returns trigger
3212
+ language plpgsql
3213
+ security definer
3214
+ set search_path = ''
3215
+ as $$
3216
+ declare
3217
+ v_change record;
3218
+ v_after bigint;
3219
+ begin
3220
+ if tg_op = 'INSERT' then
3221
+ ${loop(inserted)}
3222
+ elsif tg_op = 'DELETE' then
3223
+ ${loop(deleted)}
3224
+ else
3225
+ ${loop(`${inserted} union all ${deleted}`)}
3226
+ end if;
3227
+ return null;
3228
+ end;
3229
+ $$;
3230
+ ${trigger("insert")}
3231
+ ${trigger("update")}
3232
+ ${trigger("delete")}`;
3233
+ }
3234
+ //#endregion
3235
+ //#region src/cli/rls/ownership-sources.ts
3236
+ /**
3237
+ * The membership sources holding scope `name`: `undefined` when there are
3238
+ * none, `'not-tables'` when one of them carries no table to put a trigger on.
3239
+ */
3240
+ function holderSources(ctx, name) {
3241
+ const sources = memberForSources(ctx, name);
3242
+ if (sources.length === 0) return;
3243
+ const held = [];
3244
+ for (const source of sources) {
3245
+ if (source.sql.holders === void 0) return "not-tables";
3246
+ held.push({
3247
+ table: source.sql.table,
3248
+ holders: source.sql.holders
3249
+ });
3250
+ }
3251
+ return held;
3252
+ }
3253
+ function indent(sql, by) {
3254
+ return sql.replaceAll(/^/gmu, by);
3255
+ }
3256
+ /** Every source's rows of one instance, each compared with its own typed `v_key_<n>`. */
3257
+ function sourceRowsOf(scope, sources) {
3258
+ return sources.map((source, index) => indent(source.holders.rows(scope, { id: `v_key_${String(index)}` }), " ")).join("\n union all\n");
3259
+ }
3260
+ function sourceKeys(sources) {
3261
+ return {
3262
+ declare: sources.map((source, index) => ` v_key_${String(index)} ${source.holders.idType};`).join("\n"),
3263
+ assign: (value, by) => sources.map((_, index) => `${by}v_key_${String(index)} := ${value};`).join("\n")
3264
+ };
3265
+ }
3266
+ function regclassOf(table) {
3267
+ return `${quoteLiteral(quoteQualified(table))}::regclass`;
3268
+ }
3269
+ function sourceTables(sources) {
3270
+ return [...new Set(sources.map((source) => quoteQualified(source.table)))];
3271
+ }
3272
+ /** `min` and `max` over membership sources: the same commit-time check as on a mapped table, counted over every source of the scope. */
3273
+ function sourceHoldersTriggerSql(ctx, scope, sources, counted) {
3274
+ const fn = qualified(ctx, `${OWNERSHIP.holders}_${scope}`);
3275
+ const keys = sourceKeys(sources);
3276
+ const rows = sourceRowsOf(scope, sources);
3277
+ const collect = sources.map((source) => ` if tg_relid = ${regclassOf(source.table)} then
3278
+ if tg_op in ('UPDATE', 'DELETE') then
3279
+ v_ids := v_ids || ${source.holders.id(scope, "old")};
3280
+ end if;
3281
+ if tg_op in ('INSERT', 'UPDATE') then
3282
+ v_ids := v_ids || ${source.holders.id(scope, "new")};
3283
+ end if;
3284
+ end if;`);
3285
+ const checks = counted.map((rule) => {
3286
+ const filters = ["h.live", `h.role = ${quoteLiteral(rule.role)}`];
3287
+ const kind = roleKindSql(ctx, rule.role, "h.via");
3288
+ if (kind !== void 0) filters.push(kind);
3289
+ const lines = [` select count(distinct h.user_id) into v_count
3290
+ from (
3291
+ ${rows}
3292
+ ) h
3293
+ where ${filters.join("\n and ")};`];
3294
+ if (rule.min > 0) lines.push(` if v_total > 0 and v_count < ${String(rule.min)} then
3295
+ raise exception using
3296
+ errcode = '23514',
3297
+ message = ${quoteLiteral(`permdock: ${scope} `)} || v_id || ${quoteLiteral(` keeps at least ${String(rule.min)} ${rule.role}`)},
3298
+ hint = 'last-holder';
3299
+ end if;`);
3300
+ if (rule.max !== void 0) lines.push(` if v_count > ${String(rule.max)} then
3301
+ raise exception using
3302
+ errcode = '23514',
3303
+ message = ${quoteLiteral(`permdock: ${scope} `)} || v_id || ${quoteLiteral(` has at most ${String(rule.max)} ${rule.role}`)},
3304
+ hint = 'max-holders';
3305
+ end if;`);
3306
+ return lines.join("\n");
3307
+ });
3308
+ const triggers = sourceTables(sources).map((table) => `drop trigger if exists ${objectName(OWNERSHIP.holders, scope)} on ${table};
3309
+ create constraint trigger ${objectName(OWNERSHIP.holders, scope)}
3310
+ after insert or update or delete on ${table}
3311
+ deferrable initially deferred
3312
+ for each row execute function ${fn}();`);
3313
+ return `-- ${scope}: holder counts (min / max) over the membership sources, checked at commit
3314
+ create or replace function ${fn}()
3315
+ returns trigger
3316
+ language plpgsql
3317
+ security definer
3318
+ set search_path = ''
3319
+ as $$
3320
+ declare
3321
+ v_ids text[] := '{}';
3322
+ v_id text;
3323
+ ${keys.declare}
3324
+ v_total bigint;
3325
+ v_count bigint;
3326
+ begin
3327
+ ${collect.join("\n")}
3328
+ foreach v_id in array v_ids loop
3329
+ continue when v_id is null;
3330
+ ${keys.assign("v_id", " ")}
3331
+ select count(*) into v_total
3332
+ from (
3333
+ ${rows}
3334
+ ) h;
3335
+ ${checks.join("\n")}
3336
+ end loop;
3337
+ return null;
3338
+ end;
3339
+ $$;
3340
+ ${triggers.join("\n")}`;
3341
+ }
3342
+ function collectChangesSql(changed) {
3343
+ return ` v_changes := v_changes || coalesce((select jsonb_agg(to_jsonb(c)) from (${changed}) c), '[]'::jsonb);`;
3344
+ }
3345
+ /** `transferOnly` over membership sources: each source table's statement triggers diff its transition tables, and the count after reads every source. */
3346
+ function sourceTransferTriggerSql(ctx, scope, sources, roles) {
3347
+ const fn = qualified(ctx, `${OWNERSHIP.transferOnly}_${scope}`);
3348
+ const keys = sourceKeys(sources);
3349
+ const list = `array[${roles.map(quoteLiteral).join(", ")}]::text[]`;
3350
+ const kind = kindFilterSql(ctx, "h.role", "h.via");
3351
+ const changes = (source, from, delta) => `select h.id, h.role, ${delta} as delta from (${source.holders.rows(scope, { from }).replaceAll("\n", " ")}) h where h.role = any(${list})`;
3352
+ const branches = sources.map((source) => ` if tg_relid = ${regclassOf(source.table)} then
3353
+ if tg_op in ('INSERT', 'UPDATE') then
3354
+ ${collectChangesSql(changes(source, "permdock_new", "1"))}
3355
+ end if;
3356
+ if tg_op in ('UPDATE', 'DELETE') then
3357
+ ${collectChangesSql(changes(source, "permdock_old", "-1"))}
3358
+ end if;
3359
+ end if;`);
3360
+ const triggers = sourceTables(sources).flatMap((table) => [
3361
+ "insert",
3362
+ "update",
3363
+ "delete"
3364
+ ].map((op) => {
3365
+ const name = quoteIdent(`${OWNERSHIP.transferOnly}_${scope}_${op}`);
3366
+ return `drop trigger if exists ${name} on ${table};
3367
+ create trigger ${name}
3368
+ after ${op} on ${table}
3369
+ referencing ${op === "insert" ? "new table as permdock_new" : op === "delete" ? "old table as permdock_old" : "old table as permdock_old new table as permdock_new"}
3370
+ for each statement execute function ${fn}();`;
3371
+ }));
3372
+ return `-- ${scope}: transfer-only roles (${roles.join(", ")}) keep their holder count per statement, over the membership sources
3373
+ create or replace function ${fn}()
3374
+ returns trigger
3375
+ language plpgsql
3376
+ security definer
3377
+ set search_path = ''
3378
+ as $$
3379
+ declare
3380
+ v_changes jsonb := '[]'::jsonb;
3381
+ v_change record;
3382
+ v_after bigint;
3383
+ ${keys.declare}
3384
+ begin
3385
+ ${branches.join("\n")}
3386
+ for v_change in
3387
+ select x.id, x.role, sum(x.delta) as delta
3388
+ from jsonb_to_recordset(v_changes) x(id text, role text, delta bigint)
3389
+ group by x.id, x.role
3390
+ loop
3391
+ continue when v_change.delta = 0 or v_change.id is null;
3392
+ ${keys.assign("v_change.id", " ")}
3393
+ select count(distinct h.user_id) into v_after
3394
+ from (
3395
+ ${sourceRowsOf(scope, sources)}
3396
+ ) h
3397
+ where h.live
3398
+ and h.role = v_change.role${kind === void 0 ? "" : `\n and ${kind}`};
3399
+ if v_after > 0 and v_after - v_change.delta > 0 then
3400
+ raise exception using
3401
+ errcode = '23514',
3402
+ message = 'permdock: ' || v_change.role || ${quoteLiteral(` in ${scope} `)} || v_change.id || ' moves only by transfer',
3403
+ hint = 'transfer-only';
3404
+ end if;
3405
+ end loop;
3406
+ return null;
3407
+ end;
3408
+ $$;
3409
+ ${triggers.join("\n")}`;
3410
+ }
3411
+ function pairsSql(pairs) {
3412
+ return `values ${pairs.map((pair) => `(${quoteLiteral(pair.assigner)}, ${quoteLiteral(pair.role)})`).join(", ")}`;
3413
+ }
3414
+ /**
3415
+ * Membership rows of the membership sources for scope `name`, each source's
3416
+ * `select` comparing its user column with a variable `user` names.
3417
+ */
3418
+ function sourceRowsSql(sources, user) {
3419
+ const keep = sources.some((source) => source.sql.keeps);
3420
+ return sources.map((source) => source.sql.select(user(source), keep).replaceAll(/^/gmu, " ")).join("\n union all\n");
3421
+ }
3422
+ //#endregion
3423
+ //#region src/cli/rls/can-assign.ts
3424
+ /** Whether the signed-in user (or `p_user` in the `_for` form) holds, in instance `p_scope_id`, a role whose `assigns` lists `p_role`. */
3425
+ function canAssignSql(base, own, forUser = false) {
3426
+ const ctx = forUser ? {
3427
+ ...base,
3428
+ subjectId: "p_user",
3429
+ tenants: "all"
3430
+ } : base;
3431
+ const fn = qualified(ctx, forUser ? OWNERSHIP.canAssignFor : OWNERSHIP.canAssign);
3432
+ const parts = [];
3433
+ const used = [];
3434
+ const userOf = (source) => {
3435
+ const index = used.includes(source) ? used.indexOf(source) : used.push(source) - 1;
3436
+ return `v_user_${String(index)}`;
3437
+ };
3438
+ const global = own.assigns.filter((pair) => pair.scope === "global");
3439
+ for (const [instance, pairs] of [["p_scope_id is not null", global.filter((pair) => pair.at !== "global")], ["p_scope_id is null", global.filter((pair) => pair.at === "global")]]) {
3440
+ if (pairs.length === 0) continue;
3441
+ if (ctx.authorize === "database") {
3442
+ const ur = globalRoleRows(ctx);
3443
+ const kind = globalKindFilterSql(ctx, ur.roleSql);
3444
+ parts.push(`${instance} and exists (
3445
+ select 1 from ${ur.from}
3446
+ where ${ur.userSql} = ${subjectIdSql(ctx)}
3447
+ and (${ur.roleSql}, p_role) in (${pairsSql(pairs)})${kind === void 0 ? "" : `\n and ${kind}`}
3448
+ )`);
3449
+ } else {
3450
+ const kind = globalKindFilterSql(ctx, "r.role");
3451
+ parts.push(`${instance} and exists (
3452
+ select 1 from ${roleRows(ctx)}
3453
+ where (r.role, p_role) in (${pairsSql(pairs)})${kind === void 0 ? "" : `\n and ${kind}`}
3454
+ )`);
3455
+ }
3456
+ }
3457
+ for (const { name } of ctx.scopes) {
3458
+ const pairs = own.assigns.filter((pair) => pair.scope === name);
3459
+ if (pairs.length === 0) continue;
3460
+ const sources = scopeSources(ctx, name);
3461
+ if (sources.length > 0) {
3462
+ const kind = kindFilterSql(ctx, "r.role", "ms.via");
3463
+ parts.push(`exists (
3464
+ select 1
3465
+ from (
3466
+ ${sourceRowsSql(sources, userOf)}
3467
+ ) ms
3468
+ cross join lateral jsonb_array_elements_text(
3469
+ case jsonb_typeof(ms.roles) when 'array' then ms.roles else '[]'::jsonb end
3470
+ ) r(role)
3471
+ where ms.scope = ${quoteLiteral(name)}
3472
+ and ms.id = p_scope_id
3473
+ and (r.role, p_role) in (${pairsSql(pairs)})${kind === void 0 ? "" : `\n and ${kind}`}${sourceFilters(ctx, name, void 0, void 0, sourcesKeep(sources)).replaceAll("\n and ", "\n and ")}
3474
+ )`);
3475
+ continue;
3476
+ }
3477
+ if (ctx.authorize === "database") {
3478
+ const mapped = scopeTable(ctx, name);
3479
+ if (mapped === void 0) continue;
3480
+ const { table, column } = mapped;
3481
+ const role = memberRoleOf(table, "m", " ");
3482
+ const filters = [
3483
+ `${memberColumn(table.user)} = ${subjectIdSql(ctx)}`,
3484
+ `${memberColumn(column)}::text = p_scope_id`,
3485
+ `(${role.sql}, p_role) in (${pairsSql(pairs)})`
3486
+ ];
3487
+ if (table.expiresAt !== void 0) {
3488
+ const expires = memberColumn(table.expiresAt);
3489
+ filters.push(`(${expires} is null or ${expires} > now())`);
3490
+ }
3491
+ const disabled = disabledColumn(table);
3492
+ if (disabled !== void 0) filters.push(`${disabled} is null`);
3493
+ const kind = kindFilterSql(ctx, role.sql, memberVia(table));
3494
+ if (kind !== void 0) filters.push(kind);
3495
+ parts.push(`exists (
3496
+ select 1 from ${quoteQualified(table.table)} m${role.join}
3497
+ where ${filters.join("\n and ")}
3498
+ )`);
3499
+ } else {
3500
+ const kind = kindFilterSql(ctx, "r.role", "m ->> 'via'");
3501
+ parts.push(`exists (
3502
+ select 1 from ${membershipRows(ctx)}
3503
+ where m ->> 'scope' = ${quoteLiteral(name)}
3504
+ and m ->> 'id' = p_scope_id
3505
+ and (r.role, p_role) in (${pairsSql(pairs)})
3506
+ and case jsonb_typeof(m -> 'expires_at')
3507
+ when 'number' then (m ->> 'expires_at')::numeric > extract(epoch from now())
3508
+ else true
3509
+ end${kind === void 0 ? "" : `\n and ${kind}`}${claimKeptLines(ctx, " ").map((line) => `\n${line}`).join("")}
3510
+ )`);
3511
+ }
3512
+ }
3513
+ const body = parts.length === 0 ? "false" : parts.join("\n or ");
3514
+ const language = used.length === 0 ? `language sql
3515
+ stable
3516
+ security definer
3517
+ set search_path = ''
3518
+ as $$
3519
+ select ${signedIn(ctx)} and (
3520
+ ${body}
3521
+ )
3522
+ $$;` : `language plpgsql
3523
+ stable
3524
+ security definer
3525
+ set search_path = ''
3526
+ as $$
3527
+ declare
3528
+ ${used.map((source, index) => ` v_user_${String(index)} ${source.sql.userType} := ${subjectIdSql(ctx)};`).join("\n")}
3529
+ begin
3530
+ return ${signedIn(ctx)} and (
3531
+ ${body}
3532
+ );
3533
+ end;
3534
+ $$;`;
3535
+ if (forUser) {
3536
+ const user = ctx.dialect === "supabase" ? "uuid" : "text";
3537
+ return `-- who may assign, for a user the caller names: trusted SQL acting later for a stored user; no client role may execute it
3538
+ create or replace function ${fn}(p_user ${user}, p_role text, p_scope_id text)
3539
+ returns boolean
3540
+ ${language}
3541
+ revoke execute on function ${fn}(${user}, text, text) from public, anon, authenticated;`;
3542
+ }
3543
+ return `-- who may assign: a held role whose assigns lists p_role, in instance p_scope_id (its scope or an ancestor's); a null p_scope_id assigns a global role, which only global assigners may
3544
+ create or replace function ${fn}(p_role text, p_scope_id text)
3545
+ returns boolean
3546
+ ${language}
3547
+ revoke execute on function ${fn}(text, text) from public, anon;
3548
+ grant execute on function ${fn}(text, text) to authenticated;`;
3549
+ }
3550
+ /** Whether the helpers keep custom roles in tables, so assigning one is checked against its stored definition. */
3551
+ function assignsCustomRoles(ctx) {
3552
+ return ctx.customRoles !== void 0 && ctx.authorize === "database";
3553
+ }
3554
+ /** Whether `permdock_can_assign_any_for` is written: every function it calls has a `_for` form. */
3555
+ function hasCanAssignAnyFor(ctx) {
3556
+ return ctx.authorize === "database" && (!assignsCustomRoles(ctx) || hasCustomRoleChecksFor(ctx));
3557
+ }
3558
+ /**
3559
+ * `permdock_can_assign_any`: one check for a declared or a custom role, so
3560
+ * SQL that assigns roles calls one function: `permdock_can_assign` for a
3561
+ * declared role, `permdock_can_assign_custom_role` for a custom one. `p_scope`
3562
+ * `'global'` (no tenant, no instance) assigns a platform role.
3563
+ */
3564
+ function canAssignAnySql(ctx, custom, forUser = false) {
3565
+ const tenantType = tenantTypeOf(ctx);
3566
+ const fn = qualified(ctx, forUser ? OWNERSHIP.canAssignAnyFor : OWNERSHIP.canAssignAny);
3567
+ const declaredFn = qualified(ctx, forUser ? OWNERSHIP.canAssignFor : OWNERSHIP.canAssign);
3568
+ const customFn = qualified(ctx, forUser ? ASSIGNMENTS.customFor : ASSIGNMENTS.custom);
3569
+ const userType = ctx.dialect === "supabase" ? "uuid" : "text";
3570
+ const userParam = forUser ? `p_user ${userType}, ` : "";
3571
+ const userArg = forUser ? `${userType}, ` : "";
3572
+ const user = forUser ? "p_user, " : "";
3573
+ const declared = `${declaredFn}(${user}p_role, case when p_scope = 'global' then null else p_scope_id end)`;
3574
+ const body = custom && ctx.customRoles !== void 0 ? `case
3575
+ when p_role = any(${declaredArray(ctx.customRoles.declared)}) then ${declared}
3576
+ when p_scope = 'global' then ${customFn}(${user}null, 'global', null, p_role)
3577
+ else ${customFn}(${user}p_tenant, p_scope, p_scope_id, p_role)
3578
+ end` : declared;
3579
+ const grants = forUser ? `revoke execute on function ${fn}(${userArg}text, ${tenantType}, text, text) from public, anon, authenticated;` : `revoke execute on function ${fn}(text, ${tenantType}, text, text) from public, anon;
3580
+ grant execute on function ${fn}(text, ${tenantType}, text, text) to authenticated;`;
3581
+ return `-- whether ${forUser ? "user p_user" : "the caller"} may assign p_role at instance p_scope_id of p_scope in tenant p_tenant, declared or custom (p_scope 'global' for a platform role)${forUser ? "; no client role may execute it" : ""}
3582
+ create or replace function ${fn}(${userParam}p_role text, p_tenant ${tenantType}, p_scope text, p_scope_id text)
3583
+ returns boolean
3584
+ language sql
3585
+ stable
3586
+ security definer
3587
+ set search_path = ''
3588
+ as $$
3589
+ select coalesce(${body}, false)
3590
+ $$;
3591
+ ${grants}`;
3592
+ }
3593
+ //#endregion
3594
+ //#region src/cli/rls/ownership.ts
3595
+ function roleScope(policy, scopes, name) {
3596
+ const on = policy.rolesByName.get(name)?.on ?? findRole(policy.vocabulary.roles, name)?.on;
3597
+ return typeof on === "string" ? resolveScope(scopes, on) : void 0;
3598
+ }
3599
+ function isGlobalRole(policy, name) {
3600
+ const binding = policy.rolesByName.get(name);
3601
+ if (binding !== void 0) return binding.on === void 0;
3602
+ const leaf = findRole(policy.vocabulary.roles, name);
3603
+ return leaf !== void 0 && leaf.on === void 0;
3604
+ }
3605
+ /** The rules `rls generate` enforces; `undefined` when no role declares one. */
3606
+ function ownershipRules(policy, scopes) {
3607
+ const kinds = {};
3608
+ const assigns = [];
3609
+ const counted = [];
3610
+ for (const binding of policy.roles) {
3611
+ const scope = roleScope(policy, scopes, binding.name);
3612
+ if (binding.for !== void 0) kinds[binding.name] = binding.for;
3613
+ const assigner = scope ?? (binding.on === void 0 ? "global" : void 0);
3614
+ for (const target of binding.assigns ?? []) {
3615
+ const at = roleScope(policy, scopes, target);
3616
+ if (assigner !== void 0 && at !== void 0 && (assigner === "global" || scopeChain(scopes, at).includes(assigner))) assigns.push({
3617
+ assigner: binding.name,
3618
+ scope: assigner,
3619
+ role: target,
3620
+ at
3621
+ });
3622
+ else if (assigner === "global" && isGlobalRole(policy, target)) assigns.push({
3623
+ assigner: binding.name,
3624
+ scope: "global",
3625
+ role: target,
3626
+ at: "global"
3627
+ });
3628
+ }
3629
+ const min = binding.min ?? 0;
3630
+ const transferOnly = binding.transferOnly === true;
3631
+ if (scope !== void 0 && (min > 0 || binding.max !== void 0 || transferOnly)) counted.push(binding.max === void 0 ? {
3632
+ role: binding.name,
3633
+ scope,
3634
+ min,
3635
+ transferOnly
3636
+ } : {
3637
+ role: binding.name,
3638
+ scope,
3639
+ min,
3640
+ max: binding.max,
3641
+ transferOnly
3642
+ });
3643
+ }
3644
+ if (Object.keys(kinds).length === 0 && assigns.length === 0 && counted.length === 0) return;
3645
+ return {
3646
+ kinds,
3647
+ assigns,
3648
+ counted
3649
+ };
3650
+ }
3651
+ /**
3652
+ * The ownership objects: holder-count and transfer-only triggers on each
3653
+ * scope's membership table, and `permdock_can_assign` for the application's
3654
+ * own policies on those tables. Empty when no role declares a rule.
3655
+ */
3656
+ function ownershipSql(ctx) {
3657
+ const own = ctx.ownership;
3658
+ if (own === void 0) return "";
3659
+ const chunks = [];
3660
+ const skipped = ctx.skipOwnershipTriggers;
3661
+ for (const { name } of ctx.scopes) {
3662
+ const counted = own.counted.filter((rule) => rule.scope === name);
3663
+ if (counted.length === 0) continue;
3664
+ if (skipped === "all" || skipped?.includes(name) === true) {
3665
+ chunks.push(`-- ${name}: rls.ownershipTriggers leaves out the triggers, so min, max and transferOnly are checked only by decideRoleChange`);
3666
+ continue;
3667
+ }
3668
+ const transfer = counted.filter((rule) => rule.transferOnly).map((rule) => rule.role);
3669
+ const mapped = scopeTable(ctx, name);
3670
+ if (mapped === void 0) {
3671
+ const sources = holderSources(ctx, name);
3672
+ if (sources === void 0) chunks.push(`-- ${name}: no memberships table configured, so min, max and transferOnly are checked only by decideRoleChange`);
3673
+ else if (sources === "not-tables") chunks.push(`-- ${name}: a membership source is not a table, so min, max and transferOnly are checked only by decideRoleChange`);
3674
+ else {
3675
+ chunks.push(sourceHoldersTriggerSql(ctx, name, sources, counted));
3676
+ if (transfer.length > 0) chunks.push(sourceTransferTriggerSql(ctx, name, sources, transfer));
3677
+ }
3678
+ continue;
3679
+ }
3680
+ chunks.push(holdersTriggerSql(ctx, name, mapped.table, mapped.column, counted));
3681
+ if (transfer.length > 0) chunks.push(transferTriggerSql(ctx, name, mapped.table, mapped.column, transfer));
3682
+ }
3683
+ if (own.assigns.length > 0) {
3684
+ chunks.push(canAssignSql(ctx, own));
3685
+ if (ctx.authorize === "database") chunks.push(canAssignSql(ctx, own, true));
3686
+ const custom = assignsCustomRoles(ctx);
3687
+ if (custom) {
3688
+ chunks.push(canAssignCustomSql(ctx));
3689
+ if (hasCustomRoleChecksFor(ctx)) chunks.push(canAssignCustomSql(ctx, true));
3690
+ }
3691
+ chunks.push(canAssignAnySql(ctx, custom));
3692
+ if (hasCanAssignAnyFor(ctx)) chunks.push(canAssignAnySql(ctx, custom, true));
3693
+ }
3694
+ return chunks.length === 0 ? "" : `${chunks.join("\n\n")}\n`;
3695
+ }
3696
+ //#endregion
3697
+ export { activeUserSql as $, collectGrants as A, hasPermittedUsers as B, byKey as C, apiKeysPlan as D, apiKeyFields as E, CLOSURE as F, memberIdsHelper as G, memberForHelper as H, graphHelper as I, permittedIdsHelper as J, memberRoleOf as K, graphSqlName as L, compileConditionSql as M, contextRefs as N, breakGlassHolder as O, sqlFunctionNames as P, activeRowSql as Q, graphSqlText as R, byCodeUnit as S, apiKeyAllowsCall as T, memberForSources as U, linkHelper as V, memberForTable as W, scopeSources as X, restrictedHelper as Y, scopeTable as Z, capabilityAccessSql as _, guardedTables as a, currentKeyExpr as b, PERMISSION_HELPERS as c, permissionKeysForHelper as d, checkSuspension as et, permittedByPermissionHelper as f, seedSql as g, helpersSql as h, assignmentSql as i, roleNames as j, breakGlassKey as k, checkName as l, accessSql as m, ownershipSql as n, ASSIGNMENTS as o, requiresSql as p, permittedForHelper as q, holderSources as r, OWNERSHIP as s, ownershipRules as t, permissionHelpersSql as u, globalRoleRows as v, API_KEY_ALLOWS as w, rolesTableScope as x, globalRoleSource as y, hasMemberFor as z };