permdock 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (552) hide show
  1. package/README.md +37 -35
  2. package/dist/{openapi-schema--ARjyfQr.js → 2020-C9VN-x8c.js} +5 -132
  3. package/dist/a2a/index.d.ts +8 -6
  4. package/dist/a2a/index.js +21 -31
  5. package/dist/{adapter-context-mlWsVGGg.js → adapter-context-CRhqyzCk.js} +1 -1
  6. package/dist/adoption-scan-B4R5fnMU.js +107 -0
  7. package/dist/agent/index.d.ts +3 -0
  8. package/dist/agent/index.js +1 -0
  9. package/dist/ai-sdk/index.d.ts +18 -23
  10. package/dist/ai-sdk/index.js +38 -21
  11. package/dist/app-data-MuyzVqX8.js +60 -0
  12. package/dist/approval-header-CJACtHkv.js +23 -0
  13. package/dist/approval-prompt-Dg5LeAcT.js +62 -0
  14. package/dist/approval-prompt-Djo_PyZc.d.ts +57 -0
  15. package/dist/approvals/index.d.ts +5 -3
  16. package/dist/approvals/index.js +8 -5
  17. package/dist/approvals-DJ6Yd5z1.js +995 -0
  18. package/dist/{approvers-DT4AVyIg.js → approvers-CqPOLD-o.js} +1 -1
  19. package/dist/arazzo-B6lebmiQ.js +36 -0
  20. package/dist/{arazzo-BAgw9ap6.js → arazzo-BWhNF-k0.js} +24 -17
  21. package/dist/args-IIolxxKA.js +197 -0
  22. package/dist/astro/index.d.ts +43 -0
  23. package/dist/astro/index.js +27 -0
  24. package/dist/audience-Dg8XsRaO.js +55 -0
  25. package/dist/auth-DRLEizbL.js +178 -0
  26. package/dist/authzen/index.d.ts +26 -6
  27. package/dist/authzen/index.js +228 -75
  28. package/dist/automation-BMZPIVUM.d.ts +114 -0
  29. package/dist/automation-DVjOckjV.js +157 -0
  30. package/dist/better-auth/index.d.ts +8 -6
  31. package/dist/better-auth/index.js +11 -23
  32. package/dist/better-supabase/index.d.ts +438 -86
  33. package/dist/better-supabase/index.js +1027 -184
  34. package/dist/bind--9EFVYWF.d.ts +38 -0
  35. package/dist/{bind-CuzGytRf.js → bind-CZzhx-Vj.js} +22 -10
  36. package/dist/body-CH5of8Ne.js +10 -0
  37. package/dist/body-text-QcXok_SI.js +52 -0
  38. package/dist/build-instance-BMnAudHn.js +52 -0
  39. package/dist/{canonical-json-DemiK4Vp.js → canonical-json-BC8_nMTw.js} +6 -6
  40. package/dist/{capability-2kYTMWDv.d.ts → capability-BSslw1QV.d.ts} +44 -25
  41. package/dist/{capability-4qPdO9gk.js → capability-N1K9IpqS.js} +69 -42
  42. package/dist/casbin-Cr7RLtA3.js +30 -0
  43. package/dist/casbin-Dyu2gnUD.js +234 -0
  44. package/dist/casl-64gpT-8x.js +24 -0
  45. package/dist/casl-DjEDhTCP.js +299 -0
  46. package/dist/catalog/index.d.ts +74 -2
  47. package/dist/catalog/index.js +51 -2
  48. package/dist/catalog-BECMPrwc.js +100 -0
  49. package/dist/catalog-DjhMYYCq.js +1 -0
  50. package/dist/catalog-R_A__RsH.js +52 -0
  51. package/dist/{catalog-doc-DdGm82XT.js → catalog-doc-C9wjsfNY.js} +54 -25
  52. package/dist/catalog-fingerprint-CCP0-yQS.js +153 -0
  53. package/dist/chat/index.d.ts +2 -0
  54. package/dist/chat/index.js +224 -0
  55. package/dist/chat-sdk/index.d.ts +87 -0
  56. package/dist/chat-sdk/index.js +197 -0
  57. package/dist/claims-map-7oQ3ue4X.js +13 -0
  58. package/dist/claims-map-Ca-QX8C6.d.ts +32 -0
  59. package/dist/claude-agent/index.d.ts +7 -14
  60. package/dist/claude-agent/index.js +26 -30
  61. package/dist/clerk/index.d.ts +9 -8
  62. package/dist/clerk/index.js +15 -45
  63. package/dist/cli/bin.js +36 -17
  64. package/dist/cli/index.d.ts +20 -6
  65. package/dist/cli/index.js +164 -4
  66. package/dist/cloud/index.d.ts +102 -63
  67. package/dist/cloud/index.js +240 -347
  68. package/dist/cloud-CIoHNevm.js +87 -0
  69. package/dist/codemod-Bmj9dWMH.js +37 -0
  70. package/dist/codemod-Cxvvc70T.js +2084 -0
  71. package/dist/{collect-DSTE3ml9.js → collect-CrZJJnEF.js} +79 -17
  72. package/dist/collect-DloHbDVa.js +55 -0
  73. package/dist/collect-FlXJlhus.js +1 -0
  74. package/dist/command-L8EstFNm.js +6 -0
  75. package/dist/{compact-y6vfu2_f.js → compact-CAf557ZB.js} +11 -1
  76. package/dist/compile/index.d.ts +1 -1
  77. package/dist/compile/index.js +1 -2
  78. package/dist/{compile-Ba4blROR.js → compile-CMqMx_IN.js} +13 -9
  79. package/dist/{compile-BkRyBDBw.d.ts → compile-Ckm2HqBZ.d.ts} +5 -3
  80. package/dist/completion-BqhRBJD8.js +143 -0
  81. package/dist/config-B46rm9sc.js +26 -0
  82. package/dist/{config-BJiM-xAm.js → config-bs_kZeYq.js} +15 -10
  83. package/dist/{config-report-CdCgbRnV.js → config-report-DUgj62Ai.js} +17 -8
  84. package/dist/{connection-CU8MQxRi.js → connection-DjhIJSs2.js} +8 -6
  85. package/dist/constants-DcJABbLW.js +20 -0
  86. package/dist/{context-Bcy85rGi.js → context-bbFHR-0D.js} +16 -5
  87. package/dist/convex/index.d.ts +11 -10
  88. package/dist/convex/index.js +18 -18
  89. package/dist/create-0VZ7Pdiu.d.ts +107 -0
  90. package/dist/create-BDS7wc3y.d.ts +138 -0
  91. package/dist/create-BFfm17UA.d.ts +110 -0
  92. package/dist/create-BWRSXgOF.js +403 -0
  93. package/dist/create-C3b4B5eF.js +130 -0
  94. package/dist/create-DnrmLnLW.js +323 -0
  95. package/dist/{credential-8P81ff4u.js → credential-DM2um3ha.js} +153 -32
  96. package/dist/credentials-DPyHmYz5.js +74 -0
  97. package/dist/deciding-columns-Sq12mEA6.js +123 -0
  98. package/dist/delegation-DaXVdDhM.js +286 -0
  99. package/dist/delegation-input-FTxMK3ji.js +19 -0
  100. package/dist/delivery/index.d.ts +5 -0
  101. package/dist/delivery/index.js +5 -0
  102. package/dist/delivery-DCAgNyCn.js +499 -0
  103. package/dist/delivery-event-xX4obB8F.js +40 -0
  104. package/dist/denied-text-D8jDHgsl.d.ts +19 -0
  105. package/dist/{describe-DMYiCjJV.d.ts → describe-BZUsixiL.d.ts} +11 -2
  106. package/dist/{describe-B_gVZEVi.js → describe-BybMRY7A.js} +10 -5
  107. package/dist/device-BARQ8eYG.js +193 -0
  108. package/dist/diff-CxZo9pTq.js +37 -0
  109. package/dist/{diff-2VaeChzX.js → diff-DCePiN6M.js} +61 -55
  110. package/dist/doctor-CxiwSfqH.js +44 -0
  111. package/dist/doctor-DFjUxf2f.js +2361 -0
  112. package/dist/drizzle/index.d.ts +5 -3
  113. package/dist/drizzle/index.js +6 -14
  114. package/dist/elysia/index.d.ts +16 -13
  115. package/dist/elysia/index.js +14 -13
  116. package/dist/env-DJZrPjRi.js +8 -0
  117. package/dist/errors-BDUwg2qC.js +27 -0
  118. package/dist/{errors-Cz5zn0mW.js → errors-BKy0EfhN.js} +6 -10
  119. package/dist/errors-BWXBMaKK.d.ts +11 -0
  120. package/dist/{errors-DPdqI50v.d.ts → errors-C2PXIv4c.d.ts} +2 -2
  121. package/dist/errors-CWTY6uih.js +143 -0
  122. package/dist/evaluate-_R8QkWaf.js +1732 -0
  123. package/dist/evaluations-16UqqQ6W.d.ts +32 -0
  124. package/dist/{evaluations-tz6D9A2G.js → evaluations-DZ2CbV5k.js} +181 -32
  125. package/dist/eve/index.d.ts +12 -16
  126. package/dist/eve/index.js +23 -29
  127. package/dist/event-time-CsegxXxB.js +23 -0
  128. package/dist/expiry-D4_YKRKD.js +52 -0
  129. package/dist/express/index.d.ts +13 -10
  130. package/dist/express/index.js +14 -12
  131. package/dist/fastify/index.d.ts +26 -13
  132. package/dist/fastify/index.js +17 -36
  133. package/dist/fetch-adapter-g0ypLkID.js +120 -0
  134. package/dist/fetch-adapter-qKJBuRqy.d.ts +31 -0
  135. package/dist/fields-D9tPVUUS.js +203 -0
  136. package/dist/files-C5ZI27eV.js +124 -0
  137. package/dist/{files-DWUN9PzI.js → files-D_JIzUrP.js} +12 -3
  138. package/dist/{fixtures-ht28Cplr.js → fixtures-B7XrLeY7.js} +12 -12
  139. package/dist/flags-C3DktBDY.js +86 -0
  140. package/dist/{from-snapshot-CTLijvdm.js → from-snapshot-D2lZt4Dd.js} +78 -65
  141. package/dist/{generate-CH7ThJ14.js → generate-CNU1pIOo.js} +1 -3
  142. package/dist/grantee-Cn1HyUsI.js +254 -0
  143. package/dist/grantee-conditions-CcXf8hMM.js +271 -0
  144. package/dist/grantee-match-DHArqY82.js +142 -0
  145. package/dist/h3/index.d.ts +37 -0
  146. package/dist/h3/index.js +35 -0
  147. package/dist/headers-BIjfMaoC.js +8 -0
  148. package/dist/headers-CIbDTUTK.d.ts +7 -0
  149. package/dist/{helpers-H_KARDVC.js → helpers-DTcZOzhF.js} +47 -39
  150. package/dist/hono/index.d.ts +12 -9
  151. package/dist/hono/index.js +14 -13
  152. package/dist/{hosted-DJQw46hn.js → hosted-BZIAYvon.js} +64 -15
  153. package/dist/hosted-DBDX0M-P.d.ts +78 -0
  154. package/dist/{http-SO7qGttf.js → http-BBoKefeS.js} +35 -2
  155. package/dist/{http-DPuDaChb.d.ts → http-B_LphE5Y.d.ts} +1 -2
  156. package/dist/http-DXJ37RDA.js +46 -0
  157. package/dist/{http-BIhmLfCc.js → http-ujKm0JXE.js} +3 -26
  158. package/dist/{openapi-import-cmwetbzw.js → import-CNV7I2Dw.js} +30 -26
  159. package/dist/{rls-import-4lLW_pbb.js → import-DMmOG27O.js} +271 -128
  160. package/dist/index-BcHcPyIo.d.ts +110 -0
  161. package/dist/index-CaxrMD1A.d.ts +1 -0
  162. package/dist/index-Chrs5fQ-.d.ts +210 -0
  163. package/dist/index-CxGUswIC.d.ts +431 -0
  164. package/dist/index-D8EovfsP.d.ts +91 -0
  165. package/dist/index-DOVf7RJv.d.ts +900 -0
  166. package/dist/{index-C8z4RjQV.d.ts → index-SGdIQyzc.d.ts} +34 -16
  167. package/dist/index.d.ts +20 -14
  168. package/dist/index.js +33 -23
  169. package/dist/init-DeMYsnYJ.js +96 -0
  170. package/dist/init-GG6Jh8HD.js +95 -0
  171. package/dist/{instance-options-jARdwfPN.d.ts → instance-options-Omfd793B.d.ts} +2 -2
  172. package/dist/instance-snapshot-Cxq4ygFc.js +600 -0
  173. package/dist/instant-BluqDvyS.js +19 -0
  174. package/dist/internals-Dk8A8fX3.js +17 -0
  175. package/dist/{json-schema-BsAflUGW.js → json-schema-B-vAwlde.js} +46 -9
  176. package/dist/json-schema-CN_iLFH2.d.ts +40 -0
  177. package/dist/jwt/index.d.ts +18 -11
  178. package/dist/jwt/index.js +166 -145
  179. package/dist/{kernel-_RRHmnMY.js → kernel-5JatXyND.js} +120 -43
  180. package/dist/kysely/index.d.ts +5 -3
  181. package/dist/kysely/index.js +6 -14
  182. package/dist/{live-options-H2eG8dfJ.js → live-options-BaAFCIiu.js} +16 -3
  183. package/dist/load-BxEtwlBS.js +1 -0
  184. package/dist/{load-DhWEB9P1.js → load-DFS_ry0W.js} +31 -7
  185. package/dist/{load-jose-BL6LH4bL.js → load-jose-DZ4dnjri.js} +1 -4
  186. package/dist/login-Dfe5RlYk.js +68 -0
  187. package/dist/login-imhuwvn3.js +254 -0
  188. package/dist/manifest-C8IzW3Do.js +42 -0
  189. package/dist/manifest-CtKR3GXL.js +370 -0
  190. package/dist/manifest-json-CAGzls-C.js +9 -0
  191. package/dist/{manifest-uDS9y5de.d.ts → manifest-mx5ghSFM.d.ts} +9 -2
  192. package/dist/manifest-schema-C0kK_8V0.js +399 -0
  193. package/dist/{map-claims-Z4QZOiFF.js → map-claims-D3oRmTk0.js} +19 -25
  194. package/dist/{map-error-pgUWNNtG.js → map-error-TCAdw-eW.js} +5 -5
  195. package/dist/{markers-B0hYLvMD.js → markers-C6joDI3o.js} +8 -3
  196. package/dist/mcp/index.d.ts +36 -9
  197. package/dist/mcp/index.js +476 -448
  198. package/dist/{rls-migrate-CN4TlJTO.js → migrate-BShP7Vf4.js} +119 -20
  199. package/dist/nest/index.d.ts +12 -9
  200. package/dist/nest/index.js +20 -20
  201. package/dist/next/index.d.ts +40 -20
  202. package/dist/next/index.js +37 -39
  203. package/dist/next/plugin.d.ts +1 -1
  204. package/dist/next/plugin.js +2 -2
  205. package/dist/node/index.d.ts +10 -10
  206. package/dist/node/index.js +7 -9
  207. package/dist/ocsf-BFp-PZZA.js +126 -0
  208. package/dist/openai/index.d.ts +9 -16
  209. package/dist/openai/index.js +18 -25
  210. package/dist/openapi/index.d.ts +3 -3
  211. package/dist/openapi/index.js +1 -1
  212. package/dist/{openapi-BqRBvAng.js → openapi-B7HZ0f-r.js} +9 -12
  213. package/dist/{openapi-CqqEbHsL.js → openapi-DTaEmTSW.js} +23 -26
  214. package/dist/openapi-ahr8rHu3.js +138 -0
  215. package/dist/operation-findings-DgATDpvh.js +36 -0
  216. package/dist/operations-CxybZErY.d.ts +59 -0
  217. package/dist/orpc/index.d.ts +13 -10
  218. package/dist/orpc/index.js +23 -20
  219. package/dist/otel/index.d.ts +3 -2
  220. package/dist/otel/index.js +53 -27
  221. package/dist/output-Dyw8fyPF.js +88 -0
  222. package/dist/output-files-B3IZJksC.js +1 -0
  223. package/dist/output-files-DhRBBE4n.js +138 -0
  224. package/dist/{rls-fields-CZOMlwHl.js → ownership-BatFgAA4.js} +2393 -2349
  225. package/dist/{package-root-zGkxO3oN.js → package-root-Dk_ETdkp.js} +4 -3
  226. package/dist/{parse-BuyjxhZV.js → parse-Cxq0WxrA.js} +3 -3
  227. package/dist/{parse-local-manifest-BrMozJCo.js → parse-local-manifest-CVSs6J9I.js} +2 -4
  228. package/dist/{parse-local-manifest-JLnaBH0S.d.ts → parse-local-manifest-DgqtruPn.d.ts} +1 -1
  229. package/dist/parse-request-D_tUXG6M.js +26 -0
  230. package/dist/paths-DXSHAMIa.js +42 -0
  231. package/dist/pdp/index.d.ts +7 -3
  232. package/dist/pdp/index.js +47 -29
  233. package/dist/{peer-BOr_kJE1.js → peer-C8swJ2GH.js} +23 -4
  234. package/dist/peer-CRvC7Hg1.js +1 -0
  235. package/dist/permdock-B9JxkBwC.js +868 -0
  236. package/dist/permdock-D7eeLJER.d.ts +502 -0
  237. package/dist/permission-registry-Br4jSNVi.js +385 -0
  238. package/dist/permissions-B-wCP5ZC.js +30 -0
  239. package/dist/{permissions-RJrovw98.js → permissions-CZKiUfFM.js} +4 -3
  240. package/dist/{permissions-CDgvKG5L.d.ts → permissions-oEWfl-kl.d.ts} +1 -2
  241. package/dist/policies-CO1oOymN.js +295 -0
  242. package/dist/{policy-D2W7uv19.d.ts → policy-Czo4n3Hs.d.ts} +2 -2
  243. package/dist/policy-DLZJ5gAp.js +399 -0
  244. package/dist/policy-from-catalog-DzhEc3Y9.js +450 -0
  245. package/dist/policy-module-CTlwMvLI.js +89 -0
  246. package/dist/{policy-IB6yxpC3.d.ts → policy-types-DzwivBgR.d.ts} +939 -1057
  247. package/dist/postgrest-CW0IqC-U.js +566 -0
  248. package/dist/postgrest-NUqZT-RB.d.ts +100 -0
  249. package/dist/powersync-BHfuehf3.js +62 -0
  250. package/dist/powersync-CbzkqL_M.js +1 -0
  251. package/dist/{powersync-Dv1_aWF7.js → powersync-bYUDLOAO.js} +30 -32
  252. package/dist/preset/index.d.ts +146 -0
  253. package/dist/preset/index.js +1 -0
  254. package/dist/preset-DZdU0tne.js +1768 -0
  255. package/dist/principal-source-pHneukrx.js +103 -0
  256. package/dist/prisma/index.d.ts +6 -3
  257. package/dist/prisma/index.js +6 -12
  258. package/dist/{problem-B43ztKIm.js → problem-CTviyH5L.js} +18 -27
  259. package/dist/{problem-details-DX075l9V.d.ts → problem-details-DXlt-vks.d.ts} +1 -1
  260. package/dist/{problem-details-_AMOYz-w.js → problem-details-cn5X30gZ.js} +45 -42
  261. package/dist/problem-types-CJ2325SZ.js +5 -0
  262. package/dist/{prompt-CXYhLfsS.js → prompt-DfZLJLIn.js} +2 -2
  263. package/dist/protected-DEPHc2xV.d.ts +38 -0
  264. package/dist/{protected-utU-SLuy.js → protected-DfjY5mLA.js} +37 -8
  265. package/dist/{provider-CSQd7m92.js → provider-CC-u3bYP.js} +23 -10
  266. package/dist/{provider-DZY281jO.d.ts → provider-DMVt6yYn.d.ts} +1 -1
  267. package/dist/{provider-store-Cfbvo4DE.js → provider-store-B5Yrkhzr.js} +6 -3
  268. package/dist/pull-BgIfno6w.js +256 -0
  269. package/dist/pull-ByOks_Tg.js +1 -0
  270. package/dist/push-CmFVBOeI.js +116 -0
  271. package/dist/pushed-policy-BImMTTIM.d.ts +57 -0
  272. package/dist/pushed-policy-DobP7AHI.js +238 -0
  273. package/dist/react/index.d.ts +8 -8
  274. package/dist/react/index.js +6 -6
  275. package/dist/react/provider-client.d.ts +1 -1
  276. package/dist/react/provider-client.js +1 -2
  277. package/dist/react-native/index.d.ts +24 -63
  278. package/dist/react-native/index.js +45 -27
  279. package/dist/{refs-KYb4ntaz.js → refs-K6eWUJ0H.js} +1 -1
  280. package/dist/relation-walk-3OyDBF0S.js +186 -0
  281. package/dist/relations-BnCWnJGI.js +163 -0
  282. package/dist/renames-CGj7YwcQ.js +175 -0
  283. package/dist/{replay-CYyiLiwO.d.ts → replay-D7iKcLFe.d.ts} +1 -1
  284. package/dist/{replay-BsOyTrdV.js → replay-Du1tJjG7.js} +2 -1
  285. package/dist/request-context-BTRTG7OT.js +71 -0
  286. package/dist/request-context-C2_1lchh.d.ts +5 -0
  287. package/dist/request-schema-CMreVuFM.js +148 -0
  288. package/dist/resend/index.d.ts +126 -0
  289. package/dist/resend/index.js +379 -0
  290. package/dist/resource-relations-D_jw0BFx.js +243 -0
  291. package/dist/retire-CALjIQ7e.js +240 -0
  292. package/dist/{revocations-Cy06JNx2.d.ts → revocations-R07lFBnX.d.ts} +2 -0
  293. package/dist/rls-BA8an5Ap.js +336 -0
  294. package/dist/{rls-DiinlRML.js → rls-j6t43t7n.js} +1066 -1068
  295. package/dist/role-builder-CXbis5-X.js +623 -0
  296. package/dist/{rpc-DL9Sqvb1.js → rpc-aPyNi6tA.js} +18 -8
  297. package/dist/run-CNsApDpd.js +1 -0
  298. package/dist/run-DOfMtc9V.js +723 -0
  299. package/dist/run-ticket-BjhTOi6u.d.ts +124 -0
  300. package/dist/run-ticket-sxFI6LAk.js +337 -0
  301. package/dist/{runtime-CB8gQFvS.js → runtime-D3VTAiCA.js} +1 -1
  302. package/dist/{saas-DhpJjnBA.js → saas-FO-Q8oj0.js} +18 -15
  303. package/dist/scan-BOI5tJ-6.js +29 -0
  304. package/dist/{schema-D3MnDMxa.js → schema-B4v0Qrac.js} +106 -10
  305. package/dist/schema-yLuBWWsh.js +130 -0
  306. package/dist/scim/index.d.ts +2 -2
  307. package/dist/scim/index.js +1 -2
  308. package/dist/{scim-BFGBBAqR.js → scim-ClHtpyV3.js} +213 -87
  309. package/dist/{scopes-CVdPrWN-.js → scopes-CUPSjo9q.js} +20 -7
  310. package/dist/server/index.d.ts +13 -134
  311. package/dist/server/index.js +13 -10
  312. package/dist/server-DqCT4CgZ.js +453 -0
  313. package/dist/{create-B1d519XK.d.ts → server-types-BLd4MnBX.d.ts} +89 -70
  314. package/dist/{sha256-BO0swRXL.js → sha256-D9ihqmvd.js} +8 -1
  315. package/dist/shared-CD11nBbC.js +304 -0
  316. package/dist/{shared-DRu8VFVM.js → shared-DvvFzYXe.js} +8 -9
  317. package/dist/signals-D3NcTKUS.js +32 -0
  318. package/dist/sink-BEZSlMS_.js +957 -0
  319. package/dist/sink-Bj_lDUDq.d.ts +225 -0
  320. package/dist/{skills-Biq5uup3.js → skills-B6lVdD_W.js} +16 -8
  321. package/dist/skills-CTYwkl44.js +51 -0
  322. package/dist/snapshot-CXUWfsNy.js +269 -0
  323. package/dist/{snapshot-cache-D6jLelln.js → snapshot-cache-DZHQ0KF6.js} +11 -5
  324. package/dist/{snapshot-cache-B-LxUahj.d.ts → snapshot-cache-DqK9eDmM.d.ts} +7 -5
  325. package/dist/solid/index.d.ts +24 -25
  326. package/dist/solid/index.js +41 -11
  327. package/dist/solid-start/index.d.ts +52 -0
  328. package/dist/solid-start/index.js +49 -0
  329. package/dist/{sources-BdOYLw9j.d.ts → sources-BV9j0DeZ.d.ts} +11 -13
  330. package/dist/{sources-4p0-4xQm.js → sources-vs5dGOsS.js} +147 -142
  331. package/dist/sql-B5T1IL83.js +55 -0
  332. package/dist/src-DLfUWFAI.js +1851 -0
  333. package/dist/ssf/index.d.ts +4 -4
  334. package/dist/ssf/index.js +440 -106
  335. package/dist/standard-webhooks-6lg8v_is.d.ts +88 -0
  336. package/dist/standard-webhooks-DgFFD-Ga.js +228 -0
  337. package/dist/storage-C2bWdvJH.js +132 -0
  338. package/dist/{store-DMAWwAJV.js → store-4u_hxjWa.js} +42 -55
  339. package/dist/{store-options-ByhZq1jV.js → store-options-BqEakYRa.js} +38 -24
  340. package/dist/{stores-BT2pSCub.js → stores-BbsD4fAR.js} +7 -3
  341. package/dist/stores-ClZUgeoP.d.ts +30 -0
  342. package/dist/{stream-D1RXubw2.d.ts → stream-7-xmnf3U.d.ts} +2 -2
  343. package/dist/{stream-DDpvynSB.js → stream-CvdUS9V1.js} +10 -2
  344. package/dist/style-DiKWnAvn.js +1 -0
  345. package/dist/{style-CTm1AXDT.js → style-Uao76wO7.js} +3 -2
  346. package/dist/subject-BGoM5o8B.js +17 -0
  347. package/dist/{subject-settings-BwBmCdTR.js → subject-settings-CkEwrw3W.js} +23 -5
  348. package/dist/supabase/index.d.ts +23 -106
  349. package/dist/supabase/index.js +5 -4
  350. package/dist/supabase/middleware.d.ts +13 -6
  351. package/dist/supabase/middleware.js +16 -11
  352. package/dist/supabase-9pqI57pW.js +92 -0
  353. package/dist/{supabase-C0qESOa-.js → supabase-Dxlp8A94.js} +124 -247
  354. package/dist/supabase-hook-CXcGpHqq.js +1 -0
  355. package/dist/{supabase-hook-ntETYCQu.js → supabase-hook-DdDQhMlw.js} +1659 -819
  356. package/dist/{supautils-u8jXTGrt.js → supautils-B6HYL9eL.js} +2 -2
  357. package/dist/support-session-C6xJQYut.js +158 -0
  358. package/dist/svelte/Protected.svelte +11 -4
  359. package/dist/svelte/index.d.ts +7 -7
  360. package/dist/svelte/index.js +26 -12
  361. package/dist/svelte/runtime.d.ts +10 -3
  362. package/dist/svelte/runtime.js +4 -3
  363. package/dist/svelte/source.d.ts +7 -7
  364. package/dist/svelte/source.js +5 -5
  365. package/dist/sveltekit/index.d.ts +51 -0
  366. package/dist/sveltekit/index.js +27 -0
  367. package/dist/tanstack-start/index.d.ts +51 -0
  368. package/dist/tanstack-start/index.js +40 -0
  369. package/dist/target-Bw37TFNA.js +51 -0
  370. package/dist/{validity-uOp8UpYn.js → tenancy-DN2QAx9z.js} +6 -42
  371. package/dist/terminal/index.d.ts +11 -9
  372. package/dist/terminal/index.js +36 -316
  373. package/dist/testing/index.d.ts +391 -106
  374. package/dist/testing/index.js +4574 -1286
  375. package/dist/testing/saas/index.d.ts +9 -8
  376. package/dist/testing/saas/index.js +2 -2
  377. package/dist/testing/saas/permissions.d.ts +1 -1
  378. package/dist/testing/saas/permissions.js +1 -2
  379. package/dist/text-BMFRfBqc.js +8 -0
  380. package/dist/{text-diff-BdkkYSbi.js → text-diff-BQcUapOb.js} +1 -1
  381. package/dist/thenable-yD7SabqG.js +31 -0
  382. package/dist/time-DkWjdL98.js +71 -0
  383. package/dist/{token-CIWxJOCh.js → token-BLZcjQgt.js} +15 -6
  384. package/dist/tool-refusal-CMLN5xhn.js +41 -0
  385. package/dist/trpc/index.d.ts +12 -9
  386. package/dist/trpc/index.js +18 -17
  387. package/dist/{types-DbjRN55U.d.ts → types-5Ut_pbKy.d.ts} +141 -13
  388. package/dist/types-BNORmDM8.d.ts +172 -0
  389. package/dist/types-BjV-fqcA.d.ts +57 -0
  390. package/dist/{types-QCuYJh4q.d.ts → types-BqAKcRNW.d.ts} +2 -1
  391. package/dist/types-C8-KNsFf.d.ts +107 -0
  392. package/dist/{types-aqOHTHcp.d.ts → types-CCpc2Dcv.d.ts} +77 -17
  393. package/dist/{types-Dnemwd77.d.ts → types-CZTnPFzM.d.ts} +50 -11
  394. package/dist/{types-C97IskBw.d.ts → types-CdlxsYQh.d.ts} +23 -11
  395. package/dist/{types-DxiCGmmd.d.ts → types-ClZaWzfb.d.ts} +9 -6
  396. package/dist/types-DX0LaKTb.d.ts +157 -0
  397. package/dist/types-DiQjupG2.d.ts +332 -0
  398. package/dist/types-DqzsnlSA.js +67 -0
  399. package/dist/types-_sHy8BQf.d.ts +65 -0
  400. package/dist/types-fbZ7JEXr.d.ts +89 -0
  401. package/dist/types-tIgIeIKJ.d.ts +66 -0
  402. package/dist/unplugin/index.d.ts +1 -1
  403. package/dist/unplugin/index.js +2 -2
  404. package/dist/usage-CHOtwL19.js +1 -0
  405. package/dist/usage-DHtE8j4E.js +32 -0
  406. package/dist/{usage-ItS7nLcl.js → usage-T5mXq98v.js} +56 -13
  407. package/dist/{validation-error-Ba264OaJ.d.ts → validation-error-Bg5Tpluk.d.ts} +1 -1
  408. package/dist/{validation-error-CnjZ4t92.js → validation-error-ZfUHbKvf.js} +1 -1
  409. package/dist/validity-CZ7-y0Cj.js +79 -0
  410. package/dist/{verifier-BxBo6I44.js → verifier-DHuh-okk.js} +68 -21
  411. package/dist/verify-DbCL9bnB.js +1770 -0
  412. package/dist/{version-BvacVTZR.js → version-CEbQ5UeM.js} +4 -3
  413. package/dist/version-DXkYUISx.js +1 -0
  414. package/dist/{views-Soe7bGSC.js → views-DTIIO-oQ.js} +41 -2
  415. package/dist/{permissions-DttnqqxC.d.ts → vocabulary-D6lPdb4Z.d.ts} +120 -29
  416. package/dist/vue/index.d.ts +25 -21
  417. package/dist/vue/index.js +25 -9
  418. package/dist/waiter-BGUtGYoU.js +67 -0
  419. package/dist/waiter-CKy4MQSY.d.ts +36 -0
  420. package/dist/watch-Cb-NcaUw.js +1 -0
  421. package/dist/{watch-DVBJ9cBq.js → watch-CszoTJMq.js} +47 -8
  422. package/dist/webmcp/index.d.ts +2 -2
  423. package/dist/webmcp/index.js +13 -37
  424. package/dist/where-scope-Dv-WTP-w.js +306 -0
  425. package/dist/who-can-D0QoYbEb.js +364 -0
  426. package/dist/wire-delegation-Cv3W2eaM.js +36 -0
  427. package/dist/wire-denial-C7PaVYEM.js +27 -0
  428. package/dist/workflow/index.d.ts +5 -0
  429. package/dist/workflow/index.js +223 -0
  430. package/dist/workflow-sdk/index.d.ts +56 -0
  431. package/dist/workflow-sdk/index.js +78 -0
  432. package/dist/wrap-Bd1Tejur.d.ts +15 -0
  433. package/dist/wrap-gX61EtpB.js +27 -0
  434. package/package.json +140 -41
  435. package/schemas/access-event-v1.json +118 -0
  436. package/schemas/approval-request-v1.json +34 -1
  437. package/schemas/approval-signature-v1.json +94 -0
  438. package/schemas/automation-event-v1.json +113 -0
  439. package/schemas/capability-v1.json +84 -0
  440. package/schemas/catalog-event-v1.json +87 -0
  441. package/schemas/catalog-v1.json +129 -12
  442. package/schemas/cli-commands-v1.json +164 -0
  443. package/schemas/cli-result-v1.json +96 -0
  444. package/schemas/cloud-api-v1.json +5407 -0
  445. package/schemas/connector-v1.json +408 -0
  446. package/schemas/credential-event-v1.json +81 -0
  447. package/schemas/credential-v1.json +134 -0
  448. package/schemas/decision-event-v1.json +288 -0
  449. package/schemas/delivery-event-v1.json +117 -0
  450. package/schemas/directory-event-v1.json +85 -0
  451. package/schemas/evidence-export-v1.json +54 -0
  452. package/schemas/membership-event-v1.json +93 -0
  453. package/schemas/policy-v1.json +68 -0
  454. package/schemas/pushed-policy-v1.json +236 -0
  455. package/schemas/run-v1.json +162 -0
  456. package/schemas/snapshot-v1.json +138 -0
  457. package/schemas/supabase-claims-v1.json +4 -4
  458. package/schemas/supabase-manifest-v1.json +5 -1
  459. package/schemas/support-session-v1.json +111 -0
  460. package/skills/permdock/SKILL.md +16 -12
  461. package/skills/permdock-agents/SKILL.md +40 -23
  462. package/skills/permdock-agents/references/operations-automations-chat.md +221 -0
  463. package/skills/permdock-agents/references/runtimes.md +22 -18
  464. package/skills/permdock-approvals/SKILL.md +29 -13
  465. package/skills/permdock-approvals/references/delivery-and-waiters.md +172 -0
  466. package/skills/permdock-approvals/references/stores-and-resume.md +21 -7
  467. package/skills/permdock-audit/SKILL.md +3 -3
  468. package/skills/permdock-credentials/SKILL.md +5 -5
  469. package/skills/permdock-credentials/references/tokens-keys-links.md +19 -10
  470. package/skills/permdock-data/SKILL.md +6 -6
  471. package/skills/permdock-data/references/queries-and-rls.md +13 -8
  472. package/skills/permdock-data/references/relationships.md +3 -3
  473. package/skills/permdock-tenancy/SKILL.md +20 -7
  474. package/skills/permdock-tenancy/references/contacts-and-support-sessions.md +122 -0
  475. package/skills/permdock-tenancy/references/ownership-and-custom-roles.md +6 -4
  476. package/skills/permdock-wire/SKILL.md +18 -12
  477. package/skills/permdock-wire/references/adapters.md +146 -50
  478. package/skills/permdock-wire/references/policy-options.md +15 -6
  479. package/skills/permdock-wire/references/presets-and-recipes.md +58 -0
  480. package/dist/approvals-DbYAuNkG.js +0 -224
  481. package/dist/arazzo-C9chp3ST.js +0 -54
  482. package/dist/bind-C_ob3LKa.d.ts +0 -25
  483. package/dist/catalog-CRW5Mx_B.js +0 -2
  484. package/dist/catalog-CYj-mX2O.js +0 -48
  485. package/dist/catalog-DWF5cYqB.js +0 -55
  486. package/dist/cloud-CfwZ39Rl.js +0 -50
  487. package/dist/cloud-DWLXO2af.js +0 -118
  488. package/dist/collect-CHB22Sx4.js +0 -83
  489. package/dist/collect-DSWOk19R.js +0 -2
  490. package/dist/commands-BeaGNj_o.js +0 -97
  491. package/dist/config-ir40y8m0.js +0 -36
  492. package/dist/create-CMwIl-w2.js +0 -253
  493. package/dist/delegation-D8pUEuce.js +0 -103
  494. package/dist/diff-C9RDjjhs.js +0 -48
  495. package/dist/doctor-Djh6JDEk.js +0 -53
  496. package/dist/doctor-DvH0IQff.js +0 -1194
  497. package/dist/doctor-collect-CjdO0wQX.js +0 -665
  498. package/dist/duration-xK2jGFVc.js +0 -25
  499. package/dist/errors-CWfoNg9N.js +0 -47
  500. package/dist/errors-DRnGZHzF.js +0 -30
  501. package/dist/evaluation-items-CfCCejQd.js +0 -40
  502. package/dist/expiry-BoBJ8Tvc.js +0 -7
  503. package/dist/fields-BMl1RxoX.js +0 -26
  504. package/dist/grantee-Cv5Sogfv.js +0 -602
  505. package/dist/headers-BZSDieNT.js +0 -6
  506. package/dist/headers-Cf-zWvsO.d.ts +0 -6
  507. package/dist/ids-DC4V4CFu.js +0 -18
  508. package/dist/index-0nNqHspG.d.ts +0 -6
  509. package/dist/index-BNc7Wer_.d.ts +0 -483
  510. package/dist/index-C7Tw7MaW.d.ts +0 -201
  511. package/dist/instance-options-Dhhcnm3y.js +0 -22
  512. package/dist/load-DwJUafYN.js +0 -2
  513. package/dist/manifest-iaoBXHKc.js +0 -783
  514. package/dist/map-AcxK9o_D.js +0 -147
  515. package/dist/may-use-DndQ5tQ-.js +0 -77
  516. package/dist/openapi-CQTsgKoB.js +0 -164
  517. package/dist/permdock-CbgvLnuo.js +0 -3479
  518. package/dist/permissions-DVoF6dMt.js +0 -662
  519. package/dist/policy-xyWg7QdR.js +0 -1122
  520. package/dist/powersync-CrBX8hY6.js +0 -2
  521. package/dist/powersync-DE19b9dT.js +0 -62
  522. package/dist/protected-BdnIEFkY.d.ts +0 -27
  523. package/dist/protected-xAr24FEC.d.ts +0 -40
  524. package/dist/relations-cjOL01hZ.js +0 -505
  525. package/dist/rls-P-BeBx0n.js +0 -238
  526. package/dist/rls-graph-CHwx0V8q.js +0 -435
  527. package/dist/rls-ownership-DcYlZ09B.js +0 -843
  528. package/dist/rls-shared-Cewd_-zI.js +0 -495
  529. package/dist/rls-verify-CMv9H3wV.js +0 -746
  530. package/dist/run-BWc9qqvE.js +0 -174
  531. package/dist/server-DlZ28Oz5.js +0 -268
  532. package/dist/sink-B1fZzIMf.d.ts +0 -104
  533. package/dist/sink-Bd9PSyfq.js +0 -141
  534. package/dist/skills-C5CbhhpG.js +0 -56
  535. package/dist/snapshot-Enxd0n-s.js +0 -975
  536. package/dist/sql-xdg5f4Fi.js +0 -27
  537. package/dist/src-BDwEZWrZ.js +0 -568
  538. package/dist/store-jOBYpiXC.d.ts +0 -36
  539. package/dist/stores-C0IMQrGI.d.ts +0 -52
  540. package/dist/subject-C6dLxqHU.js +0 -24
  541. package/dist/supabase-config-Bj3iDH2I.js +0 -472
  542. package/dist/supabase-hook-gd3s8bxB.js +0 -2
  543. package/dist/supabase-s4o9s1O1.js +0 -83
  544. package/dist/thenable-okc2xDsj.js +0 -10
  545. package/dist/types-BnIEz1sU.d.ts +0 -48
  546. package/dist/types-Bvc4oWT6.d.ts +0 -9
  547. package/dist/types-CKNSyIf_.d.ts +0 -52
  548. package/dist/types-Cq_H8N_X.d.ts +0 -197
  549. package/dist/usage-B0YuTQRO.js +0 -42
  550. package/dist/usage-DorAJiU_.js +0 -2
  551. package/dist/vocabulary-DA8a8zif.d.ts +0 -56
  552. package/dist/watch-BwxLTYMZ.js +0 -2
@@ -0,0 +1,2361 @@
1
+ import { a as hasConditionOp } from "./refs-K6eWUJ0H.js";
2
+ import { c as listPermissions, h as formerKeys } from "./permission-registry-Br4jSNVi.js";
3
+ import { u as isPrincipalRelation } from "./resource-relations-D_jw0BFx.js";
4
+ import { t as canonicalJson } from "./canonical-json-BC8_nMTw.js";
5
+ import { a as normalizeMemberships, d as scopeList, o as resolveScope } from "./scopes-CUPSjo9q.js";
6
+ import { o as flattenGrantee } from "./grantee-Cn1HyUsI.js";
7
+ import { c as separationConflicts } from "./policy-DLZJ5gAp.js";
8
+ import { x as validateCustomRole } from "./evaluate-_R8QkWaf.js";
9
+ import { s as parseCredential } from "./credential-DM2um3ha.js";
10
+ import { a as describeError } from "./errors-CWTY6uih.js";
11
+ import { t as policyRowConditionKeys } from "./row-conditions-h2HythXQ.js";
12
+ import { a as rel, n as doctorSrcPath, o as sqlFiles, r as listSourceFiles } from "./files-D_JIzUrP.js";
13
+ import { i as DOCTOR_REPORT_SCHEMA } from "./version-CEbQ5UeM.js";
14
+ import { r as catalogOAuthScopes, s as jsonSchemaOf } from "./catalog-doc-C9wjsfNY.js";
15
+ import { a as loadModule, i as loadConfiguredPolicy, n as asPolicy, s as pickNamed } from "./load-DFS_ry0W.js";
16
+ import { r as runCollect } from "./collect-CrZJJnEF.js";
17
+ import { t as catalogPath } from "./catalog-path-CnI0XTFK.js";
18
+ import { n as supabaseTenantClaim } from "./budget-NbEtbJC_.js";
19
+ import { s as quoteSqlLiteral } from "./sql-B5T1IL83.js";
20
+ import { d as keptKeys, r as PERMDOCK_SCHEMA } from "./sources-vs5dGOsS.js";
21
+ import { t as parseCatalog } from "./parse-Cxq0WxrA.js";
22
+ import "./markers-C6joDI3o.js";
23
+ import { t as didYouMean } from "./args-IIolxxKA.js";
24
+ import { t as DOCS_BASE } from "./manifest-CtKR3GXL.js";
25
+ import { t as createStyle } from "./style-Uao76wO7.js";
26
+ import { r as findingOf } from "./output-Dyw8fyPF.js";
27
+ import { i as runSkillsInstall } from "./skills-B6lVdD_W.js";
28
+ import { n as MIGRATION_DIRS, t as DEFAULT_SENSITIVE_ACTIONS } from "./constants-DcJABbLW.js";
29
+ import { l as partPath } from "./output-files-DhRBBE4n.js";
30
+ import { _ as pd045, a as supabaseHookManifest, f as pd039, g as attrsPlan, m as resolveSchemas, n as hookOut, v as readSupabaseConfig, y as supabaseConfig } from "./supabase-hook-DdDQhMlw.js";
31
+ import { i as group, n as membershipColumns, o as sqlStatements, r as tableKey } from "./deciding-columns-Sq12mEA6.js";
32
+ import { I as graphSqlName, M as contextRefs, S as byKey, a as guardedTables, t as ownershipRules } from "./ownership-BatFgAA4.js";
33
+ import { a as pd010, c as pd014, d as pd038, f as pd041, i as pd008, l as pd015, n as pd001, o as pd011, p as pd057, r as pd007, s as pd013, t as runUsage, u as pd036 } from "./usage-T5mXq98v.js";
34
+ import { d as rowConditionMessage, f as commandFor, i as graphPlan, m as tableFor, p as compileGrants, s as ROW_CONDITION_FIX, u as helperTablePolicies } from "./verify-DbCL9bnB.js";
35
+ import { t as FIELD_VIEWS } from "./fields-D9tPVUUS.js";
36
+ import { r as staleFiles, t as powersyncFiles } from "./powersync-bYUDLOAO.js";
37
+ import { n as legacyHelperNames, r as retirePlan, t as legacyHelperCalls } from "./retire-CALjIQ7e.js";
38
+ import { t as supautilsRejections } from "./supautils-B6HYL9eL.js";
39
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
40
+ import { basename, dirname, join, resolve } from "node:path";
41
+ import { createRequire } from "node:module";
42
+ //#region src/cli/shared/doctor-files.ts
43
+ const nodeFiles = Object.freeze({
44
+ exists: (path) => existsSync(path),
45
+ read: (path) => readFileSync(path, "utf8"),
46
+ list: (path) => readdirSync(path),
47
+ isDirectory: (path) => statSync(path, { throwIfNoEntry: false })?.isDirectory() === true
48
+ });
49
+ function filesOf(input) {
50
+ return input.files ?? nodeFiles;
51
+ }
52
+ /** Parses the JSON file at `path`; throws when it is missing or invalid. */
53
+ function readJson(files, path) {
54
+ return JSON.parse(files.read(path));
55
+ }
56
+ //#endregion
57
+ //#region src/cli/project/project.ts
58
+ function loadProject(input) {
59
+ const { cwd, config } = input;
60
+ const files = input.files ?? nodeFiles;
61
+ let sources;
62
+ let policyLoad;
63
+ let collected;
64
+ const loadPolicy = () => {
65
+ policyLoad ??= readPolicy(cwd, config.policy);
66
+ return policyLoad;
67
+ };
68
+ const policy = async () => {
69
+ const load = await loadPolicy();
70
+ return load.status === "loaded" ? load.policy : void 0;
71
+ };
72
+ return {
73
+ cwd,
74
+ config,
75
+ files,
76
+ now: input.now,
77
+ io: input.io,
78
+ sources: () => {
79
+ sources ??= listSourceFiles(cwd, doctorSrcPath(config)).map((file) => ({
80
+ file: rel(cwd, file),
81
+ text: files.read(file)
82
+ }));
83
+ return sources;
84
+ },
85
+ policyLoad: loadPolicy,
86
+ policy,
87
+ collected: () => {
88
+ collected ??= runCollect({
89
+ cwd,
90
+ config,
91
+ collect: config.collect ?? {},
92
+ scanPath: doctorSrcPath(config),
93
+ check: true,
94
+ now: input.now,
95
+ io: input.io,
96
+ loadPolicy: policy
97
+ });
98
+ return collected;
99
+ }
100
+ };
101
+ }
102
+ async function readPolicy(cwd, path) {
103
+ if (path === void 0) return { status: "unset" };
104
+ try {
105
+ return {
106
+ status: "loaded",
107
+ policy: asPolicy(pickNamed(await loadModule(resolve(cwd, path)), ["policy"]))
108
+ };
109
+ } catch (error) {
110
+ return {
111
+ status: "failed",
112
+ path,
113
+ message: describeError(error)
114
+ };
115
+ }
116
+ }
117
+ //#endregion
118
+ //#region src/cli/doctor/inputs.ts
119
+ /** The configured policy, through the project's memoised load when there is one. */
120
+ function policyOf(input) {
121
+ const path = input.config.policy;
122
+ if (path === void 0) return Promise.resolve(void 0);
123
+ return input.policy === void 0 ? loadConfiguredPolicy(input.cwd, path) : input.policy();
124
+ }
125
+ /** `collect --check`, through the project's memoised run when there is one. */
126
+ function collectedOf(input) {
127
+ return input.collected === void 0 ? runCollect({
128
+ cwd: input.cwd,
129
+ config: input.config,
130
+ collect: input.config.collect ?? {},
131
+ scanPath: doctorSrcPath(input.config),
132
+ check: true,
133
+ now: input.now,
134
+ io: input.io
135
+ }) : input.collected();
136
+ }
137
+ function asMembershipsFixture(parsed) {
138
+ return parsed !== null && typeof parsed === "object" ? parsed : {};
139
+ }
140
+ function readFixture(input, path) {
141
+ const files = filesOf(input);
142
+ if (path === void 0) return;
143
+ const absolute = resolve(input.cwd, path);
144
+ if (!files.exists(absolute)) return;
145
+ try {
146
+ return readJson(files, absolute);
147
+ } catch {
148
+ return;
149
+ }
150
+ }
151
+ function listField(value, field) {
152
+ if (value === null || typeof value !== "object") return [];
153
+ const list = value[field];
154
+ return Array.isArray(list) ? list : [];
155
+ }
156
+ /** Reads the doctor memberships fixture, if one is configured and parseable. */
157
+ function membershipsFixture(input) {
158
+ const files = filesOf(input);
159
+ const fixturePath = input.config.doctor?.memberships;
160
+ if (fixturePath === void 0) return {};
161
+ const absolute = resolve(input.cwd, fixturePath);
162
+ if (!files.exists(absolute)) return {};
163
+ try {
164
+ return asMembershipsFixture(readJson(files, absolute));
165
+ } catch {
166
+ return {};
167
+ }
168
+ }
169
+ //#endregion
170
+ //#region src/cli/doctor/assignments.ts
171
+ const TRIGGER = new RegExp(String.raw`^create\s+(?:or\s+replace\s+)?trigger\s+"?permdock_assignment"?\s[\s\S]*?\bon\s+((?:"[^"]+"|\w+)(?:\.(?:"[^"]+"|\w+))?)`, "iu");
172
+ /** The tables a `permdock_assignment` trigger is created on in the migrations and the `rls generate` output. */
173
+ function triggeredTables(input) {
174
+ const out = input.config.rls?.out ?? "rls.sql";
175
+ const files = sqlFiles(input.cwd, [...input.config.doctor?.migrations ?? MIGRATION_DIRS, ...new Set([
176
+ out,
177
+ partPath(out, "helpers"),
178
+ partPath(out, "policies")
179
+ ].filter((file) => file.endsWith(".sql")))]);
180
+ const tables = /* @__PURE__ */ new Set();
181
+ for (const file of files) for (const statement of sqlStatements(filesOf(input).read(file))) {
182
+ const match = TRIGGER.exec(statement.text);
183
+ if (match !== null) tables.add(tableKey(group(match, 1)));
184
+ }
185
+ return tables;
186
+ }
187
+ /**
188
+ * PD064: `rls.assignments` guards a table (a membership table, the
189
+ * global-roles table or a listed table) that no `permdock_assignment`
190
+ * trigger is created on in the migrations or the `rls generate` output, so a
191
+ * client may write any role there.
192
+ */
193
+ async function pd064(input) {
194
+ if (input.config.rls?.assignments === void 0) return [];
195
+ const policy = await policyOf(input);
196
+ if (policy === void 0) return [];
197
+ const scopes = scopeList(policy.scopes);
198
+ if ((ownershipRules(policy, scopes)?.assigns.length ?? 0) === 0) return [];
199
+ const found = triggeredTables(input);
200
+ return guardedTables(input.config, scopes).filter((table) => !found.has(tableKey(table))).map((table) => ({
201
+ code: "PD064",
202
+ severity: "warning",
203
+ message: `rls.assignments guards ${table}, but no migration creates its permdock_assignment trigger, so a client may write any role there`,
204
+ fix: "run permdock rls generate and apply its output in a migration, or set rls.out or doctor.migrations to where it lives"
205
+ }));
206
+ }
207
+ //#endregion
208
+ //#region src/cli/doctor/catalog.ts
209
+ /**
210
+ * A policy that declares `oauthScopes` against a catalog on disk without
211
+ * them, or with another set. `authorizationProvider` builds better-supabase's
212
+ * `delegation.scopes` from the catalog, so its SQL checks deny a token that
213
+ * holds only the coarse scope.
214
+ */
215
+ async function pd070(input) {
216
+ const declared = (await policyOf(input))?.oauthScopes ?? [];
217
+ if (declared.length === 0) return [];
218
+ const files = filesOf(input);
219
+ const path = catalogPath(input.config, input.cwd);
220
+ if (!files.exists(path)) return [];
221
+ let listed;
222
+ try {
223
+ listed = parseCatalog(files.read(path)).oauthScopes;
224
+ } catch {
225
+ return [];
226
+ }
227
+ const expected = catalogOAuthScopes(declared);
228
+ if (listed !== void 0 && canonicalJson(listed) === canonicalJson(expected)) return [];
229
+ return [{
230
+ code: "PD070",
231
+ severity: "warning",
232
+ message: listed === void 0 ? `the policy declares oauthScopes (${expected.map(({ scope }) => scope).join(", ")}) and ${rel(input.cwd, path)} lists none, so better-supabase's SQL checks deny a token that holds only a coarse scope` : `${rel(input.cwd, path)} lists other oauthScopes than the policy declares`,
233
+ fix: "pnpm exec permdock collect"
234
+ }];
235
+ }
236
+ async function pd002(input) {
237
+ const collected = await collectedOf(input);
238
+ if (collected.scan === void 0) return [];
239
+ return collected.scan.unknown.map((usage) => ({
240
+ code: "PD002",
241
+ severity: "error",
242
+ message: `unknown permission ${usage.call} at ${usage.file}:${String(usage.line)}`,
243
+ fix: "use a defined permission reference"
244
+ }));
245
+ }
246
+ async function pd003(input) {
247
+ if (input.config.policy === void 0) return [];
248
+ const usage = await runUsage({
249
+ cwd: input.cwd,
250
+ config: input.config,
251
+ ignore: [],
252
+ strict: false,
253
+ json: true,
254
+ dynamicAsUsed: false,
255
+ now: input.now,
256
+ io: input.io,
257
+ collected: () => collectedOf(input),
258
+ policy: () => policyOf(input)
259
+ });
260
+ if (usage.code === 2) return [];
261
+ return JSON.parse(usage.output).ungranted.map((item) => ({
262
+ code: "PD003",
263
+ severity: "warning",
264
+ message: `${item.key} is used but never granted (${item.detail})`,
265
+ fix: "add an allow() with a to: selector, or a role() binding, in definePolicy"
266
+ }));
267
+ }
268
+ async function pd004(input) {
269
+ const collected = await collectedOf(input);
270
+ if (collected.code === 0) return [];
271
+ if (collected.code === 2) return [];
272
+ return [{
273
+ code: "PD004",
274
+ severity: "error",
275
+ message: collected.message,
276
+ fix: "pnpm exec permdock collect"
277
+ }];
278
+ }
279
+ async function pd067(input) {
280
+ if (input.config.operations === void 0) return [];
281
+ const document = (await collectedOf(input)).document;
282
+ if (document?.operations === void 0) return [];
283
+ const declared = new Set(document.permissions.map(({ key }) => key));
284
+ return document.operations.flatMap((operation) => [["permission", operation.permission], ["results", operation.results]].flatMap(([field, key]) => key === void 0 || declared.has(key) ? [] : [{
285
+ code: "PD067",
286
+ severity: "error",
287
+ message: `operation ${operation.name} has ${field} ${key}, which the permissions module does not declare`,
288
+ fix: "pass the same permission tree to defineOperations and to permdock.config.ts permissions, or declare the permission"
289
+ }]));
290
+ }
291
+ //#endregion
292
+ //#region src/cli/doctor/declarative.ts
293
+ const SCHEMAS = "supabase/schemas";
294
+ const MIGRATIONS = "supabase/migrations";
295
+ const CREATES_HOOK = /\bcreate\s+(?:or\s+replace\s+)?function\s+[\w."]*custom_access_token_hook\b/iu;
296
+ const DROPS_HOOK = /\bdrop\s+function\s+(?:if\s+exists\s+)?[\w."]*custom_access_token_hook\b/iu;
297
+ const GRANTS_HOOK = /\bgrant\s+execute\s+on\s+function\s+[\w."]*custom_access_token_hook\s*\(\s*jsonb\s*\)\s+to\s+supabase_auth_admin\b/iu;
298
+ const DEFINES_HELPER = /\bcreate\s+(?:or\s+replace\s+)?function\s+[\w."]*permdock_has\s*\(/iu;
299
+ const CALLS_HELPER = /\b(?:permdock_has|(?:permitted|member)_[a-z][a-z0-9_]*_ids)\s*\(/iu;
300
+ function uncommented(text) {
301
+ return text.replaceAll(/--[^\n]*/gu, "").replaceAll(/\/\*[\s\S]*?\*\//gu, "");
302
+ }
303
+ /** Whether `text` starts with a line `rls generate` or `supabase hook generate` writes. */
304
+ function generatedByPermDock(text) {
305
+ return text.startsWith("-- permdock:") || text.startsWith("-- generated by permdock");
306
+ }
307
+ /**
308
+ * PD042: under migra, a hook declared under `supabase/schemas` reaches the
309
+ * database through `supabase db diff`, which drops its `supabase_auth_admin`
310
+ * grants. pg-delta keeps them, so the check runs only for `migra`. A function
311
+ * created (or dropped and created again) without them is executable by
312
+ * `public` and not by the auth server, so a migration at or after the one
313
+ * that creates it must carry the grants.
314
+ */
315
+ function pd042(cwd, config, files = nodeFiles) {
316
+ if (config.supabase?.hook === void 0 || resolveSchemas(cwd, config).mode !== "migra") return [];
317
+ if (!sqlFiles(cwd, [SCHEMAS]).some((file) => files.read(file).startsWith(`-- permdock:hook v1 `))) return [];
318
+ const migrations = sqlFiles(cwd, [MIGRATIONS]).map((file) => ({
319
+ file,
320
+ text: uncommented(files.read(file))
321
+ }));
322
+ const created = migrations.findIndex(({ text }) => CREATES_HOOK.test(text));
323
+ const dropped = migrations.findLastIndex(({ text }) => DROPS_HOOK.test(text));
324
+ const needed = Math.max(created, dropped);
325
+ if (needed === -1) return [];
326
+ if (migrations.findLastIndex(({ text }) => GRANTS_HOOK.test(text)) >= needed) return [];
327
+ return [{
328
+ code: "PD042",
329
+ severity: "error",
330
+ message: `${rel(cwd, migrations[needed]?.file ?? MIGRATIONS)} creates custom_access_token_hook from supabase/schemas, and no migration from it on grants it to supabase_auth_admin: supabase db diff drops those grants, so the auth server cannot call the hook and public can`,
331
+ fix: "switch to pg-delta, which keeps the grants ([experimental.pgdelta] enabled = true and no supabase.schemas: 'migra'), or generate the grants with permdock supabase hook generate --grants-out (or rls generate --split ...,hook --grants-out) into a new file from supabase migration new, after the db diff migration"
332
+ }];
333
+ }
334
+ /** The PermDock-generated files under the declarative schema directory. */
335
+ function generatedSchemaFiles(cwd, schemaDir, files) {
336
+ if (!files.exists(join(cwd, schemaDir))) return [];
337
+ return sqlFiles(cwd, [schemaDir]).filter((file) => generatedByPermDock(files.read(file)));
338
+ }
339
+ /**
340
+ * PD068: PermDock generates for pg-delta (the default), but
341
+ * `supabase/config.toml` does not set `[experimental.pgdelta] enabled =
342
+ * true`, so Supabase CLI 2.x diffs the schema files with migra. migra drops
343
+ * the hook's `supabase_auth_admin` grants and the helpers' privileges, and
344
+ * PermDock wrote no `--grants-out` file to restore them.
345
+ */
346
+ function pd068(cwd, config, files = nodeFiles) {
347
+ const read = readSupabaseConfig(cwd);
348
+ if (read?.ok !== true || read.config.pgDelta?.enabled === true) return [];
349
+ const schemas = resolveSchemas(cwd, config);
350
+ if (schemas.mode !== "pg-delta") return [];
351
+ const [first] = generatedSchemaFiles(cwd, schemas.schemaDir, files);
352
+ if (first === void 0) return [];
353
+ return [{
354
+ code: "PD068",
355
+ severity: "error",
356
+ message: `${rel(cwd, first)} is written for pg-delta, but supabase/config.toml does not set [experimental.pgdelta] enabled = true, so the Supabase CLI diffs it with migra and drops the hook and helper grants`,
357
+ fix: "add [experimental.pgdelta] with enabled = true to supabase/config.toml (Supabase CLI 2.119 or later), or set supabase.schemas: 'migra' in permdock.config.ts and regenerate with --grants-out"
358
+ }];
359
+ }
360
+ /**
361
+ * PD069: leftovers of declarative schemas on migra in a pg-delta project:
362
+ * `[db.migrations] schema_paths`, which pg-delta's dependency order makes
363
+ * unnecessary, and numbered `NNN_permdock_<part>.sql` files that duplicate
364
+ * the per-schema layout `rls generate --split` now writes.
365
+ */
366
+ function pd069(cwd, config, files = nodeFiles) {
367
+ const schemas = resolveSchemas(cwd, config);
368
+ if (schemas.mode !== "pg-delta") return [];
369
+ const findings = [];
370
+ if ((supabaseConfig(cwd).schemaPaths ?? []).length > 0) findings.push({
371
+ code: "PD069",
372
+ severity: "warning",
373
+ message: "supabase/config.toml sets [db.migrations] schema_paths, which pg-delta does not need: it orders the schema files by their dependencies",
374
+ fix: "remove schema_paths from [db.migrations] in supabase/config.toml"
375
+ });
376
+ for (const file of generatedSchemaFiles(cwd, schemas.schemaDir, files)) if (/^\d+_permdock_/u.test(basename(file))) findings.push({
377
+ code: "PD069",
378
+ severity: "warning",
379
+ message: `${rel(cwd, file)} is a numbered part from declarative schemas on migra; under pg-delta, rls generate --split writes the per-schema layout instead`,
380
+ fix: "delete the numbered parts, run permdock rls generate --split without --out, then supabase db schema declarative sync"
381
+ });
382
+ return findings;
383
+ }
384
+ /** The `[db.migrations] schema_paths` globs of `supabase/config.toml`, or Supabase's default. */
385
+ function schemaPaths(cwd) {
386
+ const paths = supabaseConfig(cwd).schemaPaths ?? [];
387
+ return paths.length === 0 ? ["./schemas/**/*.sql"] : paths;
388
+ }
389
+ /** Schema files in the order `supabase db diff` applies them: each glob in turn, sorted within it. */
390
+ function orderedSchemaFiles(cwd) {
391
+ return sqlFiles(join(cwd, "supabase"), schemaPaths(cwd), { order: "entry" });
392
+ }
393
+ /**
394
+ * PD043: under migra, `db diff` applies schema files in `schema_paths`
395
+ * order, so a policy that calls `permdock_has` or `permitted_<scope>_ids`
396
+ * before the file that defines them fails to apply. pg-delta orders by
397
+ * dependencies instead.
398
+ */
399
+ function pd043(cwd, config, files = nodeFiles) {
400
+ if (resolveSchemas(cwd, config).mode !== "migra") return [];
401
+ if (!files.exists(join(cwd, SCHEMAS))) return [];
402
+ const schemas = orderedSchemaFiles(cwd).map((file) => ({
403
+ file,
404
+ text: uncommented(files.read(file))
405
+ }));
406
+ const helpers = schemas.findIndex(({ text }) => DEFINES_HELPER.test(text));
407
+ const defined = sqlFiles(cwd, [SCHEMAS]).find((file) => DEFINES_HELPER.test(uncommented(files.read(file))));
408
+ if (helpers === -1) return defined === void 0 ? [] : [{
409
+ code: "PD043",
410
+ severity: "warning",
411
+ message: `${rel(cwd, defined)} defines the PermDock helpers, but schema_paths in supabase/config.toml does not list it, so supabase db diff never applies it`,
412
+ fix: `add it to [db.migrations] schema_paths before every file that calls permdock_has, permitted_<scope>_ids or member_<scope>_ids`
413
+ }];
414
+ const helperFile = rel(cwd, schemas[helpers]?.file ?? "");
415
+ return schemas.slice(0, helpers).filter(({ text }) => CALLS_HELPER.test(text)).map(({ file }) => ({
416
+ code: "PD043",
417
+ severity: "warning",
418
+ message: `${rel(cwd, file)} calls the PermDock helpers, but schema_paths applies it before ${helperFile}, which defines them`,
419
+ fix: `list ${helperFile} earlier in [db.migrations] schema_paths, or give it a lower number, such as 056_permdock_helpers.sql before 060_policies.sql`
420
+ }));
421
+ }
422
+ //#endregion
423
+ //#region src/cli/doctor/fixtures.ts
424
+ async function pd018(input) {
425
+ const files = filesOf(input);
426
+ if (input.config.policy === void 0) return [];
427
+ const policy = await policyOf(input);
428
+ if (policy === void 0) return [];
429
+ const findings = [];
430
+ const declared = new Set(policy.roles.map((item) => item.name));
431
+ for (const binding of policy.roles) for (const name of binding.exclusiveWith ?? []) if (!declared.has(name)) findings.push({
432
+ code: "PD018",
433
+ severity: "error",
434
+ message: `exclusiveWith names undeclared role ${name} on ${binding.name}`,
435
+ fix: "declare the exclusive role with role() or remove it from exclusiveWith"
436
+ });
437
+ const fixturePath = input.config.doctor?.memberships;
438
+ if (fixturePath === void 0) return findings;
439
+ const absolute = resolve(input.cwd, fixturePath);
440
+ if (!files.exists(absolute)) {
441
+ findings.push({
442
+ code: "PD018",
443
+ severity: "warning",
444
+ message: `doctor.memberships fixture ${fixturePath} is missing`,
445
+ fix: "add the JSON fixture or remove doctor.memberships"
446
+ });
447
+ return findings;
448
+ }
449
+ let parsed;
450
+ try {
451
+ parsed = readJson(files, absolute);
452
+ } catch {
453
+ findings.push({
454
+ code: "PD018",
455
+ severity: "warning",
456
+ message: `doctor.memberships fixture ${fixturePath} is not valid JSON`,
457
+ fix: "fix the JSON fixture"
458
+ });
459
+ return findings;
460
+ }
461
+ const record = asMembershipsFixture(parsed);
462
+ for (const custom of record.customRoles ?? []) {
463
+ const conflicts = separationConflicts(policy, [{
464
+ principal: custom.name,
465
+ ...custom.tenant === void 0 ? {} : { tenant: custom.tenant },
466
+ roles: custom.includes ?? []
467
+ }]);
468
+ for (const conflict of conflicts) findings.push({
469
+ code: "PD018",
470
+ severity: "warning",
471
+ message: `custom role ${custom.name} includes exclusive roles ${conflict.roles.join(" and ")}`,
472
+ fix: "split the custom role so it does not include exclusiveWith pairs"
473
+ });
474
+ }
475
+ for (const conflict of separationConflicts(policy, record.memberships ?? [])) findings.push({
476
+ code: "PD018",
477
+ severity: "warning",
478
+ message: `membership ${conflict.principal || "unknown"} holds exclusive roles ${conflict.roles.join(" and ")}`,
479
+ fix: "remove one of the exclusive roles from the membership fixture"
480
+ });
481
+ return findings;
482
+ }
483
+ function customRoleLabel(role) {
484
+ return role.tenant === void 0 ? `global custom role ${role.name}` : `custom role ${role.name} in ${role.tenant}`;
485
+ }
486
+ async function pd023(input) {
487
+ const files = filesOf(input);
488
+ const fixturePath = input.config.doctor?.memberships;
489
+ if (input.config.policy === void 0 || fixturePath === void 0) return [];
490
+ const absolute = resolve(input.cwd, fixturePath);
491
+ if (!files.exists(absolute)) return [];
492
+ let parsed;
493
+ try {
494
+ parsed = readJson(files, absolute);
495
+ } catch {
496
+ return [];
497
+ }
498
+ const policy = await policyOf(input);
499
+ if (policy === void 0) return [];
500
+ const findings = [];
501
+ for (const custom of asMembershipsFixture(parsed).customRoles ?? []) for (const entry of validateCustomRole(policy, custom).dropped) {
502
+ const what = "role" in entry ? `include ${entry.role}` : `permission ${entry.permission}`;
503
+ findings.push({
504
+ code: "PD023",
505
+ severity: "warning",
506
+ message: `${customRoleLabel(custom)} drops ${what} (${entry.reason})`,
507
+ fix: entry.reason === "outside-ceiling" ? "grant it to a declared assignable role, or remove it from the custom role" : entry.reason === "condition-not-allowed" ? "remove the condition; custom-role grants inherit the declared grant condition" : "use a declared permission key or assignable role name"
508
+ });
509
+ }
510
+ return findings;
511
+ }
512
+ /**
513
+ * PD055: a stored custom role names a permission by a key it was renamed
514
+ * from. It still resolves; the alias can only be dropped once storage holds
515
+ * the current key.
516
+ */
517
+ async function pd055(input) {
518
+ const files = filesOf(input);
519
+ const fixturePath = input.config.doctor?.memberships;
520
+ if (input.config.policy === void 0 || fixturePath === void 0) return [];
521
+ const absolute = resolve(input.cwd, fixturePath);
522
+ if (!files.exists(absolute)) return [];
523
+ let parsed;
524
+ try {
525
+ parsed = readJson(files, absolute);
526
+ } catch {
527
+ return [];
528
+ }
529
+ const policy = await policyOf(input);
530
+ if (policy === void 0) return [];
531
+ const table = `${input.config.rls?.schema ?? "permdock"}.custom_role_permissions`;
532
+ const findings = [];
533
+ for (const custom of asMembershipsFixture(parsed).customRoles ?? []) for (const { from, to } of validateCustomRole(policy, custom).renamed) findings.push({
534
+ code: "PD055",
535
+ severity: "warning",
536
+ message: `${customRoleLabel(custom)} stores ${from}, which was renamed to ${to}`,
537
+ fix: `rewrite the stored key before removing the alias: update ${table} set permission = ${quoteSqlLiteral(to)} where permission = ${quoteSqlLiteral(from)};`
538
+ });
539
+ return findings;
540
+ }
541
+ /** Fixture API keys that never expire, credential policies that allow it, and records that are not v1 credentials. */
542
+ function pd029(input) {
543
+ const parsed = readFixture(input, input.config.doctor?.credentials);
544
+ const findings = [];
545
+ for (const [index, record] of listField(parsed, "credentials").entries()) {
546
+ const credential = parseCredential(record);
547
+ if (credential === void 0) {
548
+ findings.push({
549
+ code: "PD029",
550
+ severity: "warning",
551
+ message: `credentials[${String(index)}] is not a valid v1 credential, so it verifies to no subject`,
552
+ fix: "give it v: 1, id, kind, principal, permissions, createdBy and createdAt; a service key also needs tenant and roles"
553
+ });
554
+ continue;
555
+ }
556
+ if (credential.expiresAt === void 0) findings.push({
557
+ code: "PD029",
558
+ severity: "warning",
559
+ message: `API key ${credential.id} never expires`,
560
+ fix: "set expiresAt and rotate the key before it; a leaked key without expiry stays live until someone revokes it"
561
+ });
562
+ }
563
+ const settings = parsed !== null && typeof parsed === "object" ? parsed.settings : void 0;
564
+ const tenants = settings !== null && typeof settings === "object" ? Object.entries(settings) : [];
565
+ for (const [tenant, value] of tenants) if ((value !== null && typeof value === "object" ? value.credentials : void 0)?.allowNoExpiry === true) findings.push({
566
+ code: "PD029",
567
+ severity: "warning",
568
+ message: `tenant ${tenant} allows API keys that never expire`,
569
+ fix: "drop allowNoExpiry and set maxTtl, or keep it only for a tenant that rotates keys on its own schedule"
570
+ });
571
+ return findings;
572
+ }
573
+ async function pd025(input) {
574
+ const files = filesOf(input);
575
+ const fixturePath = input.config.doctor?.memberships;
576
+ if (input.config.policy === void 0 || fixturePath === void 0) return [];
577
+ const absolute = resolve(input.cwd, fixturePath);
578
+ if (!files.exists(absolute)) return [];
579
+ let parsed;
580
+ try {
581
+ parsed = readJson(files, absolute);
582
+ } catch {
583
+ return [];
584
+ }
585
+ const policy = await policyOf(input);
586
+ if (policy === void 0) return [];
587
+ const scopes = scopeList(policy.scopes);
588
+ const names = scopes.map((scope) => scope.name).join(", ");
589
+ const findings = [];
590
+ for (const [index, membership] of (asMembershipsFixture(parsed).memberships ?? []).entries()) {
591
+ if (normalizeMemberships([membership], scopes).length > 0) continue;
592
+ findings.push({
593
+ code: "PD025",
594
+ severity: "warning",
595
+ message: `membership ${membership.principal ?? String(index)} grants nothing: its scope is not one of ${names}, a parent id is missing from within, or it mixes shapes`,
596
+ fix: "use { scope, id, within: { <parent>: id }, roles } with a declared scope, or { on: { resource, id }, roles }"
597
+ });
598
+ }
599
+ return findings;
600
+ }
601
+ //#endregion
602
+ //#region src/cli/doctor/next.ts
603
+ const ROUTE_GLOBS = ["**/api/permdock/route.{ts,tsx,js,jsx,mjs}"];
604
+ const ENDPOINT = /\bendpoint\s*(?::|=)\s*\{?\s*['"`]/u;
605
+ const NATIVE_IMPORT = /\bfrom\s*['"]permdock\/react-native['"]/u;
606
+ const CLIENT_READS = /* @__PURE__ */ new Set(["usePermission", "reference"]);
607
+ /** A grant the snapshot marks `portable: false`, so a client check needs the endpoint. */
608
+ function serverOnly(policy, grant) {
609
+ if (!grant.portable || hasConditionOp(grant.where, "related")) return true;
610
+ const relations = policy.resources.get(grant.permission.resource)?.relations;
611
+ return flattenGrantee(grant.to).some((item) => {
612
+ if (item.kind !== "relation") return false;
613
+ const spec = relations?.[item.relation];
614
+ return item.through !== void 0 || isPrincipalRelation(spec) && spec.period !== void 0;
615
+ });
616
+ }
617
+ function serverOnlyKeys(policy) {
618
+ const keys = /* @__PURE__ */ new Set();
619
+ for (const grant of [...policy.grants, ...policy.roles.flatMap((binding) => binding.grants)]) if (grant.hosted === void 0 && grant.effect === "allow" && serverOnly(policy, grant)) keys.add(grant.permission.key);
620
+ return keys;
621
+ }
622
+ /** An `app/**\/api/permdock/route.ts` that exports `permdockHandler`'s result, or an `endpoint` string in the source. */
623
+ function hasEndpoint(files, cwd, sources) {
624
+ if (listSourceFiles(cwd, ROUTE_GLOBS).filter((file) => /(?:^|[\\/])app[\\/]/u.test(rel(cwd, file))).some((file) => files.read(file).includes("permdockHandler"))) return true;
625
+ return sources.some((file) => ENDPOINT.test(files.read(file)));
626
+ }
627
+ /**
628
+ * PD044: `usePermission` reads a permission whose grant the snapshot cannot
629
+ * answer, and the app has no endpoint to ask, so the client always denies it.
630
+ */
631
+ async function pd044(input) {
632
+ const policy = await policyOf(input);
633
+ if (policy === void 0) return [];
634
+ const keys = serverOnlyKeys(policy);
635
+ if (keys.size === 0) return [];
636
+ const usages = (await collectedOf(input)).scan?.usages ?? {};
637
+ const hooked = [...keys].toSorted().flatMap((key) => (usages[key] ?? []).filter((usage) => usage.call === "usePermission").map((usage) => ({
638
+ key,
639
+ usage
640
+ })));
641
+ if (hooked.length === 0) return [];
642
+ const sources = listSourceFiles(input.cwd, doctorSrcPath(input.config));
643
+ if (hasEndpoint(filesOf(input), input.cwd, sources)) return [];
644
+ return hooked.map(({ key, usage }) => ({
645
+ code: "PD044",
646
+ severity: "warning",
647
+ message: `usePermission reads ${key} at ${usage.file}:${String(usage.line)}, whose grant needs the server (a closure, graph relation or period), and no app/**/api/permdock/route.ts exports permdockHandler and no endpoint is configured: the client always answers denied (server-only)`,
648
+ fix: "add app/api/permdock/route.ts exporting permdockHandler(), pass endpoint to createPermDock, or check it on the server with getPermission"
649
+ }));
650
+ }
651
+ /**
652
+ * PD065: a file that imports `permdock/react-native` reads a permission whose
653
+ * grant the snapshot cannot answer, so the device answers it only online.
654
+ */
655
+ async function pd065(input) {
656
+ const policy = await policyOf(input);
657
+ if (policy === void 0) return [];
658
+ const keys = serverOnlyKeys(policy);
659
+ if (keys.size === 0) return [];
660
+ const usages = (await collectedOf(input)).scan?.usages ?? {};
661
+ const native = /* @__PURE__ */ new Map();
662
+ const isNative = (file) => {
663
+ let hit = native.get(file);
664
+ if (hit === void 0) {
665
+ try {
666
+ hit = NATIVE_IMPORT.test(filesOf(input).read(join(input.cwd, file)));
667
+ } catch {
668
+ hit = false;
669
+ }
670
+ native.set(file, hit);
671
+ }
672
+ return hit;
673
+ };
674
+ return [...keys].toSorted().flatMap((key) => (usages[key] ?? []).filter((usage) => CLIENT_READS.has(usage.call) && isNative(usage.file)).map((usage) => ({
675
+ code: "PD065",
676
+ severity: "warning",
677
+ message: `React Native code reads ${key} at ${usage.file}:${String(usage.line)}, whose grant needs the server (a closure, graph relation or period): offline the device answers denied (server-only)`,
678
+ fix: "express the grant with a portable condition if the screen must work offline, or show the offline state for it"
679
+ })));
680
+ }
681
+ //#endregion
682
+ //#region src/cli/doctor/policy.ts
683
+ async function pd017(input) {
684
+ if (input.config.policy === void 0) return [];
685
+ const policy = await policyOf(input);
686
+ if (policy === void 0) return [];
687
+ const sensitive = new Set(input.config.doctor?.sensitiveActions ?? DEFAULT_SENSITIVE_ACTIONS);
688
+ const denied = new Set(policy.grants.filter((grant) => grant.effect === "deny").map((grant) => grant.permission.key));
689
+ const findings = [];
690
+ const seen = /* @__PURE__ */ new Set();
691
+ for (const grant of policy.grants) {
692
+ if (grant.effect !== "allow") continue;
693
+ if (!sensitive.has(grant.permission.action)) continue;
694
+ if (grant.approval !== void 0) continue;
695
+ if (denied.has(grant.permission.key)) continue;
696
+ if (seen.has(grant.permission.key)) continue;
697
+ seen.add(grant.permission.key);
698
+ findings.push({
699
+ code: "PD017",
700
+ severity: "warning",
701
+ message: `${grant.permission.key} is a sensitive verb without approval`,
702
+ fix: "add approval: { by } on the allow, or a deny on the same leaf"
703
+ });
704
+ }
705
+ return findings;
706
+ }
707
+ async function pd024(input) {
708
+ if (input.config.policy === void 0) return [];
709
+ const policy = await policyOf(input);
710
+ if (policy === void 0) return [];
711
+ const findings = [];
712
+ for (const grant of policy.grants) {
713
+ const approval = grant.approval;
714
+ if (grant.effect !== "allow" || approval === void 0 || approval === "human" || approval.distinct !== false) continue;
715
+ findings.push({
716
+ code: "PD024",
717
+ severity: "warning",
718
+ message: `${grant.permission.key}${grant.role === null ? "" : ` (role ${grant.role})`} sets approval.distinct: false, so the requester can approve their own request`,
719
+ fix: "remove distinct: false unless the requester confirming their own call (an agent asking its user) is the intent"
720
+ });
721
+ }
722
+ return findings;
723
+ }
724
+ async function pd021(input) {
725
+ if (input.config.policy === void 0) return [];
726
+ const policy = await policyOf(input);
727
+ if (policy === void 0 || (policy.hostable ?? []).length === 0) return [];
728
+ const missing = ["PERMDOCK_CLOUD_URL", "PERMDOCK_CLOUD_KEY"].filter((name) => (input.env[name] ?? "") === "");
729
+ if (missing.length === 0) return [];
730
+ return [{
731
+ code: "PD021",
732
+ severity: "warning",
733
+ message: `the policy lists hostable permissions but ${missing.join(" and ")} is not set, so no hosted grant can be published or fetched`,
734
+ fix: "set the variables from the PermDock Cloud dashboard, or remove hostable"
735
+ }];
736
+ }
737
+ async function pd019(input) {
738
+ const rls = input.config.rls;
739
+ if ((rls?.authorize ?? rls?.rbac?.authorize) !== "jwt") return [];
740
+ const expiry = supabaseConfig(input.cwd).jwtExpiry ?? 3600;
741
+ if (expiry <= 3600 || input.config.policy === void 0) return [];
742
+ const policy = await policyOf(input);
743
+ if (policy === void 0) return [];
744
+ const sensitive = new Set(input.config.doctor?.sensitiveActions ?? DEFAULT_SENSITIVE_ACTIONS);
745
+ const keys = [...new Set(policy.grants.filter((grant) => grant.effect === "allow" && sensitive.has(grant.permission.action)).map((grant) => grant.permission.key))];
746
+ if (keys.length === 0) return [];
747
+ return [{
748
+ code: "PD019",
749
+ severity: "warning",
750
+ message: `the RLS helpers and authorize() read roles from the JWT and jwt_expiry is ${String(expiry)}s: a revoked role keeps ${keys.join(", ")} until the token expires`,
751
+ fix: "set rls.authorize: 'database', or lower [auth] jwt_expiry in supabase/config.toml to 3600 or less"
752
+ }];
753
+ }
754
+ /** Fixture memberships the policy's scopes would drop: an undeclared scope, a missing parent id, mixed shapes. */
755
+ /**
756
+ * A scope whose roles can all be removed can end up with nobody able to
757
+ * manage it. Setting `min` on any of its roles (even `min: 0`) records the
758
+ * decision and silences the warning.
759
+ */
760
+ async function pd026(input) {
761
+ if (input.config.policy === void 0) return [];
762
+ const policy = await policyOf(input);
763
+ if (policy === void 0) return [];
764
+ const scopes = scopeList(policy.scopes);
765
+ const findings = [];
766
+ for (const { name } of scopes) {
767
+ const held = policy.roles.filter((binding) => typeof binding.on === "string" && resolveScope(scopes, binding.on) === name);
768
+ if (held.length === 0 || held.some((binding) => binding.min !== void 0)) continue;
769
+ findings.push({
770
+ code: "PD026",
771
+ severity: "warning",
772
+ message: `no role on scope '${name}' keeps a holder: every ${name} can lose its last ${held.map((binding) => binding.name).join(" / ")}`,
773
+ fix: `set min: 1 on the role that manages a ${name} (role(..., { on: '${name}', min: 1 })), or min: 0 to record that none must stay`
774
+ });
775
+ }
776
+ return findings;
777
+ }
778
+ /**
779
+ * PD033: a role activation without a `maxDuration` (an elevation with no
780
+ * ceiling never expires on its own), and an activation role that a fixture
781
+ * membership also holds standing (an eligible-only role must never be held
782
+ * directly).
783
+ */
784
+ async function pd033(input) {
785
+ if (input.config.policy === void 0) return [];
786
+ const policy = await policyOf(input);
787
+ if (policy === void 0) return [];
788
+ const findings = [];
789
+ const activation = /* @__PURE__ */ new Set();
790
+ for (const binding of policy.roles) {
791
+ if (binding.activation === void 0) continue;
792
+ activation.add(binding.name);
793
+ if (binding.activation.maxDuration === void 0) findings.push({
794
+ code: "PD033",
795
+ severity: "warning",
796
+ message: `role '${binding.name}' has activation without maxDuration: an elevation never expires on its own`,
797
+ fix: `set activation: { maxDuration: 14_400, ... } on role '${binding.name}'`
798
+ });
799
+ }
800
+ const fixture = membershipsFixture(input);
801
+ const standing = /* @__PURE__ */ new Set();
802
+ const eligible = /* @__PURE__ */ new Set();
803
+ for (const membership of fixture.memberships ?? []) {
804
+ for (const name of membership.roles ?? []) standing.add(name);
805
+ for (const name of membership.eligible ?? []) eligible.add(name);
806
+ }
807
+ for (const name of activation) if (standing.has(name)) findings.push({
808
+ code: "PD033",
809
+ severity: "warning",
810
+ message: `activation role '${name}' is held standing by a fixture membership: an eligible-only role must never be held directly`,
811
+ fix: `list '${name}' under eligible, never roles, and activate it with permdock.activate`
812
+ });
813
+ return findings;
814
+ }
815
+ /**
816
+ * PD035: a `supportAccess` role without `actorRequired`. Support access is
817
+ * impersonation; without an actor the session is unattributed.
818
+ */
819
+ async function pd035(input) {
820
+ if (input.config.policy === void 0) return [];
821
+ const policy = await policyOf(input);
822
+ if (policy === void 0) return [];
823
+ const findings = [];
824
+ for (const binding of policy.roles) if (binding.support !== void 0 && !binding.support.actorRequired) findings.push({
825
+ code: "PD035",
826
+ severity: "warning",
827
+ message: `support access role '${binding.name}' has actorRequired: false: a support session then runs as the tenant, unattributed`,
828
+ fix: `set actorRequired: true on supportAccess({ role: '${binding.name}', ... })`
829
+ });
830
+ return findings;
831
+ }
832
+ //#endregion
833
+ //#region src/cli/doctor/powersync.ts
834
+ /**
835
+ * PD058: `sync-config.yaml` or the `powersync.manifest` file is not what the
836
+ * policy compiles to, so PowerSync syncs rows, or the device builds
837
+ * snapshots, by an older policy. A policy that does not compile is a finding
838
+ * too.
839
+ */
840
+ async function pd058(input) {
841
+ const { cwd, config } = input;
842
+ if (config.powersync === void 0 || config.policy === void 0) return [];
843
+ const policy = await policyOf(input);
844
+ if (policy === void 0) return [];
845
+ let compiled;
846
+ try {
847
+ compiled = powersyncFiles(policy, config);
848
+ } catch (cause) {
849
+ return [{
850
+ code: "PD058",
851
+ severity: "warning",
852
+ message: `the policy does not compile to Sync Streams: ${cause instanceof Error ? cause.message : String(cause)}`,
853
+ fix: "fix the powersync and rls config, then run permdock powersync generate"
854
+ }];
855
+ }
856
+ return staleFiles(cwd, compiled.files).map((file) => ({
857
+ code: "PD058",
858
+ severity: "warning",
859
+ message: file.missing ? `${file.path} is missing, so ${file.path === config.powersync?.manifest ? "the device builds no local snapshot" : "PowerSync syncs no stream the policy compiles to"}` : `${file.path} is not what the policy compiles to, so ${file.path === config.powersync?.manifest ? "the device builds snapshots" : "PowerSync syncs rows"} by an older policy`,
860
+ fix: "run permdock powersync generate and deploy the files"
861
+ }));
862
+ }
863
+ //#endregion
864
+ //#region src/cli/doctor/project.ts
865
+ /**
866
+ * The directories from `cwd` up to the workspace root: the first one with a
867
+ * `pnpm-workspace.yaml`, a `package.json` that declares `workspaces`, or a
868
+ * `.git`. Skills are often installed once at a monorepo's root.
869
+ */
870
+ function workspaceDirs(files, cwd) {
871
+ const dirs = [];
872
+ let dir = resolve(cwd);
873
+ for (;;) {
874
+ dirs.push(dir);
875
+ if (isWorkspaceRoot(files, dir)) return dirs;
876
+ const parent = dirname(dir);
877
+ if (parent === dir) return [resolve(cwd)];
878
+ dir = parent;
879
+ }
880
+ }
881
+ function isWorkspaceRoot(files, dir) {
882
+ if (files.exists(join(dir, "pnpm-workspace.yaml")) || files.exists(join(dir, ".git"))) return true;
883
+ const manifest = join(dir, "package.json");
884
+ if (!files.exists(manifest)) return false;
885
+ try {
886
+ const parsed = readJson(files, manifest);
887
+ return parsed !== null && typeof parsed === "object" && Object.hasOwn(parsed, "workspaces");
888
+ } catch {
889
+ return false;
890
+ }
891
+ }
892
+ /** Whether `dir` holds a skills lock: PermDock's own, or the `skills` CLI's naming the `permdock` skill. */
893
+ function hasSkillsLock(files, dir) {
894
+ if (files.exists(join(dir, ".permdock/skills-lock.json"))) return true;
895
+ const shared = join(dir, "skills-lock.json");
896
+ if (!files.exists(shared)) return false;
897
+ try {
898
+ const parsed = readJson(files, shared);
899
+ const skills = parsed !== null && typeof parsed === "object" ? Reflect.get(parsed, "skills") : void 0;
900
+ return skills !== null && typeof skills === "object" && Object.hasOwn(skills, "permdock");
901
+ } catch {
902
+ return false;
903
+ }
904
+ }
905
+ function pd005(cwd, files = nodeFiles) {
906
+ const dirs = workspaceDirs(files, cwd);
907
+ if (!dirs.some((dir) => [
908
+ ".agents/skills",
909
+ ".claude/skills",
910
+ ".cursor/skills"
911
+ ].some((folder) => files.exists(join(dir, folder, "permdock/SKILL.md"))))) return [{
912
+ code: "PD005",
913
+ severity: "warning",
914
+ message: "Agent Skills are not installed",
915
+ fix: "pnpm exec permdock skills install"
916
+ }];
917
+ if (!dirs.some((dir) => hasSkillsLock(files, dir))) return [{
918
+ code: "PD005",
919
+ severity: "warning",
920
+ message: "skills lock is missing",
921
+ fix: "pnpm exec permdock skills install"
922
+ }];
923
+ return [];
924
+ }
925
+ function pd006(cwd) {
926
+ try {
927
+ const pkg = createRequire(resolve(cwd, "package.json"))("typescript/package.json");
928
+ const major = Number(pkg.version.split(".")[0]);
929
+ if (major < 5 || major === 5 && Number(pkg.version.split(".")[1]) < 9) return [{
930
+ code: "PD006",
931
+ severity: "error",
932
+ message: `TypeScript ${pkg.version} is below the supported matrix (5.9, 6, 7)`,
933
+ fix: "upgrade typescript to 5.9 or later"
934
+ }];
935
+ if (major > 7) return [{
936
+ code: "PD006",
937
+ severity: "error",
938
+ message: `TypeScript ${pkg.version} is not in the supported matrix (5.9, 6, 7)`,
939
+ fix: "use TypeScript 5.9, 6 or 7"
940
+ }];
941
+ return [];
942
+ } catch {
943
+ return [{
944
+ code: "PD006",
945
+ severity: "error",
946
+ message: "typescript is not installed",
947
+ fix: "add typescript 5.9, 6 or 7"
948
+ }];
949
+ }
950
+ }
951
+ function pd009(cwd, files = nodeFiles) {
952
+ const copies = [];
953
+ function walk(dir, depth) {
954
+ if (depth > 6 || !files.exists(dir)) return;
955
+ const pkg = join(dir, "node_modules/permdock/package.json");
956
+ if (files.exists(pkg)) copies.push(pkg);
957
+ if (!files.exists(join(dir, "node_modules"))) return;
958
+ for (const name of files.list(join(dir, "node_modules"))) {
959
+ if (name.startsWith(".")) continue;
960
+ const nested = join(dir, "node_modules", name);
961
+ try {
962
+ if (files.isDirectory(nested)) walk(nested, depth + 1);
963
+ } catch {}
964
+ }
965
+ }
966
+ walk(cwd, 0);
967
+ if (copies.length > 1) return [{
968
+ code: "PD009",
969
+ severity: "error",
970
+ message: `duplicate permdock copies: ${copies.map((item) => rel(cwd, dirname(item))).join(", ")}`,
971
+ fix: "dedupe so only one permdock version is installed"
972
+ }];
973
+ return [];
974
+ }
975
+ function pd012(cwd, config, files = nodeFiles) {
976
+ const docs = config.openapi?.doc ?? [];
977
+ const findings = [];
978
+ for (const doc of docs) {
979
+ const abs = resolve(cwd, doc);
980
+ if (!files.exists(abs)) continue;
981
+ const text = files.read(abs);
982
+ if (text.includes("\"drafts\"") && !text.includes("overlay")) findings.push({
983
+ code: "PD012",
984
+ severity: "warning",
985
+ message: `${doc} carries a draft pin the CLI no longer emits`,
986
+ fix: "regenerate with permdock openapi"
987
+ });
988
+ }
989
+ return findings;
990
+ }
991
+ const VIEW_NAME = String.raw`((?:"[^"]+"|\w+)(?:\.(?:"[^"]+"|\w+))?)`;
992
+ const CREATE_VIEW = new RegExp(String.raw`\bcreate\s+(?:or\s+replace\s+)?(?:temp(?:orary)?\s+)?(?:recursive\s+)?view\s+${VIEW_NAME}([\s\S]*?)\bas\b`, "giu");
993
+ const ALTER_VIEW = new RegExp(String.raw`\balter\s+view\s+(?:if\s+exists\s+)?${VIEW_NAME}\s+set\s*\(([^)]*)\)`, "giu");
994
+ const INVOKER = /\bsecurity_invoker\s*(?:=\s*(?:true|on|'true'|'on'|1)\b|[,)]|$)/iu;
995
+ const COMPANION = new RegExp(String.raw`\bcomment\s+on\s+view\s+${VIEW_NAME}\s+is\s+'${FIELD_VIEWS.comment}\b`, "giu");
996
+ /**
997
+ * Views run as their owner unless `security_invoker` is set, so they read
998
+ * past RLS on the tables beneath them. The `<table>_visible_fields`
999
+ * companion `rls generate --revoke-columns` writes reads as its owner on
1000
+ * purpose and carries a comment that says so.
1001
+ */
1002
+ function pd022(cwd, config, files = nodeFiles) {
1003
+ if (config.rls === void 0 && config.doctor?.migrations === void 0) return [];
1004
+ const created = /* @__PURE__ */ new Map();
1005
+ const invoker = /* @__PURE__ */ new Set();
1006
+ const companions = /* @__PURE__ */ new Set();
1007
+ for (const file of sqlFiles(cwd, config.doctor?.migrations ?? MIGRATION_DIRS)) {
1008
+ const raw = files.read(file);
1009
+ const text = raw.replaceAll(/--[^\n]*/gu, "").replaceAll(/\/\*[\s\S]*?\*\//gu, "");
1010
+ for (const [, name = "", options = ""] of text.matchAll(CREATE_VIEW)) {
1011
+ const key = tableKey(name);
1012
+ created.set(key, rel(cwd, file));
1013
+ if (INVOKER.test(options)) invoker.add(key);
1014
+ else invoker.delete(key);
1015
+ }
1016
+ for (const [, name = "", options = ""] of text.matchAll(ALTER_VIEW)) if (INVOKER.test(options)) invoker.add(tableKey(name));
1017
+ for (const [, name = ""] of raw.matchAll(COMPANION)) companions.add(tableKey(name));
1018
+ }
1019
+ return [...created].filter(([key]) => !invoker.has(key) && !companions.has(key)).map(([key, file]) => ({
1020
+ code: "PD022",
1021
+ severity: "warning",
1022
+ message: `view ${key} in ${file} is not security_invoker, so it reads past row level security`,
1023
+ fix: `create the view with (security_invoker = true), or alter view ${key} set (security_invoker = true); Postgres 15 or later. For column-level reads, generate field views with permdock rls generate --fields views`
1024
+ }));
1025
+ }
1026
+ const AUTH_ROLE = /\bauth\.role\s*\(\s*\)/giu;
1027
+ /**
1028
+ * PD040: `auth.role()` in a migration. Supabase deprecated it; a policy
1029
+ * names its Postgres roles with `to authenticated` instead, and the role
1030
+ * claim it reads is the one PostgREST already switched to.
1031
+ */
1032
+ function pd040(cwd, config, files = nodeFiles) {
1033
+ const findings = [];
1034
+ for (const file of sqlFiles(cwd, config.doctor?.migrations ?? MIGRATION_DIRS)) {
1035
+ const text = files.read(file).replaceAll(/--[^\n]*/gu, (comment) => " ".repeat(comment.length)).replaceAll(/\/\*[\s\S]*?\*\//gu, (comment) => comment.replaceAll(/[^\n]/gu, " "));
1036
+ for (const match of text.matchAll(AUTH_ROLE)) {
1037
+ const line = text.slice(0, match.index).split("\n").length;
1038
+ findings.push({
1039
+ code: "PD040",
1040
+ severity: "warning",
1041
+ message: `${rel(cwd, file)}:${String(line)} calls auth.role(), which Supabase deprecated`,
1042
+ fix: "name the roles on the policy (create policy ... to authenticated) and drop the auth.role() = 'authenticated' test; call permdock_has or permitted_<scope>_ids for permissions"
1043
+ });
1044
+ }
1045
+ }
1046
+ return findings;
1047
+ }
1048
+ const GRANT = /\b(grant|revoke)\s+([\s\S]+?)\s+on\s+(?:table\s+)?([\w."]+)\s+(?:to|from)\s+([\w\s,"]+?)(?:\s+with\s+grant\s+option|\s+cascade|\s+restrict)?\s*;/giu;
1049
+ const CLIENT_ROLES = /* @__PURE__ */ new Set([
1050
+ "anon",
1051
+ "authenticated",
1052
+ "public"
1053
+ ]);
1054
+ function applyPrivilege(writable, verb, part) {
1055
+ const match = /^\s*(all(?:\s+privileges)?|insert|update)\s*(?:\(([^)]*)\))?\s*$/iu.exec(part);
1056
+ if (match === null) return;
1057
+ const kind = (match[1] ?? "").toLowerCase();
1058
+ const sets = kind === "insert" ? [writable.insert] : kind === "update" ? [writable.update] : [writable.insert, writable.update];
1059
+ const columns = match[2] === void 0 ? ["*"] : match[2].split(",").map((column) => column.trim().replaceAll("\"", ""));
1060
+ for (const set of sets) for (const column of columns) if (verb.toLowerCase() === "grant") set.add(column);
1061
+ else if (column === "*") set.clear();
1062
+ else set.delete(column);
1063
+ }
1064
+ const CREATE_TABLE$1 = /^create\s+(?:(?:global|local)\s+)?(?:(?:temp|temporary|unlogged)\s+)?table\s+(?:if\s+not\s+exists\s+)?((?:"[^"]+"|\w+)(?:\.(?:"[^"]+"|\w+))?)\s*\(/iu;
1065
+ const REVOKE_DEFAULT_TABLES = /^alter\s+default\s+privileges\s+(?:for\s+(?:role|user)\s+[\w\s,"]+?\s+)?in\s+schema\s+"?public"?\s+revoke\s+[\s\S]+?\s+on\s+tables\s+from\s+[\s\S]*\b(?:anon|authenticated)\b/iu;
1066
+ /**
1067
+ * Columns of `table` a client role may insert or update, per the migrations
1068
+ * in order; `*` for every column. On Supabase a table created in `public`
1069
+ * starts writable: the default privileges grant `anon` and `authenticated`
1070
+ * everything, until a migration revokes them.
1071
+ */
1072
+ function clientWritable(files, cwd, config, target) {
1073
+ const writable = {
1074
+ insert: /* @__PURE__ */ new Set(),
1075
+ update: /* @__PURE__ */ new Set()
1076
+ };
1077
+ let defaults = config.supabase !== void 0 && target.startsWith("public.");
1078
+ for (const file of sqlFiles(cwd, config.doctor?.migrations ?? MIGRATION_DIRS)) for (const { text } of sqlStatements(files.read(file))) {
1079
+ if (REVOKE_DEFAULT_TABLES.test(text)) {
1080
+ defaults = false;
1081
+ continue;
1082
+ }
1083
+ const created = CREATE_TABLE$1.exec(text);
1084
+ if (created !== null) {
1085
+ if (defaults && tableKey(created[1] ?? "") === target) {
1086
+ writable.insert.add("*");
1087
+ writable.update.add("*");
1088
+ }
1089
+ continue;
1090
+ }
1091
+ for (const [, verb = "", privileges = "", name = "", roles = ""] of `${text};`.matchAll(GRANT)) {
1092
+ if (tableKey(name) !== target) continue;
1093
+ if (!roles.split(",").map((role) => role.trim().replaceAll("\"", "").toLowerCase()).some((role) => CLIENT_ROLES.has(role))) continue;
1094
+ for (const part of privileges.split(/,(?![^(]*\))/u)) applyPrivilege(writable, verb, part);
1095
+ }
1096
+ }
1097
+ return /* @__PURE__ */ new Set([...writable.insert, ...writable.update]);
1098
+ }
1099
+ /** Membership columns a client can write: a user who sets `user_id` or `role` grants themselves access. */
1100
+ function membershipFindings(files, cwd, config) {
1101
+ const findings = [];
1102
+ for (const [table, columns] of membershipColumns(config)) {
1103
+ const writable = clientWritable(files, cwd, config, table);
1104
+ const exposed = [...columns].filter((column) => writable.has("*") || writable.has(column));
1105
+ if (exposed.length > 0) findings.push({
1106
+ code: "PD028",
1107
+ severity: "warning",
1108
+ message: `${table}.${exposed.join(", ")} decide${exposed.length === 1 ? "s" : ""} memberships, and the migrations let anon or authenticated insert or update ${exposed.length === 1 ? "it" : "them"}: a user could give themselves a membership`,
1109
+ fix: `revoke insert, update on ${table} from anon, authenticated, then grant update (<columns clients edit>) on ${table} to authenticated; a column-level revoke alone leaves a table-level grant in place`
1110
+ });
1111
+ }
1112
+ return findings;
1113
+ }
1114
+ /**
1115
+ * `attrs` claims must come from server-owned columns: never `user_metadata`,
1116
+ * never a column clients can write. Membership sources' deciding columns are
1117
+ * held to the same rule.
1118
+ */
1119
+ function pd028(cwd, config, plan, files = nodeFiles) {
1120
+ const memberships = membershipFindings(files, cwd, config);
1121
+ const attrs = config.supabase?.hook?.attrs;
1122
+ if (attrs === void 0) return memberships;
1123
+ const planned = plan(attrs);
1124
+ const findings = planned.errors.map((message) => ({
1125
+ code: "PD028",
1126
+ severity: "error",
1127
+ message,
1128
+ fix: "list server-owned columns or app_metadata.<key> entries; user_metadata is user-editable"
1129
+ }));
1130
+ if (planned.table !== void 0 && planned.columns.length > 0) {
1131
+ const writable = clientWritable(files, cwd, config, tableKey(planned.table));
1132
+ const exposed = planned.columns.filter((column) => writable.has("*") || writable.has(column));
1133
+ if (exposed.length > 0) findings.push({
1134
+ code: "PD028",
1135
+ severity: "warning",
1136
+ message: `attrs reads ${exposed.join(", ")} from ${tableKey(planned.table)}, which the migrations let anon or authenticated insert or update: a user could set their own attribute`,
1137
+ fix: `revoke insert, update on ${tableKey(planned.table)} from anon, authenticated, and grant column-level update only on columns that are not attributes; the generated hook migration refuses to install otherwise`
1138
+ });
1139
+ }
1140
+ return [...findings, ...memberships];
1141
+ }
1142
+ //#endregion
1143
+ //#region src/cli/doctor/rls.ts
1144
+ async function pd027(input) {
1145
+ if (input.config.rls === void 0 || input.config.policy === void 0) return [];
1146
+ const policy = await policyOf(input);
1147
+ if (policy === void 0) return [];
1148
+ const findings = [];
1149
+ for (const grant of policy.grants) {
1150
+ const refs = [.../* @__PURE__ */ new Set([...contextRefs(grant.where), ...contextRefs(grant.check)])];
1151
+ if (refs.length === 0) continue;
1152
+ findings.push({
1153
+ code: "PD027",
1154
+ severity: "warning",
1155
+ message: `${grant.permission.key}${grant.role === null ? "" : ` (role ${grant.role})`} reads ${refs.join(", ")}: request context is not in the token, so permdock rls generate cannot compile it and the database cannot enforce it`,
1156
+ fix: "move the value to a server-set claim and compare with principal.claims.<name>, or keep the check API-side and pass --skip-closures to rls generate"
1157
+ });
1158
+ }
1159
+ return findings;
1160
+ }
1161
+ async function pd016(input) {
1162
+ const files = filesOf(input);
1163
+ if (input.config.rls === void 0 || input.config.policy === void 0) return [];
1164
+ const policy = await policyOf(input);
1165
+ if (policy === void 0) return [];
1166
+ const findings = [];
1167
+ let opaque = false;
1168
+ let sqlFunction = false;
1169
+ for (const grant of policy.grants) {
1170
+ if (hasConditionOp(grant.where, "opaque") || hasConditionOp(grant.check, "opaque")) opaque = true;
1171
+ if (hasConditionOp(grant.where, "sqlFunction") || hasConditionOp(grant.check, "sqlFunction")) sqlFunction = true;
1172
+ }
1173
+ if (opaque) findings.push({
1174
+ code: "PD016",
1175
+ severity: "warning",
1176
+ message: "policy has opaque RLS conditions that deny in memory",
1177
+ fix: "add rls.functions.<name> with a portable twin, or rewrite as sqlFunction()"
1178
+ });
1179
+ if (sqlFunction) {
1180
+ const fixtures = input.config.rls.fixtures ?? "rls.fixtures.json";
1181
+ if (!files.exists(resolve(input.cwd, fixtures)) && !files.exists(resolve(input.cwd, "rls.fixtures.ts"))) findings.push({
1182
+ code: "PD016",
1183
+ severity: "warning",
1184
+ message: "sqlFunction grants have no rls fixtures for verify --db-url-stdin",
1185
+ fix: "add rls.fixtures.json and run permdock rls verify --db-url-stdin"
1186
+ });
1187
+ }
1188
+ return findings;
1189
+ }
1190
+ async function pd020(input) {
1191
+ if (input.config.rls === void 0 || input.config.policy === void 0) return [];
1192
+ const policy = await policyOf(input);
1193
+ if (policy === void 0) return [];
1194
+ const tables = input.config.rls.tables;
1195
+ const compiled = (policy.hostable ?? []).filter((key) => {
1196
+ const resource = key.slice(0, key.lastIndexOf("."));
1197
+ return tables === void 0 || tables[resource] !== void 0;
1198
+ });
1199
+ if (compiled.length === 0) return [];
1200
+ return [{
1201
+ code: "PD020",
1202
+ severity: "warning",
1203
+ message: `hostable permissions are also compiled into RLS, which never sees hosted grants: ${compiled.join(", ")}`,
1204
+ fix: "remove them from hostable, or drop their tables from rls.tables and enforce them in the application"
1205
+ }];
1206
+ }
1207
+ /** Columns of `resource` a field-limited read grant can hide: missing from an allow's list, or on a deny's. */
1208
+ function limitedColumns(policy, resource, actions) {
1209
+ const reads = policy.grants.filter((grant) => grant.permission.resource === resource && grant.fields !== void 0 && commandFor(grant.permission.action, actions) === "select");
1210
+ const node = policy.resources.get(resource);
1211
+ const schema = node === void 0 ? null : jsonSchemaOf(node);
1212
+ const properties = schema !== null && typeof schema === "object" ? schema.properties : void 0;
1213
+ const columns = properties !== null && typeof properties === "object" ? Object.keys(properties) : [...new Set(reads.flatMap((grant) => grant.fields ?? []))];
1214
+ const key = node?.id ?? "id";
1215
+ return columns.filter((column) => column !== key && reads.some((grant) => grant.effect === "deny" ? (grant.fields ?? []).includes(column) : !(grant.fields ?? []).includes(column)));
1216
+ }
1217
+ /**
1218
+ * `fields` redact in the application; RLS is row-level unless field views
1219
+ * compile, and a field view protects nothing while the base table still
1220
+ * returns the columns to a direct read.
1221
+ */
1222
+ async function pd030(input) {
1223
+ const rls = input.config.rls;
1224
+ if (rls === void 0 || input.config.policy === void 0) return [];
1225
+ const policy = await policyOf(input);
1226
+ if (policy === void 0) return [];
1227
+ const resources = [...new Set(policy.grants.filter((grant) => grant.fields !== void 0 && commandFor(grant.permission.action, rls.actions) === "select").map((grant) => grant.permission.resource))].toSorted();
1228
+ const findings = [];
1229
+ for (const resource of resources) {
1230
+ const columns = limitedColumns(policy, resource, rls.actions);
1231
+ if (columns.length === 0) continue;
1232
+ const table = tableFor(resource, rls.tables);
1233
+ if (rls.fields !== "views") {
1234
+ findings.push({
1235
+ code: "PD030",
1236
+ severity: "warning",
1237
+ message: `read grants on ${resource} limit ${columns.join(", ")}, but RLS on ${table} is row-level: any client that reads ${table} directly gets those columns`,
1238
+ fix: `set rls.fields: 'views' and rls.revokeColumns: true (permdock rls generate --fields views --revoke-columns), or keep direct reads server-side and redact with pick`
1239
+ });
1240
+ continue;
1241
+ }
1242
+ if (rls.revokeColumns !== true) findings.push({
1243
+ code: "PD030",
1244
+ severity: "warning",
1245
+ message: `${table}_visible masks ${columns.join(", ")}, but ${table} still returns them to a direct read`,
1246
+ fix: `set rls.revokeColumns: true (--revoke-columns) so clients read ${columns.join(", ")} only through ${table}_visible`
1247
+ });
1248
+ }
1249
+ return findings;
1250
+ }
1251
+ /**
1252
+ * A self-parented resource no `through` grant walks, or a `restricted`
1253
+ * column no graph grant reaches: the declaration has no effect on any
1254
+ * decision, and `rls generate` keeps no closure for it.
1255
+ */
1256
+ async function pd031(input) {
1257
+ if (input.config.policy === void 0) return [];
1258
+ const policy = await policyOf(input);
1259
+ if (policy === void 0) return [];
1260
+ const plan = graphPlan(policy);
1261
+ const findings = [];
1262
+ for (const node of policy.resources.values()) {
1263
+ const walked = plan.get(node.name)?.closure !== void 0;
1264
+ if (node.parent?.resource === node.name && !walked) findings.push({
1265
+ code: "PD031",
1266
+ severity: "warning",
1267
+ message: `${node.name} parents itself through ${node.parent.field}, but no grant walks it with relation(..., { through: 'parent' }): its ancestors grant nothing and rls generate keeps no closure for it`,
1268
+ fix: `grant through the chain (relation(permissions.${node.name}, '<relation>', { through: 'parent', depth: 16 })) or drop the self-parent`
1269
+ });
1270
+ const reached = [...plan.values()].some((entry) => entry.node.name === node.name || node.parent?.resource === entry.node.name && entry.node.name !== node.name);
1271
+ if (node.restricted !== void 0 && !reached) findings.push({
1272
+ code: "PD031",
1273
+ severity: "warning",
1274
+ message: `${node.name} declares restricted: '${node.restricted}', but no graph grant reaches ${node.name} rows, so the column keeps nothing out`,
1275
+ fix: `add a relation(..., { through: 'parent' }) grant that reaches ${node.name}, or remove restricted`
1276
+ });
1277
+ }
1278
+ return findings;
1279
+ }
1280
+ /**
1281
+ * Under an `rls` config, a graph resource `rls generate` cannot name a helper
1282
+ * for: one that shares a scope's name (its `permitted_<name>_ids` would
1283
+ * replace the scope helper) or is not a lowercase SQL name.
1284
+ */
1285
+ async function pd032(input) {
1286
+ if (input.config.rls === void 0 || input.config.policy === void 0) return [];
1287
+ const policy = await policyOf(input);
1288
+ if (policy === void 0) return [];
1289
+ const scopes = scopeList(policy.scopes);
1290
+ const findings = [];
1291
+ const sqlNames = /* @__PURE__ */ new Map();
1292
+ for (const name of graphPlan(policy).keys()) {
1293
+ const sql = graphSqlName(name);
1294
+ if (sql === void 0) {
1295
+ findings.push({
1296
+ code: "PD032",
1297
+ severity: "error",
1298
+ message: `graph resource ${name} is not a SQL name, so rls generate cannot name its permitted_<name>_ids helper`,
1299
+ fix: "rename the resource to letters, digits and underscores, starting with a letter"
1300
+ });
1301
+ continue;
1302
+ }
1303
+ if (scopes.some((scope) => scope.name === sql)) findings.push({
1304
+ code: "PD032",
1305
+ severity: "error",
1306
+ message: `graph resource ${name} shares its SQL name with the ${sql} scope: permitted_${sql}_ids would replace the scope helper, so rls generate refuses it`,
1307
+ fix: `rename the resource (for example ${sql}s or ${sql}_node) or the scope`
1308
+ });
1309
+ const other = sqlNames.get(sql);
1310
+ if (other !== void 0) findings.push({
1311
+ code: "PD032",
1312
+ severity: "error",
1313
+ message: `graph resources ${other} and ${name} both become permitted_${sql}_ids in SQL, so rls generate refuses them`,
1314
+ fix: "rename one of the resources"
1315
+ });
1316
+ sqlNames.set(sql, name);
1317
+ }
1318
+ return findings;
1319
+ }
1320
+ /**
1321
+ * PD034: a break-glass grant under an `rls` config. RLS never compiles a
1322
+ * break-glass override; the server must read through a `security definer`
1323
+ * function that checks a signed break-glass session and writes an audit row.
1324
+ */
1325
+ async function pd034(input) {
1326
+ if (input.config.policy === void 0 || input.config.rls === void 0) return [];
1327
+ const policy = await policyOf(input);
1328
+ if (policy === void 0) return [];
1329
+ const keys = /* @__PURE__ */ new Set();
1330
+ for (const grant of policy.grants) if (grant.breakGlass !== void 0) keys.add(grant.permission.key);
1331
+ return [...keys].map((key) => ({
1332
+ code: "PD034",
1333
+ severity: "warning",
1334
+ message: `break-glass grant on ${key} cannot be compiled to RLS: it would leak the override into a database policy`,
1335
+ fix: "read break-glass rows through a security definer function that checks a signed break-glass session and writes an audit row"
1336
+ }));
1337
+ }
1338
+ /**
1339
+ * Storage and Realtime policies written outside PermDock that call the SQL helpers for a permission whose grants
1340
+ * carry row conditions: the helpers check only role and scope.
1341
+ */
1342
+ async function pd037(input) {
1343
+ const files = filesOf(input);
1344
+ if (input.config.policy === void 0) return [];
1345
+ const policy = await policyOf(input);
1346
+ if (policy === void 0) return [];
1347
+ const conditioned = policyRowConditionKeys(policy);
1348
+ const findings = [];
1349
+ for (const file of sqlFiles(input.cwd, input.config.doctor?.migrations ?? MIGRATION_DIRS)) for (const found of helperTablePolicies(files.read(file))) {
1350
+ const keys = found.keys.filter((key) => conditioned.has(key));
1351
+ if (keys.length > 0) findings.push({
1352
+ code: "PD037",
1353
+ severity: "error",
1354
+ message: rowConditionMessage(found, keys),
1355
+ fix: ROW_CONDITION_FIX
1356
+ });
1357
+ }
1358
+ return findings;
1359
+ }
1360
+ const WRITE_VERB_PREFIXES = [
1361
+ "create",
1362
+ "update",
1363
+ "delete",
1364
+ "remove",
1365
+ "send",
1366
+ "mark",
1367
+ "cancel",
1368
+ "approve",
1369
+ "receive",
1370
+ "fulfill",
1371
+ "reimburse",
1372
+ "withdraw",
1373
+ "writeOff",
1374
+ "confirm",
1375
+ "pause"
1376
+ ];
1377
+ /** `verb` starts with a write prefix that ends at a word boundary: `markPaid`, not `market`. */
1378
+ function suggestsWrite(verb) {
1379
+ return WRITE_VERB_PREFIXES.some((prefix) => {
1380
+ if (!verb.startsWith(prefix)) return false;
1381
+ const next = verb.charAt(prefix.length);
1382
+ return next === "" || /[A-Z0-9_-]/u.test(next);
1383
+ });
1384
+ }
1385
+ async function pd071(input) {
1386
+ const rls = input.config.rls;
1387
+ if (rls === void 0 || input.config.policy === void 0) return [];
1388
+ const policy = await policyOf(input);
1389
+ if (policy === void 0) return [];
1390
+ const byVerb = /* @__PURE__ */ new Map();
1391
+ for (const leaf of listPermissions(policy.vocabulary.permissions)) {
1392
+ if (rls.actions !== void 0 && Object.hasOwn(rls.actions, leaf.action) || commandFor(leaf.action) !== void 0 || !suggestsWrite(leaf.action)) continue;
1393
+ byVerb.set(leaf.action, [...byVerb.get(leaf.action) ?? [], leaf.key]);
1394
+ }
1395
+ return [...byVerb.entries()].toSorted(byKey).map(([verb, keys]) => ({
1396
+ code: "PD071",
1397
+ severity: "warning",
1398
+ message: `action '${verb}' (${keys.toSorted().join(", ")}) reads as a write but has no rls.actions entry, so it compiles to 'none' and adds no SQL policy`,
1399
+ fix: `map it on purpose in rls.actions: ${verb}: "insert", "update", "delete" or "none"`
1400
+ }));
1401
+ }
1402
+ //#endregion
1403
+ //#region src/cli/doctor/seeds.ts
1404
+ const SEED_INSERT = /^insert\s+into\s+((?:"[^"]+"|\w+)(?:\.(?:"[^"]+"|\w+))?)\s*\(\s*role\s*,\s*permission\s*,\s*grant_key\s*,\s*scope\s*,\s*effect\s*\)\s*values\s*([\s\S]*?)\s*on\s+conflict\b/iu;
1405
+ const SEED_CLEAR = /^delete\s+from\s+((?:"[^"]+"|\w+)(?:\.(?:"[^"]+"|\w+))?)\s*$/iu;
1406
+ const LITERAL = String.raw`'((?:[^']|'')*)'`;
1407
+ const SEED_ROW = new RegExp(String.raw`\(\s*${LITERAL}\s*,\s*${LITERAL}\s*,\s*${LITERAL}\s*,\s*${LITERAL}\s*,\s*${LITERAL}\s*\)`, "gu");
1408
+ function rowKey(values) {
1409
+ return values.join(" ");
1410
+ }
1411
+ /** The rows of the last statement that seeds or clears `table`, in migration order. */
1412
+ function lastSeed(files, cwd, config, table) {
1413
+ let seeded;
1414
+ for (const file of sqlFiles(cwd, config.doctor?.migrations ?? MIGRATION_DIRS)) for (const { text } of sqlStatements(files.read(file))) {
1415
+ const insert = SEED_INSERT.exec(text);
1416
+ if (insert !== null && tableKey(group(insert, 1)) === table) {
1417
+ const rows = /* @__PURE__ */ new Set();
1418
+ for (const row of group(insert, 2).matchAll(SEED_ROW)) rows.add(rowKey(row.slice(1, 6).map((value) => value.replaceAll("''", "'"))));
1419
+ seeded = {
1420
+ file: rel(cwd, file),
1421
+ rows
1422
+ };
1423
+ continue;
1424
+ }
1425
+ const clear = SEED_CLEAR.exec(text);
1426
+ if (clear !== null && tableKey(group(clear, 1)) === table) seeded = {
1427
+ file: rel(cwd, file),
1428
+ rows: /* @__PURE__ */ new Set()
1429
+ };
1430
+ }
1431
+ return seeded;
1432
+ }
1433
+ /** The first three rows, as `(role permission grant_key scope effect)`. */
1434
+ function sample(rows) {
1435
+ return rows.slice(0, 3).map((row) => `(${row})`).join(", ");
1436
+ }
1437
+ /**
1438
+ * PD054: the `role_permissions` rows the migrations seed last are not the
1439
+ * rows the policy compiles to, so the helpers answer from a stale policy.
1440
+ */
1441
+ async function pd054(input) {
1442
+ const { cwd, config } = input;
1443
+ const rls = config.rls;
1444
+ if (config.policy === void 0 || rls === void 0 && config.supabase === void 0) return [];
1445
+ const schema = rls?.schema ?? rls?.rbac?.schema ?? "permdock";
1446
+ const seeded = lastSeed(filesOf(input), cwd, config, `${schema}.role_permissions`);
1447
+ if (seeded === void 0) return [];
1448
+ const policy = await policyOf(input);
1449
+ if (policy === void 0) return [];
1450
+ const ctx = {
1451
+ dialect: rls?.dialect ?? "supabase",
1452
+ scopes: scopeList(policy.scopes),
1453
+ tenantClaim: rls?.tenantClaim ?? "tenant_id",
1454
+ gucPrefix: rls?.gucPrefix ?? "app",
1455
+ ...rls?.actions === void 0 ? {} : { actions: rls.actions },
1456
+ schema,
1457
+ ...rls?.fields === "views" ? { fields: "views" } : {}
1458
+ };
1459
+ let expected;
1460
+ try {
1461
+ expected = new Set(compileGrants(policy, ctx, rls?.tables, [], true).rolePermissions.map((row) => rowKey([
1462
+ row.role,
1463
+ row.permission,
1464
+ row.grantKey,
1465
+ row.scope,
1466
+ row.effect
1467
+ ])));
1468
+ } catch {
1469
+ return [];
1470
+ }
1471
+ const missing = [...expected].filter((row) => !seeded.rows.has(row));
1472
+ const stale = [...seeded.rows].filter((row) => !expected.has(row));
1473
+ if (missing.length === 0 && stale.length === 0) return [];
1474
+ const parts = [...missing.length === 0 ? [] : [`${String(missing.length)} missing, such as ${sample(missing)}`], ...stale.length === 0 ? [] : [`${String(stale.length)} stale, such as ${sample(stale)}`]];
1475
+ return [{
1476
+ code: "PD054",
1477
+ severity: "warning",
1478
+ message: `${seeded.file} seeds ${schema}.role_permissions with rows the policy no longer compiles to: ${parts.join("; ")}`,
1479
+ fix: "run permdock rls generate (for declarative schemas, --split ...,seeds --seeds-out supabase/migrations/, which writes a new migration) and apply it"
1480
+ }];
1481
+ }
1482
+ //#endregion
1483
+ //#region src/cli/doctor/sql.ts
1484
+ /** Supabase's `[api] schemas` when `config.toml` sets none. */
1485
+ const API_SCHEMAS = ["public", "graphql_public"];
1486
+ const NAME = String.raw`((?:"[^"]+"|\w+)(?:\.(?:"[^"]+"|\w+))?)`;
1487
+ const IDENT = String.raw`("[^"]+"|\w+)`;
1488
+ /** A statement without its dollar-quoted bodies: what a function header says. */
1489
+ function header(text) {
1490
+ return text.replaceAll(/\$([A-Za-z_]*)\$[\s\S]*?\$\1\$/gu, "$$$$");
1491
+ }
1492
+ function isSupabase(files, cwd, config) {
1493
+ return files.exists(join(cwd, "supabase")) || config.rls?.dialect === "supabase" || config.supabase !== void 0;
1494
+ }
1495
+ function migrationStatements(files, cwd, config) {
1496
+ return sqlFiles(cwd, config.doctor?.migrations ?? MIGRATION_DIRS).flatMap((file) => sqlStatements(files.read(file)).map((statement) => Object.assign({ file: rel(cwd, file) }, statement)));
1497
+ }
1498
+ /** The schema of a `tableKey`, which always names one. */
1499
+ function schemaOf(key) {
1500
+ return key.slice(0, key.indexOf("."));
1501
+ }
1502
+ function unquote(name) {
1503
+ return name.replaceAll("\"", "").toLowerCase();
1504
+ }
1505
+ /**
1506
+ * PD046: the schema of PermDock's `security definer` helpers is one the Data
1507
+ * API exposes, so PostgREST serves them as RPCs.
1508
+ */
1509
+ function pd046(cwd, config, files = nodeFiles) {
1510
+ if (!files.exists(join(cwd, "supabase/config.toml"))) return [];
1511
+ const schema = config.rls?.schema ?? config.rls?.rbac?.schema ?? "permdock";
1512
+ if (!(supabaseConfig(cwd).apiSchemas ?? API_SCHEMAS).includes(schema)) return [];
1513
+ return [{
1514
+ code: "PD046",
1515
+ severity: "warning",
1516
+ message: `the PermDock helper schema ${schema} is in [api] schemas in supabase/config.toml, so the Data API serves its security definer helpers as RPCs`,
1517
+ fix: schema === "public" ? `drop rls.schema: 'public' so the helpers move to the private ${PERMDOCK_SCHEMA} schema, regenerate with permdock rls generate, and keep ${PERMDOCK_SCHEMA} out of [api] schemas` : `remove ${schema} from [api] schemas; policies reach the helpers through grant usage, not the Data API`
1518
+ }];
1519
+ }
1520
+ const USER_METADATA = /\b(raw_user_meta_data|user_metadata)\b/giu;
1521
+ /** PD047: SQL that reads `user_metadata`, which every user can edit for themselves. */
1522
+ function pd047(cwd, config, files = nodeFiles) {
1523
+ if (!isSupabase(files, cwd, config)) return [];
1524
+ return migrationStatements(files, cwd, config).flatMap((statement) => [...statement.text.matchAll(USER_METADATA)].map((match) => ({
1525
+ code: "PD047",
1526
+ severity: "warning",
1527
+ message: `${statement.file}:${String(statement.line + statement.text.slice(0, match.index).split("\n").length - 1)} reads ${match[0]}, which a user can set for themselves`,
1528
+ fix: "decide access from app_metadata (raw_app_meta_data), a server-owned table or a hook claim; user_metadata is for display only"
1529
+ })));
1530
+ }
1531
+ const CREATE_FUNCTION = new RegExp(String.raw`^create\s+(?:or\s+replace\s+)?function\s+${NAME}\s*\(`, "iu");
1532
+ const ALTER_SEARCH_PATH = new RegExp(String.raw`^alter\s+function\s+${NAME}\s*(?:\([^)]*\))?\s+set\s+search_path\b`, "iu");
1533
+ const REVOKE_EXECUTE = new RegExp(String.raw`^revoke\s+(?:all(?:\s+privileges)?|execute)\s+on\s+(?:function|routine)\s+${NAME}\s*(?:\([^)]*\))?\s+from\s+([\s\S]+)$`, "iu");
1534
+ const REVOKE_ALL_FUNCTIONS = new RegExp(String.raw`^(?:alter\s+default\s+privileges\s+(?:for\s+role\s+\S+\s+)?in\s+schema\s+${IDENT}\s+)?revoke\s+(?:all(?:\s+privileges)?|execute)\s+on\s+(?:all\s+)?(?:functions|routines)(?:\s+in\s+schema\s+${IDENT})?\s+from\s+([\s\S]+)$`, "iu");
1535
+ function createdFunctions(statements) {
1536
+ return statements.flatMap((statement) => {
1537
+ const match = CREATE_FUNCTION.exec(statement.text);
1538
+ return match === null ? [] : [{
1539
+ ...statement,
1540
+ key: tableKey(group(match, 1)),
1541
+ head: header(statement.text)
1542
+ }];
1543
+ });
1544
+ }
1545
+ /** PD048: a function without `set search_path`, so a caller's objects can shadow the ones it names. */
1546
+ function pd048(cwd, config, files = nodeFiles) {
1547
+ if (!isSupabase(files, cwd, config)) return [];
1548
+ const statements = migrationStatements(files, cwd, config);
1549
+ const altered = new Set(statements.flatMap((statement) => {
1550
+ const match = ALTER_SEARCH_PATH.exec(statement.text);
1551
+ return match === null ? [] : [tableKey(group(match, 1))];
1552
+ }));
1553
+ return createdFunctions(statements).filter((fn) => !/\bset\s+search_path\b/iu.test(fn.head) && !altered.has(fn.key) && !/\blanguage\s+(?:c|internal)\b/iu.test(fn.head)).map((fn) => {
1554
+ const definer = /\bsecurity\s+definer\b/iu.test(fn.head);
1555
+ return {
1556
+ code: "PD048",
1557
+ severity: "warning",
1558
+ message: `${fn.file}:${String(fn.line)} ${definer ? "security definer " : ""}function ${fn.key} sets no search_path${definer ? ", so a caller can shadow what it reads with their own objects" : ""}`,
1559
+ fix: `add set search_path = '' and qualify every name in the body (Supabase advisor function_search_path_mutable)`
1560
+ };
1561
+ });
1562
+ }
1563
+ /** Roles each `revoke` statement takes `execute` from, per function key or `schema.*`. */
1564
+ function revokedExecute(statements) {
1565
+ const revoked = /* @__PURE__ */ new Map();
1566
+ const add = (key, roles) => {
1567
+ const set = revoked.get(key) ?? /* @__PURE__ */ new Set();
1568
+ for (const role of roles.split(",")) set.add(unquote(role.trim().replace(/\s+(?:cascade|restrict)$/iu, "")));
1569
+ revoked.set(key, set);
1570
+ };
1571
+ for (const statement of statements) {
1572
+ const one = REVOKE_EXECUTE.exec(statement.text);
1573
+ if (one !== null) {
1574
+ add(tableKey(group(one, 1)), group(one, 2));
1575
+ continue;
1576
+ }
1577
+ const all = REVOKE_ALL_FUNCTIONS.exec(statement.text);
1578
+ if (all !== null) {
1579
+ const schema = all[1] ?? all[2];
1580
+ add(`${schema === void 0 ? "*" : unquote(schema)}.*`, group(all, 3));
1581
+ }
1582
+ }
1583
+ return revoked;
1584
+ }
1585
+ /**
1586
+ * PD049: a `security definer` function `public` (every role) or, in Supabase's
1587
+ * `public` schema, `anon` may still execute: Postgres grants `execute` to
1588
+ * `public` on create, and Supabase's default privileges grant it to `anon`.
1589
+ */
1590
+ function pd049(cwd, config, files = nodeFiles) {
1591
+ if (!isSupabase(files, cwd, config)) return [];
1592
+ const statements = migrationStatements(files, cwd, config);
1593
+ const revoked = revokedExecute(statements);
1594
+ const findings = [];
1595
+ for (const fn of createdFunctions(statements)) {
1596
+ if (!/\bsecurity\s+definer\b/iu.test(fn.head) || /\breturns\s+(?:event_)?trigger\b/iu.test(fn.head)) continue;
1597
+ const schema = schemaOf(fn.key);
1598
+ const from = /* @__PURE__ */ new Set([
1599
+ ...revoked.get(fn.key) ?? [],
1600
+ ...revoked.get(`${schema}.*`) ?? [],
1601
+ ...revoked.get("*.*") ?? []
1602
+ ]);
1603
+ const missing = ["public", ...schema === "public" ? ["anon"] : []].filter((role) => !from.has(role));
1604
+ if (missing.length > 0) findings.push({
1605
+ code: "PD049",
1606
+ severity: "warning",
1607
+ message: `${fn.file}:${String(fn.line)} security definer function ${fn.key} can be executed by ${missing.join(" and ")}`,
1608
+ fix: `revoke execute on function ${fn.key}(…) from public, anon; then grant execute to the roles that call it`
1609
+ });
1610
+ }
1611
+ return findings;
1612
+ }
1613
+ const CREATE_TABLE = new RegExp(String.raw`^create\s+(?:(?:global|local)\s+)?(?:(?:temp|temporary|unlogged)\s+)?table\s+(?:if\s+not\s+exists\s+)?${NAME}\s*\(`, "iu");
1614
+ const ENABLE_RLS = new RegExp(String.raw`^alter\s+table\s+(?:if\s+exists\s+)?(?:only\s+)?${NAME}[\s\S]*\benable\s+row\s+level\s+security\b`, "iu");
1615
+ const DROP_TABLE = /^drop\s+table\s+(?:if\s+exists\s+)?([\s\S]+?)(?:\s+(?:cascade|restrict))?$/iu;
1616
+ /** PD050: a `public` table without row level security, which the Data API serves whole. */
1617
+ function pd050(cwd, config, files = nodeFiles) {
1618
+ if (!isSupabase(files, cwd, config)) return [];
1619
+ const created = /* @__PURE__ */ new Map();
1620
+ const enabled = /* @__PURE__ */ new Set();
1621
+ for (const statement of migrationStatements(files, cwd, config)) {
1622
+ const table = CREATE_TABLE.exec(statement.text);
1623
+ if (table !== null) {
1624
+ created.set(tableKey(group(table, 1)), statement);
1625
+ continue;
1626
+ }
1627
+ const rls = ENABLE_RLS.exec(statement.text);
1628
+ if (rls !== null) {
1629
+ enabled.add(tableKey(group(rls, 1)));
1630
+ continue;
1631
+ }
1632
+ const dropped = DROP_TABLE.exec(statement.text);
1633
+ if (dropped !== null) for (const name of group(dropped, 1).split(",")) created.delete(tableKey(name.trim()));
1634
+ }
1635
+ return [...created].filter(([key]) => schemaOf(key) === "public" && !enabled.has(key)).map(([key, statement]) => ({
1636
+ code: "PD050",
1637
+ severity: "error",
1638
+ message: `${statement.file}:${String(statement.line)} creates ${key} without row level security, so the Data API serves every row to anon and authenticated`,
1639
+ fix: `alter table ${key} enable row level security; permdock rls generate writes its policies`
1640
+ }));
1641
+ }
1642
+ const CREATE_POLICY = new RegExp(String.raw`^create\s+policy\s+${IDENT}\s+on\s+${NAME}([\s\S]*)$`, "iu");
1643
+ const UNWRAPPED_AUTH = /(?<!\bselect\s{1,20})\bauth\.(uid|jwt)\s*\(\s*\)/giu;
1644
+ /** PD051: a policy that calls `auth.uid()` or `auth.jwt()` bare, so Postgres runs it once per row. */
1645
+ function pd051(cwd, config, files = nodeFiles) {
1646
+ if (!isSupabase(files, cwd, config)) return [];
1647
+ return migrationStatements(files, cwd, config).flatMap((statement) => {
1648
+ const policy = CREATE_POLICY.exec(statement.text);
1649
+ if (policy === null) return [];
1650
+ const calls = [...new Set([...group(policy, 3).matchAll(UNWRAPPED_AUTH)].map((match) => `auth.${group(match, 1).toLowerCase()}()`))];
1651
+ const [first] = calls;
1652
+ return first === void 0 ? [] : [{
1653
+ code: "PD051",
1654
+ severity: "warning",
1655
+ message: `${statement.file}:${String(statement.line)} policy ${unquote(group(policy, 1))} on ${tableKey(group(policy, 2))} calls ${calls.join(" and ")} once per row`,
1656
+ fix: `wrap each call as (select ${first}) so Postgres evaluates it once per statement (Supabase advisor auth_rls_initplan)`
1657
+ }];
1658
+ });
1659
+ }
1660
+ /** PD052: an update policy without `with check`, so nothing states what a row may become. */
1661
+ function pd052(cwd, config, files = nodeFiles) {
1662
+ if (!isSupabase(files, cwd, config)) return [];
1663
+ return migrationStatements(files, cwd, config).flatMap((statement) => {
1664
+ const policy = CREATE_POLICY.exec(statement.text);
1665
+ const rest = policy?.[3] ?? "";
1666
+ if (policy === null || !/\bfor\s+update\b/iu.test(rest) || /\bwith\s+check\b/iu.test(rest)) return [];
1667
+ return [{
1668
+ code: "PD052",
1669
+ severity: "warning",
1670
+ message: `${statement.file}:${String(statement.line)} update policy ${unquote(group(policy, 1))} on ${tableKey(group(policy, 2))} has no with check, so Postgres checks the new row against using alone`,
1671
+ fix: "add with check (...) stating what the updated row must satisfy, such as the same tenant and owner test, so an update cannot move a row out of what the user may write"
1672
+ }];
1673
+ });
1674
+ }
1675
+ /** Top-level comma-separated parts of a parenthesised list. */
1676
+ function topLevelParts(body) {
1677
+ const parts = [];
1678
+ let depth = 0;
1679
+ let start = 0;
1680
+ for (let index = 0; index < body.length; index += 1) {
1681
+ const char = body[index];
1682
+ if (char === "(") depth += 1;
1683
+ else if (char === ")") depth -= 1;
1684
+ else if (char === "," && depth === 0) {
1685
+ parts.push(body.slice(start, index).trim());
1686
+ start = index + 1;
1687
+ }
1688
+ }
1689
+ parts.push(body.slice(start).trim());
1690
+ return parts.filter((part) => part !== "");
1691
+ }
1692
+ /**
1693
+ * One column definition or table constraint of a `create table` body: its
1694
+ * first column, whether it is a foreign key, and whether a primary key or
1695
+ * unique constraint indexes that column.
1696
+ */
1697
+ function tablePart(raw) {
1698
+ const part = raw.replace(/^constraint\s+(?:"[^"]+"|\w+)\s+/iu, "");
1699
+ const listed = /^(foreign\s+key|primary\s+key|unique)\s*\(([^)]*)\)/iu.exec(part);
1700
+ if (listed !== null) {
1701
+ const foreign = /^foreign/iu.test(group(listed, 1));
1702
+ return {
1703
+ column: firstColumn(group(listed, 2)),
1704
+ foreign,
1705
+ indexed: !foreign
1706
+ };
1707
+ }
1708
+ return {
1709
+ column: unquote(/^("[^"]+"|\w+)/u.exec(part)?.[1] ?? ""),
1710
+ foreign: /\breferences\b/iu.test(part),
1711
+ indexed: /\b(?:primary\s+key|unique)\b/iu.test(part)
1712
+ };
1713
+ }
1714
+ function firstColumn(list) {
1715
+ return unquote(list.replace(/,[\s\S]*$/u, "").trim());
1716
+ }
1717
+ const ADD_CONSTRAINT = new RegExp(String.raw`^alter\s+table\s+(?:if\s+exists\s+)?(?:only\s+)?${NAME}\s+add\s+(?:constraint\s+${IDENT}\s+)?(foreign\s+key|primary\s+key|unique)\s*\(([^)]*)\)`, "iu");
1718
+ const CREATE_INDEX = new RegExp(String.raw`^create\s+(?:unique\s+)?index\s+(?:concurrently\s+)?(?:if\s+not\s+exists\s+)?(?:${IDENT}\s+)?on\s+(?:only\s+)?${NAME}\s*(?:using\s+\w+\s*)?\(\s*${IDENT}`, "iu");
1719
+ /** PD053: a foreign key no index starts with, so joins and deletes on the referenced row scan the table. */
1720
+ function pd053(cwd, config, files = nodeFiles) {
1721
+ if (!isSupabase(files, cwd, config)) return [];
1722
+ const keys = [];
1723
+ const indexed = /* @__PURE__ */ new Set();
1724
+ for (const statement of migrationStatements(files, cwd, config)) {
1725
+ const table = CREATE_TABLE.exec(statement.text);
1726
+ if (table !== null) {
1727
+ const key = tableKey(group(table, 1));
1728
+ const open = statement.text.indexOf("(", table[0].length - 1);
1729
+ const body = statement.text.slice(open + 1, statement.text.lastIndexOf(")"));
1730
+ for (const raw of topLevelParts(body)) {
1731
+ const found = tablePart(raw);
1732
+ if (found.foreign) keys.push({
1733
+ ...statement,
1734
+ table: key,
1735
+ column: found.column
1736
+ });
1737
+ if (found.indexed) indexed.add(`${key}.${found.column}`);
1738
+ }
1739
+ continue;
1740
+ }
1741
+ const added = ADD_CONSTRAINT.exec(statement.text);
1742
+ if (added !== null) {
1743
+ const key = tableKey(group(added, 1));
1744
+ const column = firstColumn(group(added, 4));
1745
+ if (/^foreign/iu.test(group(added, 3))) keys.push({
1746
+ ...statement,
1747
+ table: key,
1748
+ column
1749
+ });
1750
+ else indexed.add(`${key}.${column}`);
1751
+ continue;
1752
+ }
1753
+ const index = CREATE_INDEX.exec(statement.text);
1754
+ if (index !== null) indexed.add(`${tableKey(group(index, 2))}.${unquote(group(index, 3))}`);
1755
+ }
1756
+ return keys.filter((fk) => !indexed.has(`${fk.table}.${fk.column}`)).map((fk) => ({
1757
+ code: "PD053",
1758
+ severity: "warning",
1759
+ message: `${fk.file}:${String(fk.line)} ${fk.table}.${fk.column} is a foreign key no index starts with, so policies that join on it and deletes of the referenced row scan ${fk.table}`,
1760
+ fix: `create index on ${fk.table} (${fk.column}); (Supabase advisor unindexed_foreign_keys)`
1761
+ }));
1762
+ }
1763
+ const LEGACY_CLAIM = /\brequest\.jwt\.claim\.(\w+)/giu;
1764
+ function at(statement) {
1765
+ return `${statement.file}:${String(statement.line)}`;
1766
+ }
1767
+ /**
1768
+ * PD062: SQL that reads or sets a per-claim `request.jwt.claim.<name>`
1769
+ * setting. Current PostgREST, `withPostgresClient`, PermDock's `withSubject`
1770
+ * and `rls verify` set only `request.jwt.claims`. A function replaced by a
1771
+ * later migration is judged by its last definition.
1772
+ */
1773
+ function pd062(cwd, config, files = nodeFiles) {
1774
+ if (!isSupabase(files, cwd, config)) return [];
1775
+ const statements = migrationStatements(files, cwd, config);
1776
+ const latest = /* @__PURE__ */ new Map();
1777
+ for (const fn of createdFunctions(statements)) latest.set(fn.key, at(fn));
1778
+ return statements.flatMap((statement) => {
1779
+ const fn = CREATE_FUNCTION.exec(statement.text);
1780
+ if (fn !== null && latest.get(tableKey(group(fn, 1))) !== at(statement)) return [];
1781
+ return [...new Set([...statement.text.matchAll(LEGACY_CLAIM)].map((match) => group(match, 1).toLowerCase()))].map((name) => ({
1782
+ code: "PD062",
1783
+ severity: "warning",
1784
+ message: `${statement.file}:${String(statement.line)} uses the legacy request.jwt.claim.${name} setting, which PostgREST 12 and PermDock's withSubject no longer set, so it reads null`,
1785
+ fix: name === "sub" ? "read the subject with (select permdock.permdock_user_id()) or (select auth.uid()), and set request.jwt.claims in tests and jobs" : `read (select auth.jwt()) ->> '${name}', and set request.jwt.claims in tests and jobs`
1786
+ }));
1787
+ });
1788
+ }
1789
+ /**
1790
+ * PD063: a migration statement supautils rejects for `postgres`, so
1791
+ * `supabase db push` fails on it.
1792
+ */
1793
+ function pd063(cwd, config, files = nodeFiles) {
1794
+ if (!isSupabase(files, cwd, config)) return [];
1795
+ return sqlFiles(cwd, config.doctor?.migrations ?? MIGRATION_DIRS).flatMap((file) => supautilsRejections(files.read(file)).map((found) => ({
1796
+ code: "PD063",
1797
+ severity: "error",
1798
+ message: `${rel(cwd, file)}:${String(found.line)} ${found.reason}, which supautils rejects for the postgres role on Supabase`,
1799
+ fix: "remove the statement: reserved roles belong to Supabase. Grant your own role to authenticator instead, and change anon, authenticated, authenticator or service_role only with alter role … set"
1800
+ })));
1801
+ }
1802
+ /**
1803
+ * PD056: SQL still calls an `rls.migrate` helper by its legacy name, so the
1804
+ * shim (or the legacy function) cannot be dropped yet.
1805
+ */
1806
+ function pd056(cwd, config, files = nodeFiles) {
1807
+ const migrate = config.rls?.migrate;
1808
+ const helpers = migrate === void 0 ? [] : legacyHelperNames(migrate);
1809
+ if (helpers.length === 0) return [];
1810
+ const statements = migrationStatements(files, cwd, config);
1811
+ const findings = [];
1812
+ for (const name of helpers) {
1813
+ const { count, files: callers } = legacyHelperCalls(statements, name);
1814
+ if (count === 0) continue;
1815
+ findings.push({
1816
+ code: "PD056",
1817
+ severity: "warning",
1818
+ message: `legacy helper ${name} is still called ${String(count)} time(s), in ${callers.join(", ")}`,
1819
+ fix: `run permdock rls migrate for policies, rewrite function bodies, views and triggers onto the permdock helpers by hand, then drop function ${name}`
1820
+ });
1821
+ }
1822
+ return findings;
1823
+ }
1824
+ /**
1825
+ * PD066: the migrations still create an `rls.migrate.tables` table: something
1826
+ * still reads it, or `rls migrate --retire-out` can drop it now.
1827
+ */
1828
+ function pd066(cwd, config, files = nodeFiles) {
1829
+ const migrate = config.rls?.migrate;
1830
+ if (migrate?.tables === void 0) return [];
1831
+ return retirePlan(migrationStatements(files, cwd, config), migrate).tables.filter((table) => table.present).map((table) => {
1832
+ const [first] = table.readers;
1833
+ return first === void 0 ? {
1834
+ code: "PD066",
1835
+ severity: "warning",
1836
+ message: `${table.table} has no reader left, but the migrations still create it and its ${String(table.refreshTriggers.length)} refresh trigger(s)`,
1837
+ fix: "run permdock rls migrate --sql <dir> --retire-out supabase/migrations/ and apply the migration it writes"
1838
+ } : {
1839
+ code: "PD066",
1840
+ severity: "warning",
1841
+ message: `${table.table} is still read by ${String(table.readers.length)} object(s), first ${first.kind} ${first.name} at ${first.file}:${String(first.line)}`,
1842
+ fix: "rewrite each reader onto the permdock helpers, then run permdock rls migrate --retire-out"
1843
+ };
1844
+ });
1845
+ }
1846
+ //#endregion
1847
+ //#region src/cli/doctor/suspension.ts
1848
+ /**
1849
+ * PD061: a suspended scope keeps permissions. Each scope's kept keys are
1850
+ * listed for review, since its members hold them while the instance is
1851
+ * suspended, and a key the definitions do not declare is named apart.
1852
+ */
1853
+ async function pd061(input) {
1854
+ const suspension = input.config.rls?.suspension;
1855
+ const kept = [...Object.entries(suspension?.scopes ?? {}).map(([scope, row]) => ({
1856
+ path: `scopes.${scope}`,
1857
+ who: `members of a suspended ${scope}`,
1858
+ row
1859
+ })), ...suspension?.memberships === void 0 ? [] : [{
1860
+ path: "memberships",
1861
+ who: "suspended memberships",
1862
+ row: suspension.memberships
1863
+ }]].flatMap((entry) => {
1864
+ try {
1865
+ const keys = keptKeys(entry.row);
1866
+ return keys.length === 0 ? [] : [{
1867
+ path: entry.path,
1868
+ who: entry.who,
1869
+ keys
1870
+ }];
1871
+ } catch {
1872
+ return [{
1873
+ path: entry.path,
1874
+ who: entry.who,
1875
+ keys: [],
1876
+ invalid: true
1877
+ }];
1878
+ }
1879
+ });
1880
+ if (kept.length === 0) return [];
1881
+ const policy = await policyOf(input);
1882
+ const declared = /* @__PURE__ */ new Set();
1883
+ const former = /* @__PURE__ */ new Map();
1884
+ for (const leaf of policy === void 0 ? [] : listPermissions(policy.permissions)) {
1885
+ declared.add(leaf.key);
1886
+ for (const old of formerKeys(leaf)) former.set(old, leaf.key);
1887
+ }
1888
+ return kept.flatMap((item) => {
1889
+ if (item.invalid === true) return [{
1890
+ code: "PD061",
1891
+ severity: "warning",
1892
+ message: `rls.suspension.${item.path}.keep has an entry that is neither a permission nor a permission key, so generate refuses it`,
1893
+ fix: "list permission references or their keys in keep"
1894
+ }];
1895
+ const findings = [{
1896
+ code: "PD061",
1897
+ severity: "warning",
1898
+ message: `${item.who} still hold ${item.keys.join(", ")}`,
1899
+ fix: item.path === "memberships" ? "keep only what a suspended member must still do, such as leaving or exporting their own data, in rls.suspension.memberships.keep" : `keep only what a suspended ${item.path.slice(7)} must still do, such as restoring it or cancelling its deletion, in rls.suspension.${item.path}.keep`
1900
+ }];
1901
+ if (policy === void 0) return findings;
1902
+ const unknown = item.keys.filter((key) => !declared.has(key));
1903
+ if (unknown.length > 0) findings.push({
1904
+ code: "PD061",
1905
+ severity: "warning",
1906
+ message: `rls.suspension.${item.path}.keep names ${unknown.map((key) => former.has(key) ? `${key} (renamed to ${String(former.get(key))})` : key).join(", ")}, which the definitions do not declare, so a suspended ${item.path === "memberships" ? "membership" : item.path.slice(7)} keeps nothing for them`,
1907
+ fix: "keep permission references instead of keys, or replace each key with its current name"
1908
+ });
1909
+ return findings;
1910
+ });
1911
+ }
1912
+ const DOCTOR_CHECKS = [
1913
+ {
1914
+ code: "PD001",
1915
+ groups: ["imports"],
1916
+ run: (project) => pd001(project.sources(), new Set(listSourceFiles(project.cwd, project.config.doctor?.clientEntries ?? []).map((file) => rel(project.cwd, file))), project.config.policy === void 0 ? void 0 : {
1917
+ cwd: project.cwd,
1918
+ path: project.config.policy
1919
+ })
1920
+ },
1921
+ {
1922
+ code: "PD059",
1923
+ groups: ["project"],
1924
+ run: pd059
1925
+ },
1926
+ {
1927
+ code: "PD060",
1928
+ groups: ["project"],
1929
+ run: pd060
1930
+ },
1931
+ {
1932
+ code: "PD002",
1933
+ groups: ["references"],
1934
+ run: pd002
1935
+ },
1936
+ {
1937
+ code: "PD003",
1938
+ groups: ["ungranted"],
1939
+ run: pd003
1940
+ },
1941
+ {
1942
+ code: "PD004",
1943
+ groups: ["catalog"],
1944
+ run: pd004
1945
+ },
1946
+ {
1947
+ code: "PD067",
1948
+ groups: ["catalog", "operations"],
1949
+ when: (config) => config.operations !== void 0,
1950
+ run: pd067
1951
+ },
1952
+ {
1953
+ code: "PD070",
1954
+ groups: ["catalog", "better-supabase"],
1955
+ when: (config) => config.policy !== void 0,
1956
+ run: pd070
1957
+ },
1958
+ {
1959
+ code: "PD005",
1960
+ groups: ["skills"],
1961
+ run: (project) => pd005(project.cwd, project.files)
1962
+ },
1963
+ {
1964
+ code: "PD006",
1965
+ groups: ["typescript"],
1966
+ run: (project) => pd006(project.cwd)
1967
+ },
1968
+ {
1969
+ code: "PD007",
1970
+ groups: ["validation"],
1971
+ run: (project) => pd007(project.sources())
1972
+ },
1973
+ {
1974
+ code: "PD008",
1975
+ groups: ["naming"],
1976
+ run: (project) => pd008(project.sources())
1977
+ },
1978
+ {
1979
+ code: "PD009",
1980
+ groups: ["duplicates"],
1981
+ run: (project) => pd009(project.cwd, project.files)
1982
+ },
1983
+ {
1984
+ code: "PD010",
1985
+ groups: ["claims"],
1986
+ run: (project) => pd010(project.sources())
1987
+ },
1988
+ {
1989
+ code: "PD011",
1990
+ groups: ["tenant"],
1991
+ run: (project) => pd011(project.sources())
1992
+ },
1993
+ {
1994
+ code: "PD012",
1995
+ groups: ["drafts"],
1996
+ run: (project) => pd012(project.cwd, project.config, project.files)
1997
+ },
1998
+ {
1999
+ code: "PD013",
2000
+ groups: ["algorithms"],
2001
+ run: (project) => pd013(project.sources())
2002
+ },
2003
+ {
2004
+ code: "PD014",
2005
+ groups: ["discovery"],
2006
+ run: (project) => pd014(project.sources())
2007
+ },
2008
+ {
2009
+ code: "PD015",
2010
+ groups: ["typ"],
2011
+ run: (project) => pd015(project.sources())
2012
+ },
2013
+ {
2014
+ code: "PD016",
2015
+ groups: ["rls"],
2016
+ run: pd016
2017
+ },
2018
+ {
2019
+ code: "PD017",
2020
+ groups: ["sensitive"],
2021
+ run: pd017
2022
+ },
2023
+ {
2024
+ code: "PD018",
2025
+ groups: ["separation"],
2026
+ run: pd018
2027
+ },
2028
+ {
2029
+ code: "PD019",
2030
+ groups: ["jwt-roles"],
2031
+ run: pd019
2032
+ },
2033
+ {
2034
+ code: "PD020",
2035
+ groups: ["hosted"],
2036
+ run: pd020
2037
+ },
2038
+ {
2039
+ code: "PD021",
2040
+ groups: ["hosted"],
2041
+ run: (project) => pd021({
2042
+ ...project,
2043
+ env: project.io.env ?? process.env
2044
+ })
2045
+ },
2046
+ {
2047
+ code: "PD022",
2048
+ groups: ["views"],
2049
+ run: (project) => pd022(project.cwd, project.config, project.files)
2050
+ },
2051
+ {
2052
+ code: "PD023",
2053
+ groups: ["custom-roles"],
2054
+ run: pd023
2055
+ },
2056
+ {
2057
+ code: "PD024",
2058
+ groups: ["self-approval"],
2059
+ run: pd024
2060
+ },
2061
+ {
2062
+ code: "PD025",
2063
+ groups: ["scopes"],
2064
+ run: pd025
2065
+ },
2066
+ {
2067
+ code: "PD026",
2068
+ groups: ["ownership"],
2069
+ run: pd026
2070
+ },
2071
+ {
2072
+ code: "PD027",
2073
+ groups: ["rls", "context-refs"],
2074
+ run: pd027
2075
+ },
2076
+ {
2077
+ code: "PD071",
2078
+ groups: ["rls"],
2079
+ run: pd071
2080
+ },
2081
+ {
2082
+ code: "PD028",
2083
+ groups: ["attrs", "supabase"],
2084
+ run: (project) => pd028(project.cwd, project.config, attrsPlan, project.files)
2085
+ },
2086
+ {
2087
+ code: "PD029",
2088
+ groups: ["credentials"],
2089
+ run: pd029
2090
+ },
2091
+ {
2092
+ code: "PD030",
2093
+ groups: ["fields"],
2094
+ run: pd030
2095
+ },
2096
+ {
2097
+ code: "PD031",
2098
+ groups: ["graph"],
2099
+ run: pd031
2100
+ },
2101
+ {
2102
+ code: "PD032",
2103
+ groups: ["graph", "rls"],
2104
+ run: pd032
2105
+ },
2106
+ {
2107
+ code: "PD033",
2108
+ groups: ["activation"],
2109
+ run: pd033
2110
+ },
2111
+ {
2112
+ code: "PD034",
2113
+ groups: ["break-glass"],
2114
+ run: pd034
2115
+ },
2116
+ {
2117
+ code: "PD035",
2118
+ groups: ["support"],
2119
+ run: pd035
2120
+ },
2121
+ {
2122
+ code: "PD036",
2123
+ groups: ["bola"],
2124
+ run: (project) => pd036(project.sources())
2125
+ },
2126
+ {
2127
+ code: "PD037",
2128
+ groups: ["supabase", "row-conditions"],
2129
+ run: pd037
2130
+ },
2131
+ {
2132
+ code: "PD039",
2133
+ groups: ["supabase", "helpers"],
2134
+ when: (config) => config.supabase?.hook !== void 0,
2135
+ run: supabaseSetup
2136
+ },
2137
+ {
2138
+ code: "PD038",
2139
+ groups: ["supabase", "tenant"],
2140
+ when: (config) => config.rls !== void 0 || config.supabase?.hook !== void 0,
2141
+ run: (project) => pd038(project.sources(), project.config.rls?.tenantClaim ?? "tenant_id", supabaseTenantClaim)
2142
+ },
2143
+ {
2144
+ code: "PD057",
2145
+ groups: ["supabase", "anonymous"],
2146
+ when: (config) => config.rls?.anonymousSignIns === "deny",
2147
+ run: (project) => pd057(project.sources())
2148
+ },
2149
+ {
2150
+ code: "PD040",
2151
+ groups: ["supabase", "auth-role"],
2152
+ run: (project) => pd040(project.cwd, project.config, project.files)
2153
+ },
2154
+ {
2155
+ code: "PD041",
2156
+ groups: ["supabase", "capabilities"],
2157
+ run: (project) => pd041(project.sources())
2158
+ },
2159
+ {
2160
+ code: "PD042",
2161
+ groups: ["supabase", "declarative"],
2162
+ run: (project) => pd042(project.cwd, project.config, project.files)
2163
+ },
2164
+ {
2165
+ code: "PD043",
2166
+ groups: ["supabase", "declarative"],
2167
+ run: (project) => pd043(project.cwd, project.config, project.files)
2168
+ },
2169
+ {
2170
+ code: "PD068",
2171
+ groups: ["supabase", "declarative"],
2172
+ run: (project) => pd068(project.cwd, project.config, project.files)
2173
+ },
2174
+ {
2175
+ code: "PD069",
2176
+ groups: ["supabase", "declarative"],
2177
+ run: (project) => pd069(project.cwd, project.config, project.files)
2178
+ },
2179
+ {
2180
+ code: "PD045",
2181
+ groups: ["supabase"],
2182
+ run: (project) => pd045(project.cwd)
2183
+ },
2184
+ ...[
2185
+ ["PD046", pd046],
2186
+ ["PD047", pd047],
2187
+ ["PD048", pd048],
2188
+ ["PD049", pd049],
2189
+ ["PD050", pd050],
2190
+ ["PD051", pd051],
2191
+ ["PD052", pd052],
2192
+ ["PD053", pd053],
2193
+ ["PD062", pd062],
2194
+ ["PD063", pd063]
2195
+ ].map(([code, check]) => ({
2196
+ code,
2197
+ groups: ["supabase", "sql"],
2198
+ run: (project) => check(project.cwd, project.config, project.files)
2199
+ })),
2200
+ {
2201
+ code: "PD054",
2202
+ groups: ["supabase", "seeds"],
2203
+ run: pd054
2204
+ },
2205
+ {
2206
+ code: "PD055",
2207
+ groups: ["custom-roles", "renamed"],
2208
+ run: pd055
2209
+ },
2210
+ {
2211
+ code: "PD056",
2212
+ groups: ["sql", "shims"],
2213
+ run: (project) => pd056(project.cwd, project.config, project.files)
2214
+ },
2215
+ {
2216
+ code: "PD066",
2217
+ groups: ["sql", "shims"],
2218
+ when: (config) => config.rls?.migrate?.tables !== void 0,
2219
+ run: (project) => pd066(project.cwd, project.config, project.files)
2220
+ },
2221
+ {
2222
+ code: "PD058",
2223
+ groups: ["powersync"],
2224
+ run: pd058
2225
+ },
2226
+ {
2227
+ code: "PD061",
2228
+ groups: ["rls", "suspension"],
2229
+ when: (config) => config.rls?.suspension?.scopes !== void 0,
2230
+ run: pd061
2231
+ },
2232
+ {
2233
+ code: "PD064",
2234
+ groups: ["rls", "assignments"],
2235
+ when: (config) => config.rls?.assignments !== void 0,
2236
+ run: pd064
2237
+ },
2238
+ {
2239
+ code: "PD044",
2240
+ groups: ["next", "endpoint"],
2241
+ run: pd044
2242
+ },
2243
+ {
2244
+ code: "PD065",
2245
+ groups: ["react-native", "endpoint"],
2246
+ run: pd065
2247
+ }
2248
+ ];
2249
+ async function runDoctor(input) {
2250
+ const wanted = new Set(input.only.map((item) => item.toLowerCase()));
2251
+ const unknown = unknownOnly(wanted);
2252
+ if (unknown !== void 0) return {
2253
+ code: 2,
2254
+ output: unknown
2255
+ };
2256
+ const fixed = [];
2257
+ if (input.fix) {
2258
+ const skills = runSkillsInstall({
2259
+ cwd: input.cwd,
2260
+ agents: []
2261
+ });
2262
+ fixed.push(`skills: ${skills.output}`);
2263
+ const collected = await runCollect({
2264
+ cwd: input.cwd,
2265
+ config: input.config,
2266
+ collect: input.config.collect ?? {},
2267
+ check: false,
2268
+ now: input.now,
2269
+ io: input.io
2270
+ });
2271
+ fixed.push(`catalog: ${collected.message.trim()}`);
2272
+ }
2273
+ const project = loadProject(input);
2274
+ const selected = (check) => wanted.size === 0 || wanted.has(check.code.toLowerCase()) || check.groups.some((group) => wanted.has(group));
2275
+ const findings = [];
2276
+ for (const check of DOCTOR_CHECKS) {
2277
+ if (check.when !== void 0 && !check.when(input.config)) continue;
2278
+ if (selected(check)) findings.push(...await check.run(project));
2279
+ }
2280
+ const errors = findings.filter((item) => item.severity === "error").length;
2281
+ const warnings = findings.filter((item) => item.severity === "warning").length;
2282
+ const report = {
2283
+ $schema: DOCTOR_REPORT_SCHEMA,
2284
+ fixed,
2285
+ findings,
2286
+ errors,
2287
+ warnings
2288
+ };
2289
+ return {
2290
+ code: errors > 0 || input.strict && warnings > 0 ? 1 : 0,
2291
+ output: input.json ? `${JSON.stringify(report, null, 2)}\n` : formatDoctor(report, input.color),
2292
+ findings: findings.map((item) => findingOf({
2293
+ ...item,
2294
+ docs: `${DOCS_BASE}/doctor#checks`
2295
+ }))
2296
+ };
2297
+ }
2298
+ /** PD059: the configured policy module throws or exports no policy, so every policy check is skipped. */
2299
+ async function pd059(project) {
2300
+ const load = await project.policyLoad();
2301
+ if (load.status !== "failed") return [];
2302
+ return [{
2303
+ code: "PD059",
2304
+ severity: "error",
2305
+ message: `policy module ${load.path} did not load (${load.message}), so doctor skipped every check that reads the policy`,
2306
+ fix: "fix the module so it exports policy from definePolicy, or point policy in permdock.config.ts at the module that does"
2307
+ }];
2308
+ }
2309
+ /** PD060: a source file under srcPath has a syntax error, so collect and usage miss what it references. */
2310
+ async function pd060(project) {
2311
+ return ((await project.collected()).scan?.unparsed ?? []).map((item) => ({
2312
+ code: "PD060",
2313
+ severity: "warning",
2314
+ message: `${item.file}:${String(item.line)} does not parse (${item.message}), so the catalog and usage report miss the permissions it references`,
2315
+ fix: "fix the syntax error, or exclude the file from collect.srcPath"
2316
+ }));
2317
+ }
2318
+ /** A usage message for the first `--only` value that names no check code or group. */
2319
+ function unknownOnly(wanted) {
2320
+ const known = /* @__PURE__ */ new Set();
2321
+ for (const check of DOCTOR_CHECKS) {
2322
+ known.add(check.code.toLowerCase());
2323
+ for (const group of check.groups) known.add(group);
2324
+ }
2325
+ for (const item of wanted) if (!known.has(item)) {
2326
+ const suggestion = didYouMean(item, [...known]);
2327
+ return `doctor: --only ${item} names no check code or group${suggestion === void 0 ? "" : `. Did you mean ${suggestion}?`}`;
2328
+ }
2329
+ }
2330
+ function formatDoctor(report, color) {
2331
+ const style = createStyle(color);
2332
+ const lines = [style.paint("bold", "permdock doctor"), ""];
2333
+ for (const change of report.fixed) lines.push(` ${style.okMark} fixed ${change}`);
2334
+ if (report.fixed.length > 0) lines.push("");
2335
+ for (const finding of report.findings) {
2336
+ const mark = finding.severity === "error" ? style.errorMark : style.warnMark;
2337
+ lines.push(` ${mark} ${style.paint("bold", finding.code)} ${finding.message}`);
2338
+ lines.push(` ${style.paint("dim", `fix: ${finding.fix}`)}`);
2339
+ }
2340
+ if (report.findings.length === 0) lines.push(" no findings");
2341
+ lines.push("");
2342
+ lines.push(` ${String(report.errors)} error${report.errors === 1 ? "" : "s"}, ${String(report.warnings)} warning${report.warnings === 1 ? "" : "s"}`);
2343
+ return `${lines.join("\n")}\n`;
2344
+ }
2345
+ async function supabaseSetup(input) {
2346
+ const policy = await input.policy();
2347
+ if (policy === void 0) return [];
2348
+ let manifest;
2349
+ try {
2350
+ manifest = supabaseHookManifest(scopeList(policy.scopes), input.config, { out: hookOut(input.cwd, input.config) }, policy);
2351
+ } catch {
2352
+ return [];
2353
+ }
2354
+ return pd039({
2355
+ cwd: input.cwd,
2356
+ config: input.config,
2357
+ manifest
2358
+ });
2359
+ }
2360
+ //#endregion
2361
+ export { DOCTOR_CHECKS, runDoctor };